Updated values get overridden on Widget redraw - flutter

I am playing around with a simple flutter countdown app. It consists of 2 pages, the clock and a settings page to set minutes and seconds to be counted down.
On the clock page (HomeWidget) the user clicks a button to navigate to the settings page. After editing the values the user presses the back hardware key or the button in the app bar to navigate back to the clock page.
class _HomeWidgetState extends State<HomeWidget> {
#override
Widget build(BuildContext context) {
TimeService _timeService = ScopedModel.of<TimeService>(context);
SettingsModel _settingsModel = ScopedModel.of<SettingsModel>(context);
_timeService.setTime(_settingsModel.minutes, _settingsModel.seconds);
return Scaffold( ... display the clock, navigation buttons, etc ... )}
My problem to understand is that when navigating back I am setting the new values in the time service class that handles counting down. But in the code sample the time service is updated every time the clock gets redrawn (every second). The countdown doesn't work, the value remains the same. Instead of displaying "10:29", it sticks with "10:30". I don't know how to handle the dependency between my TimeService class and my SettingsModel class.
How can I handle the assignment of the settings values in the time service class properly when the user navigates back? The build method is obviously the wrong place. Can anyone give me a hint?

Ok, I found a solution for my problem. It is described in detail (with some other content) here.
Basically when navigating between pages you can pass objects along. So now I just pass the edited SettingsModel on the settings page via a Navigator.of(context).pop({'newSetting': _settingsModel}); command and the clock page then handles the result. I wasn't aware that navigation works like this.
ControlButtonWidget(
icon: Icons.settings,
iconSize: 72.0,
onPressedHandler: () async {
Map results = await Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) => SettingsWidget(model)));
if (results != null && results.containsKey("newSetting")){
SettingsModel model = results["newSetting"];
ScopedModel.of<TimeService>(context).setTime(model.minutes, model.seconds);
}
})
Make sure to wrap your page in a WillPopScope Widget.
#override
Widget build(BuildContext context) {
return new WillPopScope(
onWillPop: _backRequestedHandler,
child: LayoutBuilder(builder:
(BuildContext context, BoxConstraints viewportConstraints) {
return Scaffold(
appBar: new AppBar(title: Text("Settings")),
body: ...
}));
}
Future<bool> _backRequestedHandler() {
Navigator.of(context).pop({'newSetting': _settingsModel});
return new Future.value(true);
}

Related

When do we initialise a provider in flutter?

I just arrived on a flutter project for a web app, and all developers have a problem using flutter provider for state management.
What is the problem
When you arrive on a screen, the variables of the corresponding provider are initialised by calling a function of the provider. This function calls an api, and sets the variables in the provider.
Problem : This function is called in the build section of the widget. Each time the window is resized, the widget is rebuilt, and the function is called again.
What we want
We want to call an api when the page is first displayed, set variables with the result, and not call the api again when the widget is rebuilt.
What solution ?
We use a push from the first screen to go to the second one. We can call the function of the provider at this moment, to initialise the provider just before the second screen.
→ But a refresh on the second page will clear the provider variables, and the function to initialise them will not be called again.
We call the function to initialise the provider in the constructor of the second screen. Is it a good pattern ?
Thank you for your help in my new experience with flutter :)
I think you're mixing a couple different issues here:
How do you correctly initialize a provider
How do you call a method on initialization (only once)
For the first question:
In your main.dart file you want to do something like this:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => SomeProvider()),
ChangeNotifierProvider(create: (context) => AnotherProvider()),
],
child: YourRootWidget();
);
}
Then in a widget (that probably represents a "screen" in your app), you need to do something like this to consume state changes from that provider:
#override
Widget build(BuildContext context) {
return Container(
child: Consumer<SomeProvider>(
builder: (context, provider, child) {
return Text(provider.someState);
}
),
)
}
And you need to do something like this to get access to the provider to mutate state:
#override
Widget build(BuildContext context) {
SomeProvider someProvider = Provider.of<SomeProvider>(context, listen: false);
return Container(
child: TextButton(
child: Text('Tap me'),
onPressed: () async {
await someProvider.mutateSomeState();
}
),
)
}
Regarding the second question... You can (I think) just use the initState() method on a widget to make the call only 1 time. So...
#override
void initState() {
super.initState();
AnotherProvider anotherProvider = Provider.of<AnotherProvider>(context, listen: false);
Future.microtask(() {
anotherProvider.doSomethingElse();
});
}
If I'm off on any of that, I'm sorry. That mirrors my implementation and works fine/well.
A caveat here is that I think RiverPod is likely the place you really want to go (it's maybe easier to work with and has additional features that are helpful, etc.) but I've not migrated to RiverPod yet and do not have that figured out all the way.
Anyway... Good luck!
As far as I understood, you can wrap your application with MultiProvider and call the API before going to the second screen.

Flutter - Error in hot reload using lazy internationalization

I'm building an application that uses lazy internationalization, this way there will be no translation files in the application and all translations will be fetched from the internet when a new page is opened. For that I am using a localization cubit.
Each screen of my application is divided into a "view" that receives the translated messages as a parameter, a "cubit" that contains the cubit screen and its states, and a "container" that contains the BlocProvider for the cubit and the screen.
For now my app starts in the presentation screen, after that it goes to the login screen and finally goes to the home screen.
So in the main file, instead of using the presentation screen directly, I use the localization container and the presentation container comes as its child:
return MaterialApp(
title: 'My App',
theme: myTheme(context),
debugShowCheckedModeBanner: false,
home: LocalizationContainer(
child: PresentationContainer(),
),
);
The PresentationContainer is composed this way:
class PresentationContainer extends BlocContainer {
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => PresentationCubit(),
child: I18NLoadingContainer(
language: BlocProvider.of<CurrentLocaleCubit>(context).state,
viewKey : "Presentation",
creator: (messages) => PresentationView(PresentationViewLazyI18N(messages)),
),
);
}
}
So in the container I have a BlocProvider with PresentationCubit and I18NLoadingContainer as a child.
I18NLoadingContainer just obtains the transalted messages according to the language provided and the screen name, that is "Presentation" in this case. The translated messages are returned in the variable messages, so this messages are passed as parameter to the screen.
If I use this only for my presentation screen everything works fine, but the issue comes when I need to open a new page.
After the presentation screen I need to open the login screen. So in the PresentationView I have the following function when the user clicks the button to open the login screen:
void _goToLogin(BuildContext blocContext) {
Navigator.of(blocContext).pushReplacement(
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: BlocProvider.of<CurrentLocaleCubit>(blocContext),
child: LoginContainer(),
),
),
);
}
And the LoginContainer works exaclty as the PresentationContainer:
class LoginContainer extends BlocContainer {
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => LoginCubit(),
child: I18NLoadingContainer(
language: BlocProvider.of<CurrentLocaleCubit>(context).state,
viewKey : "Login",
creator: (messages) => LoginView(LoginViewLazyI18N(messages)),
),
);
}
}
If I keep in the presentation screen and use the hot reload everything works fine, but if I open a new screen using this method, I got the following error when try to use hot reload:
The following _CastError was thrown building Builder(dirty): Null
check operator used on a null value
I'm not sure your LoginContainer is still wrapped by the LocalizationContainer when you change the route. I would suggest you to provide a CurrentLocaleCubit above the MaterialApp widget and check whether it's working or not. I think you're loosing a CurrentLocaleCubit instance

