Flutter Pageview : swiping backward calls build method on widgets in unexpected ways - flutter

Consider this simple code snippet, a snippet I wrote to reproduce my problem. It is just a pageview with 3 stateful widgets into it. (It is stand alone, you can copy-paste it and run it):
import 'package:flutter/material.dart';
void main() => runApp(new TestInherited());
class TestInherited extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Pageview test',
home: PageView(
children: <Widget>[TestWidget("1"), TestWidget("2"), TestWidget("3")],
));
}
}
class TestWidget extends StatefulWidget {
final String _name;
String get name => _name;
#override
TestWidgetState createState() {
return TestWidgetState();
}
TestWidget(this._name);
}
class TestWidgetState extends State<StatefulWidget> {
#override
Widget build(BuildContext context) {
TestWidget w = widget;
print("building ${w._name}");
return Center(child: Text(w._name));
}
}
When swiping forward from page 1 to 2 to 3, you see the build methods being called nicely (due to the print statements).
But when going backward, from page 3 to page 2 this does not happen for page 2. But going from 2 to 1 it is called for page 1.
So:
3 to 2: nothing happens
2 to 1: prints "building 1"
That is a problem for me since in my project on page 3 I change stuff that influences page 2 so it needs to rebuild.
It even gets more strange when just adding a few more pages (say we have 5). Then going forward works fine, (the print happens on every page) but going backward...
5 to 4: prints "building 3" (??)
4 to 3: nothing
3 to 2: prints "building 2"
2 to 1: prints "building 1"
Can anyone explain this to me, please?
Thanks a lot for your time and help!

As a general rule, you should assume that nothing will be rebuilt if you want it to be rebuilt and everything will be rebuilt if you want it not to be. The model is that we rebuild everything every frame, but we have optimizations to remove rebuilds that we can prove aren't necessary.
In this case, since you never call setState, we don't really need to rebuild anything. We probably only rebuild the widgets that we discarded to save memory when they're not visible.
The TestWidget should call setState when it needs to be rebuilt. You could have page 3 send a notification to page 2 via a Listenable or some such and have page 2 use that to know when to rebuild.

Related

How to get the State<> instance inside of its StatefulWidget?

I have an unusual use case where I'd like to add a getter to a StatefulWidget class that accesses its State instance. Something like this:
class Foo extends StatefulWidget {
Foo({super.key});
int get bar => SomethingThatGetsFooState.bar;
#override
State<Foo> createState() => _FooState();
}
class _FooState extends State<Foo> {
int bar = 42;
#override
Widget build(BuildContext context) {
return Container();
}
}
Does SomethingThatGetsFooState exist?
I wonder, if your approach is the right way.
Flutter's way isn't 'Ask something about its state'
Flutter is much more like this: 'The consumer of a Widget passes something to another Widget, which the other Widget e.g. calls in case of certain situations (e.g. value change).'
Approach 1
You map pass a Callback Function to Foo and pass that along to _FooState.
If something special happens inside _FooState, you may call the callback and thus pass some value back to the provider of the Callback.
Approach 2
Probably use a state management solution like Flutter Redux. Using Flutter Redux, you establish a state store somewhere at the top of the widget tree, e.g. in MaterialApp.
Then you subscribe to the store at certain other locations, where dependent widgets exist, which need to update accordingly.
In one project I created certain Action classes, which I send to certain so called reducers of those states, to perform a change on the state:
StoreProvider.of<EModel>(context).dispatch( SaveToFileAction())
This call finds the relevant EModel state and asks it to perform the SaveToFileAction().
This way, a calling Widget not even needs to know, who is responsible for the Action.
The responsible Widget can even be moved around the widget tree - and the application still works. The initiator of SaveToFileAction() and the receiver are decoupled. The receiver you told a coordination 'Tell me, if someone tried to ask me for something.'
Could your provide some further details? Describe the usage pattern?
#SteAp is correct for suggesting there's a code smell in my OP. Typically there's no need to access State thru its StatefulWidget. But as I responded to his post, I'm fleshing out the first pass at a state management package, so it's an unusual case. Once I get it working, I'll revisit.
Below worked without Flutter complaining.
class _IntHolder {
int value;
}
class Foo extends StatefulWidget {
Foo({super.key});
final _intHolder = _IntHolder();
int get bar => _intHolder.value;
#override
State<Foo> createState() => _FooState();
}
class _FooState extends State<Foo> {
int value = 42;
#override
Widget build(BuildContext context) {
widget._intHolder.value = value;
return Container();
}
}

