2

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!

MMDev
  • 21
  • 3
  • To clarify, the issue I'm having is when I call setUser with my UserProvider in the FutureBuilder. I'm just not sure the proper way to set this User on the fly so that I can retrieve it in the screen that loads with User user = Provider.of(context).user; – MMDev Dec 24 '20 at 17:53

2 Answers2

1

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);
}
ishak Akdaş
  • 43
  • 2
  • 9
  • 1
    Sorry if I wasn't clear, the Shared preferences part is working fine for me. The issue is with the setting the user in the provider since it raises the assertion mentioned above. – MMDev Dec 24 '20 at 17:30
0

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.

Dharman
  • 30,962
  • 25
  • 85
  • 135
Marcel Dz
  • 2,321
  • 4
  • 14
  • 49
  • I'm not using firebase for my implementation. How is your auth provider that you use in your main.dart file declared? My trouble is when I go to use my UserProvider to set the user data I got from my shared preferences. I then get the assertion that I posted. – MMDev Dec 24 '20 at 17:43
  • First of all you have to fix your future builder. The future you defined in there needs to be outside your build, this is really important. you have to call it in the initstate before the widget build. You can simply create a function for it. Calling a function in the build forces your app state to rebuild more than one time. according to providers and your error, its receiving a new state all the time – Marcel Dz Dec 25 '20 at 06:30