Flutter RepositoryProvider and Hive LateInitializationError - flutter

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.

Related

How to call bloc inside initState method?

I need to call the fetchProfile() method and get the profileState.user data in the initState method right after the page opens. Tell me, how can I write this correctly, how can I correctly call Cubit inside the initState method?
#override
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) {
_emailDialog();
});
super.initState();
}
cubit
class ProfileCubit extends Cubit<ProfileState> {
final UserRepository _repository;
ProfileCubit(this._repository) : super(ProfileInitial());
Future fetchProfile() async {
try {
final User? user = await _repository.me();
if(user != null) {
emit(ProfileLoaded(user));
} else {
emit(ProfileError());
}
} catch (_) {
emit(ProfileError());
}
}
state
abstract class ProfileState {}
class ProfileInitial extends ProfileState {}
class ProfileLoaded extends ProfileState {
final User? user;
ProfileLoaded(this.user);
}
class ProfileError extends ProfileState {}
If your intention is to run the method fetchProfile directly when the widget (page in this case) will be built, I'd run the method when providing the bloc using cascade notation as such:
home: BlocProvider(
create: (_) => ProfileCubit()..fetchProfile(),
child: YourPageOrWidget(),
),
The fetchProfile() method will be called as soon as the Bloc/Cubit is created.
Note that by default, the cubit is created lazily, so it will be created when needed by a BlocBuilder or similar. You can toggle that so it isn't created lazily.
You can check the Readme of flutter_bloc. There is a full tutorial and you can learn a lot.
#override
void initState() {
super.initState();
context.read<ProfileCubit>().fetchProfile()
}
Wrap BlocListener for your widget tree. You can listen to ProfileLoaded state here and get the user data immediately.
BlocListener<ProfileCubit, ProfileState >(
listener: (context, state) {
// Do whatever you want.
},
child: Container(),
)

i have two blocs that must listen to each other, how do i pass one as a parameter to the other?

**Hello I am new to flutter and bloc architecture.
I am trying to build a simple quiz app that has a timer.
On the quiz page, I have two blocs, a counter cubit to navigate to the next question, and a triviabloc for quiz activities like answer selection.
I am using MultiBlovProvider to provide the blocs.
I need each bloc to communicate with each other. Since each of the blocs is a parameter to the other, how do I pass it in the multiblocprovider
?**
var bloc = TriviaBloc();
var con = CountDownController();
// ignore: close_sinks
var cubit = CounterCubit(
bloc: bloc, controller: con);
return MultiBlocProvider(
providers: [
BlocProvider<TriviaBloc>(
create: (context) => bloc,
),
BlocProvider<CounterCubit>(
create: (context) => cubit)
],
child:
QuestionScreen(trivia: questions),
);
the cubit
class CounterCubit extends Cubit<int> {
StreamSubscription sub;
CounterCubit({this.controller, this.bloc}) : super(0) {
sub = bloc.listen((state) {
if (state is AnswerCorrect || state is AnswerNotCorrect) {
controller.pause();
}
});
}
final TriviaBloc bloc;
final CountDownController controller;
void increment() => emit(state + 1);
#override
Future<void> close() {
sub?.cancel();
return super.close();
}
#override
void onChange(Change<int> change) {
print(change);
super.onChange(change);
}
}
the bloc that must listen to the cubit
class TriviaBloc extends Bloc<TriviaEvent, TriviaState> {
StreamSubscription sub;
TriviaBloc({this.cubit}) : super(TriviaInitial()) {
sub = cubit.listen(
(state) async* {
if (state != 0) {
yield TriviaInitial();
}
},
);
}
final CounterCubit cubit;
Stream<TriviaState> mapEventToState(TriviaEvent event) async* {
if (event is AnswerCLicked) {
print(event.answer);
if (event.answer == event.correctAnswer) {
yield AnswerCorrect();
} else {
yield AnswerNotCorrect();
}
}
if (event is NoAnswerChosen) {
yield ShowAnswer();
}
}
#override
Future<void> close() {
sub?.cancel();
return super.close();
}
}
Thank you
You pass one bloc as an argument to a 2nd bloc. Now, within the 2nd bloc, you can get values from the 1st bloc's state. This is an approach for that:
if (userBloc.state is AppSettled) {
achievements = (userBloc.state as AppSettled).achievements;
userBloc is the bloc that I passed to the 2nd bloc, AppSettled is a state of userBloc, and achievements is a variable defined within that state.
In order to pass data back, you can this answer
Not sure if this is what you are looking for, but this is what I do to make sure that if the user authorization state changes they JobListCubit actually triggers a route to authorization screen.
In my main.dart:
MultiBlocProvider(
providers: [
BlocProvider<UserAuthCubit>(
lazy: true,
create: (context) => UserAuthCubit(
UserAuthRepository(),
),
),
BlocProvider<JobListCubit>(
lazy: true,
create: (context) => JobListCubit(
jobListRepository: JobListRepository(),
userAuthCubit: BlocProvider.of<UserAuthCubit>(context),
)),
....
Then in my JobListCubit:
class JobListCubit extends Cubit<JobListState>
with HydratedMixin<JobListState> {
JobListState get initialState {
return initialState ?? JobListInitial();
}
final JobListRepository jobListRepository;
final UserAuthCubit userAuthCubit;
JobListCubit({this.jobListRepository, this.userAuthCubit})
: super(JobListInitial());
...
Hope this is what you were looking for. I am a novice and it took me a lot of time to find a solution...

Bloc listener not invoked without a delay

I have defined the following cubit.
#injectable
class AuthCubit extends Cubit<AuthState> {
final IAuthService _authService;
AuthCubit(this._authService) : super(const AuthState.initial());
void authCheck() {
emit(_authService.signedInUser.fold(
() => AuthState.unauthenticated(none()),
(user) => AuthState.authenticated(user),
));
}
}
But the BlocListener which listens to this bloc is not getting invoked even after emit is called. But everything works as expected when I add a zero delay before the emit call.
Future<void> authCheck() async {
await Future.delayed(Duration.zero);
emit(_authService.signedInUser.fold(
() => AuthState.unauthenticated(none()),
(user) => AuthState.authenticated(user),
));
}
I tried out this delay because for other events which made some backend call (with some delay) emit worked perfectly. But I'm pretty sure this is not how it should work. Am I missing something here?
EDIT:
Adding the SplashPage widget code which uses BlocListener.
class SplashPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocListener<AuthCubit, AuthState>(
listener: (context, state) {
print(state);
},
child: Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
);
}
}
Place where authCheck() is called,
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<AuthCubit>(
create: (_) => getIt<AuthCubit>()..authCheck(),
),
],
child: MaterialApp(
....
),
);
}
}
and the AuthState is a freezed union
#freezed
abstract class AuthState with _$AuthState {
const factory AuthState.initial() = _Initial;
const factory AuthState.authenticated(User user) = _Authenticated;
const factory AuthState.unauthenticated(Option<AuthFailure> failure) = _Unauthenticated;
const factory AuthState.authInProgress() = _AuthInProgress;
}
Also, when I implemented a bloc (instead of Cubit) with the same functionality, everything worked as expected.
Without the delay the emit is called directly from the create method of the provider. This means that the listener is not (completely) built yet and thus there is no listener to be called when you emit the state.
So by adding the delay you allow the listener to subscribe to the stream first and thus it gets called when you emit the new state.
For me, the delay does not work perfectly. So I found this solution, maybe help someone:
#override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback((_) async {
await myCubit.doSomethingFun();
});
}
And #Pieter is right, listener only be invoked when the widget is built.