Is it normal that when I run my flutter code the first time it is rederized 2 times? (I only use one class )

I am new to flutter. Something worries me, I don't know if it is normal. I know the code will render if the widget is of type StateFulWidget. But in this case, I have a stateLessWidget and for some reason it renders 2 times. is this normal?
this is my code:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
print("main");
return MaterialApp(title: 'Provider Example', initialRoute: '/', routes: {
'/': (context) => Page2(),
});
}
}
class Page2 extends StatelessWidget {
Page2() {
print("page2");
}
#override
Widget build(BuildContext context) {
return Container();
}
}
this is the output:
Restarted application in 832ms.
I/flutter ( 4439): main
I/flutter ( 4439): page2 --> next two lines are the same, the widget is render again
I/flutter ( 4439): main
I/flutter ( 4439): page2
Flutter build method is what creates and return your rendered widget on the screen, so the build must be called everytime something change in the UI, and it need to rebuild. According to the Documentation
The build method will be called after:
After calling initState.
After calling didUpdateWidget.
After receiving a call to setState.
After a dependency of this State object changes (e.g., an InheritedWidget referenced by the previous build changes).
After calling deactivate and then reinserting the State object into the tree at another location.
So even if you have a Stateless Widget, flutter can and will rebuild multiple times, this why you should avoid putting logic handlers inside your Widgets, specially the build, let this method as simple as possible with only what it actually needs to build the widget.
Also, during animations, transitions... Your widget will be rebuild a lot of times in order to perfom the animation. If you want to avoid unnecessary builds there's some ways you can do it, by making using of the const widgets, if you that a certain Widget won't change during runtime like a Text('Hi') this kind of Widget just need to be build once, so you can use a const keyword to it.

Nested Flutter Futurebuilder is not updating the view

