У меня проблема с тем, что мой виджет несколько раз запускает код 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
Я ответил ниже. Это помогает?
Пожалуйста, попробуйте это решение /// provider package up super.initState();
Ваш код будет таким
@override
void initState() {
/// provider package
final homeService = context.read<HomeService>();
myFuture = _memoizer.runOnce(homeService.getMyData);
super.initState();
}
Пожалуйста, после того, как попробуете, сообщите мне результат
К сожалению, это не изменило ситуацию
У вас есть setState
Я обновил вопрос воспроизводимым примером - похоже, он связан с тем, как фокус удаляется с клавиатуры.
Ваша проблема в том, что будущий строитель работает дважды: первый ждет, а второй находится в состоянии готовности - или ваша проблема при нажатии на текстовое поле, строитель снова работает
В соответствии с тем, что я вижу, когда вы нажимаете на текстовое поле, флаттер обновляет состояние и перестраивает страницу, что означает, что флаттер снова создаст FutureBuilder как виджет, не вызывая снова только процесс построения будущего. Процесс вызова происходит только один раз, но процесс построения происходит в каждом setState. если я правильно думаю, значит такое поведение нормально
Передача потомка 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 .of регистрируют виджет для перестроения только в том случае, если вызов используется непосредственно внутри метода build(), а не где-либо еще.
@CopsOnRoad, вы говорите мне, что вызов .of внутри didChangeDependencies не вызовет вызовы build? Насколько мне известно, это так. didChangeDependencies используется для кэширования значений вне build (это может быть полезно, когда build вызывается несколько раз, независимо от .of)... но это все равно считается зависимостью.
@venir Нет, я не это имел в виду. В коде OP использует Focus.of(context) в обратном вызове, который не находится ни внутри build, ни didChangeDependencies, и поэтому сборка не должна запускаться им.
@CopsOnRoad в вашем случае метод builder() не считается методом build()?
@divillysausages builder() — это build(), но ваш FocusScope.of(context) не был внутри build или builder, он был внутри onTap, что является обратным вызовом. Таким образом, использование FocusScope.of(context) внутри обратного вызова не настроит прослушиватель. Вы можете прочитать мой ответ, чтобы лучше понять, почему build() не вызывали.
@CopsOnRoad Я не понимаю. Он использует внутренний context внутри этого обратного вызова, поэтому builder регистрируется как зависимость. Как на это влияет местоположение?
Вы должны понимать роль BuildContext.
Я использую 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(),
),
),
);
});
}
Я использую 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
Я не могу воспроизвести это, не могли бы вы поподробнее рассказать об этой «ошибке», о которой вы говорите? Не могли бы вы опубликовать полный воспроизводимый пример?