Flutter Bloc is not rebuilding state

I have implement a this simple bloc with the flutter_bloc package:
class MainBloc extends Bloc<MainEvent, MainState> {
#override
MainState get initialState => Init();
#override
Stream<MainState> mapEventToState(MainEvent event) async* {
if (event is Event) {
yield* _mapEventToState();
}
}
Stream<MainState> _mapEventToState() async* {
final loadState = Load();
print("Yield state: $loadState");
yield loadState;
await sleep(Duration(seconds: 1));
final initState = Init();
print("Yield state: $initState");
yield initState;
}
}
with this event:
abstract class MainEvent {}
class Event extends MainEvent {}
and this state:
abstract class MainState {}
class Load extends MainState {}
class Init extends MainState {}
My UI looks like this:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
home: BlocProvider(
create: (context) => MainBloc(),
child: Scaffold(
appBar: AppBar(),
body: BlocBuilder<MainBloc, MainState>(
builder: (context, state) {
print("Build state: $state");
if (state is Init) {
return MaterialButton(
onPressed: () => BlocProvider.of<MainBloc>(context).add(Event()),
child: Text("Press"),
);
} else {
return Text("Loading");
}
},
),
),
),
);
}
}
Unfortunately if I add my Event with the MaterialButton, the Load() state gets ignored. The UI doesn't rebuild the Load state. The output is the following:
I/flutter ( 1955): Yield state: Instance of 'Load'
I/flutter ( 1955): Yield state: Instance of 'Init'
I/flutter ( 1955): Build state: Instance of 'Init'
It's because you're using await sleep(Duration(seconds: 1)); (the await statement is of no use here, it is a synchronous function).
The sleep function pauses the execution of the main thread, meaning it will also block the rebuilding of the Main page of your app, and so it doesn't get the chance to rebuild because of the new Load state. Once freezing the app is over, it immediately gets a new state, so that's why you will never see the loading page.
Use await Future.delayed(Duration(seconds: 1)); instead, which pauses the execution of the rest of the _mapEventToState function but doesn't block the main thread and so your MainPage gets a rebuild.

