Правильная последовательность виджетов Flutter для извлечения данных при загрузке приложения

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

У меня есть унаследованный виджет, содержащий информацию для аутентификации текущего пользователя. Когда приложение загружается, я хочу посмотреть в локальное хранилище токены сеанса. Если токены сеанса существуют, я хотел бы обновить унаследованный виджет этой информацией.

Мои экраны динамические. Если он знает, что пользователь аутентифицирован, он переводит их на запрошенный экран, в противном случае - на экран регистрации.

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

Как я могу читать данные из локального хранилища, когда приложение загружает и обновляет унаследованный виджет?

Ошибка при запуске приложения:

flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
flutter: The following assertion was thrown building _InheritedAuthContainer:
flutter: inheritFromWidgetOfExactType(_InheritedAuthContainer) or inheritFromElement() was called before
flutter: RootState.initState() completed.
flutter: When an inherited widget changes, for example if the value of Theme.of() changes, its dependent
flutter: widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor
flutter: or an initState() method, then the rebuilt dependent widget will not reflect the changes in the
flutter: inherited widget.
flutter: Typically references to inherited widgets should occur in widget build() methods. Alternatively,
flutter: initialization based on inherited widgets can be placed in the didChangeDependencies method, which
flutter: is called after initState and whenever the dependencies change thereafter.

Виджет маршрутизатора (корень)

class Root extends StatefulWidget {
  @override
  State createState() => RootState();
}

class RootState extends State<Root> {
  static Map<String, Widget> routeTable = {Constants.HOME: Home()};
  bool loaded = false;
  bool authenticated = false;

  @override
  void initState() {
    super.initState();
    if (!loaded) {
      AuthContainerState data = AuthContainer.of(context);
      data.isAuthenticated().then((authenticated) {
        setState(() {
          authenticated = authenticated;
          loaded = true;
        });
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        initialRoute: '/',
        onGenerateRoute: (routeSettings) {
          WidgetBuilder screen;
          if (loaded) {
            if (authenticated) {
              screen = (context) => SafeArea(
                  child: Material(
                      type: MaterialType.transparency,
                      child: routeTable[routeSettings.name]));
            } else {
              screen = (conext) => SafeArea(
                  child: Material(
                      type: MaterialType.transparency, child: Register()));
            }
          } else {
            screen = (context) => new Container();
          }
          return new MaterialPageRoute(
            builder: screen,
            settings: routeSettings,
          );
        });
  }
}

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

Future<bool> isAuthenticated() async {
    if (user == null) {
      final storage = new FlutterSecureStorage();
      List results = await Future.wait([
        storage.read(key: 'idToken'),
        storage.read(key: 'accessToken'), 
        storage.read(key: 'refreshToken'),
        storage.read(key: 'firstName'),
        storage.read(key: 'lastName'),
        storage.read(key: 'email')
      ]);
      if (results != null && results[0] != null && results[1] != null && results[2] != null) {
        //triggers a set state on this widget
        updateUserInfo(
          identityToken: results[0], 
          accessToken: results[1], 
          refreshToken: results[2],
          firstName: results[3],
          lastName: results[4],
          email: results[5]
        );
      }
    }
    return user != null && (JWT.isActive(user.identityToken) || JWT.isActive(user.refreshToken));
  }

Главный

void main() => runApp(
  EnvironmentContainer(
    baseUrl: DEV_API_BASE_URL,
    child: AuthContainer(
      child: Root()
    )
  )
);

Как правильно проверять локальное хранилище при загрузке приложения и обновлять унаследованный виджет, содержащий эту информацию?


person TemporaryFix    schedule 09.03.2019    source источник
comment
Я думаю, ваш initState код следует переместить в didChangeDependencies. Этот метод, например, может быть повторно запущен при обновлении AuthContainer.   -  person Günter Zöchbauer    schedule 18.03.2019


Ответы (3)


Фактически вы не можете получить доступ к InheritedWidget из initState метода. Вместо этого попробуйте получить к нему доступ из didChangeDependencies.

Пример:

@override
void didChangeDependencies() {
  super.didChangeDependencies();
  if (!loaded) {
    AuthContainerState data = AuthContainer.of(context);
    data.isAuthenticated().then((authenticated) {
      setState(() {
        authenticated = authenticated;
        loaded = true;
      });
    });
  }
}

Другой способ - запланировать выборку данных в initState с помощью SchedulerBinding. Вы можете найти документы здесь

SchedulerBinding.instance.addPostFrameCallback((_) {
  // your login goes here
});

Примечание: помните, что didChangeDependencies будет вызываться всякий раз, когда изменяется состояние или зависимости любого родительского InheritedWidget. См. Документы здесь.

Надеюсь это поможет!

person Hemanth Raj    schedule 18.03.2019
comment
Правильный ли способ получать вызовы API в didChangeDependencies, проверяя, загружен ли класс в первый раз? - person Febin K R; 03.01.2020
comment
Не обычно, но здесь сценарий таков, что доступ к контексту необходимо получить одним из двух вышеупомянутых способов. - person Hemanth Raj; 03.01.2020

Хотя ответ @hemanth-raj правильный, я бы на самом деле рекомендовал немного другой способ сделать это. Вместо того, чтобы создавать AuthContainer без данных, вы могли бы фактически выполнить загрузку пользовательского сеанса, прежде чем создавать свои виджеты и передавать данные напрямую. В этом примере используется подключаемый модуль scoped_model, чтобы абстрагироваться от унаследованного шаблона виджета (который я настоятельно рекомендую вместо написания унаследованные виджеты вручную!), но в остальном очень похоже на то, что вы сделали.

Future startUp() async {
  UserModel userModel = await loadUser();
  runApp(
    ScopedModel<UserModel>(
      model: userModel,
      child: ....
    ),
  );
}

void main() {
  startup();
}

Это более или менее то, что я делаю в своем приложении, и у меня не было никаких проблем с ним (хотя вы, вероятно, захотите добавить некоторую обработку ошибок, если есть вероятность сбоя loadUser)!

Это должно сделать ваш код userState намного чище =).

И, к вашему сведению, то, что я сделал в своей пользовательской модели, - это bool get loggedIn => ..., который знает, какая информация должна быть там, чтобы определить, вошел ли пользователь в систему или нет. Таким образом, мне не нужно отслеживать его отдельно, но я все равно получаю простой простой способ отличить от модели.

person rmtmckenzie    schedule 21.03.2019
comment
Что, если я хочу сделать сетевой вызов до загрузки приложения, например, получить конфигурацию темы - person Arul Mani; 20.04.2021
comment
Вы можете просто заменить функцию loadUser на loadThemeConfiguration или что-то еще, что вам нужно сделать. Однако вы можете подумать о том, есть ли у них подключение к Интернету или медленная сеть, и как это может повлиять на время запуска. - person rmtmckenzie; 21.04.2021

Сделайте это, как пример, приведенный в this:

void addPostFrameCallback(FrameCallback callback) {
  // Login logic code 
}

Запланируйте обратный вызов для конца этого кадра.

Не запрашивает новый кадр.

Этот обратный вызов выполняется во время кадра сразу после постоянных обратных вызовов кадра (когда основной конвейер рендеринга был сброшен). Если кадр выполняется, а обратные вызовы после кадра еще не выполнены, зарегистрированный обратный вызов все еще выполняется во время кадра. В противном случае зарегистрированный обратный вызов выполняется в следующем кадре.

Обратные вызовы выполняются в том порядке, в котором они были добавлены.

Обратные вызовы после кадра не могут быть отменены. Вызываются они ровно один раз.

person Hank Moody    schedule 24.03.2019