Anyone used Streambuilder with Flutter and GoRouter before to persist User state?
Currently, I'm using the default Flutter navigation and it's working fine (see below) but I'm trying to replace with GoRouter now
My previous navigation setup using default Flutter navigation
home: StreamBuilder(
stream: AuthMethods().authChanges,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasData) {
return const TabsScreen();
}
return const AuthScreen();
}),
);
}
My current goRouter code. I'm wondering where I can use Streambuilder in d code below to persist user state for my app (or whether Streambuilder can be used at all with go router)
GoRouter routeConstruct() {
return GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const PublicScreen();
},
routes: <RouteBase>[
GoRoute(
path: Events_Screen,
builder: (BuildContext context, GoRouterState state) {
return const EventsScreen();
},
),
GoRoute(
path: "$EventDetail_Screen/:eventId",
builder: (BuildContext context, GoRouterState state) {
return EventDetailScreen(eventId: state.params['eventId']!);
},
),
...
]),
],
);
Can I use Streambuilder with Flutter and GoRouter to persist User authentication state? If can, where in the goRouter code can I use Streambuilder to determine which widget to show based on Auth state? Thanks
###########################
class AuthMethods
class AuthMethods {
final FirebaseAuth _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
Stream<User?> get authChanges => _auth.authStateChanges();
// --> use of bang ! operator here
User get user => _auth.currentUser!;
Future<bool> signInWithGoogle(BuildContext context) async {
bool res = false;
try {
final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
final GoogleSignInAuthentication? googleAuth =
await googleUser?.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth?.accessToken, idToken: googleAuth?.idToken);
UserCredential userCredential =
await _auth.signInWithCredential(credential);
User? user = userCredential.user;
if (user != null) {
if (userCredential.additionalUserInfo!.isNewUser) {
await _firestore.collection('users').doc(user.uid).set({
'username': user.displayName,
'uid': user.uid,
'profilePhoto': user.photoURL,
});
}
res = true;
}
} on FirebaseAuthException catch (e) {
showSnackBar(context, e.message!);
res = false;
}
return res;
}
class RouteNotifier
class RouterNotifier extends ChangeNotifier {
RouterNotifier(this._authMethods) {
_authMethods.authChanges.listen((event) {
notifyListeners();
});
}
final AuthMethods _authMethods;
String? redirect(BuildContext context, GoRouterState state) {
final onLoginPage = state.location == '/$AuthScreen';
final onHomePage = state.location == '/$Tabs_Screen';
// --> The getter 'isEmpty' isn't defined for the type 'User'.
// --> User get user => _auth.currentUser!; used a bang operator
if (_authMethods.user.isEmpty && onHomePage) {
return '/$AuthScreen';
}
// --> The getter 'isEmpty' isn't defined for the type 'User'.
if (_authMethods.currentUser.isNotEmpty && onLoginPage) {
return '/$Tabs_Screen';
}
return null;
}
}
router.dart
GoRouter routeConstruct() {
return GoRouter(
refreshListenable: router,
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const PublicScreen();
},
routes: <RouteBase>[
GoRoute(
path: Events_Screen,
builder: (BuildContext context, GoRouterState state) {
return const EventsScreen();
},
),
GoRoute(
path: "$EventDetail_Screen/:eventId",
builder: (BuildContext context, GoRouterState state) {
return EventDetailScreen(eventId: state.params['eventId']!);
},
),
]),
],
);
}
before go router 5.0.0 :
go router has class called GoRouterRefreshStream, so in go router
GoRouter (
....
refreshListenable: GoRouterRefreshStream(AuthMethods().authChanges),
but after go router 5.0.0, they remove it
so you must create your own notifier :
class RouterNotifier extends ChangeNotifier {
RouterNotifier(this._authMethods) {
_authMethods.authChanges.listen((event) {
notifyListeners();
});
}
final AuthMethods _authMethods;
}
on go Router:
GoRouter(
debugLogDiagnostics: false,
....
...
refreshListenable: RouterNotifier(AuthMethods()),
...
);
and how go router can work like stream builder you use before?
redirect come in handy:
final router = RouterNotifier(AuthMethods());
GoRouter (
...
refreshListenable: router,
redirect: router.redirect,
in RouterNotifier class :
class RouterNotifier extends ChangeNotifier {
RouterNotifier(this._authMethods) {
_authMethods.authChanges.listen((event) {
notifyListeners();
});
}
final AuthMethods _authMethods;
String? redirect(BuildContext context, GoRouterState state) {
final onLoginPage = state.location == '/login';
final onHomePage = state.location == '/';
if (_authMethods.currentUser.isEmpty && onHomePage) {
return '/login';
}
if (_authMethods.currentUser.isNotEmpty && onLoginPage) {
return '/';
}
return null;
}
}
class User model :
class User {
String id;
.........
some variable i need from db;
static const empty = User(id: '');
bool get isEmpty => this == User.empty;
bool get isNotEmpty => this != User.empty;
}
on Stream :
Stream<User> get user {
// map the user if user null is mean empty
return _firebaseAuth.authStateChanges().asyncMap((fUser) async {
if (fUser == null) {
const User user = User.empty;
return user;
} else {
// in my case i need to take some data from Firestore
....
await datafromDb() // if you dont need to wait future, remove async map, just use normal map
return fUser.withBiodata();
Related
I'm trying to implement redirecting to the login/main page when the login state is changed, but when the login state is changed, nothing happens. Here is the code:
return Scaffold(
body: StreamBuilder<AuthState>(
stream: Auth.instance.stateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
assert(snapshot.connectionState != ConnectionState.done);
if (snapshot.data == AuthState.loggedIn) {
return Navigator(
onGenerateRoute: (_) {
return MaterialPageRoute(
builder: (context) => const MainWidget(),
);
}
);
}
if (snapshot.data == AuthState.loggedOut) {
return Navigator(
onGenerateRoute: (_) {
return MaterialPageRoute(
builder: (context) => const LoginWidget(),
);
}
);
}
if (snapshot.data == AuthState.verifyEmail) {
return const EmailNotVerWidget();
}
throw Exception('Unknown Auth State');
}
),
);
Here, Auth.instance.stateChanges() is just a wrapper for the corresponding FirebaseAuth stream with additional logic.
As the result of debugging, it turned out that, when the login state is changed, the corresponding Navigator widget is returned but its onGenerateRoute() method isn't called, however it's called for the first time when the app is loading. I'm new in Flutter.
Upd. Here is the content of Auth and AuthState:
enum AuthState {
loggedIn,
loggedOut,
verifyEmail,
}
class Auth {
static final Auth _instance = Auth();
static Auth get instance => _instance;
UserObject? user;
String getCurUserId() {
return FirebaseAuth.instance.currentUser!.uid;
}
Future<void> login(String email, String password) async {
await FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: password);
}
Future<void> logOut() async {
await FirebaseAuth.instance.signOut();
}
Future<void> register(String email, String password) async {
final credential = await FirebaseAuth.instance
.createUserWithEmailAndPassword(email: email, password: password);
await credential.user!.sendEmailVerification();
}
Future<void> sendEmailVerEmailCurUser() async {
await FirebaseAuth.instance.currentUser?.sendEmailVerification();
}
Future<void> sendPasswordResetEmail(String email) async {
await FirebaseAuth.instance.sendPasswordResetEmail(email: email);
}
Stream<AuthState> stateChanges() async* {
await for (final user in FirebaseAuth.instance.authStateChanges()) {
if (user == null) {
//this.user = null;
yield AuthState.loggedOut;
} else {
/* To Do: implement runtime detection of email verification */
if (!user.emailVerified) {
user.reload();
}
if (user.emailVerified) {
/*try {
this.user = await UserObject.loadData(user.uid);
} catch (e) {
print(e.toString());
}*/
yield AuthState.loggedIn;
} else {
yield AuthState.verifyEmail;
}
}
}
}
}
remove the onGeneratedRuote; Not just it can be called during rebuild
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<AuthState>(
stream: Auth.instance.stateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
assert(snapshot.connectionState != ConnectionState.done);
if (snapshot.data == AuthState.loggedIn) {
return const MainWidget()
}
if (snapshot.data == AuthState.loggedOut) {
return const LoginWidget();
}
if (snapshot.data == AuthState.verifyEmail) {
const EmailNotVerWidget();;
}
throw Exception('Unknown Auth State');
})
// })
);
}
So I am using a riverpod with ChangeNotifier, GoRouter and Hive
So this is my RouteService class with parameter of HiveBox;
class RouteService extends ChangeNotifier {
late final Box box;
bool _initialized = false;
bool _loginState = false;
RouteService({required this.box});
bool get loginState => _loginState;
bool get initialized => _initialized;
set loginState(bool value) {
_loginState = value;
box.put('login', value);
notifyListeners();
}
set initializedState(bool value) {
_initialized = value;
notifyListeners();
}
Future<void> onAppStart() async {
_initialized = box.get('initialized', defaultValue: false);
_loginState = box.get('login', defaultValue: false);
await Future.delayed(const Duration(seconds: 2));
///TODO set a downloading resources here
_initialized = true;
notifyListeners();
}
}
And this is my BlackboltRouter class which contains the GoRouter and the Ref object from flutter riverpod package. (please focus on redirect)
class BlackboltRouter {
GoRouter get router => _goRouter;
BlackboltRouter(this.ref);
late Ref ref;
late final GoRouter _goRouter = GoRouter(
// refreshListenable: routeService,
initialLocation: APP_PAGE.home.toPath,
routes: [
ShellRoute(
builder: (context, state, child) => ResponsiveWrapper.builder(
ClampingScrollWrapper.builder(context, child),
maxWidth: 1200,
minWidth: 480,
breakpoints: const [
ResponsiveBreakpoint.resize(480, name: MOBILE),
ResponsiveBreakpoint.autoScale(600, name: TABLET),
ResponsiveBreakpoint.resize(1000, name: DESKTOP),
ResponsiveBreakpoint.autoScale(2460, name: '4K')
],
),
routes: [
GoRoute(
path: APP_PAGE.home.toPath,
name: APP_PAGE.home.toName,
builder: (_, __) => RootPage(),
),
GoRoute(
path: APP_PAGE.splash.toPath,
name: APP_PAGE.splash.toName,
builder: (context, state) => SplashPage(),
),
GoRoute(
path: APP_PAGE.login.toPath,
name: APP_PAGE.login.toName,
// TODO add login page
builder: (context, state) => SizedBox.shrink(),
),
GoRoute(
path: APP_PAGE.game.toPath,
name: APP_PAGE.game.toName,
builder: (context, state) {
final questGameKey = state.params['questGameKey'] ?? 'unknown';
return GamePage(questGameKey: questGameKey);
},
),
],
),
],
// errorBuilder: (context, state) => ErrorPage(),
redirect: (context, state) {
final routeService = ref.read(routerServiceProvider);
final homeLocation = state.namedLocation(APP_PAGE.home.toName);
final splashLocation = state.namedLocation(APP_PAGE.splash.toName);
final isInitialized = routeService.initialized;
final isGoingToInit = state.subloc == splashLocation;
if (!isInitialized && !isGoingToInit) {
return splashLocation;
} else if (!isInitialized && isGoingToInit) {
return homeLocation;
}
return null;
// return null;
},
);
}
This my MyApp class
const MyApp({
required this.playerProgressPersistence,
required this.settingsPersistence,
required this.adsController,
required this.gamesServicesController,
required this.questGameBox,
required this.utilGameBox,
super.key,
});
#override
Widget build(BuildContext context) {
return AppLifecycleObserver(
child: ProviderScope(
overrides: [
gameCategoryRepositoryProvider.overrideWithValue(
GameCategoryHiveRepository(
HiveDatabaseGameCategoryApi(questGameBox),
),
),
routerServiceProvider.overrideWithValue(
RouteService(box: utilGameBox),
),
],
child: Consumer(builder: (context, ref, _) {
final goRouter = ref.watch(blackboltRouterProvider);
return BlackboltWidget(
title: 'One sound, One word',
routerConfig: goRouter.router,
);
}),
),
);
}
}
Lastly my providers:
final routerServiceProvider =
ChangeNotifierProvider<RouteService>((ref) => throw UnimplementedError());
/// The provider for the BlackboltRouter
final blackboltRouterProvider = Provider<BlackboltRouter>((ref) {
// final routerService = ref.watch(routerServiceProvider);
return BlackboltRouter(ref);
});
As you can see in our riverpod for the routeServiceProvider, I returned a throw UnimplementedError because I need to pass a value for HiveBox so I decide to throw UnimpletedError first. Then in our MyApp class is that I begin to initialized the Hive boxes and I override the value of my routeServiceProvider. So finally, in my Blackbolt Router I subscribe the routeServiceProvide, which is the expected value of routeServiceProvider is not null. But the error occurs there, it gives me a report that
The following ProviderException was thrown building DefaultSelectionStyle:
An exception was thrown while building ChangeNotifierProvider<RouteService>#c5428.
Thrown exception:
An exception was thrown while building _NotifierProvider<RouteService>#d5e71.
Thrown exception:
UnimplementedError
So I am using this answer Prevent unauthenticated users from navigating to a route using a URL in a flutter web app? to make my task. But I always have this error: Error: Could not find the correct Provider<UserProvider> above this Consumer<UserProvider> Widget.
My main.dart:
import '/providers/user_provider.dart';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
return FutureBuilder(
future: _initialization,
builder: (context, appSnapshot) {
return MaterialApp(
//smth
onUnknownRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) => NotFoundErrorScreen(),
);
},
routes: {
AuthScreen.routeName: (context) => AuthScreen(),
DashboardScreen.routeName: (context) => DashboardScreen(),
ProductsScreen.routeName: (context) => ProductsScreen(),
PermissionErrorScreen.routeName: (context) => PermissionErrorScreen(),
},
builder: (context, child) {
return Consumer<UserProvider>(
child: DashboardScreen(),
builder: (context, provider, child) {
if (provider.isLoading) {
return SplashScreen();
}
final value = provider.user;
if (value == null) {
return Navigator(
onGenerateRoute: (settings) => MaterialPageRoute(
settings: settings, builder: (context) => AuthScreen()),
);
}
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => UserProvider()),
],
child: child,
);
},
);
},
);
},
);
}
}
My user_provider.dart:
class UserProvider with ChangeNotifier {
bool _isLoading = true;
Map<String, dynamic> _user;
bool get isLoading => _isLoading;
Map<String, dynamic> get user => _user;
getUser() async {
FirebaseAuth.instance.authStateChanges().listen((currentUser) async {
_isLoading = false;
if (currentUser == null) {
_user = null;
notifyListeners();
return;
}
final data = await FirebaseFirestore.instance
.collection('users')
.doc(currentUser.uid)
.snapshots()
.first;
if (data.exists) {
print(data.data());
if (data.data()['role'] == 'admin') {
_user = (data.data());
notifyListeners();
}
return null;
}
});
}
}
This is my first experience with flutter web, so I am confused.
My first thaught was that it does not work, because I don't invoke my getUser() function, however I do not know how to fix it.
Thank you in advance.
The issue is that my app does not navigate to another page automatically when user logs in or out.
class MyApp extends StatelessWidget {
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
#override
Widget build(BuildContext context) {
return FutureBuilder(
// Initialize FlutterFire:
future: _initialization,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return StreamProvider<User>.value(
value: AuthService().user,
child: MaterialApp(home: Wrapper()),
);
}
return Center(child: CircularProgressIndicator());
},
);
}
}
class Wrapper extends StatelessWidget {
const Wrapper({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
if (user != null) {
return MaterialApp(initialRoute: '/', routes: {
'/': (context) => Home(),
'/profile': (context) => Profile()
});
}
return MaterialApp(initialRoute: '/', routes: {
'/': (context) => Welcome(),
'/signup': (context) => SignUp(),
'/signin': (context) => SignIn()
});
}
}
When the app starts it does show the Welcome() page. Then i am able to navigate to the signup page by pressing a signup button as such
onPressed: () {Navigator.pushNamed(context, "/signup");}),
but then when the user signs up, the app doesn't automatically navigate to Home()
class AuthService {
FirebaseAuth auth = FirebaseAuth.instance;
User _userFromFirebaseUser(User user) {
return user != null ? User(id: user.uid) : null;
}
Stream<User> get user {
return auth.authStateChanges().map(_userFromFirebaseUser);
}
Future<String> signUp(email, password) async {
try {
UserCredential user = await auth.createUserWithEmailAndPassword(
email: email, password: password);
await FirebaseFirestore.instance
.collection('users')
.doc(user.user.uid)
.set({'name': email, 'email': email});
_userFromFirebaseUser(user.user);
} on FirebaseAuthException catch (e) {
return e.code;
} catch (e) {
return e;
}
return "";
}
}
I am not sure what the issue is. Any help is appreciated.
First of all you need 1 MaterialApp not 3, then try to debug signUp method maybe there is an erorr for instance signUp returns Future<String> but in catch block you are returning an Exception and finally I suggest you to use Cubit whenever you need to listen state changes to navigate.
I have been struggling with this problem for like two days. My social media app should save its state, when signed in so that when you leave the app and come back again it should start from the home page, not the sign in page. I have found that it is possible to do this with StreamBuilder and FutureBuilder. I have tried some things with FutureBuilder and I have some errors.
Below is how my main page looks like:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (BuildContext context) => UserData(),
child: MaterialApp(
title: 'Curtain App',
debugShowCheckedModeBanner: false,
home: FutureBuilder(
future: SharedPreferencesHelper.getPrefs(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasData) {
Provider.of<UserData>(context).currentUserId =
snapshot.data.token;
return HomeScreen();
} else {
return LoginScreen();
}
},
),
),
);
}
}
class SharedPreferencesHelper {
static final String _tokenCode = "token";
static Future<String> getPrefs() async {
final SharedPreferences preferences = await SharedPreferences.getInstance();
return preferences.getString(_tokenCode) ?? "empty";
}
}
And this is my LoginPage submit btn code:
_submit() async {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
// logging in the user w/ Firebase
//AuthService.login(_email, _password);
var user = await DatabaseService.loginUser(_username, _password);
final data = json.decode(user);
SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
print("Hi ${data['username']}");
print("Status ${data['status']}");
print("Token ${data['token']}");
if (data['username'] != null) {
setState(() {
_message = "Hi ${data['username']}";
sharedPreferences.setString('token', data['token']);
});
Navigator.of(context).pushAndRemoveUntil(
CupertinoPageRoute(
builder: (context) => HomeScreen(),
),
(Route<dynamic> route) => false);
}
}
}
Any ideas on how to solve this ?
Just remove the .token from the line where the error occurs. snapshot.data already is the token.