How to get top most screens route name - flutter

I am following this Navigate with named routes. Where I am passing a route name to view the new screen.
Navigator.pushNamed(context, '/second');
So, what I need is the topmost screens routeName. in this case /second.
How can I get it?

other than Modal.of(context), flutter doesn't provide any easier way to get the latest route name.
but anyone who is using route handling using onRouteGenerated property can do this
class AllRoutes{
static String _lastRoute = "/";
static Route onGenerated(RouteSettings settings){
_lastRoute = settings.name;
//handle route changes here rather passing the route Map to the App
}
static String get lastRoute=>_lastRoute;
}
and add the static method as the onGenerateRoute property value of the App
MaterialApp(
onGenerateRoute: AllRoutes.onGenerated,
)
and get the last route like this
var lastRoute = AllRoutes.lastRoute
You could add this as a extension to Navigator so you could feel at home ;)

String? getTopRouteName (BuildContext context) {
String? top;
Navigator.popUntil(context, (route) {
top = route.settings.name;
return true;
});
return top;
}
Unfortunately, Navigator does not provide such API. But the top most route name could be found through this tricky method, which is trying to pop the top most route, but prevented by the return value true.

You can register as an observer in navigatorObservers in MaterialApp to get notified when routes change, and keep the last one:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'test',
home: const MyHomePage(),
navigatorObservers: [
RouteObserver(),
],
);
}
}
class RouteObserver extends NavigatorObserver {
String? lastRoute;
#override
void didReplace({Route? newRoute, Route? oldRoute}) {
lastRoute = newRoute?.settings.name;
}
#override
void didPush(Route route, Route? previousRoute) {
lastRoute = route.settings.name;
}
#override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
lastRoute = previousRoute?.settings.name;
}
#override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
lastRoute = previousRoute?.settings.name;
}
}

Related

How to make Flutter with GetX wait until data is loaded

