In my flutter app, screen A has no AppBar.
So I call SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark) in build.
After another screen B which has an AppBar was pushed and then popped,
screen A has light status bar.
I'd like the system UI to return to the original setting when the screen is popped.
The reason behind this is the fact that your new screen will have its own lifecycle and thus, might use another color for the status bar.
You can call SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark) in your initState method but that won't trigger after a stacked screen is popped. There are two problems here, you can, however, call that back again after returning from a screen pop(). Simple enough right? Almost there.
When you press the back button on the AppBar widget, will return immediately from your Navigator.of(context).push(someroute), even if the navigation animation is still being rendered from the stacked screen.
To handle this, you can add a little "tweak" that will set the status bar color again after 500 milseconds, that should be enough for the animation to fully complete. So, you'll want something more or less like this:
class HomeScreen extends StatefulWidget {
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
#override
void initState() {
_updateAppbar();
super.initState();
}
void _updateAppbar() {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark);
}
#override
Widget build(BuildContext context) {
return Container(
child: RaisedButton(
child: Text('Navigate to second screen'),
onPressed: () => Navigator.of(context)
.push(MaterialPageRoute(builder: (BuildContext context) => SecondScreen()))
.whenComplete(() => Future.delayed(Duration(milliseconds: 500)).then((_) => _updateAppbar()))));
}
}
class SecondScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
);
}
}
Although this works, I'm still curious to know if someone knows a better way to handle this though and keep a status bar color binding to each screen.
maybe you can wrap the whole page widget with AnnotatedRegion like this:
AnnotatedRegion(
value: _currentStyle,
child: Center(
child: ElevatedButton(
child: const Text('Change Color'),
onPressed: _changeColor,
),
),
);
you can follow the full example here:
https://api.flutter.dev/flutter/services/SystemChrome/setSystemUIOverlayStyle.html
maybe that works
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.transparent)
Add this package to your project Need Resume and extends your screen state to ResumableState
import 'package:need_resume/need_resume.dart';
class WelcomeScreen extends StatefulWidget {
final String title;
const WelcomeScreen({Key key, this.title}) : super(key: key);
#override
_WelcomeScreenState createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends ResumableState<WelcomeScreen> {
#override
void onResume() {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark);
super.onResume();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [],
),
);
}
}
This solutions works as expected with very little changes in the code.
Related
I'm new to Flutter and confused about how InheritedWidget works with routes. I'm using an SQLite database with the sqflite library. Basically, what I'm trying to achieve is, when my app is launched, I want all widgets that don't require the database to show right away. For instance, the bottomNavigationBar of my Scaffold doesn't need the database but the body does. So I want the bottomNavigationBar to show right away, and a CircularProgressIndicator to be shown in the body. Once the database is open, I want the body to show content loaded from the database.
So, in my attempt to achieve this, I use FutureBuilder before my Scaffold to open the database. While the Future is not completed, I pass null for the drawer and a CircularProgressBar for the body, and the bottomNavigationBar as normal. When the Future completes, I wrap the drawer and body (called HomePage) both with their own InheritedWidget (called DataAccessor). This seems to work, as I can access the DataAccessor in my HomePage widget. But, when I use the Navigator in my drawer to navigate to my SettingsScreen, my DataAccessor is not accessible and returns null.
Here's some example code, not using a database but just a 5 second delayed Future:
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder(
future: Future.delayed(Duration(seconds: 5)),
builder: (context, snapshot) {
Widget drawer;
Widget body;
if (snapshot.connectionState == ConnectionState.done) {
drawer = DataAccessor(
child: Drawer(
child: ListView(
children: <Widget>[
ListTile(
title: Text("Settings"),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SettingsScreen()))
)
]
)
)
);
body = DataAccessor(child: HomePage());
}
else {
drawer = null;
body = Center(child: CircularProgressIndicator());
}
return Scaffold(
drawer: drawer,
body: body,
bottomNavigationBar: BottomNavigationBar(
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Container(), title: Text("One")),
BottomNavigationBarItem(icon: Container(), title: Text("Two"))
]
)
);
}
)
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
DataAccessor dataAccessor = DataAccessor.of(context); //dataAccessor IS NOT null here
print("HomePage: ${dataAccessor == null}");
return Text("HomePage");
}
}
class SettingsScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
DataAccessor dataAccessor = DataAccessor.of(context); //dataAccessor IS null here
print("SettingsScreen: ${dataAccessor == null}");
return Text("SettingsScreen");
}
}
class DataAccessor extends InheritedWidget {
DataAccessor({Key key, Widget child}) : super(key: key, child: child);
#override
bool updateShouldNotify(InheritedWidget oldWidget) => false;
static DataAccessor of(BuildContext context) => context.inheritFromWidgetOfExactType(DataAccessor);
}
It's possible I'm doing things wrong. Not sure how good of practice storing widgets in variables is. Or using the same InheritedWidget twice? I've also tried wrapping the entire Scaffold with my DataAccessor (and having the database as null while it is loading), but the issue still remains where I can't get my DataAccessor in my SettingsScreen.
I've read that a possible solution is to put my InheritedWidget before the MaterialApp but I don't want to resort to this. I don't want a whole new screen to show while my database is opening, I want my widgets that don't need the database to be shown. This should be possible somehow.
Thanks!
The solution in the last paragraph is what you need. The MaterialApp contains the Navigator which manages the routes, so for all of your routes to have access to the same InheritedWidget that has to be above the Navigator, i.e. above the MaterialApp.
Use Remi's method and you end up with a widget tree like this:
MyApp (has the static .of() returning MyAppState)
MyAppState, whose build returns _MyInherited(child: MaterialApp(...)) and whose initState starts loading the database, calling setState when loaded.
When building your home page you have access to MyAppState via .of, so can ascertain whether the database has loaded or not. If it has not, just build the database independent widgets; if it has, build all the widgets.
I have tried to use the Flutter camera plugin (0.2.1) in combination with a PageView and a BottomNavigationBar, but everytime the page gets switched, a few frames get skipped and the UI freezes for a second.
I've simplified my codebase for this example:
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
void main() => runApp(new Pages());
class Pages extends StatefulWidget {
#override
_PagesState createState() => _PagesState();
}
class _PagesState extends State<Pages> {
PageController _pageController;
int _page = 0;
#override
void initState() {
_pageController = new PageController();
super.initState();
}
void navTapped(int page) {
_pageController.animateToPage(page,
duration: const Duration(milliseconds: 300), curve: Curves.ease);
}
void onPageChanged(int page) {
setState(() {
this._page = page;
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: new Text("CameraTest"),
),
body: PageView(
children: <Widget>[Feed(), Camera(), Profile()],
controller: _pageController,
onPageChanged: onPageChanged,
),
bottomNavigationBar: new BottomNavigationBar(
items: [
new BottomNavigationBarItem(
icon: new Icon(Icons.home), title: new Text("Feed")),
new BottomNavigationBarItem(
icon: new Icon(Icons.camera), title: new Text("Capture")),
new BottomNavigationBarItem(
icon: new Icon(Icons.person), title: new Text("Profile"))
],
onTap: navTapped,
currentIndex: _page,
),
),
);
}
}
class Camera extends StatefulWidget {
#override
_CameraState createState() => _CameraState();
}
class _CameraState extends State<Camera> {
List<CameraDescription> _cameras;
CameraController _controller;
initCameras() async{
_cameras = await availableCameras();
_controller = new CameraController(_cameras[0], ResolutionPreset.medium);
await _controller.initialize();
setState(() {});
}
#override
void initState() {
initCameras();
super.initState();
}
#override
void dispose() {
_controller?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
if (_controller == null || !_controller.value.isInitialized) {
return new Center(
child: new Text("Waiting for camera...", style: TextStyle(color: Colors.grey),),
);
}
return new AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: new CameraPreview(_controller));
}
}
//just placeholder widgets
class Feed extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(child: new Text("Feed"));
}
}
class Profile extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(child: new Text("Profile"));
}
}
There are basically three pages with the middle one showing a camera-preview
(how it's supposed to look), but on switching to the camera and back from it this happens. This is really annoying since it ruins the user experience and is not smooth at all. The lag appears when calling initCameras() or when disposing the camera-controller. I tried using initCameras() in combination with a FutureBuilder, which didn't help at all, and running the method in a seperate isolate, but platform calls seem to be only allowed on the main isolate. It seems a bit weird to me since opening the camera doesn't need too much cpu power, so an async method should be fine. I am aware there is an image-picker plugin, but I want to have the preview in the app directly. I have also considered to run initCameras() on app start, but i don't want to have the camera running all the time when the user is just using another page of the app.
Is there any way to improve upon initCameras() or perhaps use a different implementation to fix the stuttering? I wouldn't even care if it takes a second to load, but i don't want any frame skips.
I followed the example on the bottom of the camera page.
Tested on physical devices as well as emulators on different Android versions.
I have solved a similar issue by adding a delay before initializing the camera:
Future.delayed(Duration(seconds: 1), () {
initCameras();
});
This way the initCameras() is called after the page navigation animation is completed. You can show a CircularProgressIndicator() to make this delay more user friendly.
I am sure there is a more neat way to work around the issue, but this seems to be the simplest solution.
If you want to use image path in the different page than store image path as the global variable, and use it where you want.
I want to create an App with Tabs to get the users input. The Problem is, that the different Tabs get different inputs, but i have to collect the inputs for the Database. My idea her was, that the main scaffold collects the inputs from all Tabs and write it in a database! My problem is that I don't know to send data from the tab (statefullWidget in an other file) to the parent class (Scaffold) or run a function from there!
Please help me and sorry for my bad English!
Jonas
You can pass a Function that can be called whenever you want.
Small example
MamaBear class
...
class _MamaBear extends State<MamaBear> {
void hungryBear(String babyBear) {
print("$babyBear is hungry");
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Column(children: <Widget>[
BabyBear(
"Mark",
(babyBear) {
hungryBear(babyBear);
},
)])));}
BabyBear class
class BabyBear extends StatefulWidget {
final String babyBearName;
final Function onBearAction;
BabyBear(this.babyBearName, this.onBearAction);
#override
_BabyBear createState() => _BabyBear();
}
class _BabyBear extends State<BabyBear> {
#override
Widget build(BuildContext context) {
return Card(
child: RaisedButton(
child: Text("Mama I'm hungry"),
onPressed: () {
widget.onBearAction(widget.babyBearName);
}),
);
}
}
I'm trying to change the state from a different widget in Flutter. For example, in the following example I set the state after a few seconds.
Here is the code for that:
class _MyAppState extends State<MyApp> {
int number = 1;
#override
void initState() {
super.initState();
new Future.delayed(new Duration(seconds: 5)).then((_) {
this.setState(() => number = 2);
print("Changed");
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new FlatButton(
color: Colors.blue,
child: new Text("Next Page"),
onPressed: () {
Navigator.of(context).push(new MaterialPageRoute(
builder: (BuildContext context) => new StatefulBuilder(builder: (BuildContext context, setState) =>new MySecondPage(number))
));
},
),
),
);
}
}
I tried using an InheritedWidget, but that won't work unless I wrap it around my top level widget, which is not feasible for what I'm trying to do (the code above is a simplification of what I'm trying to achieve).
Any ideas on what the best way of achieving this is in Flutter?
Avoid this whenever possible. It makes these widgets depends on each others and can make things harder to maintain in the long term.
What you can do instead, is having both widgets share a common Listenable or something similar such as a Stream. Then widgets interact with each other by submitting events.
For easier writing, you can also combine Listenable/Stream with respectively ValueListenableBuilder and StreamBuilder which both do the listening/update part for you.
A quick example with Listenable.
class MyHomePage extends StatelessWidget {
final number = new ValueNotifier(0);
#override
Widget build(BuildContext context) {
return Scaffold(
body: ValueListenableBuilder<int>(
valueListenable: number,
builder: (context, value, child) {
return Center(
child: RaisedButton(
onPressed: () {
number.value++;
},
child: MyWidget(number),
),
);
},
),
);
}
}
class MyWidget extends StatelessWidget {
final ValueListenable<int> number;
MyWidget(this.number);
#override
Widget build(BuildContext context) {
return new Text(number.value.toString());
}
}
Notice here how we have our UI automatically updating when doing number.value++ without ever having to call setState.
Actually the most effective way to do this is using BLoC package in flutter and implement it from the top of the widget tree so all inheriting widgets can use the same bloc. If you have worked with Android before - it works like Android Architecture Components - you separate data and state management from the UI - so you do not setState in the UI, but instead use the block to manage state. So you can set and access the same data - from any widget that inherits from the top widget where the bloc is implemented, for more complex apps, it is very useful.
This is where you can find the package: https://pub.dev/packages/flutter_bloc#-readme-tab-
Write-up: https://www.didierboelens.com/2018/08/reactive-programming-streams-bloc/
And a great tutorial on youtube https://www.youtube.com/watch?v=hTExlt1nJZI&list=PLB6lc7nQ1n4jCBkrirvVGr5b8rC95VAQ5&index=7
Is there a way to force Flutter to redraw all widgets (e.g. after locale change)?
Your Widget should have a setState() method, everytime this method is called, the widget is redrawn.
Documentation : Widget setState()
Old question, but here is the solution:
In your build method, call the rebuildAllChildren function and pass it the context:
#override
Widget build(BuildContext context) {
rebuildAllChildren(context);
return ...
}
void rebuildAllChildren(BuildContext context) {
void rebuild(Element el) {
el.markNeedsBuild();
el.visitChildren(rebuild);
}
(context as Element).visitChildren(rebuild);
}
This will visit all children and mark them as needing to rebuild.
If you put this code in the topmost widget in your widgets tree, it will rebuild everything.
Also note you must order that specific widget to rebuild. Also you could have some boolean so that the rebuild of that widget only rebuilds all of its children when you really need it (it's an expensive operation, of course).
IMPORTANT: This is a hack, and you should only do this if you know what you are doing, and have strong reason to do so. One example where this is necessary is in my internationalization package: i18_extension. As Collin Jackson explained in his answer, you are really not supposed to do this in general.
This type of use case, where you have data that children can read but you don't want to explicitly pass the data to the constructor arguments of all your children, usually calls for an InheritedWidget. Flutter will automatically track which widgets depend on the data and rebuild the parts of your tree that have changed. There is a LocaleQuery widget that is designed to handle locale changes, and you can see how it's used in the Stocks example app.
Briefly, here's what Stocks is doing:
Put a callback on root widget (in this case, StocksApp) for handling locale changes. This callback does some work and then returns a customized instance of LocaleQueryData
Register this callback as the onLocaleChanged argument to the MaterialApp constructor
Child widgets that need locale information use LocaleQuery.of(context).
When the locale changes, Flutter only redraws widgets that have dependencies on the locale data.
If you want to track something other than locale changes, you can make your own class that extends InheritedWidget, and include it in the hierarchy near the root of your app. Its parent should be a StatefulWidget with key set to a GlobalKey that accessible to the children. The State of the StatefulWidget should own the data you want to distribute and expose methods for changing it that call setState. If child widgets want change the State's data, they can use the global key to get a pointer to the State (key.currentState) and call methods on it. If they want to read the data, they can call the static of(context) method of your subclass of InheritedWidget and that will tell Flutter that these widgets need to rebuilt whenever your State calls setState.
Refreshing the whole widget tree might be expensive and when you do it in front of the users eyes that wouldn't seem sweet.
so for this purpose flutter has ValueListenableBuilder<T> class. It allows you to rebuild only some of the widgets necessary for your purpose and skip the expensive widgets.
you can see the documents here ValueListenableBuilder flutter docs
or just the sample code below:
return Scaffold(
appBar: AppBar(
title: Text(widget.title)
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
ValueListenableBuilder(
builder: (BuildContext context, int value, Widget child) {
// This builder will only get called when the _counter
// is updated.
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text('$value'),
child,
],
);
},
valueListenable: _counter,
// The child parameter is most helpful if the child is
// expensive to build and does not depend on the value from
// the notifier.
child: goodJob,
)
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.plus_one),
onPressed: () => _counter.value += 1,
),
);
And also never forget the power of setState(() {});
I explain how to create a custom 'AppBuilder' widget in this post.
https://hillelcoren.com/2018/08/15/flutter-how-to-rebuild-the-entire-app-to-change-the-theme-or-locale/
You can use the widget by wrapping your MaterialApp with it, for example:
Widget build(BuildContext context) {
return AppBuilder(builder: (context) {
return MaterialApp(
...
);
});
}
You can tell the app to rebuild using:
AppBuilder.of(context).rebuild();
Simply Use:
Navigator.popAndPushNamed(context,'/screenname');
Whenever you need to refresh :)
What might work for your use case is using the Navigator to reload the page. I do this when switching between "real" and "demo" mode in my app. Here's an example :
Navigator.of(context).push(
new MaterialPageRoute(
builder: (BuildContext context){
return new SplashPage();
}
)
);
You can replace "new SplashPage()" in the above code with whatever main widget (or screen) you would like to reload. This code can be called from anywhere you have access to a BuildContext (which is most places in the UI).
Just use a Key on one of your high-level widgets, everything below this will lose state:
Key _refreshKey = UniqueKey();
void _handleLocalChanged() => setState((){
_refreshKey = UniqueKey()
});
Widget build(BuildContext context){
return MaterialApp(
key: _refreshKey ,
...
)
}
You could also use a value key like:
return MaterialApp(
key: ValueKey(locale.name)
...
);
Why not just have Flutter.redrawAllWidgetsBecauseISaidSo();? –
TimSim
There kinda is:
Change to key to redraw statefull child widgets.
Jelena Lecic explained it good enough for me on medium.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
var _forceRedraw; // generate the key from this
void _incrementCounter() {
setState(() {
_counter++;
_forceRedraw = Object();
});
}
#override
void initState() {
_forceRedraw = Object();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
MyStatefullTextWidget(
key: ValueKey(_forceRedraw),
counter: _counter,
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class MyStatefullTextWidget extends StatefulWidget {
final int counter;
const MyStatefullTextWidget({
required this.counter,
Key? key,
}) : super(key: key);
#override
_MyStatefullTextWidgetState createState() => _MyStatefullTextWidgetState();
}
class _MyStatefullTextWidgetState extends State<MyStatefullTextWidget> {
#override
Widget build(BuildContext context) {
return Text(
'You have pushed the button this many times:${widget.counter}',
);
}
}
Simply Use:
Navigator.popAndPushNamed(context,'/xxx');
I my case it was enough to reconstruct the item.
Changed:
return child;
}).toList(),
To:
return SetupItemTypeButton(
type: child.type,
icon: child.icon,
active: _selected[i] == true,
onTap: ...,
);
}).toList(),
class SetupItemTypeButton extends StatelessWidget {
final dynamic type;
final String icon;
estureTapCallback onTap;
SetupItemTypeButton({Key? key, required this.type, required this.icon, required this.onTap}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container();
}
}
class SetupItemsGroup extends StatefulWidget {
final List<SetupItemTypeButton> children;
final Function(int index)? onSelect;
SetupItemsGroup({required this.children, this.onSelect});
#override
State<SetupItemsGroup> createState() => _SetupItemsGroupState();
}
class _SetupItemsGroupState extends State<SetupItemsGroup> {
final Map<int, bool> _selected = {};
#override
Widget build(BuildContext context) {
int index = 0;
return Container(
child: GridView.count(
children: widget.children.map((child) {
return SetupItemTypeButton(
type: child.type,
icon: child.icon,
active: _selected[i] == true,
onTap: () {
if (widget.onSelect != null) {
int i = index++;
child.active = _selected[i] == true;
setState(() {
_selected[i] = _selected[i] != true;
child.onTap();
widget.onSelect!(i);
});
}
},
);
}).toList(),
),
);
}
}