Установка значения провайдера в 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 даже если запрос уже выполнен, вызов функции одинаков для первого и последующих вызовов (просто данные загружаются только один раз).

Этот код настроен так, чтобы лучше соответствовать вашим требованиям, однако он все равно кажется тем, что я никогда бы не включил в свое приложение. Вероятно, у вас уже достаточно информации, и это работает, поэтому этого должно быть достаточно. Любые другие изменения должны быть сделаны самостоятельно, потому что только вы знаете свое заявление.

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