I'm looking for a best-practice way to implement waiting for my app to initialize data before displaying the first page.
The app has a main controller as well as a controller per page. The main controller initially loads data from a server, and until that's done I'd like to display a splash page (or at least wait before opening the actual app-page)
An simple solution would be, that a page waits for the main controller to be initialized
class MainController extends GetxController {
final isInitialized = false.obs;
#override
void onInit() async {
Future f1 = server.get('service1').then(....)
Future f2 = server.get('service1').then(....)
Future f3 = server.get('service1').then(....)
await Future.wait([f1, f2, f3]);
isInitialized.value = true;
}
}
And a page component could:
class HandleTaskPage extends GetView<HandleTaskPageController> {
#override
Widget build(BuildContext context) {
MainController mainController = Get.find();
return Obx(() {
if (mainController.isInitialized().value) {
return TaskPanelWidget();
} else {
return WaitingPage();
}
})
}
}
But my app allows the user to start at any given page using a direct url (web-app) e.g. http://app.com/showtask/123
Which means that I must put the wait for global controller on every page.
Is there some way I could simply make Get wait (and possibly display a Welcome page) until GlobalController is ready before moving on to the page described in the route?
I've tried to add a WelcomePage to GetMaterialApp, to stop the app from going directly to the requested url. The WelcomeController should then await MainController before redirecting. But even though welcome-page does get rendered, the app still automatically continues to the page requested in the url.
void main() {
runApp(GetMaterialApp(
home: WelcomePage(),
...
...
...
You can implement mixin with name StateMixin.
Example (Controller):
class UserController extends GetxController with StateMixin {
getData() {
// make status to loading
change(null, status: RxStatus.loading());
// Code to get data
await service.getData()
// if done, change status to success
change(null, status: RxStatus.success());
}
}
Example (UI):
class HomePage extends GetView<UserController> {
#override
Widget build(BuildContext context) {
// controller from GetView
return controller.obx((state) {
return OtherWidget()
},
onLoading: CircularProgressIndicator(),
)
}
}
You can use the builder method on MaterialApp to build/show the widget you want.
Using GetX - you can show a loader while waiting for your data to load as follows:
Widget build(BuildContext context) {
final MainController mainController = Get.find();
final isInitialized = mainController.isInitialized().value;
return GetMaterialApp(
title: 'Example',
debugShowCheckedModeBanner: false,
theme: ...,
initialRoute: '/',
getPages: [...],
builder: (context, child) => isInitialized
? Container(child: child)
: const Center(
child: CircularProgressIndicator(color: Colors.blue),
),
);
}
The above should display a blue loader until isInitialized becomes true.

How can I get all pushed routes from the navigation stack in flutter?

Is there is a way to get names of the pushed routes from the navigation stack ?
Maybe NavigatorObserver can help to achieve what you want
MaterialApp(
navigatorObservers: [MyNavigatorObserver()],...)
class MyNavigatorObserver extends NavigatorObserver {
List<Route<dynamic>> routeStack = List();
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
routeStack.add(route);
}
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
routeStack.removeLast();
}
#override
void didRemove(Route route, Route previousRoute) {
routeStack.removeLast();
}
#override
void didReplace({Route newRoute, Route oldRoute}) {
routeStack.removeLast();
routeStack.add(newRoute);
}
}

Flutter - Using GetIt with BuildContext

I'm using Localizations in my app based on the flutter documentation.
See here: https://flutter.dev/docs/development/accessibility-and-localization/internationalization
I use get_it package (version 4.0.4) to retrieve singleton objects like the Localization delegate. Unfortunately it needs a BuildContext property. Sometimes in my app I don't have the context reference so it would be nice if it would work like this: GetIt.I<AppLocalizations>() instead of this: AppLocalizations.of(context). It still can be achieved without a problem if you setup get_it like this: GetIt.I.registerLazySingleton(() => AppLocalizations.of(context)); The problem is that you need the context at least once to make it work. Moreover if you would like to display a localized text instantly in your initial route it's more difficult to get a properly initialized BuildContext at a time when you need it.
It's a little hard for me to explain it properly so I recreated the issue in a minimal example.
I commented out some code that would cause compile time errors, but it shows how I imagined it to be done.
main.dart
GetIt getIt = GetIt.instance;
void setupGetIt() {
// How to get BuildContext properly if no context is available yet?
// Compile time error.
// getIt.registerLazySingleton(() => AppLocalizations.of(context));
}
void main() {
setupGetIt();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
MyApp({Key key}) : super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
// The above line also won't work. It has BuildContext but Applocalizations.of(context) won't work
// because it's above in the Widget tree and not yet setted up.
getIt.registerLazySingleton(() => AppLocalizations.of(context));
return MaterialApp(
supportedLocales: const [
Locale('en', 'US'),
Locale('hu', 'HU'),
],
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
localeResolutionCallback: (locale, supportedLocales) {
// check if locale is supported
for (final supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale?.languageCode &&
supportedLocale.countryCode == locale?.countryCode) {
return supportedLocale;
}
}
// if locale is not supported then return the first (default) one
return supportedLocales.first;
},
// You may pass the BuildContext here for Page1 in it's constructor
// but in a more advanced routing case it's not a maintanable solution.
home: Page1(),
);
}
}
Initial route
class PageBase extends StatelessWidget {
final String title;
final Widget content;
PageBase(this.title, this.content);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: content,
);
}
}
class Page1 extends PageBase {
// It won't run because I need the context but clearly I don't have it.
// And in a real app you also don't want to pass the context all over the place
if you have many routes to manage.
Page1(String title)
: super(AppLocalizations.of(context).title, Center(child: Text('Hello')));
// Intended solution
// I don't know how to properly initialize getIt AppLocalizations singleton by the time
// it tries to retrieve it
Page1.withGetIt(String title)
: super(getIt<AppLocalizations>().title, Center(child: Text('Hello')));
}
locales.dart
String globalLocaleName;
class AppLocalizations {
//AppLocalizations(this.localeName);
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
static Future<AppLocalizations> load(Locale locale) async {
final String name =
locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
final String localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
globalLocaleName = localeName;
return AppLocalizations();
});
}
String get title => Intl.message(
'This is the title.',
name: 'title',
);
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
// This delegate instance will never change (it doesn't even have fields!)
// It can provide a constant constructor.
const _AppLocalizationsDelegate();
#override
bool isSupported(Locale locale) {
return ['en', 'hu'].contains(locale.languageCode);
}
#override
Future<AppLocalizations> load(Locale locale) => AppLocalizations.load(locale);
#override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
And some intl generated dart code and .arb files that is not so important to illustrate the problem.
So all in all, how can I achive to use my AppLocalizations class as a singleton without using a context for example in a situation like this? Maybe my initial approach is bad and it can be done in other ways that I represented. Please let me know if you have a solution.
Thank you.
To achieve what you have described you need to first make the navigation service using get_it. Follow these steps to achieve the result :
1. Create a navigation service
import 'package:flutter/material.dart';
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey =
new GlobalKey<NavigatorState>();
Future<dynamic> navigateTo(String routeName) {
return navigatorKey.currentState!
.push(routeName);
}
goBack() {
return navigatorKey.currentState!.pop();
}
}
This allows you to navigate anywhere from any point throughout the app without build context. This navigator key is what you can use to achieve the AppLocalization instance for the current context.
Refer to the FilledStacks tutorials for this method of navigating without build context.
https://www.filledstacks.com/post/navigate-without-build-context-in-flutter-using-a-navigation-service/
2. Register
GetIt locator = GetIt.instance;
void setupLocator() {
...
locator.registerLazySingleton(() => NavigationService());
...
}
3. Assign the navigator key in the material app
return MaterialApp(
...
navigatorKey: navigationService.navigatorKey,
...
),
3. Create an instance for the AppLocalizations and import it wherever you want to use
localeInstance() => AppLocalizations.of(locator<NavigationService>().navigatorKey.currentContext!)!;
3. The actual use case
import 'package:{your_app_name}/{location_to_this_instace}/{file_name}.dart';
localeInstance().your_localization_variable
You can add a builder to your MaterialApp and setup the service locator inside it with the context available. Example:
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, widget) {
setUpServiceLocator(context);
return FutureBuilder(
future: getIt.allReady(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return widget;
} else {
return Container(color: Colors.white);
}
});
},
);
}
Service Locator Setup:
void setUpServiceLocator(BuildContext context) {
getIt.registerSingleton<AppLocalizations>(AppLocalizations.of(context));
}
You could use some non-localizable splash screen with FutureBuilder and getIt.allReady().
Something like:
class SplashScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: getIt.allReady(),
builder: (context, snapshot) {
if (snapshot.hasData) {
// Navigate to main page (with replace)
} else if (snapshot.hasError) {
// Error handling
} else {
// Some pretty loading indicator
}
},
);
}
I'd like to recommend the injectable package for dealing with get_it also.

