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

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.

Related

In Flutter, navigating to a page that have a heavy widget is too slow in an old device environmnet

I'd like to implement Survey Platform
A survey has many questions and each of them has a part (ex. Part1, Part2, and so on...)
Each part is displayed in one page. (One page equals to one page)
After user finished the survey, they could check the result in one page. That page has a SingleChildScrollView and the SingleChildScrollView contains all of the question's answers. (This is a client's request, so it could not be revised)
Also, I have selected GetX library to administrate State and used RxDart to manage async.
At the onInit method in a answers page's controller, I call the result api and save it to RxList variable.
In answers page's view, if the RxList variable's value is null, it builds CircularProgressIndicator. And in a case of not null, it builds SingleChildScrollView.
I'd like to make the answers page right away pop up when the Get.toNamed method is called and the CircularProgressIndicator has been displayed until the RxList variable is initialized.
But, when I called the Get.toNamed method, the screen had been stoped in the page that called Get.toNamed method. And a few seconds later, the answers page finally pop up.
What should I do to solve this problem?? My code is like belows.
// Survey Answers View
class SurveyResultDetailView extends BaseView<SurveyResultDetailController> {
#override
PreferredSizeWidget? appBar(BuildContext context) {
#override
Widget body(BuildContext context) {
return Obx(
() {
if (controller.surveyDiagnosisUiModel == null) {
return CircularProgressIndicator();
}
return SingleChildScrollView(
child: Column(
children: [
So long childrens~~~
],
),
);
}
);
}
}
// Survey Answers Controller
class SurveyResultDetailController extends BaseController {
final PreferenceManager _preferenceManager =
Get.find(tag: (PreferenceManager).toString());
late final String surveyConfigId;
final Rxn<SurveyDiagnosisResultUiModel> _rxModel =
Rxn<SurveyDiagnosisResultUiModel>(null);
SurveyDiagnosisResultUiModel? get surveyDiagnosisUiModel => _rxModel.value;
void setSurveyDiagnosisUiModel(SurveyDiagnosisResultUiModel? value) {
_rxModel.value = value;
}
final RxList<SurveyQuestionListUiModel> _surveyQuestionListUiModel =
RxList<SurveyQuestionListUiModel>();
List<SurveyQuestionListUiModel> get surveyQuestionListUiModel =>
_surveyQuestionListUiModel.toList();
void getDiagnosisResultUiModel() {
Future<String> diagnosisResultFuture =
_preferenceManager.getString('recent_detail_result');
callDataService(
diagnosisResultFuture,
onSuccess: _handleDiagnosisResultResponseSuccess,
);
}
void _handleDiagnosisResultResponseSuccess(String response) {
~~~~~ response entity mapping to ui model code
_surveyQuestionListUiModel.refresh();
}
List<SurveyQuestionListUiModel> parseQuestionListByPartNumber(int number) {
return surveyQuestionListUiModel
.where((element) => element.partNumber == number)
.toList();
}
/// ------------> 생애설계 행동 진단 결과 관련 값
#override
void onInit() {
surveyConfigId = Get.arguments as String;
getDiagnosisResultUiModel();
super.onInit();
}
}

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.

Controlling State from outside of a StatefulWidget

I'm trying to understand the best practice for controlling a StatefulWidget's state outside of that Widgets State.
I have the following interface defined.
abstract class StartupView {
Stream<String> get onAppSelected;
set showActivity(bool activity);
set message(String message);
}
I would like to create a StatefulWidget StartupPage that implements this interface. I expect the Widget to do the following:
When a button is pressed it would send an event over the onAppSelected stream. A controller would listen to this event and perform some action ( DB call, service request, etc ).
The controller can call showActivity or set message to have the view show progress with a message.
Because a Stateful Widget does not expose its State as a property, I don't know the best approach for accessing and modifying the State's attributes.
The way I would expect to use this would be something like this:
Widget createStartupPage() {
var page = new StartupPage();
page.onAppSelected.listen((app) {
page.showActivity = true;
//Do some work
page.showActivity = false;
});
}
I've thought about instantiating the Widget by passing in the state I want it to return in createState() but that feels wrong.
Some background on why we have this approach: We currently have a Dart web application. For view-controller separation, testability, and forward-thinking towards Flutter, we decided that we would create an interface for every view in our application. This would allow a WebComponent or a Flutter Widget to implement this interface and leave all of the controller logic the same.
There are multiple ways to interact with other stateful widgets.
1. findAncestorStateOfType
The first and most straightforward is through context.findAncestorStateOfType method.
Usually wrapped in a static method of the Stateful subclass like this :
class MyState extends StatefulWidget {
static of(BuildContext context, {bool root = false}) => root
? context.findRootAncestorStateOfType<_MyStateState>()
: context.findAncestorStateOfType<_MyStateState>();
#override
_MyStateState createState() => _MyStateState();
}
class _MyStateState extends State<MyState> {
#override
Widget build(BuildContext context) {
return Container();
}
}
This is how Navigator works for example.
Pro:
Easiest solution
Con:
Tempted to access State properties or manually call setState
Requires to expose State subclass
Don't use this method when you want to access a variable. As your widget may not reload when that variable change.
2. Listenable, Stream and/or InheritedWidget
Sometimes instead of a method, you may want to access some properties. The thing is, you most likely want your widgets to update whenever that value changes over time.
In this situation, dart offer Stream and Sink. And flutter adds on the top of it InheritedWidget and Listenable such as ValueNotifier. They all do relatively the same thing: subscribing to a value change event when coupled with a StreamBuilder/context.dependOnInheritedWidgetOfExactType/AnimatedBuilder.
This is the go-to solution when you want your State to expose some properties. I won't cover all the possibilities but here's a small example using InheritedWidget :
First, we have an InheritedWidget that expose a count :
class Count extends InheritedWidget {
static of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<Count>();
final int count;
Count({Key key, #required Widget child, #required this.count})
: assert(count != null),
super(key: key, child: child);
#override
bool updateShouldNotify(Count oldWidget) {
return this.count != oldWidget.count;
}
}
Then we have our State that instantiate this InheritedWidget
class _MyStateState extends State<MyState> {
int count = 0;
#override
Widget build(BuildContext context) {
return Count(
count: count,
child: Scaffold(
body: CountBody(),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
count++;
});
},
),
),
);
}
}
Finally, we have our CountBody that fetch this exposed count
class CountBody extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Text(Count.of(context).count.toString()),
);
}
}
Pros:
More performant than findAncestorStateOfType
Stream alternative is dart only (works with web) and is strongly integrated in the language (keywords such as await for or async*)
Automic reload of the children when the value change
Cons:
More boilerplate
Stream can be complicated
3. Notifications
Instead of directly calling methods on State, you can send a Notification from your widget. And make State subscribe to these notifications.
An example of Notification would be :
class MyNotification extends Notification {
final String title;
const MyNotification({this.title});
}
To dispatch the notification simply call dispatch(context) on your notification instance and it will bubble up.
MyNotification(title: "Foo")..dispatch(context)
Note: you need put above line of code inside a class, otherwise no context, can NOT call notification.
Any given widget can listen to notifications dispatched by their children using NotificationListener<T> :
class _MyStateState extends State<MyState> {
#override
Widget build(BuildContext context) {
return NotificationListener<MyNotification>(
onNotification: onTitlePush,
child: Container(),
);
}
bool onTitlePush(MyNotification notification) {
print("New item ${notification.title}");
// true meaning processed, no following notification bubbling.
return true;
}
}
An example would be Scrollable, which can dispatch ScrollNotification including start/end/overscroll. Then used by Scrollbar to know scroll information without having access to ScrollController
Pros:
Cool reactive API. We don't directly do stuff on State. It's State that subscribes to events triggered by its children
More than one widget can subscribe to that same notification
Prevents children from accessing unwanted State properties
Cons:
May not fit your use-case
Requires more boilerplate
You can expose the state's widget with a static method, a few of the flutter examples do it this way and I've started using it as well:
class StartupPage extends StatefulWidget {
static StartupPageState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<StartupPageState>());
#override
StartupPageState createState() => new StartupPageState();
}
class StartupPageState extends State<StartupPage> {
...
}
You can then access the state by calling StartupPage.of(context).doSomething();.
The caveat here is that you need to have a BuildContext with that page somewhere in its tree.
There is another common used approach to have access to State's properties/methods:
class StartupPage extends StatefulWidget {
StartupPage({Key key}) : super(key: key);
#override
StartupPageState createState() => StartupPageState();
}
// Make class public!
class StartupPageState extends State<StartupPage> {
int someStateProperty;
void someStateMethod() {}
}
// Somewhere where inside class where `StartupPage` will be used
final startupPageKey = GlobalKey<StartupPageState>();
// Somewhere where the `StartupPage` will be opened
final startupPage = StartupPage(key: startupPageKey);
Navigator.push(context, MaterialPageRoute(builder: (_) => startupPage);
// Somewhere where you need have access to state
startupPageKey.currentState.someStateProperty = 1;
startupPageKey.currentState.someStateMethod();
I do:
class StartupPage extends StatefulWidget {
StartupPageState state;
#override
StartupPageState createState() {
this.state = new StartupPageState();
return this.state;
}
}
class DetectedAnimationState extends State<DetectedAnimation> {
And outside just startupPage.state
While trying to solve a similar problem, I discovered that ancestorStateOfType() and TypeMatcher have been deprecated. Instead, one has to use findAncestorStateOfType(). However as per the documentation, "calling this method is relatively expensive". The documentation for the findAncestorStateOfType() method can be found here.
In any case, to use findAncestorStateOfType(), the following can be implemented (this is a modification of the correct answer using the findAncestorStateOfType() method):
class StartupPage extends StatefulWidget {
static _StartupPageState of(BuildContext context) => context.findAncestorStateOfType<_StartupPageState>();
#override
_StartupPageState createState() => new _StartupPageState();
}
class _StartupPageState extends State<StartupPage> {
...
}
The state can be accessed in the same way as described in the correct answer (using StartupPage.of(context).yourFunction()). I wanted to update the post with the new method.
You can use eventify
This library provide mechanism to register for event notifications with emitter
or publisher and get notified in the event of an event.
You can do something like:
// Import the library
import 'package:eventify/eventify.dart';
final EventEmitter emitter = new EventEmitter();
var controlNumber = 50;
List<Widget> buttonsGenerator() {
final List<Widget> buttons = new List<Widget>();
for (var i = 0; i < controlNumber; i++) {
widgets.add(new MaterialButton(
// Generate 10 Buttons afterwards
onPressed: () {
controlNumber = 10;
emitter.emit("updateButtonsList", null, "");
},
);
}
}
class AState extends State<ofYourWidget> {
#override
Widget build(BuildContext context) {
List<Widget> buttons_list = buttonsGenerator();
emitter.on('updateButtonsList', null, (event, event_context) {
setState(() {
buttons_list = buttonsGenerator();
});
});
}
...
}
I can't think of anything which can't be achieved by event driven programming. You are limitless!
"Freedom cannot be bestowed — it must be achieved."
- Elbert Hubbard
Have you considered lifting the state to the parent widget? It is a common, though less ideal than Redux, way to manage state in React as far as I know, and this repository shows how to apply the concept to a Flutter app.