Flutter FutureBuilder разрешается несколько раз без вызова build()

У меня проблема с тем, что мой виджет несколько раз запускает код FutureBuilder с уже разрешенным Future. В отличие от других вопросов на SO об этом, мой метод build() не вызывается несколько раз.

Мое будущее называют вне build() в initState() - оно также обернуто AsyncMemoizer.

Соответствующий код:

class _HomeScreenState extends State<HomeScreen> {
  late final Future myFuture;
  final AsyncMemoizer _memoizer = AsyncMemoizer();

  @override
  void initState() {
    super.initState();

    /// provider package
    final homeService = context.read<HomeService>();
    myFuture = _memoizer.runOnce(homeService.getMyData);
  }

  @override
  Widget build(BuildContext context) {
    print("[HOME] BUILDING OUR HOME SCREEN");

    return FutureBuilder(
      future: myFuture,
      builder: ((context, snapshot) {
        print("[HOME] BUILDER CALLED WITH SNAPSHOT: $snapshot - connection state: ${snapshot.connectionState}");

Когда я запускаю код и запускаю ошибку (показанная программная клавиатура вызывает ее в 50% случаев, но не все время), мои журналы:

I/flutter (29283): [HOME] BUILDING OUR HOME SCREEN
I/flutter (29283): [HOME] BUILDER CALLED WITH SNAPSHOT: AsyncSnapshot<dynamic>(ConnectionState.waiting, null, null, null) - connection state: ConnectionState.waiting
I/flutter (29283): [HOME] BUILDER CALLED WITH SNAPSHOT: AsyncSnapshot<dynamic>(ConnectionState.done, Instance of 'HomeData', null, null) - connection state: ConnectionState.done
...
/// bug triggered
...
I/flutter (29283): [HOME] BUILDER CALLED WITH SNAPSHOT: AsyncSnapshot<dynamic>(ConnectionState.done, Instance of 'HomeData', null, null) - connection state: ConnectionState.done

Первоначальный вызов с ConnectionState.waiting нормальный, затем мы получаем первую сборку с ConnectionState.done.

После того, как ошибка срабатывает, я получаю еще одно решение FutureBuilder без вызова метода build().

Я что-то упустил здесь?

Изменить с полным примером

Это показывает рассматриваемую ошибку — если вы щелкаете в TextField и выходите из него, FutureBuilder вызывается снова.

Кажется, это связано с тем, как клавиатура скрыта. Если я использую метод FocusScopeNode, он перестроится, а если я использую FocusManager, то нет, поэтому я не уверен, ошибка это или нет.

import 'package:flutter/material.dart';

void main() async {
  runApp(const TestApp());
}

class TestApp extends StatelessWidget {
  const TestApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Testapp',
      home: Scaffold(
        body: TestAppHomeScreen(),
      ),
    );
  }
}

class TestAppHomeScreen extends StatefulWidget {
  const TestAppHomeScreen({super.key});

  @override
  State<TestAppHomeScreen> createState() => _TestAppHomeScreenState();
}

class _TestAppHomeScreenState extends State<TestAppHomeScreen> {
  late final Future myFuture;

  @override
  void initState() {
    super.initState();

    myFuture = Future.delayed(const Duration(milliseconds: 500), () => true);

    print("[HOME] HOME SCREEN INIT STATE CALLED: $hashCode");
  }

  @override
  Widget build(BuildContext context) {
    print("[HOME] HOME SCREEN BUILD CALLED: $hashCode");
    return FutureBuilder(
      future: myFuture,
      builder: (context, snapshot) {
        print("[HOME] HOME SCREEN FUTURE BUILDER CALLED WITH STATE ${snapshot.connectionState}: $hashCode");
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Scaffold(
            body: Center(
              child: CircularProgressIndicator(),
            ),
          );
        }

        return GestureDetector(
          onTapUp: (details) {
            // hide the keyboard if it's showing
            FocusScopeNode currentFocus = FocusScope.of(context);
            if (!currentFocus.hasPrimaryFocus) {
              currentFocus.unfocus();
            }
            // FocusManager.instance.primaryFocus?.unfocus();
          },
          child: const Scaffold(
            body: Center(
              child: Padding(
                padding: EdgeInsets.symmetric(horizontal: 32.0),
                child: TextField(),
              ),
            ),
          ),
        );
      },
    );
  }
}

Я не могу воспроизвести это, не могли бы вы поподробнее рассказать об этой «ошибке», о которой вы говорите? Не могли бы вы опубликовать полный воспроизводимый пример?

venir 24.11.2022 10:11

Я добавил воспроизводимый пример @venir

divillysausages 24.11.2022 11:01

Я ответил ниже. Это помогает?

venir 25.11.2022 11:21
Как настроить Tailwind CSS с React.js и Next.js?
Как настроить Tailwind CSS с React.js и Next.js?
Tailwind CSS - единственный фреймворк, который, как я убедился, масштабируется в больших командах. Он легко настраивается, адаптируется к любому...
LeetCode запись решения 2536. Увеличение подматриц на единицу
LeetCode запись решения 2536. Увеличение подматриц на единицу
Увеличение подматриц на единицу - LeetCode
Переключение светлых/темных тем
Переключение светлых/темных тем
В Microsoft Training - Guided Project - Build a simple website with web pages, CSS files and JavaScript files, мы объясняем, как CSS можно...
Отношения &quot;многие ко многим&quot; в Laravel с методами присоединения и отсоединения
Отношения &quot;многие ко многим&quot; в Laravel с методами присоединения и отсоединения
Отношения "многие ко многим" в Laravel могут быть немного сложными, но с помощью Eloquent ORM и его моделей мы можем сделать это с легкостью. В этой...
В PHP
В PHP
В большой кодовой базе с множеством различных компонентов классы, функции и константы могут иметь одинаковые имена. Это может привести к путанице и...
Карта дорог Беладжар PHP Laravel
Карта дорог Беладжар PHP Laravel
Laravel - это PHP-фреймворк, разработанный для облегчения разработки веб-приложений. Laravel предоставляет различные функции, упрощающие разработку...
0
3
122
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Пожалуйста, попробуйте это решение /// provider package up super.initState();

Ваш код будет таким

  @override
  void initState() {
    /// provider package
    final homeService = context.read<HomeService>();
    myFuture = _memoizer.runOnce(homeService.getMyData);
    super.initState();
  }

Пожалуйста, после того, как попробуете, сообщите мне результат

К сожалению, это не изменило ситуацию

divillysausages 24.11.2022 10:21

У вас есть setState

Abdullatif Eida 24.11.2022 10:27

Я обновил вопрос воспроизводимым примером - похоже, он связан с тем, как фокус удаляется с клавиатуры.

divillysausages 24.11.2022 11:01

Ваша проблема в том, что будущий строитель работает дважды: первый ждет, а второй находится в состоянии готовности - или ваша проблема при нажатии на текстовое поле, строитель снова работает

Abdullatif Eida 24.11.2022 11:21

В соответствии с тем, что я вижу, когда вы нажимаете на текстовое поле, флаттер обновляет состояние и перестраивает страницу, что означает, что флаттер снова создаст FutureBuilder как виджет, не вызывая снова только процесс построения будущего. Процесс вызова происходит только один раз, но процесс построения происходит в каждом setState. если я правильно думаю, значит такое поведение нормально

Abdullatif Eida 24.11.2022 11:26

Передача потомка context в FocusScope.of не вызовет build(), я думаю, потому что диспетчер фокуса удаляет дочерний элемент для этого родителя (FutureBuilder) и переназначает его на основе текущего контекста, в данном случае build() контекста, поэтому futurebuilder необходимо перестроить.

Widget build(BuildContext context) {
    print("[HOME] HOME SCREEN BUILD CALLED: $hashCode");
    return FutureBuilder(
      future: myFuture,
      builder: (context, snapshot) {
        print("[HOME] HOME SCREEN FUTURE BUILDER CALLED WITH STATE ${snapshot.connectionState}: $hashCode");
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Scaffold(
            body: Center(
              child: CircularProgressIndicator(),
            ),
          );
        }
        //make StatefulBuilder as parent will prevent it
        return StatefulBuilder(
          builder: (context, setState) {
            return GestureDetector(
              onTapUp: (details) {
                // hide the keyboard if it's showing
                FocusScopeNode currentFocus = FocusScope.of(context);
                if (!currentFocus.hasPrimaryFocus) {
                  currentFocus.unfocus();
                }
                // FocusManager.instance.primaryFocus?.unfocus();
              },
              child: const Scaffold(
                body: Center(
                  child: Padding(
                    padding: EdgeInsets.symmetric(horizontal: 32.0),
                    child: TextField(),
                  ),
                ),
              ),
            );
          }
        );
      },
    );
  }

Чтобы доказать это, я пытаюсь деформировать родителя (FutureBuilder) с помощью другого строителя:

return LayoutBuilder(
      builder: (context, box) {
        print('Rebuild');
        return FutureBuilder(
          future: myFuture,
          builder: (context, snapshot) {
            print("[HOME] HOME SCREEN FUTURE BUILDER CALLED WITH STATE ${snapshot.connectionState}: $hashCode");
            if (snapshot.connectionState == ConnectionState.waiting) {
              return const Scaffold(
                body: Center(
                  child: CircularProgressIndicator(),
                ),
              );
            }

            return GestureDetector(
              onTapUp: (details) {
                // hide the keyboard if it's showing
                FocusScopeNode currentFocus = FocusScope.of(context);
                if (!currentFocus.hasPrimaryFocus) {
                  currentFocus.unfocus();
                }
                // FocusManager.instance.primaryFocus?.unfocus();
              },
              child: const Scaffold(
                body: Center(
                  child: Padding(
                    padding: EdgeInsets.symmetric(horizontal: 32.0),
                    child: TextField(),
                  ),
                ),
              ),
            );


          },
        );
      }
    );

Build() метод не вызывается повторно, потому что менеджер focusScope только перестраивает контекст из FutureBuilder (родительский)

Ответ принят как подходящий

Спасибо за полный воспроизводимый пример.

Print утверждения внутри builder метода вашего FutureBuilder, вероятно, вводят вас в заблуждение относительно неправильного «виновника».

Ключевая «проблема» возникает из этой строки:

FocusScopeNode currentFocus = FocusScope.of(context);

Если вы не знали, .of статические методы Flutter предоставляют API InheritedWidget. По соглашению, в методе .of вы обычно можете найти вызов dependOnInheritedWidgetOfExactType, который предназначен для регистрации вызывающей стороны, то есть дочерних Widget, как зависимости, то есть Widget, который зависит и реагирует на изменения InheritedWidget этого типа.

Короче говоря, размещение .of внутри метода build предназначено для запуска перестроений вашего Widget: он активно зарегистрирован для прослушивания изменений!

В вашем коде метод FutureBuilder builder регистрируется как зависимый от FocusScope.of и будет перестроен, если FocusScope изменится. И да, это происходит всякий раз, когда мы меняем фокус. В самом деле, вы даже можете переместить эти несколько строк вверх (снаружи GestureDetector, непосредственно в области builder), и вы получите еще больше перестроек (4: одна для первого изменения фокуса, затем другие последующие, вызванные сдвигом фокуса, вызванным такими перестраивает).

Одним из быстрых решений может быть прямой поиск связанных InheritedWidget этих API-интерфейсов, а затем вместо простого .of вы должны вызвать:

context.getElementForInheritedWidgetOfExactType<T>();

РЕДАКТИРОВАТЬ. Я просто искал T в вашем случае использования. К сожалению, оказывается, что это класс _FocusMarker extends InheritedWidget, который является закрытым классом, и поэтому его нельзя использовать вне своего файла/пакета. Я не уверен, почему они разработали API таким образом, но я не знаком с FocusNodes.

Альтернативным подходом было бы просто изолировать детей для вашего FutureBuilder, например так:

builder: (context, snapshot) {
  print("[HOME] HOME SCREEN FUTURE BUILDER CALLED WITH STATE ${snapshot.connectionState}: $hashCode");
  // ...
  return Something();
}

Где Something — это просто рефакторинг StatelessWidget, который содержит пользовательский интерфейс, который вы там показали. Это перестроит только Something, а не весь builder метод, если это вас беспокоит.

Если вы хотите углубить «как» и «почему» InheritedWidget, обязательно сначала посмотрите это видео , чтобы правильно понять, что такое InheritedWidget. Затем, если вы хотите понять, как использовать didChangeDependencies, посмотрите это другое видео, и все будет хорошо.

Я полностью избавился от того, что вызов методов .of регистрирует виджет для перестроения. Спасибо за объяснение!

divillysausages 25.11.2022 15:28

Методы @divillysausages .of регистрируют виджет для перестроения только в том случае, если вызов используется непосредственно внутри метода build(), а не где-либо еще.

CopsOnRoad 25.11.2022 15:59

@CopsOnRoad, вы говорите мне, что вызов .of внутри didChangeDependencies не вызовет вызовы build? Насколько мне известно, это так. didChangeDependencies используется для кэширования значений вне build (это может быть полезно, когда build вызывается несколько раз, независимо от .of)... но это все равно считается зависимостью.

venir 25.11.2022 16:34

@venir Нет, я не это имел в виду. В коде OP использует Focus.of(context) в обратном вызове, который не находится ни внутри build, ни didChangeDependencies, и поэтому сборка не должна запускаться им.

CopsOnRoad 25.11.2022 16:45

@CopsOnRoad в вашем случае метод builder() не считается методом build()?

divillysausages 25.11.2022 16:46

@divillysausages builder() — это build(), но ваш FocusScope.of(context) не был внутри build или builder, он был внутри onTap, что является обратным вызовом. Таким образом, использование FocusScope.of(context) внутри обратного вызова не настроит прослушиватель. Вы можете прочитать мой ответ, чтобы лучше понять, почему build() не вызывали.

CopsOnRoad 25.11.2022 17:13

@CopsOnRoad Я не понимаю. Он использует внутренний context внутри этого обратного вызова, поэтому builder регистрируется как зависимость. Как на это влияет местоположение?

venir 25.11.2022 18:03

Вы должны понимать роль BuildContext.

Пример-1:

Я использую context, переданный методу Widget.build(), и делаю

FocusScope.of(context).unfocus();

Вызовет как build(), так и builder() метод, потому что вы говорите Flutter убрать фокус с любого виджета в context, и поэтому вызывается Widget.build(), который далее вызывает метод Builder.builder().

// Example-1
@override
Widget build(BuildContext context) {
  print("Widget.build()");

  return Builder(builder: (context2) {
    print('Builder.builder()');
    return GestureDetector(
      onTap: () => FocusScope.of(context).unfocus(), // <-- Using `context`
      child: Scaffold(
        body: Center(
          child: TextField(),
        ),
      ),
    );
  });
}

Пример-2:

Я использую context2, переданный методу Builder.builder(), и делаю

FocusScope.of(context2).unfocus();

Будет вызывать только метод builder(), потому что вы говорите Flutter убрать фокус с любого виджета внутри context2, и поэтому вызывается Builder.builder().

// Example-2
@override
Widget build(BuildContext context) {
  print("Widget.build()");

  return Builder(builder: (context2) {
    print('Builder.builder()');
    return GestureDetector(
      onTap: () => FocusScope.of(context2).unfocus(), // <-- Using `context2`
      child: Scaffold(
        body: Center(
          child: TextField(),
        ),
      ),
    );
  });
}

Чтобы ответить на ваш вопрос, если вы замените

builder: (context, snapshot) { ...}

С

builder: (_, snapshot) { }

Тогда ваш build() также будет вызван.

  • Разница возникла из-за того, что context, который вы используете, является родителем контекст (из метода будущего строителя). Просто оберните GestureDetector с помощью Builder, тогда результат будет таким же, как и во втором способе.

         return Builder(builder: (_context) {
           return GestureDetector(
             onTapUp: () {
               // hide the keyboard if it's showing
               final currentFocus = FocusScope.of(_context);
               if (!currentFocus.hasPrimaryFocus) {
                 currentFocus.unfocus();
               },
             } ...
    
  • При попытке отключить клавиатуру мы должны использовать второй способ FocusManager.instance.primaryFocus?.unfocus(); как обсуждение в официальном выпуске здесь: https://github.com/flutter/flutter/issues/20227#issuecomment-512860882 https://github.com/flutter/flutter/issues/19552

Другие вопросы по теме