Generic navigation listener for whole application

I'm trying to find a way to setup a global Navigation listener in the Root of my app (MaterialApp) which can allow me to track all navigation events including the actual route name (/home/page1 for ex.), navigation type etc.
I search something like:
MaterialApp(
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) => route(settings),
onNavigationChange: (RouteType type, String currentRoute) {
// type can an enum be like: 'pop', 'push', 'replace' etc.
// currentRoute is the current route name defined in a pushNamed for example
}
),
Note: this code is non functional, only to illustrate what I want ;)
During my search, I found quite some solutions like RouteObserver, route_observer_mixin which are almost what I'm looking for but I need to wrap all my pages with a mixin... so no a global solution :/
Do you have any clues ?
Thanks in advance,
EDIT thanks to #pskink, see the solution below
The final solution is to create an extends of the RouteObserver like this:
class MyNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
#override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
super.didPush(route, previousRoute);
if (route is PageRoute) {
// do stuff
}
}
#override
void didReplace({Route<dynamic> newRoute, Route<dynamic> oldRoute}) {
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
if (newRoute is PageRoute) {
// do stuff
}
}
#override
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
super.didPop(route, previousRoute);
if (previousRoute is PageRoute && route is PageRoute) {
// do stuff
}
}
}
and then setup it on the MaterialApp widget:
static MyNavigatorObserver observer =
new MyNavigatorObserver();
MaterialApp(
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) => route(settings),
navigatorObservers: [observer],
),
Thanks #pskink !
Inspired by #pskink, I've build a generic observer for logging purposes.
Designed to work with navigation 1.0 and unamed routes.
import 'package:flutter/material.dart';
class GlobalNavigatorObserver extends RouteObserver<PageRoute> {
#override
void didPush(route, previousRoute) {
super.didPush(route, previousRoute);
_log('push', previousRoute, route);
}
#override
void didPop(route, previousRoute) {
super.didPop(route, previousRoute);
_log('pop', route, previousRoute);
}
void _log(String method, Route? fromRoute, Route? toRoute) {
debugPrint('[Navigator] ${_getPageName(fromRoute)} => ${_getPageName(toRoute)} ($method)');
}
/// Try to get page name from route.
String _getPageName(dynamic route) {
// Because the builder field is not on a common class, we must try dynamically.
try {
final routeBuilderString = route.builder.toString(); // Example: 'Closure: (BuildContext) => WashingsPage'
return routeBuilderString.substring(routeBuilderString.lastIndexOf(' ') + 1);
} catch(_) {
return 'X';
}
}
}

