I'm trying to understand RiverPod by migrating my simple FireStore auth Provider example to RiverPod.
This is my AuthenticationService:
import 'package:firebase_auth/firebase_auth.dart';
class AuthenticationService {
final FirebaseAuth _firebaseAuth;
AuthenticationService(this._firebaseAuth);
// with StreamProvider we listen to these changes
Stream<User> get authStateChanges => _firebaseAuth.authStateChanges();
Future<String> signIn({String email, String password}) async {
try {
await _firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
return 'Signed in';
} on FirebaseAuthException catch (e) {
return e.message;
}
}
Future<String> signUp({String email, String password}) async {
try {
await _firebaseAuth.createUserWithEmailAndPassword(
email: email, password: password);
return 'Signed up ';
} on FirebaseAuthException catch (e) {
return e.message;
}
}
Future<void> signOut() async {
await _firebaseAuth.signOut();
}
}
In main.dart I made 2 providers so I can use the service and listen to the property inside of the AuthenticationService
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:meditatie_app/authentication_service.dart';
import 'package:meditatie_app/home_page.dart';
import 'package:meditatie_app/signin_page.dart';
import 'package:provider/provider.dart';
Future<void> main() async {
// initalize Firebase and before that we need to initialize the widgets.
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// Normal provider to serve the AuthenticationService in the widgettree
// so the login form can use this provider to use .singIn()
Provider<AuthenticationService>(
create: (_) => AuthenticationService(FirebaseAuth.instance),
),
// also a StreamProvider that serves the AuthenticationSerivce authStateChanges
// this stream is updated by the FirebaseAuth package when users signin or out
// this provider use context.read<AuthenticationService> to find the
// provider dived here above
StreamProvider(
create: (context) =>
context.read<AuthenticationService>().authStateChanges,
)
],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: AuthenticationWrapper(),
),
);
}
}
class AuthenticationWrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final firebaseUser = context.watch<User>();
if (firebaseUser != null) {
return HomePage();
}
return SignInPage();
}
}
Here the SingIn page:
import 'package:flutter/material.dart';
import 'package:meditatie_app/authentication_service.dart';
import 'package:provider/provider.dart';
class SignInPage extends StatelessWidget {
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
...
RaisedButton(
onPressed: () {
// Sign in code
context.read<AuthenticationService>().signIn(
email: emailController.text.trim(),
password: passwordController.text.trim(),
);
},
...
This works fine with normal Provider, but I can't get it to work with RiverPod
What I did was:
These providers I made global in providers.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/all.dart';
import 'authentication_service.dart';
final authenticationSeriviceProvider =
Provider((ref) => AuthenticationService(FirebaseAuth.instance));
final authStateChangeProvider = StreamProvider.autoDispose<User>((ref) {
return ref
.watch(authenticationSeriviceProvider)
.authStateChanges;
});
Is this correct? The authStateChangeProvider is using the authenticationSeriviceProvider
When is use it like:
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:meditatie_app/home_page.dart';
import 'package:meditatie_app/signin_page.dart';
import 'package:flutter_riverpod/all.dart';
import 'providers.dart';
Future<void> main() async {
// initialize Firebase and before that we need to initialize the widgets.
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
// riverpod needs at toplevel a Provider container
// for storing state of different providers.
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: AuthenticationWrapper(),
);
}
}
// Riverpods ConsumerWidget
// which can consume a provider
// rebuild if the value of the provider changes
class AuthenticationWrapper extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
final firebaseUser = watch(authStateChangeProvider);
if (firebaseUser != null) {
return HomePage();
}
return SignInPage();
}
}
My 'firebaseUser' is not a User anymore, but an AsyncValue
When I change it to:
class AuthenticationWrapper extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
final User firebaseUser = watch(authStateChangeProvider).data?.value;
if (firebaseUser != null) {
return HomePage();
}
return SignInPage();
}
}
It is working, but what am I doing wrong that I now work with AsyncValue
Expanding the previous answer AsyncValue<T> is a sealed class, think of it as StreamBuilder in Flutter having AsyncSnapshot<T> which wraps the value returned from the stream and gives you options to check if its connecting, waiting, withError or withData. In your case
class AuthenticationWrapper extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
return watch(authStateChangeProvider).when(
data: (user) => user == null ? SignInPage() : HomePage(),
loading: () => CircularProgressIndicator(),
error: (err, stack) => SignInPage(),
);
}
}
should handle all the options, now when loading it will show a progress indicator, if there is an error (connection, bad result, etc) it will display the SignInPage, and finally when there is a value you still will need to check if the value returned from the Stream is null (As far as I understand Firebase returns null when there is no user signed in, it doesn't mean the stream is empty) and display the right widget if its null or not.
Just like Provider, after retrieving the user you still have to do the logic with that
See the documentation.
You should use AsyncValue's exposed states to decide what to render. Your code could look something like the following:
class AuthenticationWrapper extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
return watch(authStateChangeProvider).when(
data: (user) => user == null ? SignInPage() : HomePage(),
loading: () => CircularProgressIndicator(),
error: (err, stack) => SignInPage(),
);
}
}
So adjust your return logic to what you'd like for the data, loading, and error states, but this should give you a general idea on how to use AsyncValue.
Another way I found was to use it the way this tutorial did, but with the new riverpod changes:
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_shopping_list/repositories/auth_repository.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
final authControllerProvider = StateNotifierProvider<AuthController, User?>(
(ref) => AuthController(ref.read)..appStarted(),
);
class AuthController extends StateNotifier<User?> {
final Reader _read;
StreamSubscription<User?>? _authStateChangesSubscription;
AuthController(this._read) : super(null) {
_authStateChangesSubscription?.cancel();
_authStateChangesSubscription = _read(authRepositoryProvider)
.authStateChanges
.listen((user) => state = user);
}
#override
void dispose() {
_authStateChangesSubscription?.cancel();
super.dispose();
}
void appStarted() async {
final user = _read(authRepositoryProvider).getCurrentUser();
if (user == null) {
await _read(authRepositoryProvider).signInAnonymously();
}
}
}
And then I used it like this:
#override
Widget build(BuildContext context, WidgetRef ref) {
User? user = ref.watch<User?>(authControllerProvider);
return user != null
? MaterialApp(
title: 'My App',
builder: (context, child) => _Unfocus(child: child!),
home: MainNavigation(),
debugShowCheckedModeBanner: false,
)
: const MaterialApp(
title: 'My App,
home: LoginPage(),
debugShowCheckedModeBanner: false,
);
}
Related
Using flutter_riverpod, given the following providers:
final s = StreamProvider.autoDispose<bool>((ref) async* {
yield true;
});
final f = FutureProvider.autoDispose<bool>((ref) async {
final source = await ref.watch(s.last);
print('Stream value received: $source');
return source;
});
When f is referenced from a Widget, it will print the message twice, i.e. it will rebuild twice, even though the referenced StreamProvider's stream only emits a single value.
Stream value received: true
Stream value received: true
Why is that? How can I make sure that my FutureProvider will only run once for each emitted stream value?
Here is the full minimal code to reproduce the issue:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final s = StreamProvider.autoDispose<bool>((ref) async* {
yield true;
});
final f = FutureProvider.autoDispose<bool>((ref) async {
final source = await ref.watch(s.last);
print('Stream value received: $source');
return source;
});
void main() {
runApp(ProviderScope(child: (MyApp())));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
watch(f);
return const Text('hi');
}
}
Hy here everyone. I am new to flutter and i want to check if User is SignedIn. If so the user navigate to HomeScreen else SplashScreen.
Here is my main.dart
void main() async{
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Instant Tasker',
theme: theme(),
initialRoute: SplashScreen.routeName,
routes: routes,
);
}
}
Here is Splash Screen
class SplashScreen extends StatefulWidget {
static String routeName = "/splash";
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
startTime() async {
var _duration = new Duration(seconds: 2);
return new Timer(_duration, navigationPage);
}
void navigationPage() {
var auth = FirebaseAuth.instance;
// ignore: deprecated_member_use
auth.onAuthStateChanged.listen((user) {
if (user != null) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => MainScreen()),
(Route<dynamic> route) => false);
} else {}
});
}
#override
void initState() {
super.initState();
startTime();
}
#override
Widget build(BuildContext context) {
SizeConfig().init(context);
return Scaffold(
body: Body()
);
}
}
However i achieved to check user at splash screen but it stays at splash screen to check user then move to HomeScreen which doesn't seems to be good.
Or can anybody suggest how to show CircularProgressIndicator instead of Splash Screen body when it is checking for user
You can achieve it using StreamProvder
Implementation
Steps
Create a CustomUser Data model.
class CustomUser {
final String userId;
CustomUser({this.userId});
}
Create a class named FirebaseAuthService and create a stream to listen to Firebase AuthStateChanges
import 'package:firebase_auth/firebase_auth.dart';
class FirebaseAuthService {
final FirebaseAuth auth = FirebaseAuth.instance;
// create user obj based on firebase user
CustomUser _userFromFirebaseUser(User user) {
return user != null ? CustomUser(userId: user.uid) : null;
}
// auth change user stream
//Required stream
Stream<CustomUser> get user {
return auth.authStateChanges().map(_userFromFirebaseUser);
}
}
}
Add a StreamProvider on top of the widget tree where you want to check for the AuthState.
void main() async{
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<CustomUser>.value(
value: FirebaseAuthService().user,
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Instant Tasker',
theme: theme(),
initialRoute: SplashScreen.routeName,
routes: routes,
)
);
}
}
Create a Wrapper and return SplashScreen or HomeScreen based on AuthState.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Wrapper extends StatefulWidget {
#override
_WrapperState createState() => _WrapperState();
}
class _WrapperState extends State<Wrapper> {
#override
Widget build(BuildContext context) {
final user = Provider.of<CustomUser>(context);
if (user == null) {
return SplashScreen();
}
return HomeScreen();
}
}
Now you can use final user = Provider.of<CustomUser>(context);
in the widget tree to check if the user is null.
https://www.youtube.com/watch?v=z05m8nlPRxk&list=PL4cUxeGkcC9j--TKIdkb3ISfRbJeJYQwC&index=3
I have tired to stream user authentication state changes using StreamProvider.value to MaterialApp. But i got this Error: A value of type 'MultiProvider' can't be returned from method 'build' because it has a return type of 'Widget'.
This are my the dependency in pubspec.yaml
firebase_core: ^0.5.0 firebase_auth: ^0.18.0+1 cloud_firestore: ^0.14.0+2 provider: ^4.3.2+2
And couldn't be able to figure it out. I'm stuck please help. Thank you for your help in advance.
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
class MyAwesomeApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [StreamProvider<Usr>.value(value: Authenticate().userStream)],
builder: (context, child) {
return MaterialApp(
home: Wrapper(),
);
},
);
}
}
//This is Authenticate class which holds the stream
import 'package:firebase_auth/firebase_auth.dart';
class Authenticate {
//create an instance of firebase auth
FirebaseAuth _auth = FirebaseAuth.instance;
// Create a new user instance from my user model
Usr _userFromFirebaseUser(User user) {
return user != null ? Usr(uid: user.uid) : null;
}
// set up auth stream to listen to user auth status
Stream<Usr> get userStream {
return _auth.authStateChanges().map(
_userFromFirebaseUser); // shorter way of mapping to user custom user object
// .map((User user) => _userFromFirebaseUser(user)); //longer one
}
// sign in anon
Future signInAnon() async {
try {
UserCredential response = await _auth.signInAnonymously();
User user = response.user;
return _userFromFirebaseUser(user);
} catch (e) {
print(e.toString());
return null;
}
}
}
Try this
import 'package:flutter/material.dart';
import 'package:provider/provider.dart' as statemanagement;
import 'models/model_provider.dart';
import 'modules/screen_root.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return statemanagement.MultiProvider(
providers: [
statemanagement.Provider<ModelProvider>(
create: ((_) => ModelProvider())),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Multi Provider',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const ScreenRoot(),
),
);
}
}
I want to write the data to the fire store database.
I wrote the code in this way in subscriptions class:
import 'package:flutter/material.dart';
import 'package:flutterapptest/services/database.dart';
import 'package:provider/provider.dart';
class Subscriptions extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Subscribed'),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => _addTrack(context),
),
);
}
Future<void> _addTrack(BuildContext context) async {
final database = Provider.of<Database>(context, listen: false);
await database.addTrack({
'name': 'Track',
'time': 20,
});
}
}
in the database class ......
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:meta/meta.dart';
abstract class Database {
Future<void> addTrack(Map<String, dynamic> trackData);
}
class FirestoreDatabase implements Database {
FirestoreDatabase({#required this.uid}) : assert(uid != null);
final String uid;
Future<void> addTrack(Map<String, dynamic> trackData) async{
final path = '/users/$uid/track/track_abc';
final documentReference = Firestore.instance.document(path);
await documentReference.setData(trackData);
}
}
for this i am getting the error:
could not find the correct Provider<Database> above the subscriptions widget
For this a friend suggested me to do:
If you push the Subscriptions widget inside a route, it won't have access to Provider.of<Database>.
The quickest solution is to pass Database as a constructor argument to the Subscriptions class, and use that as an instance variable rather than with Provider.of<Database>
Can anyone please help me what should i do now? I am new to flutter.
This is how I would do it. I know this includes much more then just the addTrack function but I guess you also need a global state somewhen and a changeNotifier, otherwise it makes no sense to use the provider.
First create the service:
import 'package:cloud_firestore/cloud_firestore.dart';
final CollectionReference firestoreUsers =
Firestore.instance.collection('users');
class UserService {
static Future<Stream<Map<String, dynamic>>> streamTrack(String userId) async {
Stream<DocumentSnapshot> query =
await firestoreUsers.document('pathToListen...').snapshots();
return query.map((doc) => Map.from(doc.data));
}
static addTrack(String userId, Map<String, dynamic> trackData) async {
await firestoreUsers
.document(userId)
.collection('track')
.document('track_abc')
.setData(trackData);
}
}
Then the provider which holds the global state and uses the changeNotifier. These are just placeholders. Put there whatever you want to listen in the database.
import 'dart:async';
import 'package:flutter/material.dart';
import '../models_services/user_services.dart';
class UserProvider with ChangeNotifier {
// your global state, if you want to listen for data from database
StreamSubscription<Map<String, dynamic>> trackStream;
Map<String, dynamic> _streamedTrack;
Map<String, dynamic> get streamedTrack => _streamedTrack;
Future streamCurrentTrack(uid) async {
if(trackStream != null) {
trackStream.cancel();
}
Stream<Map<String, dynamic>> res = await UserService.streamTrack(uid);
trackStream = res.listen((track) {
_streamedTrack = track;
notifyListeners();
});
}
Future<void> addTrack(Map<String, dynamic> trackData) async {
// not sure what this is doing, i guess uid is null here...
FirestoreDatabase({#required this.uid}) : assert(uid != null);
final String uid;
await UserService.addTrack(uid, trackData);
}
}
Use the provider with changeNotifier in your code:
import 'package:flutter/material.dart';
import 'package:flutterapptest/provider/user_provider.dart';
import 'package:provider/provider.dart';
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => UserProvider()),
...
],
child: MaterialApp(
home: Scaffold(body: Subscriptions()
)));
}
}
class Subscriptions extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Subscribed'),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => _addTrack(context),
),
);
}
Future<void> _addTrack(BuildContext context) async {
final database = Provider.of<UserProvider>(context, listen: false);
await database.addTrack({
'name': 'Track',
'time': 20,
});
}
}
I have a simple Provider class:
import 'package:flutter/foundation.dart';
class AppState with ChangeNotifier {
bool _isLoggedIn = false;
bool get isLoggedIn => _isLoggedIn;
set isLoggedIn(bool newValue) {
_isLoggedIn = newValue;
notifyListeners();
}
}
And in the login class I just set isLoggedIn to true if login is successful:
void _signInWithEmailAndPassword(appState) async {
try {
final FirebaseUser user = await _auth.signInWithEmailAndPassword(
...
);
if (user != null) {
appState.isLoggedIn = true;
appState.userData = user.providerData;
...
}
} catch (e) {
setState(() {
_errorMessage = e.message;
});
}
}
Pressing the back button on Android lets users go back to this page even after successfully logging in. So I wanted to know if Provider.of can be accessed before Widget build and redirect a user if isLoggedIn is true.
Now I have something like:
#override
Widget build(BuildContext context) {
final appState = Provider.of<AppState>(context);
...
This is only one use case for the login view, but I'm sure this functionality can be used in other cases.
If you are going to use the FirebaseUser or Logged in user throughout your app, i would suggest that you add the Provider on the highest level of your app. Example
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp();
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<FirebaseUser>.value(
stream: FirebaseAuth.instance.onAuthStateChanged, // Provider here
),
],
child: MaterialApp(
title: 'My App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.green,
primarySwatch: Colors.green,
accentColor: Colors.yellow,
),
home: MainPage(),
),
);
}
}
class MainPage extends StatefulWidget {
MainPage({Key key, this.storage}) : super(key: key);
final FirebaseStorage storage;
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage>
with SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) {
final user = Provider.of<FirebaseUser>(context); // gets the firebase user
bool loggedIn = user != null;
return loggedIn ? HomePage() : LoginPage(); // Will check if the user is logged in. And will change anywhere in the app if the user logs in
}
}
References
Fireship 185 Provider
Great Youtube video explaining the code