I'm trying to have multiple ChangeNotifierProvider in my app, and the first one I created works great, but when I literally copied over the code into another provider the second one will not work. I've tried nesting them, only creating the second one and using a MultiProvider.
Here is my main:
runApp(MultiProvider(providers: [
ChangeNotifierProvider(create: (context) => ArticleProvider()),
ChangeNotifierProvider(create: (context) => CategoryProvider()),
], child: const InsidanApp()));
My first provider:
import 'package:flutter/material.dart';
...
class ArticleProvider extends ChangeNotifier {
/* Data handling and stuffs */
/// Fetch articles from the API.
Future<void> fetchArticles() async {
print('fetchArticles()');
// Calculate page number.
const perPage = 50;
final page = (_articles.length / perPage).ceil() + 1;
// Fetch articles from API.
final newArticles =
await ArticleService.fetchArticles(page: page, count: perPage);
addAll(newArticles);
print('Fetched ${newArticles.length} articles.');
}
/// Constructor.
ArticleProvider() {
print('ArticleProvider()');
fetchArticles();
}
}
My second provider:
import 'package:flutter/material.dart';
...
class CategoryProvider extends ChangeNotifier {
/* Data handling stuff. */
/// Fetch categories from the API.
Future<void> fetchCategories() async {
final newCategories = await CategoryService.fetchCategories();
addAll(newCategories);
}
/// Constructor.
CategoryProvider() {
fetchCategories();
}
}
Do you have any ideas for what the issue could be?
MultiProvider(providers: [
ChangeNotifierProvider.value(value: ArticleProvider()),
ChangeNotifierProvider.value(value: CategoryProvider()),
]
Try it this way to see if it works.
Related
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.
What would be the correct way to call (and pass values to) ProxyProvider from its "sub"providers?
Currently I'm passing a callback function to "sub"provider as a parameter, storing it as a Function and then I can call it when needed.
It works in a sense that ProxyProvider is called (and value is passed), but at the same time it breaks notifyListeners(), which is called next - searches getter in "sub"provider (and can't find it) despite that Consumer is used just for ProxyProvider.
This is the error I receive:
error: org-dartlang-debug:synthetic_debug_expression:1:1: Error: The
getter 'audInd' isn't defined for the class 'AudioModel'.
'AudioModel' is from 'package:quiz_game_new/models/audioModel.dart' ('lib/models/audioModel.dart'). Try correcting the name to the name of
an existing getter, or defining a getter or field named 'audInd'.
audInd ^^^^^^
Code
Provider (audioModel.dart):
class AudioModel extends ChangeNotifier {
int _audioIndex = -1;
Function? audioIndexChanged;
void setCallbacks(Function _audioPlaybackCompleted, Function _audioIndexChanged) {
audioPlaybackCompleted = _audioPlaybackCompleted;
audioIndexChanged = _audioIndexChanged;
}
//Some code that changes _audioIndex and afterwards calls audioIndexChanged!(_audioIndex)
}
ProxyProvider (commonModel.dart)
class CommonModel extends ChangeNotifier {
CommonModel(this.audioModel);
final AudioModel audioModel;
int _audioIndex = -1;
int get audioIndex => _audioIndex;
void setCallbacksForAudioPlayback() {
audioModel.setCallbacks(audioPlaybackCompleted, audioIndexChanged);
}
void audioIndexChanged(int audInd) {
_audioIndex = audInd;
notifyListeners();
}
}
Initialization:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<STTModel>(create: (context) => STTModel()),
ChangeNotifierProvider<QuestionModel>(
create: (context) => QuestionModel()),
ChangeNotifierProvider<AudioModel>(create: (context) => AudioModel()),
ChangeNotifierProxyProvider3<STTModel, QuestionModel, AudioModel,
CommonModel>(
create: (BuildContext context) => CommonModel(
Provider.of<STTModel>(context, listen: false),
Provider.of<QuestionModel>(context, listen: false),
Provider.of<AudioModel>(context, listen: false)),
update:
(context, sttModel, questionModel, audioModel, commonModel) =>
CommonModel(sttModel, questionModel, audioModel))
],
child: MaterialApp(
title: 'Flutter Demo',
initialRoute: '/',
routes: {
'/': (context) => ScreenMainMenu(),
'/game': (context) => ScreenGame(),
}));
}
}
What would be the correct way to call (and pass values to)
ProxyProvider from its "sub"providers?
I'm not a big fan of "nested" Providers : it often leads to this kind of issues and doesn't ease the readability.
In my projects, I usually use a Provider for each Feature, which I declare and Consume at the lowest level possible.
In your case, I guess I'd juste have used your STTModel, QuestionModel and AudioModel and would have forgotten the idea of a CommonModel (whom only job is is to merge all your Providers I guess?).
You can still keep your logic, but you should take in consideration the following :
In your AudioModel class, update the method where the _audioIndex and add a notifyListeners()
class AudioModel extends ChangeNotifier {
//...
int get audioIndex => _audioIndex;
void updateIndex(int index) {
_audioIndex = index;
//The rest of your code
notifyListeners();
}
//...
}
The creation of your Providers looks alright, but consider updating the update method of your ChangeNotifierProxyProvider for something like that :
update: (_, sttModel, questionModel, audioModel) =>
commonModel!..update(sttModel, questionModel, audioModel),
and in your CommonModel
void update(SttModel sttModelUpdate, QuestionModel questionModelUpdate, AudioModel audioModelUpdate) {
audioModel = audioModelUpdate;
questionModel = questionModelUpdate;
sttModel = sttModelUpdate;
//Retrieve the index here from your audioModel
_audioIndex = audioModel.audioIndex;
notifyListeners();
}
This way, whenever you call your updateIndex method in your AudioModel class, the notifyListeners() will update the CommonModel and you'll have the _audioIndex up to date.
And then it should work fine, no need for your callback methods anymore !
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);
I have a Flutter project, with Provider.
I set a MultiProvider in main.dart
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => VehiclesProvider(),
),
],
child: MyApp(),
),
);
}
Here is my Vehicle Provider.
class VehiclesProvider with ChangeNotifier, DiagnosticableTreeMixin {
List<Vehicle> _vehicles = [];
late VehicleDataModel _selectedVehicle;
VehicleService _vehicleService = VehicleService();
UnmodifiableListView get vehicles => UnmodifiableListView(_vehicles);
dynamic get selectedVehicle => _selectedVehicle;
void getData() async {
final all = await _vehicleService.getAllVehicles();
_vehicles.addAll(all.response);
notifyListeners();
final selected =
await _vehicleService.getSelectedVehicleData(all.response.first.id);
_selectedVehicle = selected;
notifyListeners();
}
}
As you can see the idea is to store vehicles (coming from an API), then the user can select a vehicle (by default its the first). I followed the getData() with breakpoints, and everything seems to be working.
But
When I try to access the data... and I'm trying it multiple ways, I just can't. I only get this: []
I tried to get them with a button's onclick function like this:
getSelectedCar() {
final p = Provider.of<VehiclesProvider>(context, listen: false);
print('p.vehicles');
print(p.vehicles);
print(context.read<VehiclesProvider>().vehicles);
}
and in the layout like this:
Text(jsonEncode(Provider.of<VehiclesProvider>(context).vehicles),),
Each way the only thing I get is the empty array. I'm showing a toast message when the data from the API is here, and only then I navigate to the screen where I want to access it, and try to push the button to get it, but nothing seems to help.
Change it to :
final p = Provider.of<VehiclesProvider>(context, listen: true); // make listen : true.
So you get updated when the list is populated, in other words, when you notifyListeners().
I've looked around in StackoverFlow and was not able to find myself a solution to this.
Scenario:
I have a Flutter SharedPreferences Provider with ChangeNotifier Class, that will get updated with the current Logged In User info.
Simplified content:
class SharedPreferences {
final String userId;
final String userName;
SharedPreferences({
#required this.userId,
#required this.userName,
});
}
class SharedPreferencesData with ChangeNotifier {
var _sharedPreferencesData = SharedPreferences(
userId: 'testUserId',
userName: 'testUserName',
);}
And a database.dart file with Class containing DataBaseServices to get FireStore Streams from Snapshots:
class DatabaseService {
final CollectionReference companiesProfilesCollection =
Firestore.instance.collection('companiesProfiles');
List<CompanyProfile> _companiesProfilesFromSnapshot(QuerySnapshot snapshot) {
return snapshot.documents.map((doc) {
return CompanyProfile(
docId: doc.documentID,
companyName: doc.data['companyName'] ?? '',
maxLocationsNumber: doc.data['maxLocationsNumber'] ?? 0,
maxUsersNumber: doc.data['maxUsersNumber'] ?? 0,
);
}).toList();
}
Stream<List<CompanyProfile>> get getCompaniesProfiles {
return companiesProfilesCollection
.where('userId', isEqualTo: _userIdFromProvider)
// My problem is above -----
.snapshots()
.map(_companiesProfilesFromSnapshot);
}
}
I Don't want to fetch the entire Stream data as it could be massive for other Streams, I just want to pass the userID under .where('userId', isEqualTo:_userIdFromProvider).
I couldn't access the context in this class to get the data from the Provider
Couldn't send the userId to getCompaniesProfiles getter, as getter don't take parameters
And if I convert this getter to a regular method, I wont be able to send the userID to it, as this has to run under void main() {runApp(MyApp());} / return MultiProvider(providers: [ and By then I cannot call fetch the sharedPreferences with a context that does not contain the provider info ...
Couldn't figure out how to receive the context as a constructor in this class, when I did, I got the following Only static members can accessed in initializers in class DatabaseService.
I'm still a beginner, so I would appreciate if you could share with me the best approach to handle this.
Thank you!
*********** Re-Edited by adding the below: **************
I'm trying to implement the same scenario, here is my code:
Main file:
return MultiProvider(
providers: [
ChangeNotifierProvider<SpData>(
create: (context) => SpData(),
),
ProxyProvider<SpData, DS>(
create: (context) => DS(),
update: (ctx, spData, previousDS) {
print('ChangeNotifierProxyProvider RAN');
previousDS.dbData = spData;
return previousDS;
},
),
],
SP File:
class SP {
final String companyId;
SP({
#required this.companyId,
});
}
class SpData with ChangeNotifier {
var _sPData = SP(
companyId: '',
);
void setCompanyId(String cpID) {
final newSharedPreferences = SP(
companyId: cpID,
);
_sPData = newSharedPreferences;
print('_spData companyId:${_sPData.companyId}');
notifyListeners();
}
String get getCompanyId {
return _sPData.companyId;
}
}
DS file:
class DS with ChangeNotifier {
SpData dbData;
void printCompanyId() {
var companyId = dbData.getCompanyId;
print('companyId from DataBase: $companyId');
}
}
The SpData dbData; inside Class DS does not update. I've added the prints to figure out what is running and what is not. When I run my code, the print function in main.dart file print('ChangeNotifierProxyProvider RAN'); does not run.
What am I missing? Why ChangeNotifierProxyProvider is not being triggered, to update dbData inside DS file? Thanks!
You can use ProxyProvider for this purpose.
ProxyProvider is a provider that builds a value based on other providers.
You said you have a MultiProvider, so I guess you have SharedPreferencesData provider in this MultiProvider and then DatabaseService provider. What you need to do is use ProxyProvider for DatabaseService instead of a regular provider and base it on the SharedPreferencesData provider.
Here is an example:
MultiProvider(
providers: [
ChangeNotifierProvider<SharedPreferencesData>(
create: (context) => SharedPreferencesData(),
),
ProxyProvider<SharedPreferencesData, DatabaseService>(
create: (context) => DatabaseService(),
update: (context, sharedPreferencesData, databaseService) {
databaseService.sharedPreferencesData = sharedPreferencesData;
return databaseService;
},
dispose: (context, databaseService) => databaseService.dispose(),
),
],
child: ...
Here is what happens in the code snippet above:
ProxyProvider calls update everytime SharedPreferencesData changes.
DatabaseService gets its sharedPreferencesData variable set inside update.
Now that you have access to sharedPreferencesData inside the DatabaseService instance, you can do what you want easily.