Flutter await Provider to be ready inside multiprovider - flutter

I have a problem with synchronize Providers creation.
I'm new to flutter, I'll try to explain better as i can.
In my main.dart i have two providers one for the user auth and one for another widget into the code, it simply have a list to display.
I use ChangeNotifierProxyProvider because all other provider needs to access to user auth tokens and details. All methods for user to store,read tokens are in the userProvider.
When UserProvder.init() is called the object is created but is still not ready beacuse of the http request, the code in the main continue to execute and pass the UserProvider to the Conto Provider that thinks UserProvider is ready but it is not. When the ContoProvider start to retrive the List with the token inside UserProvider access through userService failed the call because is still null.
So, How can i synchronize the creation of provider in order to wait the UserProvider to be full ready and then initialize all other providers?
main.dart
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<UserProvider>(
create: (_) => UserProvider.init(),
),
ChangeNotifierProxyProvider<UserProvider, ContoProvider>(
create: (_) {
return ContoProvider.init(
Provider.of<UserProvider>(_, listen: false));
},
update: (_, userProvider, contoProvider) =>
contoProvider..update(userProvider),
),
],
child:...
userProvider.dart
User activeUser = User(null, null, null);
UserStatus status = UserStatus.Checking;
UserProvider.init() {
checkUserPresence();
}
void checkUserPresence() async {
/*here i check if in secure storage there is a refreshtoken if there is i make an http
request for a new token and a second request to fill the User into UserProvider so i need await async*/
}
ContoProvider.dart
UserProvider userService;
ContoProvider.init(UserProvider user) {
userService = user;
lookUpConti();
}
void lookUpConti() async {
/*here i make an http call to retrive some data, i access to
userService for get token and refresh token if needed*/
}

You can use WidgetsFlutterBinding.ensureInitialized()
void main() {
/** WidgetsFlutterBinding.ensureInitialized() is required in Flutter v1.9.4+ before using any plugins if the code is executed before runApp. */
WidgetsFlutterBinding.ensureInitialized();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<UserProvider>(
create: (_) => UserProvider.init()),
ChangeNotifierProxyProvider<UserProvider, ContoProvider>(
create: (_) {
return ContoProvider.init(
Provider.of<UserProvider>(_, listen: false));
},
update: (_, userProvider, contoProvider) =>
contoProvider..update(userProvider),
),
],
child: MyApp(),
),
}
PS: I will recommend that you separate your repositories from your provider. That is, not API call to external/web resources should be found in your provider. You can pass such class into your provider as arguments.

Related

Flutter RepositoryProvider and Hive LateInitializationError

