I have two app 'screens', A and B. Both set the SystemChrome.setEnabledSystemUIOverlays in the build methods.
A sets as per
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
return WillPopScope(
....
whilst B sets as per
Widget build(BuildContext context) {
if (MediaQuery.of(context).orientation == Orientation.landscape) {
SystemChrome.setEnabledSystemUIOverlays([]);
}
else {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
}
return WillPopScope(
.....
Navigating from A to B in portrait works fine. If however I enter landscape in B flutter removes the overlays and then re-implements them immediately, ie I see the overlay disappear and reappear quickly. If I remove the overlay line completely from A, B works fine. Its like B is rebuilt followed by A but in the background. Is that a thing?
I'm navigating like so
Navigator.push(context, MaterialPageRoute(builder: (mContext) => DataForm(file: _file)))
I've been tying to track this bug for three days. For reference overlays are set here an only here in all my code. The issue only occurs on Android.
I appreciate the first response will be 'its in your code we need to see it to solve it'. I tried reproducing the issue in 'minimal code' to post an example but I cannot get the error to reoccur and my code is so extensive I am not sure how to show it all.
As per the title I am instead asking what scenarios might cause screen A overlay setting to influence screen B at each build.
Thanks in advance!
Answering my own question after 5 days of tracking down my bug in the hope it might help some else if future. I suspect those with experience will be thinking 'yes of course and' so this is probably more for someone new like myself.
SystemUIOverlay is like a global variable that persists in the app and is implemented across screens automatically. A new screen/code can change this overlay setting at any time and know if will be applied globally and retrospectively to all screens. In my case SystemUIOverlay is set in build so any action in Screen A that forces a rebuild will therefore enforce the update and apply the global overlay value to B.
Even though Screen A is not showing it can continue to rebuild in the background if triggered, even when Screen B is opened. Therefore anything triggering a rebuild of A whilst B is open means B will adopt A's overlay setting globally. This was the result I was seeing.
A couple of examples:
As referenced here, MediaQuery.of will cause your widget to rebuild automatically whenever the MediaQueryData changes (e.g., if the user rotates their device). So for instance if you include 'MediaQuery.of' anywhere in you build of Screen A, a build can occur even when not in focus. This rebuild of A then enforces it's overlay value globally which is picked up by B whilst it is open.
In my case rebuilding B set the overlay to [] and affected the insets. MediaQuery in A picked up on this, rebuilt and reset the overlay to .values and forced these onto B. And round she goes.
A periodic timer calling setState in A will do the same thing. I am sure there are many more examples.
So look out for and avoid any rebuild potential in A when it has lost focus.
Related
Here is the problem: I have a list of items displayed in a list view. I can edit these items with the dialogs displayed by clicking on them. After editing and closing the dialog, I also need to update the items on the list view. I'm currently doing this with the following code snippet in my list view item widget:
showDialog(
context: context,
builder: (context) {
return UpdateItemDialog(item: _item);
},
).then((updatedItem) {
if (updatedItem != null) {
setState(() => _item = updatedItem);
}
});
and by calling Navigator.of(context).pop(_item); from my dialog.
It works perfectly fine when there are no rebuilds occur until the dialog is dismissed. However, for example, if the orientation is changed when the dialog is open, I get Unhandled Exception: setState() called after dispose() error since the list view also rebuilt because of the orientation change and the then clause in my code runs on the destroyed widget.
Is there a way to access the rebuilt widget from a destroyed widget?
Or should I use a different approach to this problem?
By the way, changes should only be accepted after the dialog is dismissed, so I should return the item from the dialog.
I believe your best bet would be to introduce even a simple state management solution (Provider would be good) to handle communication between a dialog and other widgets.
Check out the Gist below (Run it on Dartpad.dev) as an example how you can keep it simple, yet clean and decoupled
https://gist.github.com/romanejaquez/8aed8d699fba8fdfff4b0966dfe47663
in which I show that instead of passing data from a dialog back to another widget, a State Management solution would allow you for a decoupled way of passing data back and forth, notifying each other and triggering rebuilds (which is kind of calling setState() since pretty much that's what you want - trigger a rebuild on the other widget. In this example, I'm sending a value back to the originating widget and triggering a rebuild (thanks to the Consumer widget listening to the changes triggered in the common service. That'd be my honest suggestion.
I am employing Riverpod for state management via StateNotifierProvider. My switch button just won't enable upon clicking and thus no state changes to effect theme mode switching. I have spent a ton of hours researching similar issues but none like mine as I am not using setState(). I expect the state of the button to change on clicking on dragging but the thumb of the switch just returns to its initial off position. Please help me with what I am doing wrong. Here are snapshots of my main.dart, home_screen.dart, and the theme controller notifier as I am unable to copy and paste my code correctly as advised with 4 spaces.
You need to update the state (not the _isDarkvariable) this way:
toggleTheme(bool value) {
state = value;
_safePrefs;
}
The image is not complete, so I can't see the use of _isDark variable (Probably nothing). But You shoul be fine now, at least updating the state.
I have a typical CRUD operation task: (List Sites, Add Site, ...)
I created a SitesProvider. In the same provider, I added 2 methods for add and edit. I was thinking to use the same provider in the 2 screens (ListSites and AddEditSite)
In the ListSites screen, it works fine. Here is the problem:
I open the AddSite screen by clicking the AddButton in the ListSites screen
Hit submit (did not enter data, simulating error case)
The error gets displayed in the ListSites screen, not in the AddSite screen.
It makes sense. They both use the same provider, both screens are on the stack. It seems that the first one only consumes the state update and displays the error.
I use MultipleProviders approach that wraps the MaterialApp with all providers in the app.
Can we fix that without creating separate providers for each of the 2 screens?
EDIT:
I used provider.removeListener in the ListSites screen right before I open the AddEdit and it shows the error in the correct screen now. I still have to do some other tweaks to get it back to listen after I add. Not efficient I think but it is a step.
I ended up doing that:
Added Another Field in the provider (stateType: list/addedit)
Change the type per the screen I'm currently in
provider.stateType = UIStateType.add_edit;
await Navigator.push(context,
MaterialPageRoute(builder: (context) => AddEditSiteScreen()));
provider.stateType = UIStateType.list;
in build(), I check for the type
sitesProvider = Provider.of(context);
if (sitesProvider.stateType != UIStateType.add_edit) return Container();
Ok, I am working on a media player app using flutter. Now when I press the play button, it changes to pause button. (Only the icon on the button changes)
The onPressed function for the button changes a boolean value and calls the setState() method.
It goes something like this.
bool _playing = false;
void _onPlayButtonPressed() {
setState () {
if (_playing)
_playing = false;
else
_playing = true;
}
}
I also have a function that returns the icon based on the value of _playing.
Widget _playButtonIcon() {
//This function has no setState() call
if (_playing)
//return pause icon
else
//return play icon
}
Everything works fine. The icon changes everytime I press the button. However as mentioned in docs and also in Flutter Demo App, setState() method calls the build method. Which redraws the entire widget tree including child widgets that are completely unchanged.
Is this approach justified or is this an overkill?
Do I have to put the button on a different Stateful Widget Class and call its build method via setState() everytime I tap this playButton?
What if I have other widgets that also changes the state of UI. Possibly changing different widgets?
What is the proper way to do this without having a performance hit?
Creating a play button that is a widget of its own with a state that maintains whether it is playing or not definitely makes sense. Now when you call setState on the parent widget it does call the build method, but as far as I know it does not necessarily redraw everything from scratch. If no changes are found in some of the embedded widgets it does not redraw them since they are already in the widget tree. Finally, it is okay to call setstate, however if your app starts getting bigger and you find yourself calling set state in too many places, and want to use global keys, I would advise looking into the Provider package, and making use of the ChangeNotifier/Consumer pattern.
I've been developing in Flutter for a few months and I'm not yet very experienced. These days I'm working on an app that I didn't create from the beginning and I'm having a strange problem, unfortunately I can't paste too much pieces of code but I try to explain the wrong behavior.
The state of the app is contained in an InheritedWidget that is called before all the others. For example, in this InheritedWidget there is a value that must always be visible at the top of the app (in the AppBar). The problem is that if at runtime this value is changed in the InheritedWidget, the view shows the previous value (as if it wasn't updated), but if I do Navigator.push() to a new page, the AppBar shows the correct value (i.e. the updated one). If I pop to the previous page, the old value reappears in the AppBar. If I put the app in the background and bring it back to the foreground, the correct value finally appears.
It seems that the view does not update even if the value changes in the InheritedWidget. I specify that before being displayed, this value is extracted directly from the InheritedWidget using context.dependOnInheritedWidgetOfExactType<InheritedWidgetName>(). I also specify that "updateShouldNotify" is set this way:
#override
bool updateShouldNotify(Session oldWidget) {
return true;
}
I wanted to ask if anyone knows what might be causing this problem.
Thanks in advance.