Flutter - How to pass user data to all views

I'm new to the flutter world and mobile app development and struggling with how I should pass user data throughout my app.
I've tried several things, but none seem great and I'm sure there are best practice patterns I should be following.
Because it makes examples easier, I'm using firebase for authentication.
I currently have a separate route for logging in. Once I'm logged in I want the User model in most views for checking permissions on what to show, displaying user info in the drawer, etc...
Firebase has an await firebaseAuth.currentUser(); Is it best practice to call this everywhere you might need the user? and if so, where is the best spot to place this call?
The flutter codelab shows a great example of authenticating users before allowing writes. However, if the page needs to check auth to determine what to build, the async call can't go in the build method.
initState
One method I've tried is to override initState and kick off the call to get the user. When the future completes I call setState and update the user.
FirebaseUser user;
#override
void initState() {
super.initState();
_getUserDetail();
}
Future<Null> _getUserDetail() async {
User currentUser = await firebaseAuth.currentUser();
setState(() => user = currentUser);
}
This works decent but seems like a lot of ceremony for each widget that needs it. There is also a flash when the screen loads without the user and then gets updated with the user upon the future's completion.
Pass the user through the constructor
This works too but is a lot of boilerplate to pass the user through all routes, views, and states that might need to access them. Also, we can't just do popAndPushNamed when transitioning routes because we can't pass a variable to it. We have to change routes similar to this:
Navigator.push(context, new MaterialPageRoute(
builder: (BuildContext context) => new MyPage(user),
));
Inherited Widgets
https://medium.com/#mehmetf_71205/inheriting-widgets-b7ac56dbbeb1
This article showed a nice pattern for using InheritedWidget. When I place the inherited widget at the MaterialApp level, the children aren't updating when the auth state changed (I'm sure I'm doing it wrong)
FirebaseUser user;
Future<Null> didChangeDependency() async {
super.didChangeDependencies();
User currentUser = await firebaseAuth.currentUser();
setState(() => user = currentUser);
}
#override
Widget build(BuildContext context) {
return new UserContext(
user,
child: new MaterialApp(
title: 'TC Stream',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new LoginView(title: 'TC Stream Login', analytics: analytics),
routes: routes,
),
);
}
FutureBuilder
FutureBuilder also seems like a decent option but seems to be a lot of work for each route. In the partial example below, _authenticateUser() is getting the user and setting state upon completion.
#override
Widget build(BuildContext context) {
return new FutureBuilder<FirebaseUser>(
future: _authenticateUser(),
builder: (BuildContext context, AsyncSnapshot<FirebaseUser> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return _buildProgressIndicator();
}
if (snapshot.connectionState == ConnectionState.done) {
return _buildPage();
}
},
);
}
I'd appreciate any advice on best practice patterns or links to resources to use for examples.
I'd recommend investigating inherited widgets further; the code below shows how to use them with asynchronously updating data:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(new MaterialApp(
title: 'Inherited Widgets Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new Scaffold(
appBar: new AppBar(
title: new Text('Inherited Widget Example'),
),
body: new NamePage())));
}
// Inherited widget for managing a name
class NameInheritedWidget extends InheritedWidget {
const NameInheritedWidget({
Key key,
this.name,
Widget child}) : super(key: key, child: child);
final String name;
#override
bool updateShouldNotify(NameInheritedWidget old) {
print('In updateShouldNotify');
return name != old.name;
}
static NameInheritedWidget of(BuildContext context) {
// You could also just directly return the name here
// as there's only one field
return context.inheritFromWidgetOfExactType(NameInheritedWidget);
}
}
// Stateful widget for managing name data
class NamePage extends StatefulWidget {
#override
_NamePageState createState() => new _NamePageState();
}
// State for managing fetching name data over HTTP
class _NamePageState extends State<NamePage> {
String name = 'Placeholder';
// Fetch a name asynchonously over HTTP
_get() async {
var res = await http.get('https://jsonplaceholder.typicode.com/users');
var name = json.decode(res.body)[0]['name'];
setState(() => this.name = name);
}
#override
void initState() {
super.initState();
_get();
}
#override
Widget build(BuildContext context) {
return new NameInheritedWidget(
name: name,
child: const IntermediateWidget()
);
}
}
// Intermediate widget to show how inherited widgets
// can propagate changes down the widget tree
class IntermediateWidget extends StatelessWidget {
// Using a const constructor makes the widget cacheable
const IntermediateWidget();
#override
Widget build(BuildContext context) {
return new Center(
child: new Padding(
padding: new EdgeInsets.all(10.0),
child: const NameWidget()));
}
}
class NameWidget extends StatelessWidget {
const NameWidget();
#override
Widget build(BuildContext context) {
final inheritedWidget = NameInheritedWidget.of(context);
return new Text(
inheritedWidget.name,
style: Theme.of(context).textTheme.display1,
);
}
}
I prefer to use Services with Locator, using Flutter get_it.
Create a UserService with a cached data if you like:
class UserService {
final Firestore _db = Firestore.instance;
final String _collectionName = 'users';
CollectionReference _ref;
User _cachedUser; //<----- Cached Here
UserService() {
this._ref = _db.collection(_collectionName);
}
User getCachedUser() {
return _cachedUser;
}
Future<User> getUser(String id) async {
DocumentSnapshot doc = await _ref.document(id).get();
if (!doc.exists) {
log("UserService.getUser(): Empty companyID ($id)");
return null;
}
_cachedUser = User.fromDocument(doc.data, doc.documentID);
return _cachedUser;
}
}
Then create create a Locator
GetIt locator = GetIt.instance;
void setupLocator() {
locator.registerLazySingleton(() => new UserService());
}
And instantiate in main()
void main() {
setupLocator();
new Routes();
}
That's it! You can call your Service + cachedData everywhere using:
.....
UserService _userService = locator<UserService>();
#override
void initState() {
super.initState();
_user = _userService.getCachedUser();
}
I crashed into another problem because of this problem you can check it out here
So the solution I came up with is a bit untidy,I created a separate Instance dart page and imported it to every page.
GoogleSignInAccount Guser = googleSignIn.currentUser;
FirebaseUser Fuser;
I stored the user there on login and checked on every StateWidget if it was null
Future<Null> _ensureLoggedIn() async {
if (Guser == null) Guser = await googleSignIn.signInSilently();
if (Fuser == null) {
await googleSignIn.signIn();
analytics.logLogin();
}
if (await auth.currentUser() == null) {
GoogleSignInAuthentication credentials =
await googleSignIn.currentUser.authentication;
await auth.signInWithGoogle(
idToken: credentials.idToken,
accessToken: credentials.accessToken,
);
}
This is my old code I did cleaned it up on my current app but I don't have that code now in handy. Just check out for null user and log it in again
I did it for most of the Firebase instances too because I have more than 3 pages on my app and Inherited Widgets was just too much work
You can use the GetX package to check whether or not the user is logged in, get user data and have it accessible throughout your app
For my lazy mathod,
i just create new file like userdata.dart and then put any variable on it for example like dynamic Profile = null
inside userdata.dart
//only put this or anything u want.
dynamic Profile = null;
at startingpage.dart
//import that file
import '../userdata.dart';
class startingpage extends ...{
...
//set data to store..
Profile = 'user profile';
...
}
to use the data just declare and use in
anotherpage.dart
//import that file
import '../userdata.dart';
class anotherpage extends...{
...
}
class .. State ...{
...
//set the data to variable
dynamic userdata = Profile;
print('this is my lazy pass data' + userdata.toString());
...
}