Flutter navigator 2.0 force new state to be created - flutter

I'm using the new Pages and Router APIs from Flutter's Navigator 2.0
My issue is that I have pages A and B opened and then a deepLink is called to page A. So what I do is clear the list of pages used in the navigator and recreate the pages with a new page A.
But what happens in this moment is that Navigator reuses the same Page A that existed before and only updates its state and properties (of the widget).
I tried overriding the method canUpdate on Page (below) in order to tell that page that now it should be rebuild, but the issue is that the Page class is immutable, so it complains that I can't have a non-final variable on it.
class CustomPage<T> extends Page<T> {
final Widget child;
bool _pageCanBeUpdated = true;
CustomPage({#required this.child, Key key}) : super(key: key);
#override
Route<T> createRoute(BuildContext context) {
return MaterialPageRoute(
builder: (context) => child,
settings: this,
);
}
void shouldCleanPage() {
_pageCanBeUpdated = false;
}
#override
bool canUpdate(Page other) {
return _pageCanBeUpdated && super.canUpdate(other);
}
}

I was able to solve it in another way. Instead of overriding the canUpdate method, I created a different key for the page.
In my RouterDelegate I keep a "salt" variable as int _salt and whenever I need to clean the whole stack of pages, I increment this salt and add append to the key. This way it is different until the next time I have to recreate it all.
Salt: int _salt = 0;
Initially: [Page(ValueKey('home $_salt')), Page(ValueKey('invites $_salt'))]
Clean pages: []
Increment salt: _salt++
Recreate page with [Page(ValueKey('home $_salt')
It forces the recreation of the whole page instead of reusing the ones that already exist.

Related

How to change the default routing behavior when entering a new URL through address bar in a flutter web app? (using getx)

I am working on a Flutter Web App using Getx for navigation and state management. One of the routes in my flutter app has two query parameters. Let us call these parameters Dataset and Index. When the Dataset parameter is changed through the URL, I want to make an API call to retrieve the new dataset, and when the Index parameter is changed, I want to display the data from the dataset at that particular index on the app. Index in this case is an observable RxInt variable defined in the controller.
However, the default behavior when I change the URL and press enter is for the Flutter app to push a new page on to the navigation stack. The behavior I prefer is to simply update the values and make a new API call if necessary. The API call may be done by simply refreshing the page since it is handled by the Getx controller onInit function.
I'm not very familiar with how routing in flutter works and I haven't found a solution to change the behavior for routing itself. I've tried a few ways to update the values despite pushing the new page on to the stack, such as setting the value for index through the initState or build calls on my widgets but those changes aren't visible on my UI. I've also tried reinitializing the controller by deleting it but that didn't work either.
EDIT: I have added a code example:
Widget:
class MainscreenView extends StatefulWidget {
const MainscreenView({Key? key}) : super(key: key);
#override
State<MainscreenView> createState() => _MainscreenViewState();
}
class _MainscreenViewState extends State<MainscreenView> {
late MainscreenController mainscreenController;
#override
Widget build(BuildContext context) {
return GetX<MainscreenController>(
init: MainscreenController(),
initState: (_) {
mainscreenController = Get.find<MainscreenController>();
},
builder: (_) {
return Scaffold(
body: Center(
child: Text(
'Current index is ${mainscreenController.index.value}',
style: const TextStyle(fontSize: 20),
),
),
);
});
}
}
Controller:
class MainscreenController extends GetxController {
final index = 0.obs;
late String? dataset;
#override
void onInit() {
super.onInit();
final String? datasetQuery = Get.parameters['dataset'];
if (datasetQuery != null) {
dataset = datasetQuery; //API call goes here
} else {
throw Exception('Dataset is null');
}
final String? indexString = Get.parameters['index'];
if (indexString == null) {
throw Exception('Index is null');
} else {
final int? indexParsed = int.tryParse(indexString);
if (indexParsed == null) {
throw Exception('Index Cannot be parsed');
} else {
index.value = indexParsed;
}
}
}
}
The initial route is /mainscreen?dataset=datasetA&index=0. If I were to modify the route in the address bar to /mainscreen?dataset=datasetA&index=5 for example and press enter, The current behavior of Flutter is to push a new page onto the navigation stack. I would like to update the value of index instead and display it on the same page, but I haven't found a way to accomplish this. Also, if dataset parameter is updated I would like to again avoid pushing a new page onto the stack and refresh the current page instead so that the onInit function is run again and the API call is made automatically.

Pass parameters to a route widget correctly

I am new to flutter, and have some difficulties understanding how to correctly pass parameters to a widget that is navigated to.
My goal is, that when the users clicks on a button, I want to start up a wizard controller with a certain enum parameter based on what button the user clicked.
The wizard controller has an app bar but the primary content is a dynamic child wizard flow widget which is chosen based on the enum parameter. The wizard controller (and its children) needs to be stateful because it, among other things, holds information about the current page in the chosen wizard flow and a model which holds data for the whole wizard flow.
As far as I can see there are two options of instantiating the wizard controller with the enum parameter:
Option 1.
//Pass the parameters when the route is pushed
onPressed: () {
Navigator.pushNamed(context, '/wizard', arguments: EFlowType.WizardFlow2);
},
//In the build method, extract the parameter from the navigator, and use it here:
class WizardController extends StatefulWidget {
WizardController({Key? key}) : super(key: key);
#override
State<WizardController> createState() => _WizardControllerState();
}
class _WizardControllerState extends State<WizardController> {
StatefulWidget? dynamicWidget;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
final flowType = ModalRoute.of(context)!.settings.arguments as EFlowType;
switch (flowType) {
case EFlowType.WizardFlow1:
//Prepare models, set dynamicWidget and do alot of work
break;
case EFlowType.WizardFlow2:
//Prepare models, set dynamicWidget and do alot of work
break;
}
return Scaffold(
appBar: AppBar(
title: Text('Wizard controller'),
),
body: dynamicWidget
);
}
....
The problem is here that I would like to access the flowType parameter in the initState (or in the WizardController class constructor), so I dont need to do all the initialization work every time the widget is being rebuild/UI is updated.
If I try to access the flowtype in the initstate I get this error which I cannot come around: FlutterError (dependOnInheritedWidgetOfExactType<_ModalScopeStatus>() or dependOnInheritedElement() was called before _WizardControllerState.initState() completed.
It is not possible to access the context in the WizardController class, so that is not an option.
Option 2.
In the MaterialApp method I can declare an extra onGenerateRoute setting, for these routes in my app that has parameters:
class WizardController extends StatefulWidget {
EFlowType flowType = EFlowType.MeterChange;
WizardController({Key? key, required this.flowType}) : super(key: key);
#override
State<WizardController> createState() => _WizardControllerState();
}
onGenerateRoute: (RouteSettings settings) {
if (settings.name == '/wizard') {
return MaterialPageRoute(builder: (_) => WizardController(flowType: settings.arguments as EFlowType));
}
This makes the flowType available in the initState. But why on earth would I declare context and logic-specific stuff where I am defining my routes? Is this a preferred way of defining widget parameters?
So how would you normally go around this quite normal problem?

Flutter - Cubit & Navigation 2.0: emitting new page from page

I am trying to create a website with Flutter using Navigation 2.0 and BLoC pattern. To do so, I read the following guides:
https://medium.com/#JalalOkbi/flutter-navigator-2-0-with-bloc-the-ultimate-guide-6672b115adf
https://lucasdelsol01.medium.com/flutter-navigator-2-0-for-mobile-dev-bloc-state-management-integration-3a180b4d25b3
and this repo: https://lucasdelsol01.medium.com/flutter-navigator-2-0-for-mobile-dev-bloc-state-management-integration-3a180b4d25b3 (which implements the first guide).
However I am facing an issue where I am trying to push a new page from one of my website displayed page: the new page is never displayed!
To understand:
Each pages are pushed via a MainNavigationCubit. This cubit's state (meaning pages) is maintained within the NavigationStack.
My MainNavigationCubit is responsible for building the Navigator in my custom RouterDelegate (see code below). So upon a state change it rebuilds the Navigator with the proper list of pages.
The problem context:
I have a "Book" page which displays the details about a specific book.
In order to get the details, it expects a book id.
If the book id is invalid or not found, then the "404 not found page" is pushed via MainNavigationCubit.
This can happen, eg, if the user is manually inputting a correct URL to the book page but with an invalid ID.
However the "404 not found page" is never displayed although the MainNavigationCubit properly emits a new NavigationStack with relevant pages.
This is the code from my custom RouterDelegate:
#override
GlobalKey<NavigatorState> get navigatorKey => GlobalKey<NavigatorState>(debugLabel: 'main_navigation_key');
#override
Future<void> setNewRoutePath(PageConfig configuration) {
if (configuration.route != homeRoute) {
mainNavigationCubit.push(configuration.route, configuration.args);
} else {
mainNavigationCubit.clearToHome();
}
return SynchronousFuture(null);
}
#override
Widget build(BuildContext context) {
return BlocBuilder<MainNavigationCubit, NavigationStack>(
builder: (context, stack) {
return Navigator(
pages: stack.pages,
key: navigatorKey,
onPopPage: (route, result) => _onPopPage.call(route, result),
);
},
);
#override
PageConfig get currentConfiguration => mainNavigationCubit.state.last;
bool _onPopPage(Route<dynamic> route, dynamic result) {
final didPop = route.didPop(result);
if (!didPop) {
return false;
}
if (mainNavigationCubit.canPop()) {
mainNavigationCubit.pop();
return true;
} else {
return false;
}
}
And this is the code from my "Book" StatelessWidget page:
#override
Widget build(BuildContext context) {
if (bookId == -1) {
context.read<MainNavigationCubit>().showNotFound(); // let's assume this will be properly handled when I'll be creating this page's BLoC.
}
return // full book details UI;
}
And just in case the code of MainNavigationCubit.showNotFound():
void showNotFound() {
clearAndPush(notFound);
}
void clearAndPush(String path, [Map<String, dynamic>? args]) {
final PageConfig pageConfig = PageConfig(location: path, args: args);
emit(state.clearAndPush(pageConfig));
}
OK, so after a lot of investigation I have found the reason for my issue.
As the documentation says: a Cubit won't notify listeners upon emitting a new state that is equal to the current state.
In my case, my MainNavigationCubit's state is a NavigationStack which I took from this guide: https://medium.com/#JalalOkbi/flutter-navigator-2-0-with-bloc-the-ultimate-guide-6672b115adf
Looking at the code, the NavigationStack exposes methods that mutates an internal list of pages.
The problem is this list belongs to the current state, therefore modifying it means to also modify the current state.
As both current and new state rely on the same exact list, the Cubit won't emit the new state.

Flutter : how to fetch data and manage app state

I'm trying Flutter and I need (I think I do) an app state management to share datas across widgets and dont have to make an http request each time a route is called.
I have Places and Events, so I first load my Places to list them at creation of app state with :
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => AppStateModel()..fetchPlaces(),
...
)
);
}
When I click on a Place, I go on place/id screen and as Places doesnt have events props yet, I'm trying to load them with :
class PlacePageArguments {
final String id;
PlacePageArguments(this.id);
}
class PlacePage extends StatefulWidget {
const PlacePage({Key? key}) : super(key: key);
#override
State<PlacePage> createState() => _PlacePageState();
}
class _PlacePageState extends State<PlacePage> {
String id = '';
#override
Widget build(BuildContext context) {
final args =
ModalRoute.of(context)!.settings.arguments as PlacePageArguments;
return Consumer<AppStateModel>(builder: (context, appState, child) {
id = args.id;
appState.fetchEvents(id);
final place = appState.getPlaceById(id);
return Scaffold(...);
})
}
}
But for sure, as I notifyChange to update widget, It does an infinite loop on fetch events.
What should I do ?
What is the best to achieve something like that, maybe a simple futurBuilder will work, but I want to add events and stay on the same page (add event with modal) and want instant result.
thanks for all
You have two options for bringing in the data for this and none of them require ChangeNotifierProvider.
You can pass data via constructors. This works fine for small widget trees but it can easily get complicated.
You can use Providers. Providers allow you to manage data and functions in one class that stays in one file. For more deals please look here.

Why variable value is not refreshing in flutter?

The variable value is not refreshing in flutter(which I created outside the class). The following way I have added.So in button press am setting different value. So if I come back to that screen again it is not showing 1. May I know why it is happening?
int _currVal = 1;
class AskFragment extends StatefulWidget {
static const String routeName = "//";
static const
int currState = -1;
#override
HomeScreenState createState() => HomeScreenState();
}
To update the values, use Stateful Widget and initialise values inside that Widget only, for any updates in the values of variables mention that in setState((){}) method to notify the change in the values.
See this for documentation of stateful widgets
See this for documentation of setState((){}) method
setState(() { _myVariable = newValue });
Note:- Never initialise changing values in build method they will not get updated instead they will be reinitialised with same values again and again