How do I automatically route when in Flutter from within main()? - flutter

I implemented a third-party "listener" in my Flutter ios app. I have two goals:
Whenever the listener receives an event, route to LandingPage;
Pass to LandingPage the values captured by the listener -- globalReferralData.
Values should also be written to local variables on disk. The screen/page that opens should be able read that variable.
Because the listener is in main(), there is no context.
I also cannot use Get for GetPage, because it seems to collide with using GoRouter as it is used and defined throughout the rest of the app in the nav.dart file.
ReferralData globalReferralData;
List<BuildContext> buildContextList = [];
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FFAppState(); // Initialize FFAppState
GetSocial.addOnInitializedListener(() => {
// GetSocial SDK is ready to use
});
runApp(MaterialApp(
home: MyApp(),
routes: {
'/landingPage': (context) => const LandingPageWidget(),
},
navigatorKey: locator<NavigationService>().navigatorKey));
registerListeners();
}
void registerListeners() {
Invites.setOnReferralDataReceivedListener((received) {
globalReferralData = received;
print(globalReferralData);
print(globalReferralData.linkParams);
print(globalReferralData.linkParams['referralID']);
// pass value and open route -- this line failed to do anything
Navigator.pushNamed(context, '/landingPage');
// showAlert(buildContextList.last, 'Referral Data Received', '$received');
});
}

Based on your comment
... generate state from outside build or State and then access it if I can only route that way. Ideally, I can route from within main().....
Yes it's possible. That's with reference to routing without BuildContext. The following is the mechanism behind Navigation in Stacked Architecture.
In summary, you provide MaterialApp with your own navigatorKey and use that key to navigate anywhere in the Flutter code. A clean way to do this is to use dependency injection. Have a service provided by GetIt package that will make the navigatorKey accessible to the entire Flutter app.
In details, we can use the following 4 steps:
1. NavigationService and key
Have a navigation.service.dart file with a navigatorKey. This key will have the type and value of GlobalKey<NavigatorState>
import 'package:flutter/material.dart';
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
}
This NavigationService class can do other things but let's leave it like this for now.
2. GetIt package
The package provides some mechanism to make services available app-wide. You actually initialise it before the runApp() call just as you do with Firebase for example.
Install the package. Run the following command.
flutter pub add get_it
Then, have a file, let's say app.locator.dart with the following.
import 'package:get_it/get_it.dart';
import 'navigation.service.dart';
final locator = GetIt.instance;
void setupLocator {
locator.registerLazySingleton(() => NavigationService());
}
Then in main.dart, call setupLocator() before runApp(). Based on the main method you provided:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FFAppState(); // Initialize FFAppState
GetSocial.addOnInitializedListener(() => {
// GetSocial SDK is ready to use
});
setupLocator(); //add this line
runApp(MyApp());
registerListeners();
}
3. Key and MaterialApp
Attach the navigatorKey from the NavigationService (obtained from locator) to the navigatorKey on the topmost MaterialApp.
So in whatever file where you defined MaterialApp, have something like this
// ...
return MaterialApp(
navigatorKey: locator<NavigationService>().navigatorKey,
// ...
);
4. Routing without BuildContext
In the registerListeners() function (called from main and not having access to BuildContext), you navigate with the navigatorKey. You can also use the context attached to the navigatorKey to showAlert too. Change your registerListeners() function to the following
void registerListeners() {
Invites.setOnReferralDataReceivedListener((received) {
globalReferralData = received;
print(globalReferralData);
print(globalReferralData.linkParams);
print(globalReferralData.linkParams['referralID']);
// instead of navigating with Navigator,
// Navigator.pushNamed(context, '/landingPage');
// navigate with the navigatorKey
locator<NavigationService>().navigatorKey.currentState!.pushNamed('/landingPage');
// You can as well use the key's context to showAlert.
// so instead of the following
// showAlert(buildContextList.last, 'Referral Data Received', '$received');
// You can have
showAlert(
locator<NavigationService>().navigatorKey.currentContext!,
'Referral Data Received',
'$received',
);
});
}
This way you have access to routing without the BuildContext and can use it in any other Dart file in the same project.
And hence also you've ideally routed from within main.
The above doesn't invalidate any routing with Navigator.of(context).navigate... in other parts of the app as it'll still be the same NavigationState that is been used by Flutter.
Update
I've updated the snippet of step 4 above.
From your comment
I'm unclear how from within main() which is where I put the listener class to capture the globalReferralData how I can pass that based on the example above to then route....
If you carry step 4 well above (and all the other steps) the problem should be solved. It should be solved because your main method calls this registerListeners() function, so that should do the trick.
Do you now understand?
It's also good that you called registerListeners after runApp, that way you're sure the navigatorKey must have been attached to MaterialApp.
...
That should solve one part of the problem, that's navigation. The above is just minimalist setup to serve the purpose you want. You can go further to use the entire StackedServices and or StackedApp if you wish.
The other issue about setState not working in main, it's really not possible. It's just natural. main is an entry point for compiled code and not a StatefulWidget. I'm supposing that if the above routing works for you, you might not have the issue of setState again.
But if you still need something, I propose you notifyListeners. Other state management architectures, not only Stacked, use notifyListeners (a method from ChangeNotifier) to update UI from outside StatefulWidget.
I guess you would have your way around this. And you could still go ahead with the View and ViewModel classes of Stacked and call notifyListeners in a viewModel (maybe that of a splash screen) when the globalReferredData is received.