I have app where I am using Bloc and Hive.
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final appDocumentDirectory =
await path_provider.getApplicationDocumentsDirectory();
Hive.init(appDocumentDirectory.path);
runApp(
const MyApp(),
);
}
On MyApp widget registered MultiRepositoryProvider
return MultiRepositoryProvider(
providers: [
RepositoryProvider(create: (context) => AccountService()),
],
child: MultiBlocProvider(
providers: [
BlocProvider<AccountBloc>(
create: (context) => AccountBloc(context.read<AccountService>()),
),
],
child: MaterialApp(
home: const AppPage(),
),
),
);
AppPage Contains bottomNavigationBar and some pages
account.dart
class AccountService {
late Box<Account> _accounts;
AccountService() {
init();
}
Future<void> init() async {
Hive.registerAdapter(AccountAdapter());
_accounts = await Hive.openBox<Account>('accounts');
}
On appPage have BlocBuilder
BlocBuilder<AccountBloc, AccountState>(
builder: (context, state) {
if (state.accountStatus == AccountStatus.loading) {
return const CircularProgressIndicator();
} else if (state.accountStatus == AccountStatus.error) {
Future.delayed(Duration.zero, () {
errorDialog(context, state.error);
});
}
return SingleChildScrollView(....
When app first loaded I receive LateInitializationError that late Box <Account> _accounts from account Repository not initialized. But as soon as I navigate to another page and go back, the Box <Account> _accounts are initialized and the data appears.
How can I avoid this error and initialize the Hive box on application load?
Can you try this? I think you need to await Hive init function
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final appDocumentDirectory =
await path_provider.getApplicationDocumentsDirectory();
await Hive.init(appDocumentDirectory.path);
runApp(
const MyApp(),
);
}
It's been like 7 months, but if you are still looking for an answer, not sure if it's optimal but below should work.
My understanding on the issue you are having is that the reason why there is that "LateInitializationError" is because that your init function call in your constructor is asynchronously invoked without await for its result. As a result, there is a possibility that when you are calling functions on the box, the initialisation is not yet finished. When you navigate to another page and go back, the function init run happened to be finished. Hence, the error is gone. The complexity here is that constructor can not be marked as async for you to use that await keyword. Since you are using bloc, one possible workaround is to call the init function of your repo when bloc is in init state.
For demo purpose I defined below bloc states and events,
you can absolutely change them based on your needs.
// bloc states
abstract class AccountState{}
class InitState extends AccountState{}
class LoadedState extends AccountState{
LoadedState(this.accounts);
final List<Account> accounts;
}
class LoadingErrorState extends AccountState{}
//bloc events
abstract class AccountEvent {}
class InitEvent extends AccountEvent {}
... // other events
in your bloc logic you can call the init function from you repo on InitEvent
class AccountBloc extends Bloc<AccountEvent, AccountState> {
AccountBloc(this.repo) : super(InitState()) {
on<InitEvent>((event, emit) async {
await repo.init();
emit(LoadedState(account: repo.getAccounts()));
});
...// define handlers for other events
}
final AccountRepository repo;
}
in your service class you can remove the init from the constructor like:
class AccountService {
late Box<Account> _accounts;
AccountService();
Future<void> init() async {
Hive.registerAdapter(AccountAdapter());
_accounts = await Hive.openBox<Account>('accounts');
}
List<Account> getAccounts(){
return _accounts.values.toList();
}
}
Then in your bloc builder, you can add init event to your bloc when the state is InitState as below:
BlocBuilder<AccountBloc, AccountState>(
builder: (context, state) {
if (state is InitState) {
context.read<AccountBloc>.add(InitEvent());
return const CircularProgressIndicator();
} else if (state is LoadingErrorState) {
Future.delayed(Duration.zero, () {
errorDialog(context, state.error);
});
}
else if (state is LoadedState){
return SingleChildScrollView(....
}
Also, FYI, you can if you want the init to be called when the object of your account service is instantiated, you can take a look at below answer:
https://stackoverflow.com/a/59304510/16584569
However, you still going to need to await for the initialisation of your service. One possible way is just do it in your main function and pass down to your app, but it makes the structure of your code messy and when you want to swap to another repo, you need to remember to change code in main function as well.

ChangeNotifier not updating Consumer

not sure why my ChangeNotifier isn't working.
This is my Class:
class LoadingProv with ChangeNotifier {
bool globalLoading;
void setGlobalLoading(bool truefalse) {
if (truefalse == true) {
globalLoading = true;
} else {
globalLoading = false;
}
notifyListeners();
}
bool get getGlobalLoadingState {
return globalLoading;
}
}
This is my Multiprovider in main.dart:
MultiProvider(
providers: [
ChangeNotifierProvider<MapData>(create: (ctx) => MapData()),
ChangeNotifierProvider<LoadingProv>(create: (ctx) => LoadingProv()),
],
child: MaterialApp(
This is my code in the main.dart Widget build(BuildContext context):
Consumer<LoadingProv>(builder: (context, loadingState, child) {
return Text(loadingState.getGlobalLoadingState.toString());
}),
And this is how I call setGlobalLoading:
final loadingProv = LoadingProv();
loadingProv.setGlobalLoading(true);
Unfortunately my loadingState.getGlobalLoadingState is always printed as false. But I can debug that it becomes actually true.
From my understanding, you are creating 2 LoadingProv object.
One is when initialising the Provider
ChangeNotifierProvider<LoadingProv>(create: (ctx) => LoadingProv()),
One is when some places you call
final loadingProv = LoadingProv();
So the one you updating is not the one inherit on the widget, then you cannot see the value updating the Consumer.
(1) if you want to keep create along with the create method, you should call setGlobalLoading via
Provider.of<LoadingProv>(context).setGlobalLoading(true);
(2) Or if you want to directly access the value like loadingProv.setGlobalLoading(true), you should initialise your provider like this
final loadingProv = LoadingProv();
MultiProvider(
providers: [
ChangeNotifierProvider<MapData>(create: (ctx) => MapData()),
ChangeNotifierProvider<LoadingProv>.value(value: loadingProv),
],
you can use this code to read data when change it automatically refresh the Text widget
Text(context.watch<LoadingProv>().getGlobalLoadingState.toString());
on for calling the void you can use this
context.read<LoadingProv>().setGlobalLoading(true);

Flutter Authentication and Data Persistence with Provider Architecture

I come from a HTML/CSS/JQUERY for frontend background and mostly I have been a Nodejs backend developer.
However a project I took over was based on HTML/CSS/JQuery and PHP and I am doing quite well in that until they needed a mobile application.
So my choice was flutter since it was hot at that moment.
I have mastered flutter basics and took provider as my state management and also incorporated logic inside it.
However I have 2 issue that I am not being able to figure out.
One is state persistence in provider even when app is forced closed.
Maintain login persistence unless logged out and clear state if logged out.
For 1. I haven't been able to figure out a concrete solution except Application Storage but then I would have to write alot of code more to store all the data and class into it.
For 2. I have done some experimental method as below
runApp(
ChangeNotifierProvider(
create: (_) => UserRepository(), // Here is my auth state provider
child: MyApp(),
),
);
In UserRepository class user data like user id are stored in SharedPreferences which checks if there is userdata is stored or not hence Status becomes Authenticated or Uninitialized per condition.
Below is a Stateful widget that sets the status with respect to UserRepository
class _MyAppState extends State<MyApp> {
ProjectRepository projectRepository = ProjectRepository();
UIKeypadRepository uiKeypadRepository = UIKeypadRepository();
PODRepository podRepository = PODRepository();
Status currentStatus;
//Also there are more Change notifier class here that manages state of various features of the app
void initProviders() {
projectRepository = ProjectRepository();
uiKeypadRepository = UIKeypadRepository();
podRepository = PODRepository();
}
#override
void initState() {
currentStatus = Provider.of<UserRepository>(context, listen: false).status;
//if not logged out current status is authenticated
super.initState();
}
#override
Widget build(BuildContext context) {
return Consumer(builder: (context, UserRepository authStatus, child) {
if (currentStatus != Status.Authenticated) {
initProviders(); // this function is called and all changenotifier classes are reinitialized
}
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: projectRepository),
ChangeNotifierProvider.value(value: uiKeypadRepository),
ChangeNotifierProvider.value(value: podRepository),
],
//instead of create I used value so if classes are initialized initial values are taken else existing values are made available to below the widget tree
child: MaterialApp(
home: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: LandingPage())),
);
});
}
}
You can use authStateChanges() and StreamBuilder to get the authentication status.
Reference

How can I implement a provider that is depended on another provider?

I am using the provider package with flutter and dart I have a authentication provider and a stream provider that provides a user model. The stream provider that provides the user model is depended on the authentication provider. How can I get my user id to my stream provider?
This is my multiprovider
MultiProvider(
providers: [
Provider<AuthenticationProvider>(builder: (context) => AuthenticationProvider(),),
StreamProvider<UserModel>.value(value: userStream(//here we call the async method in the Authentication provider to get the user id),
),
]
This is the method inside the Authentication Provider returns a authentication model which contains the user id
Future<UserAuthenticationCertificate> userAuthenticationCertificate() async
{
FirebaseUser authenticatedUser = await _authentication.currentUser();
if(authenticatedUser != null)
return UserAuthenticationCertificate.fromFirebaseAuthentication(authenticatedUser);
return null;
}
This is the authentication certificate
import 'package:firebase_auth/firebase_auth.dart';
class UserAuthenticationCertificate
{
String _userID;
String get userID{
return _userID;
}
UserAuthenticationCertificate(this._userID);
UserAuthenticationCertificate._internal(this._userID);
factory UserAuthenticationCertificate.fromFirebase(FirebaseUser firebaseUser)
{
return UserAuthenticationCertificate._internal(
firebaseUser.uid
);
}
}
Edit
This is what I currently have
Provider<AuthenticationProvider>(create: (_) => AuthenticationProvider(),),
Provider<UserProvider>(
create: (_) => UserProvider(),
),
StreamProvider(create: (context) {
return Provider.of<UserProvider>(context).userStream(Provider.of<AuthenticationProvider>(context).userAuthenticationCertificate());
}),
So now I provide the user provider, in a streamprovider? I want to be providing the userModel as the stream how do I do that?
You can use the context parameter passed to create of your providers to read other providers:
Provider(create: (_) => Auth()),
StreamProvider(create: (context) {
return Provider.of<Auth>(context).something;
});

How to use a provider inside of another provider in Flutter

I want to create an app that has an authentication service with different permissions and functions (e.g. messages) depending on the user role.
So I created one Provider for the user and login management and another one for the messages the user can see.
Now, I want to fetch the messages (once) when the user logs in. In Widgets, I can access the Provider via Provider.of<T>(context) and I guess that's a kind of Singleton. But how can I access it from another class (in this case another Provider)?
From version >=4.0.0, we need to do this a little differently from what #updatestage has answered.
return MultiProvider(
providers: [
ChangeNotifierProvider(builder: (_) => Auth()),
ChangeNotifierProxyProvider<Auth, Messages>(
update: (context, auth, previousMessages) => Messages(auth),
create: (BuildContext context) => Messages(null),
),
],
child: MaterialApp(
...
),
);
Thanks for your answer. In the meanwhile, I solved it with another solution:
In the main.dart file I now use ChangeNotifierProxyProvider instead of ChangeNotifierProvider for the depending provider:
// main.dart
return MultiProvider(
providers: [
ChangeNotifierProvider(builder: (_) => Auth()),
ChangeNotifierProxyProvider<Auth, Messages>(
builder: (context, auth, previousMessages) => Messages(auth),
initialBuilder: (BuildContext context) => Messages(null),
),
],
child: MaterialApp(
...
),
);
Now the Messages provider will be rebuilt when the login state changes and gets passed the Auth Provider:
class Messages extends ChangeNotifier {
final Auth _authProvider;
List<Message> _messages = [];
List<Message> get messages => _messages;
Messages(this._authProvider) {
if (this._authProvider != null) {
if (_authProvider.loggedIn) fetchMessages();
}
}
...
}
Passing another provider in the constructor of the ChangeNotifierProxyProvider may cause you losing the state, in that case you should try the following.
ChangeNotifierProxyProvider<MyModel, MyChangeNotifier>(
create: (_) => MyChangeNotifier(),
update: (_, myModel, myNotifier) => myNotifier
..update(myModel),
);
class MyChangeNotifier with ChangeNotifier {
MyModel _myModel;
void update(MyModel myModel) {
_myModel = myModel;
}
}
It's simple: the first Provider provides an instance of a class, for example: LoginManager. The other Provides MessageFetcher. In MessageFetcher, whatever method you have, just add the Context parameter to it and call it by providing a fresh context.
Perhaps your code could look something like this:
MessageFetcher messageFetcher = Provider.of<ValueNotifier<MessageFetcher>>(context).value;
String message = await messageFetcher.fetchMessage(context);
And in MessageFetcher you can have:
class MessageFetcher {
Future<String> fetchMessage(BuildContext context) {
LoginManager loginManager = Provider.of<ValueNotifier<LoginManager>>(context).value;
loginManager.ensureLoggedIn();
///...
}
}
Seems like this would be a lot easier with Riverpod, especially the idea of passing a parameter into a .family builder to use the provider class as a cookie cutter for many different versions.