I have a Stateful widget that I am displaying inside a stateless widget. I have reused this Widget in several parts of my application, where it worked.
The widget looks a somethin like this
class InnerWidget extends StatefulWidget {
// private keys for objects in the remote dataahase
List<int> privateKeys;
const InnerWidgt({
Key key,
this.privateKeys,
}) : super(key: key);
#override
_InnerWidgetState createState() => _InnerWidgetState();
}
class _InnerWidgetState extends State<InnerWidget> {
// data retrieved from the database
Future<List<SomeClass>> data;
#override
void initState() {
this.data = this.fetchData(widget.privateKeys);
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<List<SomeClass>> (
future: this.data,
builder: (BuildContext contest, AsyncSnapshot<List<SomeClass>> snapshot) {
if(snapshot.hasData){
// return a widget (a list of names in this case)
}
return Text('Loading...');
}
);
}
Future<List<SomeClass>> fetchData(List<int> pKeys){
// fetches the JSON objects from the server and returns them as instances
}
}
The widget is created and given a list of keys, it makes a call to the remote API to get some objects and displays them in a list. I thought it might be nice that this widget is kind of handling itself, but maybe it's bad design?
What happens, is that the fetchData() method is never called and the "Loading..." is displayed forever. However snapshot.hasData does return true, so I'm thinking this is some kind of repainting issue.
The view I am talking about, where this is used is a detail view, which you navigate to from a list (ListView). I am using this exact same InnerWidget in the the elements inside the ListView, where it works perfectly fine. But when I click on the item to navigate to the detail page, the same InnerWidget does not work. (I know fetching the same data twice is not good, I have that in the back of my head to change it, and there's already a Cache underneath). Maybe this has something to do with the preserved state in the ElementTree? That because I am using the same InnerWidgit, it's preserving its state and not rerendering?
I hope I phrased my question well enough, I did not want to throw hundreds of lines of code at you, that's why created this minimal example of the widget at least. I can add the list and navigation part too if needed, but maybe this is something, that somebody that has worked more with Flutter, knows right away, I'm only 3 weeks in.

Something like conditional routing in Flutter?

Consider I have 3 screens namely screen 1, screen 2, screen 3 and screen 4.
I want to achieve the following.
Screen 3 is opened by Screen 1 -> BackButton -> Screen 2
Screen 3 is opened by Screen 2 -> BackButton -> Screen 3
Screen 3 is opened by Screen 4 -> BackButton -> Screen 1
Moreover, iOS automatically sets a swipe back option. I want to overwrite it that a swipe back in iOS does the same as described above.
Is there something like conditional routing in Flutter which helps me to adjust the BackButton-behaviour in accordance to 'from which Screen was my current Screen opened (navigator.push)'?
Wrap your widget tree in a WillPopScope() widget. This widget has an onWillPop property that you can override to whatever you want - in this case, depending on the screen you're on you'll probably want to override it to
onWillPop: () => Navigator.pushReplacement(<correctScreenWidget>)
This should catch any attempts to go back and instead do whatever you override it to. Be sparing with it, overriding default back button behaviour can make for a weird user experience if done poorly.
As for the conditional part of it, unfortunately it's a bit tricky as Navigator._history is private, so we can't just check the previous route that way. Best bet is to set up a NavigatorObserver to keep track of previous routes, and set the name in the RouteSettings of each of your routes to keep track.
Step one is to create an observer and provide it to your Navigator, something like this:
class PreviousRouteObserver extends NavigatorObserver {
Route _previousRoute;
Route get previousRoute => _previousRoute;
String get previousRouteName => _previousRoute.settings.name;
#override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
_previousRoute = previousRoute;
}
#override
void didReplace({Route<dynamic> newRoute, Route<dynamic> oldRoute}) {
_previousRoute = oldRoute;
}
}
class MyApp extends StatelessWidget {
final PreviousRouteObserver observer = PreviousRouteObserver();
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(title: 'Flutter Demo Home Page', observer: observer),
navigatorObservers: [
observer,
],
);
}
}
Note that MyHomePage above needs to accept the observer as an argument so you can access it. Alternatively you could set up an InheritedWidget or something to maintain access to it, but this answer is getting a little long already so I'll leave that for a later question.
Then, when providing Routes to your Navigator, ensure you've got a name in the RouteSettings:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => NextScreen(observer: widget.observer),
settings: RouteSettings(name: "nextScreen"),
),
);
Finally, do conditional routing based on the current value of widget.observer.previousRouteName in any widget that has access to it. This is just a simple matter of a switch or something in your onWillPop, so I'll leave that to you.
Kind of unfortunate that it's so roundabout, but it looks like this might be your best option at the moment. Hope it helps!

What is the difference between runApp(new MyApp()) and runApp(new MaterialApp()) in flutter?

In flutter we can pass a stateless widget that returns a MaterialApp instance to the runApp() function like this:
void main()=>runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
...
);
}
}
or we can pass the instance of MaterialApp directly to the runApp() function like so:
void main()=>runApp(
new MaterialApp(
...
);
);
What is the difference between these to ways? Thanks.
There's no difference in visual behavior.
What changes is how hot reload behaves.
For example if you used runApp(MaterialApp()), changing from
runApp(MaterialApp(title: 'Foo'))
to
runApp(MaterialApp(title: 'Bar'))
then the hot reload wouldn't take changes into consideration.
While if you had the following class :
class MyApp {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Foo',
);
)
}
and used it like this :
runApp(MyApp())
then changing title of MyApp would be correctly hot reloaded.
For hot-reload to be able to keep the state, it applies code changes and re-runs build() so that the view is updated. If the whole app would be restarted, the previous state would be lost. This is not desired. If you want this use hot restart instead.
This also means that changes to code that is only executed when the whole app is restarted, will not be applied to the current state.
For more details about hot-reload and limitations see https://flutter.io/docs/development/tools/hot-reload
To add custom behavior on hot-reload the method State<T>.reassemble can be overridden https://docs.flutter.io/flutter/widgets/State/reassemble.html
In one case you have a class, which you can add more methods to, and use them. In one case you don't.