Related

how can I route using GoRouter in Flutter without context?

The problem I want to solve:
My app which uses GoRouter needs to be able to route to a named route from within main(). Since most routing is of the form 'context.go' I cannot do so within main.
Background
My app uses GoRouter. The ease with which GetX had let me define named routes and pass parameters from main() was perfect.
However, GetX and GoRouter eventually causes problems for me. GoRouter would eventually have no context in other parts of the app.
If there were a way to have them co-exist simply, I'd be open to it.
I had used the service locator pattern with the GetIt package to associate with a navigatorKey. It would work when I tested it -- but this involved creating two MaterialApps.
However, this app uses GoRouter which doesn't seem to use the navigatorKey.
I would like to go to a specific route from within main (). It seems like the service locator pattern could work for GoRouter as it did with Navigator 2.0 for MaterialApp -- but I can't find an example of how to do so.
More detailed context:
Here is what I have currently in main().
You can see the key challenge I have is that the listener for the data parameters being passed in lives in main (I got this from the third-party SDK -- I don't need it to be in main but it needs to listen regardless of the state of the app).
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FFAppState(); // Initialize FFAppState
GetSocial.addOnInitializedListener(() => {
// GetSocial SDK is ready to use
});
setupLocator();
runApp(MyApp());
locator<LandingPageData>().referralID = "defaultReferralID";
registerListeners();
}
void registerListeners() {
Invites.setOnReferralDataReceivedListener((received) {
globalReferralData = received;
print(globalReferralData);
print(globalReferralData.linkParams);
print("listener - socialdata");
String passedReferralID =
globalReferralData.linkParams['referralID'].toString();
String passedCreatorID =
globalReferralData.linkParams['creatorID'].toString();
String passedCampaignID =
globalReferralData.linkParams['\$campaign_id'].toString();
print(passedReferralID);
print(passedCreatorID);
print(passedCampaignID);
// How can I route to a named Route?
locator<LandingPageData>().referralID = passedReferralID;
locator<LandingPageData>().creatorID = passedCreatorID;
locator<LandingPageData>().campaignID = passedCampaignID;
});
}
Here is what the locator.service.dart looks like:
final locator = GetIt.instance;
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// final GlobalKey<ScaffoldMessengerState> navigatorKey = GlobalKey<ScaffoldMessengerState>();
}
The above worked when I could attach to a navigatorKey and then navigate from within the listener. But that doesn't seem to work since the rest of the application uses GoRouter.
static BuildContext? get ctx => myGoRouter.routerDelegate.navigatorKey.currentContext;
you can get context in your NavigationService in this way and use it like
NavigationService.ctx?.go(...)
the problem you may face is that ctx will be null on app state till your first page starts to be built. In the case your listener has a data while ctx is still null, routing won't work. but you can handle this situation like:
define a global tempPageToGo in main func or a service and
var _ctx = NavigationService.ctx;
if(_ctx == null) {
tempPageToGo = anyPageDataYouWant;
while((await Future.delayed(Duration(seconds: 1))) == null) {
if(_ctx != null) {
_ctx!.go(...);
break;
}
}
} else _ctx!.go(...);
Unluckily, if I were you, I'd either drop the usage of GetX or of GoRouter.
Actually, I'd just drop GetX.
The reason is that GetX performs magic under the hood that lifts the developer the responsibility and usage of BuildContext, but that's clearly an anti-pattern, as the built-in navigation from Flutter clearly uses context: think of Navigator.of, for example.
GoRouter is built around context, and simplifies a lot of the implementations needed to perform "Navigator 2.0" actions.
If you're trying to implement deep linking, your MaterialApp should look like this in your root widget:
return MaterialApp.router( // Flutter's Router 2.0 usage
title: 'MyApp',
routeInformationProvider: myGoRouter.routeInformationProvider,
routeInformationParser: myGoRouter.routeInformationParser,
routerDelegate: myGoRouter.routerDelegate,
);
If GetX enables you to put myGoRouter there, then you should be good to go. But as I said before, everytime you need explicit navigation, you need context.
I'm in researching to adopt go_router in my project, and i was also stuckled for this usecase ( in my case i tried to prove that i can navigate from deferred link that callback from appsflyer SDK ).
For solution, like that go_router allows us to either navigate from context that is below the router declaration or from the redirect state. So we can wrap up all the state that effect the navigation on that.
This is how i redirect from appRouterState
redirect: (GoRouterState state) {
String? redirection(GoRouterState state) {
final appRouterState = ref.read(appRouterStateNotifierProvider);
final isAuthed = appRouterState.email != null;
if (appRouterState.deferredLink != state.location && appRouterState.deferredLink != null) {
return appRouterState.deferredLink;
}
if (state.location != '/login' && !isAuthed) return '/login';
if (state.location == '/login' && isAuthed) return '/';
return null;
}
final result = redirection(state);
return result;
},
In your case, you may implement setOnReferralDataReceivedListener in the appRouterStateProvider or something. And use it for refreshListenable param in the GoRouter constructor.
Hope this helps.

