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
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 have to create an app for psychological reaction time testing. For that purpose I have to control exactly when an image is
visible and exactly measure the delay between the onset of the visibility of the image and the onset of the reaction (e.g. tap event).
How can I achieve that in Flutter? Specifically, what is the most efficient way to show and hide several different images in the same place and how can I exactly know the onset of the tap event in relation to the onset of the real and full visibility to the user taking into consideration the frame rate of the device? Is there way to get low-level control over that process. Flutter seems to expose a high-level api, usually.
I have made an attempt that is possibly exactly what you are looking for, my logic is as follows:
Add a listener to the image being presented.
Using Stopwatch class, I notify my object to start counting time once the image is displayed.
When clicking on the correct answer, I stop my Stopwatch to stop counting.
Save my current score, and carry on to the next question.
Note:
In this example, for the sake of simplicity, I did not make an account for which answer is correct and which is not.
I create a new StatelessWidget to hold each question, using PageView.builder might be a good use here as well.
Simple Example:
import 'dart:async';
import 'package:flutter/material.dart';
int score = 0;
class TimeTest extends StatefulWidget {
Widget nextQuestionWidget; //to navigate
String question;
NetworkImage questionImage;
List<String> answers;
TimeTest(
{this.questionImage, this.question, this.answers, this.nextQuestionWidget });
#override
_TimeTestState createState() => new _TimeTestState();
}
class _TimeTestState extends State<TimeTest> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
bool _loading = true;
Stopwatch timer = new Stopwatch();
#override
void initState() {
widget.questionImage.resolve(new ImageConfiguration()).addListener((_, __) {
if (mounted) {
setState(() {
_loading = false;
});
timer.start();
}
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
key: _scaffoldKey,
appBar: new AppBar(title: new Text("Time Test"),),
body: new Container(
alignment: FractionalOffset.center,
margin: const EdgeInsets.symmetric(vertical: 15.0),
child: new Column(
children: <Widget>[
new Text(widget.question),
new Divider(height: 15.0, color: Colors.blueAccent,),
new CircleAvatar(backgroundImage: widget.questionImage,
backgroundColor: Colors.transparent,),
new Container(height: 15.0,),
new Column(
children: new List.generate(widget.answers.length, (int index) {
return new FlatButton(
onPressed: () {
///TODO
///add conditions for correct or incorrect answers
///and some manipulation on the score
timer.stop();
score = score + timer.elapsedMilliseconds;
print(score);
_scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text(
"Your answered this question in ${timer
.elapsedMilliseconds}ms")));
///Hold on before moving to the next question
new Future.delayed(const Duration(seconds: 3), () {
Navigator.of(context).push(new MaterialPageRoute(
builder: (_) => widget.nextQuestionWidget));
});
}, child: new Text(widget.answers[index]),);
}),
)
],
),),
);
}
}
class QuestionOne extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new TimeTest(question: "Which animal is in this photo?",
questionImage: new NetworkImage(
"http://cliparting.com/wp-content/uploads/2016/09/Tiger-free-to-use-clipart.png"),
answers: ["Lion", "Tiger", "Cheetah"],
nextQuestionWidget: new QuestionTwo(),);
}
}
class QuestionTwo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new TimeTest(question: "Which bird is in this photo?",
questionImage: new NetworkImage(
"http://www.clker.com/cliparts/P/q/7/9/j/q/eagle-hi.png"),
answers: ["Hawk", "Eagle", "Falcon"],
nextQuestionWidget: new ResultPage(),);
}
}
class ResultPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text("Result"),),
body: new Center(
child: new Text("CONGRATULATIONS! Your score is $score milliseconds"),),
);
}
}
void main() {
runApp(new MaterialApp(home: new QuestionOne()));
}
I have a good listview working with a leading image, title and its clickable. I wanted to try and get it to be just a list of images with text on top of it. I have looked at gridview, but really just need 1 image per line. This is my listview code. Can this be changed or do I need to rewrite it to make this work.
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new RefreshIndicator(
child: new ListView.builder(
itemBuilder: _itemBuilder,
itemCount: mylist.length,
),
onRefresh: _onRefresh,
));
}
Widget _itemBuilder (BuildContext context, int index) {
Specialties spec = getSpec(index);
return new SpecialtyWidget(spec: spec,);
}
Specialties getSpec(int index) {
return new Specialties(mylist[index]['id'], mylist[index]['name'], mylist[index]['details'], new Photo('lib/images/'+mylist[index]['image'], mylist[index]['name'], mylist[index]['name']));
//return new Specialties.fromMap(mylist[index]);
}
class SpecialtyWidget extends StatefulWidget {
SpecialtyWidget({Key key, this.spec}) : super(key: key);
final Specialties spec;
#override
_SpecialtyWidgetState createState() => new _SpecialtyWidgetState();
}
class _SpecialtyWidgetState extends State<SpecialtyWidget> {
#override
Widget build(BuildContext context) {
return new ListTile(
leading: new Image.asset(widget.spec.pic.assetName),
//title: new Text(widget.spec.name),
onTap: _onTap,
);
}
void _onTap() {
Route route = new MaterialPageRoute(
settings: new RouteSettings(name: "/specs/spec"),
builder: (BuildContext context) => new SpecPage(spec: widget.spec),
);
Navigator.of(context).push(route);
}
}
If it will not work with the listview any guidance would be helpful.
Thanks in advance
I'm having a hard time understanding your question, but it sounds like you want the entries in your list to use a different layout than the one provided by ListTile.
You could use a Stack to put text on top of your images (compositing them together) or a Column if you want to put text vertically above your images. You can also use other Flutter layout widgets to ensure that the text appears in the right place.
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(),
),
);
}
}