I'd like to dynamically set the child widget of a specific container using Dart code, specifically on a button press. Where should I start to accomplish this? I've already explored using a PageView, but that's not really what I'm looking for.
If I were web developing, I would easily be able to edit an element via its ID. Is there a Flutter equivalent of HTML ID's? Controllers seem limiting here.
Flutter doesn't really work that way. Flutter, like React, is a declarative environment as opposed to an imperative environment. Widget trees are built from state, and as such, a widget higher in the tree doesn't know, or even care, about its children.
https://flutter.dev/docs/get-started/flutter-for/declarative
https://flutter.dev/docs/get-started/flutter-for/web-devs
Try this
class MyHomePage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _MyHomePageState();
}
}
class _MyHomePageState extends State<MyHomePage> {
Widget _dynamicWidget = FlutterLogo();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_dynamicWidget,
FlatButton(
onPressed: () {
setState(() {
_dynamicWidget = Container(
color: Colors.red,
width: 50,
height: 50,
);
});
},
child: Text('Change')),
],
),
),
);
}
}
Related
What is the difference between both options in this case regarding rebuilding widgets and performance aspects?
Widget class:
class Dummy() extends StatelessWidget {
const Dummy();
#override
Widget build(BuildContext context) {
return const Text(„text“);
}
}
Option 1:
class Option1 extends StatelessWidget {
const Option1();
#override
Widget build(BuildContext context) {
return SizedBox(
child: const Dummy(),
);
}
}
Option 2:
class Option2 extends StatelessWidget {
const Option2();
Widget createDummyWidget() {
return const Dummy();
}
#override
Widget build(BuildContext context) {
return SizedBox(
child: createDummyWidget(),
);
}
}
Splitting widgets to methods is an antipattern
So, for example, if we have a widget that looks something like this:
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
),
],
);
}
}
if use function widget
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;
Widget _buildNonsenseWidget() {
return Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
);
}
#override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
// The deeply nesting widget is now refactored into a
// separate method and we have a cleaner build method. Yay!
_buildNonsenseWidget(),
],
);
}
}
So what’s the problem, really?
Whenever the value of _counter changes, the framework calls the build method. This triggers our widget to rebuild itself. The problem is that _buildNonsenseWidget() gets called every time the value of _counter changes - which ends up rebuilding the widget tree over and over again.
Rebuilding for nothing
In this case, there’s no reason to rebuild that particular widget tree.
The widget tree returned by _buildNonsenseWidget() is stateless by nature - we only need to build it once. Sadly, because the widget tree is built by the _buildNonsenseWidget() method, the Flutter framework rebuilds it every time when the parent widget rebuilds.
Essentially, we’re wasting precious CPU cycles in rebuilding something that doesn’t need to be rebuilt. This happens because from the framework’s perspective, there’s no difference between a long-ass build method and a build method split into multiple smaller methods. Mind you, this is only a simple example - this has a more significant impact on more complex apps.
Splitting long build methods - revisited
The solution for this one is relatively simple, although it results in a couple of extra lines of code. Instead of splitting build methods into smaller methods, we split them into widgets - StatelessWidgets, that is.
When we refactor the previous example, we’ll end up with this:
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
// The deeply nesting widget is now refactored into a
// stateless const widget. No more needless rebuilding!
const _NonsenseWidget(),
],
);
}
}
class _NonsenseWidget extends StatelessWidget {
const _NonsenseWidget();
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
);
}
}
Conclusion
Instead of splitting you build methods into multiple smaller methods, split them into StatelessWidgets. This way, you won’t be rebuilding your static widget trees multiple times for nothing but wasted CPU cycles. When it comes to optimizing performance of Flutter apps, this is probably one of the lowest hanging fruits.
I used this article : https://iiro.dev/splitting-widgets-to-methods-performance-antipattern/
let's say I have an app with the following setup:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: MainLoginScreen()),
],
),
));
}
}
I would like to know how can I navigate only the MainLoginScreen widget from the MainMenu with any .push() method.
(I found a way to navigate from a context inside the mainloginscreen,by wrapping it with a MaterialApp widget, but what if I want to use the MainMenu widget instead, which has another context)
There is a general agreement that a 'screen' is a topmost widget in the route. An instance of 'screen' is what you pass to Navigator.of(context).push(MaterialPageRoute(builder: (context) => HereGoesTheScreen()). So if it is under Scaffold, it is not a screen. That said, here are the options:
1. If you want to use navigation with 'back' button
Use different screens. To avoid code duplication, create MenuAndContentScreen class:
class MenuAndContentScreen extends StatelessWidget {
final Widget child;
MenuAndContentScreen({
required this.child,
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: child),
],
),
),
);
}
}
Then for each screen create a pair of a screen and a nested widget:
class MainLoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MenuAndContentScreen(
child: MainLoginWidget(),
);
}
}
class MainLoginWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
// Here goes the screen content.
}
}
2. If you do not need navigation with 'back' button
You may use IndexedStack widget. It can contain multiple widgets with only one visible at a time.
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState(
initialContentIndex: 0,
);
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
int _index;
_MainMenuAndContentScreenState({
required int initialContentIndex,
}) : _contentIndex = initialContentIndex;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
// A callback that will be triggered somewhere down the menu
// when an item is tapped.
setContentIndex: _setContentIndex,
),
Expanded(
child: IndexedStack(
index: _contentIndex,
children: [
MainLoginWidget(),
SomeOtherContentWidget(),
],
),
),
],
),
),
);
}
void _setContentIndex(int index) {
setState(() {
_contentIndex = index;
});
}
}
The first way is generally preferred as it is declrative which is a major idea in Flutter. When you have the entire widget tree statically declared, less things can go wrong and need to be tracked. Once you feel it, it really is a pleasure. And if you want to avoid back navigation, use replacement as ahmetakil has suggested in a comment: Navigator.of(context).pushReplacement(...)
The second way is mostly used when MainMenu needs to hold some state that needs to be preserved between views so we choose to have one screen with interchangeable content.
3. Using a nested Navigator widget
As you specifically asked about a nested Navigator widget, you may use it instead of IndexedStack:
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _navigatorKey = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
navigatorKey: _navigatorKey,
),
Expanded(
child: Navigator(
key: _navigatorKey,
onGenerateRoute: ...
),
),
],
),
),
);
}
}
// Then somewhere in MainMenu:
final anotherContext = navigatorKey.currentContext;
Navigator.of(anotherContext).push(...);
This should do the trick, however it is a bad practice because:
MainMenu knows that a particular Navigator exists and it should interact with it. It is better to either abstract this knowledge with a callback as in (2) or do not use a specific navigator as in (1). Flutter is really about passing information down the tree and not up.
At some point you would like to highlight the active item in MainMenu, but it is hard for MainMenu to know which widget is currently in the Navigator. This would add yet another non-down interaction.
For such interaction there is BLoC pattern
In Flutter, BLoC stands for Business Logic Component. In its simpliest form it is a plain object that is created in the parent widget and then passed down to MainMenu and Navigator, these widgets may then send events through it and listen on it.
class CurrentPageBloc {
// int is an example. You may use String, enum or whatever
// to identify pages.
final _outCurrentPageController = BehaviorSubject<int>();
Stream<int> _outCurrentPage => _outCurrentPageController.stream;
void setCurrentPage(int page) {
_outCurrentPageController.sink.add(page);
}
void dispose() {
_outCurrentPageController.close();
}
}
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _currentPageBloc = CurrentPageBloc();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
currentPageBloc: _currentPageBloc,
),
Expanded(
child: ContentWidget(
currentPageBloc: _currentPageBloc,
onGenerateRoute: ...
),
),
],
),
),
);
}
#override
void dispose() {
_currentPageBloc.dispose();
}
}
// Then in MainMenu:
currentPageBloc.setCurrentPage(1);
// Then in ContentWidget's state:
final _navigatorKey = GlobalKey();
late final StreamSubscription _subscription;
#override
void initState() {
super.initState();
_subscription = widget.currentPageBloc.outCurrentPage.listen(_setCurrentPage);
}
#override
Widget build(BuildContext context) {
return Navigator(
key: _navigatorKey,
// Everything else.
);
}
void _setCurrentPage(int currentPage) {
// Can't use this.context, because the Navigator's context is down the tree.
final anotherContext = navigatorKey?.currentContext;
if (anotherContext != null) { // null if the event is emitted before the first build.
Navigator.of(anotherContext).push(...); // Use currentPage
}
}
#override
void dispose() {
_subscription.cancel();
}
This has advantages:
MainMenu does not know who will receive the event, if anybody.
Any number of listeners may listen on such events.
However, there is still a fundamental flaw with Navigator. It can be navigated without MainMenu knowledge using 'back' button or by its internal widgets. So there is no single variable that knows which page is showing now. To highlight the active menu item, you would query the Navigator's stack which eliminates the benefits of BLoC.
For all these reasons I still suggest one of the first two solutions.
I was always wondering, why I have to "lift the state up" of some widget down in the tree, to reflect changes in the widget's UI..
Can't I just simply have multiple stateful Widgets? and for example, import the lowest stateful widget down the tree into my top-level Widget, and from there could I not just call some method of my widget which triggers its setState() method and just updates that part in the DOM tree that is concerned with my widget?
And secondly, I would then have to move my properties and other important parts from the lower widget also up into my higher state widget, risking to clutter that class with unrelated functions and properties at some time, in my opinion, React solves that way better by just passing method callbacks down as it suits...
And is there always only one stateful widget in my Flutter app at the highest level?
Parents don't have access to child widgets/states, only the other way around. So you can not "import the lowest stateful widget down the tree into my top-level Widget"
You only have to "lift the state up" when you want to share that state between different branches of the widget tree. Then the children can lookup the parent and access the shared state.
Don't be afraid of StatefulWidget - you can use as many as you need
Research Flutter state management solutions which exist exactly for the purpose of keeping things separated (ChangeNotifier, Provider, BLOC, Redux, etc.)
Can I have multiple widgets with state in my Widgets tree?
The answer is, yes you can create multiple StatefulWidget in your widget. You can also create a callback function from the lowest StatefulWidget with Function(yourcallback). In my opinion, flutter also support component base model, and make as dynamic as possible to customize our own widget.
For example:
child widget
child widget has it's state.
class Child extends StatefulWidget {
final int counter;
final Function(int childCounter) callback; //here is your callback
const Child({
Key key,
this.counter,
this.callback,
}) : super(key: key);
#override
_ChildState createState() => _ChildState();
}
class _ChildState extends State<Child> {
String _childState = '';
#override
Widget build(BuildContext context) {
return Container(
color: Colors.green[400],
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text("Child"),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.exposure_minus_1_rounded),
onPressed: () {
this.widget.callback(this.widget.counter - 1); //here is your callback
setState(() {
_childState = "minus one";
});
}),
IconButton(
icon: Icon(Icons.plus_one_rounded),
onPressed: () {
this.widget.callback(this.widget.counter + 1); //here is your callback
setState(() {
_childState = "plus one";
});
})
],
),
Text(_childState)
],
),
);
}
}
Parent Widget
the parent get the callback from the child.
class Parent extends StatefulWidget {
#override
_ParentState createState() => _ParentState();
}
class _ParentState extends State<Parent> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Multiple State"),
),
body: Container(
child: Column(
children: [
Child(
counter: _counter,
callback: (childCounter) { //here is callback from the child
print(childCounter);
setState(() {
_counter = childCounter;
});
},
),
Text("Parent"),
Center(
child: Text(_counter.toString()),
)
],
),
),
);
}
}
to change the child state from it's parent
you can use didUpdateWidget() here you can see the docs
I have a small flutter application with several stateful and stateless widgets.
My drawer widget looks like follows:
class AppDrawer extends StatelessWidget{
#override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
_createDrawerHeader(),
ListTile(
title: Text('Scenario'),
onTap: () {
Navigator.pop(context);
Navigator.pushReplacementNamed(context, Routes.scenario);
},
),
...
This works fine for switche between my widgets. I now have a simple counter widget that stores a counter variable in its state.
class Counter extends StatefulWidget {
static const String routeName = '/Counter';
int _counter = 0;
#override
_CounterState createState() => new _CounterState();
}
class _CounterState extends State<Counter> {
void _increment() {
setState(() {
widget._counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: <Widget>[
new RaisedButton(
onPressed: _increment,
child: new Text('Increment'),
),
new Text('Count: ${widget._counter}'),
],
),
drawer: AppDrawer(),
);
}
}
If I switch between this and my other widgets with the drawer and go back to the counter widgets the counter is always 0. Looks like the state is initialized everytime.
I am a beginner using flutter and I thougt I can save a variable within this state. I think I am wrong. As my search didn't get my some usable results maybe you can give me an idea how to solve this or just provide some links with information.
Thanks for your help :)
I suggest learning some sort of State Management practice's. You will eventually need to learn this because this is an essential step.Try provider its easy and simple to use.
This is the warning:
This class (or a class which this class inherits from) is marked as '#immutable', but one or more of its instance fields are not final: MyWidget.myVar
The console tells me that the variable must be final. I suspect that I should change my widget to a stateful if I want to change variables, but to me it doesn't makes sense, as the code works as intended. When I change my variable I don't want to change anything on the screen, I just want to use it later.
What I'm doing is wrong? If not, how can I disable this warning?
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: MyWidget(),
),
);
}
class MyWidget extends StatelessWidget {
String myVar;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
MaterialButton(
child: Text('Click me do change variable'),
onPressed: () {
myVar = 'Clicked!';
},
),
MaterialButton(
child: Text('Click me to print the variable'),
onPressed: () {
print(myVar);
},
),
],
),
);
}
}
Your logic is correct and it doesn't really matter since you are not outputting anything on screen. However the best practise is to change it to a Stateful Widget.
It doesn't really affect it in a negative way.
You are getting the warning because all fields in a class extending StatelessWidget should be final.
Fix the warning by adding the final keyword before the type declaration like below:
final String myVar;
From the documentations.
StatelessWidget class. A widget that does not require mutable state.
https://docs.flutter.io/flutter/widgets/StatelessWidget-class.html
I hope this answers your question
The point of changing variable's value in a stateful widget is that you can call
setState(() {
myVar = 'clicked';
});
Which would rebuild the UI, changing a Text widget's content.
Try adding a Text(myVar) to your column, in a stateless widget it wouldn't change on a press of a button. But in a stateful widget it will change.
If you need to change the state of a variable in a Widget, you need to use a StetefullWidget.
class MyWidget extends StatefulWidget {
final String myInitialVar;
const MyWidget({Key key, this.myInitialVar}) : super(key: key);
#override
State<StatefulWidget> createState() => MyWidgetState(myInitialVar);
}
class MyWidgetState extends State<MyWidget> {
String myVar;
MyWidgetState(this.myVar);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
MaterialButton(
child: Text('Click me do change variable'),
onPressed: () {
setState(() {
myVar = 'Clicked!';
});
},
),
MaterialButton(
child: Text('Click me to print the variable'),
onPressed: () {
print(myVar);
},
),
],
),
);
}
}