Установка значения провайдера в FutureBuilder
У меня есть виджет, который делает запрос к API, который возвращает карту. Я хотел бы не делать один и тот же запрос каждый раз при загрузке виджета и сохранять список в appState.myList
но когда я делаю это appState.myList = snapshot.data;
в FutureBuilder
Я получаю следующую ошибку:
flutter: ══╡ EXCEPTION CAUGHT BY FOUNDATION LIBRARY ╞════════════════════════════════════════════════════════
flutter: The following assertion was thrown while dispatching notifications for MySchedule:
flutter: setState() or markNeedsBuild() called during build.
flutter: This ChangeNotifierProvider<MySchedule> widget cannot be marked as needing to build because the
flutter: framework is already in the process of building widgets. A widget can be marked as needing to be
flutter: built during the build phase only if one of its ancestors is currently building. ...
файл sun.dart
class Sun extends StatelessWidget {
Widget build(BuildContext context) {
final appState = Provider.of<MySchedule>(context);
var db = PostDB();
Widget listBuild(appState) {
final list = appState.myList;
return ListView.builder(
itemCount: list.length,
itemBuilder: (context, index) {
return ListTile(title: Text(list[index].title));
},
);
}
Widget futureBuild(appState) {
return FutureBuilder(
future: db.getPosts(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
// appState.myList = snapshot.data;
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return ListTile(title: Text(snapshot.data[index].title));
},
);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
return Center(
child: CircularProgressIndicator(),
);
},
);
}
return Scaffold(
body: appState.myList != null
? listBuild(appState)
: futureBuild(appState));
}
}
файл postService.dart
class PostDB {
var isLoading = false;
Future<List<Postmodel>> getPosts() async {
isLoading = true;
final response =
await http.get("https://jsonplaceholder.typicode.com/posts");
if (response.statusCode == 200) {
isLoading = false;
return (json.decode(response.body) as List)
.map((data) => Postmodel.fromJson(data))
.toList();
} else {
throw Exception('Failed to load posts');
}
}
}
Я понимаю что myList
звонки notifyListeners()
и это то, что вызывает ошибку. Надеюсь, я понял это правильно. Если да, то как мне установить appState.myList
и использовать в приложении, не получая вышеуказанную ошибку?
import 'package:flutter/foundation.dart';
import 'package:myflutter/models/post-model.dart';
class MySchedule with ChangeNotifier {
List<Postmodel> _myList;
List<Postmodel> get myList => _myList;
set myList(List<Postmodel> newValue) {
_myList = newValue;
notifyListeners();
}
}
5 ответов
Это исключение возникает потому, что вы синхронно модифицируете виджет от его потомков.
Это плохо, так как это может привести к несовместимому дереву виджетов. Какой-то виджет. могут быть построены виджеты, использующие значение до мутации, в то время как другие могут использовать мутировавшее значение.
Решение состоит в том, чтобы удалить несоответствие. С помощью ChangeNotifierProvider
обычно есть 2 сценария:
- Мутация выполнена на вашем
ChangeNotifier
всегда делается в рамках одной сборки, чем тот, который создал вашChangeNotifier
,
В этом случае вы можете просто сделать вызов прямо из конструктора вашего ChangeNotifier
:
class MyNotifier with ChangeNotifier {
MyNotifier() {
// TODO: start some request
}
}
- Произведенное изменение может произойти "лениво" (обычно после смены страницы).
В этом случае вы должны обернуть свою мутацию в addPostFrameCallback
или Future.microtask
:
class Example extends StatefulWidget {
@override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
MyNotifier notifier;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final notifier = Provider.of<MyNotifier>(context);
if (this.notifier != notifier) {
this.notifier = notifier;
Future.microtask(() => notifier.doSomeHttpCall());
}
}
@override
Widget build(BuildContext context) {
return Container();
}
}
Я столкнулся с проблемой, аналогичной вашей при использовании провайдера. Мое решение состоит в том, чтобы добавить WidgetsBinding.instance.addPostFrameCallback()
при получении данных.
Просто удалите
notifyListeners();
из кода. Я столкнулся с этой ошибкой, и я решил ее устранить.
Существует еще один способ справиться с этим, используяStreamSubscription
наFuture
. Это потребует больше стандартного кода, но может помочь убедиться, что остальная логика может быть разделена на элементы для создания виджетов, чтобы предотвратить побочные эффекты.
Future? _dataFuture;
StreamSubscription? _dataSubscription;
FutureBuilder(
future: _dataFuture,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
// removed data updating logic from here
// context.read<DataProvider>().updateData(newData: snapshot.data);
case ConnectionState.done:
return Text(
snapshot.data,
style: Theme.of(context).textTheme.headline5,
);
},
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _getData(),
child: const Icon(Icons.adb),
),
_getData() {
_dataSubscription?.cancel();
setState(() {
_dataFuture = Util.getText();
_dataSubscription = _dataFuture?.asStream().listen((data) {
if (!mounted) return;
context.read<DataProvider>().updateData(newData: data);
});
});
}
Ознакомьтесь с полным примером кода для этого отсюда .
Если вы не полагаетесь на использование MySchedule
(Я не вижу причины для этого из того, что вы предоставили), вы можете просто использовать AsyncMemoizer
из async
пакет (из стандартной библиотеки), который запускается в будущем только один раз, а не каждый раз, когда виджет перестраивается.
В вашей StatelessWidget
Вы можете сохранить свой памятник как final
переменная, а затем использовать runOnce
,
import 'package:async/async.dart';
...
final AsyncMemoizer<List<Postmodel>> memoizer = AsyncMemoizer();
Вы бы также избавились от своего условного build
функция, которую вы не можете вызвать setState
(что происходит, когда вы звоните notifyListeners
на прослушиваемом, который слушает строитель) из build
функция. Независимо от этого, он также не нужен и, скорее всего, плохой дизайн. Кроме того, как указал Русселет, StatelessWidget
потерял бы состояние и, таким образом, перезагрузить данные в любом случае, как memoizer
также создается снова. Я изначально хотел просто поделиться основной точкой AsyncMemoizer
Однако нет никаких причин для этого. Следовательно, вы должны использовать StatefulWidget
в этом случае.
class _SunState extends State<Sun> {
final AsyncMemoizer<List<Postmodel>> memoizer;
final PostDB db;
@override
void initState() {
memoizer = AsyncMemoizer();
db = PostDB();
super.initState();
}
Future<List<Postmodel>> getPosts() => db.getPosts();
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: memoizer.runOnce(getPosts),
builder: (BuildContext context, AsyncSnapshot<List<Postmodel>> snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text(snapshot.data[index].title));
});
}
if (snapshot.hasError) return Text('${snapshot.error}');
return Center(child: const CircularProgressIndicator());
},
));
}
}
Поскольку вы хотите использовать свой список для нескольких виджетов, я бы посоветовал использовать Completer
внутри вашего PostDB
класс и хранение экземпляра этого класса внутри вашего провайдера.
Возможно, вы захотите настроить точную реализацию следующего кода, однако это должно дать вам представление о том, как добиться того, чего вы хотите.
class PostDB {
Completer<List<Postmodel>> _completer;
/// Return `null` if [getPosts] has not yet been called.
bool get isLoading => _completer == null ? null : !_completer.isCompleted;
/// This will allow you to call [getPosts] multiple times
/// by supplying [reload], which will reassign [_completer].
Future<List<Postmodel>> getPosts([bool reload = false]) {
if (_completer == null || reload) {
_completer = Completer();
_completer.complete(_getPosts());
}
return _completer.future;
}
Future<List<Postmodel>> _getPosts() async {
final response = await http.get("https://jsonplaceholder.typicode.com/posts");
if (response.statusCode == 200) {
return (json.decode(response.body) as List).map((data) => Postmodel.fromJson(data)).toList();
} else {
throw Exception('Failed to load posts');
}
}
}
class MySchedule {
PostDB db;
}
class Sun extends StatelessWidget {
@override
Widget build(BuildContext context) {
final appState = Provider.of<MySchedule>(context);
return Scaffold(
body: FutureBuilder(
future: appState.db.getPosts(),
builder: (BuildContext context, AsyncSnapshot<List<Postmodel>> snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text(snapshot.data[index].title));
});
}
if (snapshot.hasError) return Text('${snapshot.error}');
return Center(child: const CircularProgressIndicator());
},
));
}
}
Completer
позволяет вернуть Future
даже если запрос уже выполнен, вызов функции одинаков для первого и последующих вызовов (просто данные загружаются только один раз).
Этот код настроен так, чтобы лучше соответствовать вашим требованиям, однако он все равно кажется тем, что я никогда бы не включил в свое приложение. Вероятно, у вас уже достаточно информации, и это работает, поэтому этого должно быть достаточно. Любые другие изменения должны быть сделаны самостоятельно, потому что только вы знаете свое заявление.