Making Private Route in Flutter

How can I make a wrapper over my private routes, which navigate to screen only when user is authorized, otherwise redirect to login and get back to the original screen after login.
How can make this in a generalized way, so that I just reuse it on my other Private future screens?
If you are using routes parameter in your MaterialApp, you can replace it with following implementation
import 'dart:collection';
import 'package:flutter/widgets.dart';
class ConditionalRouter extends MapMixin<String, WidgetBuilder> {
final Map<String, WidgetBuilder> public;
final Map<String, WidgetBuilder> private;
ConditionalRouter({this.public, this.private});
#override
WidgetBuilder operator [](Object key) {
if (public.containsKey(key))
return public[key];
if (private.containsKey(key)) {
if (MyAuth.isUserLoggedIn)
return private[key];
// Adding next page parameter to your Login page
// will allow you to go back to page, that user were going to
return (context) => LoginPage(nextPage: key);
}
return null;
}
#override
void operator []=(key, value) {}
#override
void clear() {}
#override
Iterable<String> get keys {
final set = Set<String>();
set.addAll(public.keys);
set.addAll(private.keys);
return set;
}
#override
WidgetBuilder remove(Object key) {
return public[key] ?? private[key];
}
}
And use it like that:
MaterialApp(
// ...
routes: ConditionalRouter(
public: {
'/start_page': (context) => StartPage()
},
private: {
'/user_profile': (context) => UserProfilePage()
}
)
)
Use StreamBuilder widget and provide it a Stream of access token/uid if there is no data then return login screen and when user is authenticated then put access token into the stream that will rebuild the StreamBuilder which return the page when user is authenticated.
Use bloc pattern or bloc library for better state management.
Hope this will help you.
Generalised idea, you could make the controller a Static variable
class LoginController{
final Function onContactingServerDone;
LoginController({this.onContactingServerDone,});
bool loggedIn;
login()async {
//contact server,get token or verify token etc
onContactingServerDone();
}
}
and in your screen
LoginController _loginController;
initState(){
_loginController = LoginController(onContactingServerDone: (){
if(_loginController.loggedIn){
Navigator.of(context).pushNamed('privateRoute');
} else {
Navigator.of(context).pushNamed('login');
}
},);
_loginController.login();
}
Widget build(context){
return CircularProgressIndicator();
}