不确定如何正确实现 ChangeNotifierProvider 以记住 Flutter 中的登录用户

Posted

技术标签:

【中文标题】不确定如何正确实现 ChangeNotifierProvider 以记住 Flutter 中的登录用户【英文标题】:Not sure how to properly implement ChangeNotifierProvider for remembering logged in User in Flutter 【发布时间】:2021-04-02 17:57:44 【问题描述】:

我有一个可以正常工作的用户注册的应用程序,并且使用用户提供程序可以在注册期间设置用户并在以下屏幕上检索它而不会出现问题。我遇到的问题是尝试正确实现在应用启动时设置存储的当前登录用户并继续到仪表板的功能,而不是在不抛出以下断言的情况下完成注册过程。

我可以从SharedPreferences 获取用户没问题,但是我不确定如何在我的提供程序中正确设置此用户,以便在仪表板屏幕加载时可以访问它。下面是我的main() 函数和 MyApp 类。

void main() 
  WidgetsFlutterBinding.ensureInitialized();

  runApp(MultiProvider(
    providers: [
      ChangeNotifierProvider(create: (context) => AuthProvider()),
      ChangeNotifierProvider(create: (context) => UserProvider()),
    ],
    child: MyApp(),
  ));


class MyApp extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    Future<User> getUserData() => UserPreferences().getUser();

    return MaterialApp(
        title: 'My App',
        theme: ThemeData(
          primarySwatch: Colors.red,
          accentColor: Colors.black,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: FutureBuilder(
            future: getUserData().then((value) => 
                  Provider.of<UserProvider>(context, listen: false)
                      .setUser(value)
                ),
            builder: (context, snapshot) 
              switch (snapshot.connectionState) 
                case ConnectionState.none:
                case ConnectionState.waiting:
                  return CircularProgressIndicator();
                default:
                  if (snapshot.hasError)
                    return Text('Error: $snapshot.error');
                  else if (snapshot.data.token == null)
                    return Login();
                  else if (snapshot.data.token != null) 
                    return Dashboard();
                   else 
                    UserPreferences().removeUser();
                    return Login();
                  
              
            ),
        routes: 
          '/dashboard': (context) => Dashboard(),
          '/login': (context) => Login(),
          '/register': (context) => Register(),
          '/username': (context) => Username(),
        );
  

在 FutureBuilder 中,我尝试在 getUserData() 调用上链接一个 then 来设置用户,但这当然会返回以下断言:

════════ Exception caught by foundation library ════════════════════════════════
The following assertion was thrown while dispatching notifications for UserProvider:
setState() or markNeedsBuild() called during build.

This _InheritedProviderScope<UserProvider> widget cannot be marked as needing to build because the framework is already in the process of building widgets.  A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.

根据我的阅读,这是因为在用户提供程序中调用了notifyListeners(),它将调用markNeedsBuild()。也就是说,我已经尝试在网上移动一些东西和一些建议,但无法弄清楚最佳实践和最佳实施方式是什么。

用户提供者:

class UserProvider with ChangeNotifier 
  User _user = new User();

  User get user => _user;

  void setUser(User user) 
    _user = user;
    notifyListeners();
  

如果有人已经登录而没有遇到上述断言,我如何在应用程序加载时正确设置用户?谢谢!

【问题讨论】:

澄清一下,我遇到的问题是当我在 FutureBuilder 中使用我的 UserProvider 调用 setUser 时。我只是不确定动态设置此用户的正确方法,以便我可以在加载用户 user = Provider.of(context).user; 的屏幕中检索它 【参考方案1】:

共享偏好可以如下使用:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() 
  runApp(MaterialApp(
    home: Scaffold(
      body: Center(
      child: RaisedButton(
        onPressed: _incrementCounter,
        child: Text('Increment Counter'),
        ),
      ),
    ),
  ));


_incrementCounter() async 
  SharedPreferences prefs = await SharedPreferences.getInstance();
  int counter = (prefs.getInt('counter') ?? 0) + 1;
  print('Pressed $counter times.');
  await prefs.setInt('counter', counter);

【讨论】:

对不起,如果我不清楚,共享首选项部分对我来说很好用。问题在于在提供程序中设置用户,因为它引发了上述断言。【参考方案2】:

我可以为您提供我的应用程序中的示例:

home: initScreen == 3 || auth.onBoarding == true
                      ? OnboardingScreen()
                      : auth.isAuth <------ checks if auth is available
                          ? TabsScreen()
                          : FutureBuilder(
                              future: auth.tryAutoLogin(), <--- autologin
                              builder: (ctx, authResultSnapshot) =>
                                  authResultSnapshot.connectionState ==
                                          ConnectionState.waiting
                                      ? SplashScreen()
                                      : AuthScreen()),

以上代码在我的 main.dart 中。我标记的线条很重要。您可以检查用户是否具有身份验证状态以强制他直接进入您的应用主屏幕。如果没有可用的 authState,您仍然可以通过自动登录功能强制他进入您的主屏幕。这就是我如何让人们在他们只登录一次的情况下进入应用程序。

这是我的身份验证文件中的自动登录功能(我使用的是 firebase):

Future<bool> tryAutoLogin() async 

    final prefs = await SharedPreferences.getInstance();
    if (!prefs.containsKey('userData')) 
      return false;
    
    final extractedUserData =
        json.decode(prefs.getString('userData')) as Map<String, Object>;
    final expiryDate = DateTime.parse(extractedUserData['expiryDate']);
    final emVerified = extractedUserData['emailVerified'];

     print('emVerified: $emVerified');

    if (emVerified == false || emVerified == null) 
      print('@@@ user email is not verified, logging out user');
      logout();
    

    if (expiryDate.isBefore(DateTime.now())) 
      refreshSession();
    

    notifyListeners();
    return true;
  

我正在检查来自SharedPreferences 的用户数据是否有效,我还要检查电子邮件是否经过验证,因为我只想要确认其电子邮件地址的用户(如果不需要,您可以删除这部分)最后我从用户那里检查会话,如果到期,我会刷新会话。

按照本主题使用刷新令牌刷新您的 IDToken:https://firebase.google.com/docs/reference/rest/auth#section-refresh-token

我在刷新令牌时遇到了很多麻烦,因为我没有使用 firebase 库,而是使用 http 调用来完成所有事情。我基本上从中学到了很多,并且有一个关于登录和刷新用户会话的自己的主题。如果您想了解更多信息,请查看我的一个老问题:

flutter firebase auto refresh user session with refreshToken

最后一个链接似乎是实现刷新会话的最简单方法。使用官方的flutterfire库,这里是文档中的示例如何刷新它:

https://firebase.flutter.dev/docs/auth/usage/#reauthenticating-a-user

PS:如果函数内部某些条件不成立,例如会话过期,数据错误,例如写一个注销函数来强制用户重新登录。另外不要忘记将新值(刷新和令牌后的到期日期)添加到您的Shared Preferences,因为您一直都从中获取值。

【讨论】:

我没有使用 firebase 来实现。您在 main.dart 文件中使用的身份验证提供程序是如何声明的?我的麻烦是当我去使用我的 UserProvider 来设置我从我的共享首选项中获得的用户数据时。然后我得到我发布的断言。 首先你必须修复你未来的建造者。您在那里定义的未来需要在您的构建之外,这非常重要。您必须在构建小部件之前在 initstate 中调用它。您可以简单地为它创建一个函数。在构建中调用函数会强制您的应用程序状态多次重建。根据提供者和您的错误,它一直在接收新状态

以上是关于不确定如何正确实现 ChangeNotifierProvider 以记住 Flutter 中的登录用户的主要内容,如果未能解决你的问题,请参考以下文章

异步Android SQLite数据库的正确实现

如何确定 ANSI 端子的尺寸?

如何使用 for 循环在决策树上正确实现 bagging?

在使用情节提要时不确定如何正确继承 UIApplication

iOS - 不确定如何正确更新 iOS 7 的 cocoapods 配置

C#WPF - 创建自定义控件。不确定如何正确对齐文本