Flutter Getx , Wait till all main binding load before Navigation - flutter

I am using getx on my project, I have a mainBianding page :
class MainBinding implements Bindings {
#override
Future<void> dependencies() async {
Get.putAsync<HiveService>(() => HiveService().init(), permanent: true);
Get.lazyPut<HomeController>(
() => HomeController(
dbclient: Get.find<HiveService>()),
);
}
}
I have a GETXService for initializing Hive
class HiveService extends GetxService {
late Box<Model> vBox;
Future<HiveService> init() async {
final appDocumentDirectory =
await path_provider.getApplicationDocumentsDirectory();
Hive
..init(appDocumentDirectory.path)
..registerAdapter<Model>(ModelAdaptor())
return this;
}
After lunching App HomePage and HomeController will be launched but I got this error:
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following message was thrown building Builder:
"HiveService" not found. You need to call "Get.put(HiveService())" or
"Get.lazyPut(()=>HiveService())"
The relevant error-causing widget was:
ScrollConfiguration
because Hive service is a future Is has a delay to be loaded but I used Future<void> dependencies() async on this binding class. How do I have to wait to be sure HiveService load completely and after that Home Page load?
I am using MainBinding Inside GetMaterialApp;
Future main() async {
await MainBinding().dependencies();
...
runApp(MyApp()
return GetMaterialApp(
debugShowCheckedModeBanner: false,
useInheritedMediaQuery: true,
locale: DevicePreview.locale(context),
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: Routes.HOME,
getPages: AppPages.pages,
initialBinding: MainBinding(),

test this
await Get.putAsync<HiveService>(() => HiveService().init(), permanent: true);

You need to run your service.init() function first. and then return the getx Service(HiveService) and complete to load this service to the memory.
#override
Future<void> dependencies() async {
await Get.putAsync< HiveService >(() async {
final HiveService service = HiveService();
await service.init();
return service;
});
Get.lazyPut<HomeController>(
() => HomeController(
dbclient: Get.find<HiveService>()),
);
}
}

Related

Injecting floor database dependency through GETX is not working : Flutter

I am new at using Getx for state management. I am trying to inject the dependency of my DB instance in main by Getx through initial binding I am using the floor database. can anyone help me with this. where I went wrong?
this is how my register function looks like
void registerdbInstance() {
Get.lazyPut(<AppDatabase>() async =>
{await $FloorAppDatabase.databaseBuilder('app_database.db').build()});
}
this is how my main app widget looks like
#override
Widget build(BuildContext context) {
return GetMaterialApp(
title: appName,
initialBinding: BindingsBuilder.put(() => registerdbInstance),
theme: ThemeData(
fontFamily: 'Montserrat',
backgroundColor: sdWhiteColor,
colorScheme: ColorScheme.fromSwatch()
.copyWith(primary: sdPrimaryColor, secondary: sdSecondaryColor),
),
getPages: routeList,
home: ServiceDeskHome(),
);
initialBinding: BindingsBuilder.put(() => registerdbInstance),
this is how I am trying to access this dependency
var db = Get.find();
The problem is that Getx is not able to find the dependency.
"AppDatabase" not found. You need to call "Get.put(AppDatabase())" or "Get.lazyPut(()=>AppDatabase())"
If i recall corectly when you are working with databases in getx you have to do it asynchronously and use Get.putAsync if you want to register an asynchronous instance (LINK). I would consider using fenix parameter as well.
I initialized it in this way.
it will initialize when the app starts, you can use it later anywhere by simply calling Get.find()
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Get.putAsync<AppDatabase>(permanent: true, () async {
final db = await $FloorAppDatabase
.databaseBuilder('todotask.db')
.build();
return db;
});
await GetStorage.init();
runApp(const MyApp());
}

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.

How to Navigate without context in flutter?

I ended up with using a static function but I need to do navigation and It gave me an error that no getter was found for context so I looked for a solution and found the GET package but when I tried to use it It gave me another error :
E/flutter ( 6078): [ERROR:flutter/lib/ui/ui_dart_state.cc(166)]
Unhandled Exception: NoSuchMethodError: The method 'push' was called on null.
My code:
void main() {
runApp(MyApp());
_MyAppState.autologin();
}
class _MyAppState extends State<MyApp> {
static autologin() async {
var userType;
var store = Firestore.instance;
var auth = FirebaseAuth.instance;
final FirebaseUser user = await auth.currentUser();
store.collection('Users').document(user.uid).get().then((value) {
userType = (value.data)['userType'];
if (userType == 'Student') {
Get.to(StudentsPage());
} else if (userType == 'Teacher') {
} else if (userType == 'Admin') {}
});
}
Create a navigator key
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
Assign it to MaterialApp
MaterialApp(
home: Home(),
navigatorKey: navigatorKey
),
Then push your routes by navigatorKey below
navigatorKey.currentState.push(MaterialPageRoute(
builder: (context) => AnotherPage(),
));
or
navigatorKey.currentState.pushNamed(routeName);
This solution is general if you want to navigate or to show Dialog without context using globalKey especially with Bloc or when your logic is separated from your UI part.
Firstly install this package:
Note: I'm using null safety version
get_it: ^7.2.0
Then create a separate file for your service locator:
service_location.dart
import 'package:get_it/get_it.dart';
GetIt locator = GetIt.instance;
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey =
new GlobalKey<NavigatorState>();
Future<dynamic> navigateTo(String routeName) {
return navigatorKey.currentState!.pushNamed(routeName);
}
void setupLocator() {
locator.registerLazySingleton(() => NavigationService());
}
void showMyDialog() {
showDialog(
context: navigatorKey.currentContext!,
builder: (context) => Center(
child: Material(
color: Colors.transparent,
child: Text('Hello'),
),
));
}
}
on main.dart:
void main() {
WidgetsFlutterBinding.ensureInitialized();
NavigationService().setupLocator();
runApp(MyApp());
}
// add navigatorKey for MaterialApp
MaterialApp(
navigatorKey: locator<NavigationService>().navigatorKey,
),
at your business logic file bloc.dart
define this inside the bloc class or at whatever class you want to use navigation inside
Then start to navigate inside any function inside.
class Cubit extends Cubit<CubitState> {
final NavigationService _navigationService = locator<NavigationService>();
void sampleFunction(){
_navigationService.navigateTo('/home_screen'); // to navigate
_navigationService.showMyDialog(); // to show dialog
}
}
Not: I'm using generateRoute for routing.

Where to handle Firebase Dynamic Links in Flutter?

I use Firebase dynamic links and also named routes. What I want is to install a global listener for the dynamic link events and forward to register page if a token is provided. In the code below I got the exception The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget. which means I have to put navigation code below the home: property of MaterialApp. But when doing this I had to implement the dynamic links event handler for earch route.
class MyApp extends StatelessWidget {
String title = "Framr";
#override
Widget build(BuildContext context) {
FirebaseDynamicLinks.instance.onLink(
onSuccess: (linkData) {
if (linkData != null) {
try {
Navigator.pushNamed(context, '/register', arguments: linkData);
// throws: The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.
} catch(e) {
print(e);
}
}
return null;
}
);
return MaterialApp(
title: "...",
home: LoginPage(),
routes: {
'/createEvent': (context) => CreateEventPage(),
'/showEvent': (context) => ShowEventPage(),
'/register': (context) => RegisterPage(),
},
);
}
}
I was able to get this work by following the example provided from the dynamic link README with the use of the no_context_navigation package or GlobalKey to workaround around the lack of context to call Navigator.pushNamed(...). Note: You don't have to use no_context_navigation. You can implement the no context routing yourself. Here's an example.
// Add this
import 'package:no_context_navigation/no_context_navigation.dart';
void main() {
runApp(MaterialApp(
title: 'Dynamic Links Example',
// Add this
navigatorKey: NavigationService.navigationKey,
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => MyHomeWidget(), // Default home route
'/helloworld': (BuildContext context) => MyHelloWorldWidget(),
},
));
}
class MyHomeWidgetState extends State<MyHomeWidget> {
.
.
.
#override
void initState() {
super.initState();
this.initDynamicLinks();
}
void initDynamicLinks() async {
FirebaseDynamicLinks.instance.onLink(
onSuccess: (PendingDynamicLinkData dynamicLink) async {
// Add this.
final NavigationService navService = NavigationService();
final Uri deepLink = dynamicLink?.link;
if (deepLink != null) {
// This doesn't work due to lack of context
// Navigator.pushNamed(context, deepLink.path);
// Use this instead
navService.pushNamed('/helloworld', args: dynamicLink);
}
},
onError: (OnLinkErrorException e) async {
print('onLinkError');
print(e.message);
}
);
final PendingDynamicLinkData data = await FirebaseDynamicLinks.instance.getInitialLink();
final Uri deepLink = data?.link;
if (deepLink != null) {
// This doesn't work due to lack of context
// Navigator.pushNamed(context, deepLink.path);
// Use this instead
navService.pushNamed('/helloworld', args: dynamicLink);
}
}
.
.
.
}
// pubspec.yaml
no_context_navigation: ^1.0.4

How to call sharedprefrence before first screen

I am implementing remember me option on the login screen, want to call shared preference before the widget is created. we have the one and only entry point that is the main function, but how we can call a function here to read primitive data (email/password).
void main() {
setupLocator();
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
theme: new ThemeData(fontFamily: 'OpenSans-Light'),
initialRoute: "/",
onGenerateRoute: Router.generateRoute,
));
}
reading bool value
Future<bool> read(String key) async {
final prefs = await SharedPreferences.getInstance();
return prefs.getbool(key);
}
I also try to run a asyn function before route
String firstNav;
void main() {
setupLocator();
readSharedPref();
if(firstNav!=null)
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
theme: new ThemeData(fontFamily: 'OpenSans-Light'),
initialRoute: firstNav,
onGenerateRoute: Router.generateRoute,
));
}
void readSharedPref() async {
Utiles df=Utiles();
String isRem=await df.read("remember");
if (isRem.contains("true")) {
firstNav='homeview';
} else {
firstNav='/';
}
}
You need to set your main function as async, and add an await and a line of code:
void main() async{
//Add this lines is necessary now that your main is async
WidgetsFlutterBinding.ensureInitialized();
//Now you have to "await" the readSharedPref() function
await readSharedPref();
// And here comes all your code
}
Instead of waiting waiting for sharedPreference to load before building any widgets, just show a loader widget with progress indicator until the shared preference is loaded, and when it's loaded, show the required view based on the value loaded from sharedPreference, here is how you can modify your code, (replace HomeView and RootView widgets with your respective widgets for your homeView and / routes)
void main() {
setupLocator();
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
theme: new ThemeData(fontFamily: 'OpenSans-Light'),
initialRoute: Loader(),
onGenerateRoute: Router.generateRoute,
));
}
class Loader extends StatefulWidget {
#override
_LoaderState createState() => _LoaderState();
}
class _LoaderState extends State<Loader> {
Widget firstNav;
#override
void initState() {
super.initState();
readSharedPref();
}
void readSharedPref() async {
Utiles df=Utiles();
String isRem=await df.read("remember");
if (isRem.contains("true")) {
setState(() {
// firstNav='homeview';
firstNav=HomeView(); // replace HomeView with the widget you use for homeview route
});
} else {
setState(() {
// firstNav='/';
firstNav=RootView(); // replace RootView with the widget you use for / route
});
}
}
#override
Widget build(BuildContext context) {
return firstNav != null ? firstNav : Center(child: CircularProgressIndicator(),);
}
}