MMDev
MMDev

Reputation: 21

Not sure how to properly implement ChangeNotifierProvider for remembering logged in User in Flutter

I've got an app with working user registration, and using a user provider am able to set the user during registration and retrieve it on the following screen without issue. What I'm having trouble with is trying to properly implement the ability to set the stored currently logged in user on app launch and proceed to the dashboard rather than going through the registration process without throwing the below assertion.

I can get the user from the SharedPreferences no problem however I'm not sure how to properly set this user in my provider so that it can be accessed when the dashboard screen loads. Below is my main() function and MyApp class.

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(),
        });
  }
}

In the FutureBuilder I tried chaining a then on the getUserData() call to set the user however this of course returns the following assertion:

════════ 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.

From what I've read this is because there is a call to notifyListeners() in the user provider which will call markNeedsBuild(). That said, I've tried moving things around and several suggestions online and can't figure out what the best practice and best way to implement this is.

User Provider:

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

  User get user => _user;

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

How can I properly set the user when the app is loading up if someone is already logged in without running into the above assertion? Thanks!

Upvotes: 2

Views: 839

Answers (2)

Marcel Dz
Marcel Dz

Reputation: 2714

I can provide you an example from my app:

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()),

The above code is in my main.dart. The lines I marked are important. You can check if a user has an auth state to force him directly into your app home screen. If there isn't a authState available you can still force him to your homescreen by having an autologin function. Thats how I get people in the app while they only logged in once.

This is the autologin function in my auth file (im using 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;
  }

Im checking if the user data from SharedPreferences is valid, im also checking if the email is verified, since i only want users who confirmed their email adress (You can remove this part if you don't need it) at the end im checking the session from the user and if the expirydate is reached, im refreshing the session.

Follow this topic to refresh your IDToken using the refresh token: https://firebase.google.com/docs/reference/rest/auth#section-refresh-token

I had alot of trouble refreshing the token since I dindt use the firebase library rather made everything with http calls. I learned alot from it basically and had an own topic regarding to login and refresh user sessions. If you want to know more about it check one of my old questions:

flutter firebase auto refresh user session with refreshToken

The last link seems to be the most easy way to realize refreshing a session I would say. Working with the official flutterfire librarys, heres the example from the documentation how to refresh it:

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

PS: If inside the function some conditions aren't true for example session expired, data wrong e.g. writ a logout function to force users logging in again. Also dont forget to add the new values (expiry date after refresh and tokens) to your Shared Preferences since youre getting the values from it all the time.

Upvotes: 0

ishak Akdaş
ishak Akdaş

Reputation: 53

Shared preference can be used as follows:

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);
}

Upvotes: 1

Related Questions