Flutter/Dart - Edit Page in PageView, then Refresh and Scroll back to Same Place?

Currently, I can submit edits to a single page in a PageView and then either Navigator.push to a newly created single edited page or Navigator.pop back to the original Pageview containing the unedited page.
But I'd prefer to pop back to the the same place in an updated/refreshed Pageview. I was thinking I could do this on the original PageView page:
Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
But after editing, how can I pop back to a refreshed PageView which is scrolled to the now updated original page? Or is there a better way? Someone mentioned keys, but I've not yet learned to use them.
The question deals with the concept of Reactive App-State. The correct way to handle this is through having an app state management solution like Bloc or Redux.
Explanation: The app state takes care of the data which you are editing. the EditPage just tells the store(App-State container) to edit that data and the framework takes care of the data that should be updated in the PageView.
as a temporary solution you can use an async call to Navigation.push() and refresh the PageView State once the EditPage comes back. you can also use an overloaded version of pop() to return a success condition which aids for a conditional setState().
Do you know that Navigator.pushReplacement(...) returns a Future<T> which completes when you finally return to original context ?
So how are you going to utilize this fact ?
Lets say you want to update a String of the original page :
String itWillBeUpdated="old value";
#override
Widget build(BuildContext ctx)
{
.
.
.
onPressesed:() async {
itWillBeUpdated= await Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
setState((){});
},
}
On your editing page , you can define Navigator.pop(...) like this :
Navigator.pop<String>(context, "new string");
by doing this , you can provide any data back to the original page and by calling setState((){}) , your page will reflect the changes
This isn't ideal, but works somewhat. First I created a provider class and added the following;
class AudioWidgetProvider with ChangeNotifier {
int refreshIndex;
setRefreshIndex (ri) {
refreshIndex = ri;
return refreshIndex;
}
}
Then in my PageView Builder on the first page, I did this;
Widget build(context) {
var audioWidgetProvider = Provider.of<AudioWidgetProvider>(context);
return
PreloadPageView.builder(
controller: PreloadPageController(initialPage: audioWidgetProvider.refreshIndex?? 0),
Then to get to the EditPage (2nd screen) I did this;
onPressed: () async {
audioWidgetProvider.setRefreshIndex(currentIndex);
Navigator.pushReplacement(context,new MaterialPageRoute(builder: (BuildContext context) => EditPage()),); }
And finally I did this to return to a reloaded PageView scrolled to the edited page;
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) =>HomePage()));
The only problem now is that the PageView list comes from a PHP/Mysql query and I'm not sure what to do if new items are added to the list from the Mysql database. This means the currentIndex will be wrong. But I guess that's the topic of another question.