How to attend best practice for not using UI code in the Controller with GetX flutter when I need to show a Dialog if my task complete.?

For a simple Email login with OTP code I have a structure as follows.
View
await _signUpCntrl.signUp(email, password);
Controller
_showOtpDialog(email);
_showOtpDialog func
return Get.dialog(
AlertDialog(
So the thing is _showOtpDialog function is inside a controller file. ie. /Controllers/controller_file.dart
I want do something like a blocListener, call the _showOtpDialog from a screen(view) file on signup success. (also relocate the _showOtpDialog to a view file)
Using GetX I have to use one of the builders either obs or getbuilder. Which is I think not a good approach to show a dialog box.
On internet it says Workers are the alternative to BlocListener. However Workers function resides on Controller file and with that the dialog is still being called on the controller file.
As OTP dialog will have its own state and a controller I wanted to put it inside a /view/viewfile.dart
How do I obtain this?
I tried using StateMixin but when I call Get.dialog() it throw an error.
visitChildElements() called during build
Unlike BLoC there's no BlocListener or BlocConsumer in GetX.
Instead GetX has RxWorkers. You can store your response object in a Rx variable:
class SomeController extends GetxController{
final response= Rxn<SomeResponse>();
Future<void> someMethod()async{
response.value = await someApiCall();
}
}
And then right before the return of your widget's build method:
class SomeWidget extends StatelessWidget{
final controller = Get.put(SomeController());
#override
Widget build(BuildContext context){
ever(controller.response, (SomeResponse res){
if(res.success){
return Get.dialog(SuccessDialog()); //Or snackbar, or navigate to another page
}
....
});
return UI();
}
First thing, you will need to enhance the quality of your question by making things more clearly. Add the code block and the number list, highlight those and making emphasize texts are bold. Use the code block instead of quote.
Seconds things, Depends on the state management you are using, we will have different approaches:
Bloc (As you already added to the question tag). By using this state management, you controller ( business logic handler) will act like the view model in the MVVM architecture. In terms of that, You will need to emit a state (e.g: Sent success event). Afterward, the UI will listen to the changes and update it value according to the event you have emitted. See this Bloc example
GetX (As your code and question pointed out): GetX will acts a little bit different. you have multiple ways to implement this:
Using callbacks (passed at the start when calling the send otp function)
Declare a general dialog for your application ( this is the most used when it comes to realization) and calling show Dialog from Bloc
Using Rx. You will define a Reactive Variable for e.g final success = RxBool(true). Then the view will listen and update whenever the success changes.
controller.dart
class MyController extends GetxController {
final success = RxBool(false);
void sendOtp() async {
final result = await repository.sendOTP();
success.update((val) => {true});
}
}
view.dart
class MyUI extends GetView<MyController> {
#override
Widget build(BuildContext context) {
ever(controller.success, (bool success) {
// This will update things whenever success is updated
if (success) {
Get.dialog(AlertDialog());
}
});
return Container();
}
}

Flutter: Invoking Provider method in another class outside of the widget tree

I am currently using Provider in my Flutter project to manage the authentication state of my mobile app.
The model for AuthProvider is as follow:
lib/models/auth_provider.dart
class AuthProvider with ChangeNotifier {
// Some methods and properties are removed for simplicity.
// ...
bool loggedIn;
void allowAccess() {
loggedIn = true;
notifyListeners();
}
void revokeAccess() {
loggedIn = false;
notifyListeners();
}
}
The application uses some services from another class to check the validity of the authentication token.
If the the token is not valid anymore, the method in the service in another class will need to revoke the access:
lib/services/auth_services.dart
import 'package:exampleapp/shared/global_context.dart' as global_context;
class AuthService {
// Some methods and properties are removed for simplicity.
// ...
void checkValidity() {
// ...
if(notValid) {
// Use provider to revoke access
Provider.of<AuthProvider>(
global_context.GlobalContext.globalContext!, listen: false)
.revokeAccess();
}
}
}
To achieve this (since there is no context outside of the widget tree), the app uses a global context to allow the services file to invoke the Provider method:
lib/shared/global_context.dart
import 'package:flutter/material.dart';
class GlobalContext {
static BuildContext? globalContext;
}
And add the following line in the build method of the widget that might involves auth state change:
global_context.GlobalContext.globalContext = context;
I read that we're not advised to access Provider outside the widget tree and I don't think using a GlobalContext is the best practice. Is there any other way that I could do this by using Provider?
P/S: I'm still learning Flutter, please comment below if any clarification is needed.
I usually use the get_it package, It gives the ability to call Provider without needing to specifiy a particular context.
https://pub.dev/packages/get_it
First I would call setupLocator()
import 'package:get_it/get_it.dart';
GetIt locator = GetIt.instance;
void setupLocator() {
locator
.registerLazySingleton(() => AuthProvider());
}
Then use it like this..
class SomeClass {
final _provider = locator<AuthProvider>();
void someMethod(){
_provider.revokeAccess();
}
}
This is a very good and important question obviously there are tons of approaches that we can follow here, you can use a global context but you have to make sure it's always the correct context so whenever you push/pop different routes you have to also sync the context
One easy approach i think would be to add the context to checkValidity() function as a parameter
Also i would recommend checking out stacked state management solution https://pub.dev/packages/stacked its perfect for situations like this.

Load data asynchronously into ChangeNotifier model in Flutter

in my Flutter application I have a widget
class HomeScreen extends StatelessWidget
that uses a model
class HomeScreenModel extends ChangeNotifier
These two objects are tied together using a ChangeNotifierProvider.
When the application loads the HomeScreen widget I would like to call a custom init() function of HomeScreenModel to load asynchronously some data from the disk into the model and then notify the listener with notifyListeners() function. This should be done once.
What's the right place to call this init() function?
As far as I know, for a stateless widget there are no lifecycle functions called only once. I'm pretty sure, though, that the constructor of HomeScreenModel is called only once.
Is it safe to call the async HomeScreenModel.init() function from its own constructor?
Is there any best practice on how to asynchronously load data into a model implemented as a ChangeNotifier?
Thanks to all!
After a bit of searching and tests I choose to call the async init function from the HomeScreenModel constructor. So I have
HomeScreenModel(BuildContext context) {
var initFuture = init(context);
initFuture.then((voidValue) {
_log.d('init finished');
state = HomeScreenModelState.initialized;
notifyListeners();
});
}
and the init function prototype is
Future<void> init(BuildContext context) async
I have found that another way to do this is to use a StatefulWidget and call the async init from the
initState()
function. This function is called only once so is like the ChangeNotifier constructor.
As of now I'm not using StatefulWidgets because it seems to me that they create a sort of strong coupling between ui and business logic. So as of now the above solution seems fine to me.
I hope it can help someone
Another way to call this function would be to put the init() call into the create function of the provider.
runApp(ChangeNotifierProvider(
create: (context) {
var model = TestModel();
model.init();
return model;
},
child: TestApp()));

Fully restart(discard and recreate) the whole flutter app programmatically [duplicate]

In production mode, is there a way to force a full restart of the application (I am not talking about a hot reload at development time!).
Practical use cases:
At initialization process the application detects that there is no network connection. The lack of network connectivity might have prevented a correct start up (e.g. loading of external resource such as JSON files...).
During the initial handshaking, new versions of some important resources need to be downloaded (kind of update).
In both use cases, I would like the application to proceed with a full restart, rather than having to build a complex logic at the ApplicationState level.
You could wrap your whole app into a statefulwidget. And when you want to restart you app, rebuild that statefulwidget with a child that possess a different Key.
This would make you loose the whole state of your app.
import 'package:flutter/material.dart';
void main() {
runApp(
RestartWidget(
child: MaterialApp(),
),
);
}
class RestartWidget extends StatefulWidget {
RestartWidget({this.child});
final Widget child;
static void restartApp(BuildContext context) {
context.findAncestorStateOfType<_RestartWidgetState>().restartApp();
}
#override
_RestartWidgetState createState() => _RestartWidgetState();
}
class _RestartWidgetState extends State<RestartWidget> {
Key key = UniqueKey();
void restartApp() {
setState(() {
key = UniqueKey();
});
}
#override
Widget build(BuildContext context) {
return KeyedSubtree(
key: key,
child: widget.child,
);
}
}
In this example you can reset your app from everywhere using RestartWidget.restartApp(context).
The flutter_phoenix package is based on RĂ©mi Rousselet's answer, making it even simpler.
void main() {
runApp(
Phoenix(
child: App(),
),
);
}
Then when you need to restart the app, just call:
Phoenix.rebirth(context);
I developed the restart_app plugin to restart the whole app natively.
Update:
For anyone who get this exception:
MissingPluginException(No implementation found for method restartApp on channel restart)
Just stop and rebuild the app.
You can also use the runApp(new MyWidget) function to do something similar
This is what this function does:
Inflate the given widget and attach it to the screen.
The widget is given constraints during layout that force it to fill the entire screen. If you wish to align your widget to one side of the screen (e.g., the top), consider using the Align widget. If you wish to center your widget, you can also use the Center widget
Calling runApp again will detach the previous root widget from the screen and attach the given widget in its place. The new widget tree is compared against the previous widget tree and any differences are applied to the underlying render tree, similar to what happens when a StatefulWidget rebuilds after calling State.setState.
https://docs.flutter.io/flutter/widgets/runApp.html
So simple package: flutter_restart
dependencies:
flutter_restart: ^0.0.3
to use:
void _restartApp() async {
FlutterRestart.restartApp();
}
I just want to add Regarding I have Tried #Remi answer which works great on most of the cases to restart the app. The only problem with the answer is that some things if you are doing Navigation route extensively you probably go to a state which It gives you an error like,
The method 'restartApp' was called on null.
To resolve this error you have to know the Context and use Navigator.of(context).pop(); multiples times back. For me, the solution is that just go to the initial route. It will inject all the states from a new. Where you want to restart just add this Line.
Navigator.pushNamedAndRemoveUntil(context,'/',(_) => false);
If you want to only restart a specific widget then the Remi solution is awesome. Thanks for the solution Remi though. It help me understand states in flutter.
I have found Hossein's restart_app package also pretty useful for native restarts (not only on Flutter level).
To everyone having the MissingPluginException error, just reinstall the app again on the device, means that hot reload won't work. The app has native methods which need to compiled in the Android/iOS App.
I wanted to restart my app after logout.
so I used https://pub.dev/packages/flutter_phoenix (flutter phoenix).
It worked for me.
Install flutter_phoenix by running this command on your terminal inside your flutter app directory.
$ flutter pub add flutter_phoenix
Import it inside your "main.dart".
Wrap your root widget inside Phoenix.
runApp(
Phoenix(
child: MyApp()
));
Now you can call this wherever you want to restart your app :-
Phoenix.rebirth(context)
Note: flutter_phoenix does not restart the app on OS level, it only restarts the app on app level.
Thecnically this is not a restart but it will work for most of the scenarios:
// Remove any route in the stack
Navigator.of(context).popUntil((route) => false);
// Add the first route. Note MyApp() would be your first widget to the app.
Navigator.push(
context,
CupertinoPageRoute(builder: (context) => const MyApp()),
);
Follow the steps-
Go to your terminal and type in the following:
flutter pub add flutter_restart
This will update some dependencies in pubspec.yaml file.
Import the following package in whichever file you want to implement the restart code-
import 'package:flutter_restart/flutter_restart.dart';
Create a void function
void _restartApp() async {
await FlutterRestart.restartApp();
}
Write this wherever you want to start the app-
_restartApp();
I tried the above suggested methods and none of them worked and i was using getx.
so i ended up modified the accepted answer with a delay as a workaround and it works now.
class RestartAppWidget extends StatefulWidget {
RestartAppWidget({this.child});
final Widget child;
static void restartApp(BuildContext context) {
context.findAncestorStateOfType<_RestartAppWidgetState>().restartApp();
}
#override
_RestartAppWidgetState createState() => _RestartAppWidgetState();
}
class _RestartAppWidgetState extends State<RestartAppWidget> {
bool restarting = false;
void restartApp() async {
restarting = true; // restart variable is set to true
setState(() {});
Future.delayed(Duration(milliseconds: 300)).then((value) {
setState(() {
restarting = false; //restart variable is set to false
});
});
// setState(() {
// key = UniqueKey();
// });
}
#override
Widget build(BuildContext context) {
if (restarting) {
return SizedBox(); //an empty Sizedbox is displayed for 300 milliseconds you can add a loader if you want
}
return SizedBox(
child: widget.child,
);
}
}`
wrap the root widget with RestartAppWidget
runApp(RestartAppWidget(
child: MyApp(),
))
you can use this code to restart the app at flutter level
RestartAppWidget.restartApp(Get.context);