Flutter - Keep page static throughout lifecycle of app?

I have created an AppDrawer widget to wrap my primary drawer navigation and reference it in a single place, like so:
class AppDrawer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Drawer(
child: new ListView(
children: <Widget>[
new ListTile(
title: new Text("Page1"),
trailing: new Icon(Icons.arrow_right),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => Page1.singleInstance));
}
),
new ListTile(
title: new Text("Page2"),
trailing: new Icon(Icons.arrow_right),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new Page2("Page 2")));
}
),
]
),
);
}
}
I have also created a custom AppScaffold widget, which simply returns a consistent AppBar, my custom AppDrawer, and body:
class AppScaffold extends StatelessWidget {
final Widget body;
final String pageTitle;
AppScaffold({this.body, this.pageTitle});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(title: new Text(pageTitle), backgroundColor: jet),
drawer: AppDrawer(),
body: body
);
}
}
I have created two pages: Page1, and Page2. They are simple right now, and look something like this:
class Page1 extends StatelessWidget {
final String pageText;
Page1(this.pageText);
static Page1 get singleInstance => Page1("Page1");
Widget build(BuildContext context) {
return AppScaffold(
pageTitle: this.pageText,
body: SafeArea(
child: Stack(
children: <Widget>[
Center(child: SomeCustomWidget())
],
)
),
);
}
}
class Page2 extends StatelessWidget {
final String pageText;
Page2(this.pageText);
#override
Widget build(BuildContext context) {
return AppScaffold(
pageTitle: this.pageText,
body: SafeArea(
child: Stack(
children: <Widget>[
Center(child: SomeOtherCustomWidget())
],
)
),
);
}
}
When I run my app, I can see the navbar and drawer correctly. I can click on the links in the drawer to navigate between my pages. However, each time I navigate to a page, all of the widgets on that page get reset to their initial state. I want to ensure that the widgets do not get reset. Another way to think of this is: I only want one instance of each page throughout the lifecycle of the app, instead of creating them new whenever a user navigates to them.
I tried creating a static instance of Page1 that the Drawer uses when the onTap event is fired, but this does not work. Am I thinking about this incorrectly? Do I need to convert to a Stateful widget?
Oh, you're in for a treat... This will be kinda long (sorry) but please read all of it before making decisions and taking action - I promise I am saving you time.
There are many different solutions to this problem, but in general what you're asking about is state management (which is really software engineering, more info here - Understanding state management, and why you never will).
I'll try my best to explain what is happening in your specific case...
Problem:
Think of Navigator as a List of application states, which you can manipulate via its various methods (i.e. pop(), push(), etc.), with this in mind it is clear what is happening - on a button press you're actually removing the current state (page) and right after that you're pushing a new instance of your state (page).
Solution(s):
As I said, there are many solutions to this problem, for example, you may be tempted to store the state (the changes you made to a particular "page") somewhere in a var and inject that var when navigating between "pages", when creating a new instance of that page, but you'll soon run into other problems. This is why I don't think anyone can provide a simple solution to this problem...
First, may I suggest you some useful reads on the matter:
Flutter official docs on state management - When you get to the "Options" section of this, the fun part begins and can quickly get overwhelming, but fear not :P
Be sure to read the medium article mentioned in the start of my answer too, I found it really helpful.
These reads will be more than enough to help you make a decision, plus there are a ton of articles on Medium and YouTube videos touching on the matter of state management with Flutter (even some from the authors of the framework) - just search for "State management with Flutter".
Now my own personal opinion:
If it's a really simple use case and you don't plan to grow (which is almost never the case, trust me), you can just use StatefulWidgets in combination with setState() and maybe InheritedWidget (for dependency injection down the tree, or like React guys call it "lifting state up"). Or instead of the above, maybe have a look at scoped_model, which kinda abstracts all of this for you (tho, I haven't played with it).
What I use right now for a real world project is bloc and flutter_bloc (BLoC = Business Logic Component), I will not get into the details of it, but basically it takes the idea of scoped_model one step further, without over-complicating abstractions. bloc is responsible for abstracting away the "business logic" of your application and flutter_bloc to "inject" the state in your UI and react to state changes (official Flutter position on the matter is that UI = f(State)).
A BLoC has an input and an output, it takes in events as an input (can be user input, or other, any type of event really) and produces a state. In summary that's it about bloc.
A great way to get started is BLoC's official documentation. I highly recommend it. Just go through everything.
(p.s. This may be my personal opinion, but in the end state management in Flutter is all based on some form of using InheritedWidget and setState() in response to user input or other external factors that should change the application state, so I think the BLoC pattern is really on point with abstracting those :P)

How do I show AlertDialog in Flutter

I am starting to learn Flutter and am working on a Calculator app. When I want to prevent the user from some action (let's say divide by zero), I want to display a Dialog showing an error message. This requires a context, but when I pass context, this results in an error.
The examples that I have seen that do display an alert dialog all appear to be the result of a button being pressed, and this uses the context that is present when the app Widget is created. My situation is that the dialog is displayed outside the creation of the widget, and it appears that the context is not valid there.
How can I display a dialog as a result of an action taken by the user rather than the clicking a button within the Widget that has been created for the app? An example would be great.
The error that I am getting is as follows:
I/flutter ( 6990): The getter 'modalBarrierDismissLabel' was called on null.
While I presume from what I have read that I need to restructure the code and reposition the Alert Dialog, I have no idea how to do that. The examples that I have seen that work result from a Widget created on construction that consequently uses the context available at that point. In my case, I'm attempting to create the alert dialog as a result of an outcome from the result of what a user has done, not from the pressing of a widget button.
Some of my relevant code is as follows:
} else if (pendingOperator == "/") {
if (secondValue != 0) {
setNewValue(Decimal.parse(firstValue.toString()) /
Decimal.parse(resultString));
} else {
_showAlert(context, "Divide by zero is invalid");
}
}
class MyAppState extends State<MyApp> {
Decimal firstValue;
String pendingOperator;
bool clearCurrentValue = true;
String resultString = "0";
void _showAlert(BuildContext context, String text) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Error"),
content: Text(text),
));
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new SafeArea(
child: new Material(
color: Colors.black,
child: Column(
The need to show the alert is indirectly the result of a button being pressed. When that button is created, pass the context to the function that is called and use that context in the call to _showAlert.