Flutter State and Stateful Widget Approach
Approach
Store state in Stateful Widgets at a high-enough level in the Widget tree to ensure that the data is not repeated.Pass state from parent Widgets to child Widgets through the constructor.
Pass event handler method (that modifies state) from parent Widget methods to child Widgets through the constructor. Child Widgets can then invoke method to change state in Parent Widget.
Exercise – ‘state_and_stateful_widget’
Introduction
We start off by creating a create basic app with Stateful and Stateless Widgets.Later on, we add some state & event handling so that the user can select a car and see it highlighted.
The car selection comes from a tap event in the lower-level CarWidget.
It changes the selected car state in the higher-level MyHomePageWidget.
Step 1 – Create Default Flutter App
Follow the instructions in Generate Your First App Leave project open.
Step 2 – Replace Application Code
Replace contents of file ‘main.dart’ in folder ‘lib’ with the following:
import ‘package:flutter/material.dart’;
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: ‘Flutter Demo’,
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(),
);
}
}
class Car {
String _make;
String _model;
String _imageSrc;
Car(this._make, this._model, this._imageSrc);
operator ==(other) =>
(other is Car) && (_make == other._make) && (_model == other._model);
int get hashCode => _make.hashCode ^ _model.hashCode ^ _imageSrc.hashCode;
}
class MyHomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState(“Cars”);
}
class _HomePageState extends State<MyHomePage> {
String _title;
List<Car> _cars;
_HomePageState(this._title) {
_cars = [
Car(
“Bmw”,
“M3”,
“Https://media.ed.edmundsmedia.com/bmw/m3/2018/oem/2018_bmw_m3_sedan_base_fq_oem_4_150.jpg
Https://media.ed.edmundsmedia.com/bmw/m3/2018/oem/2018_bmw_m3_sedan_base_fq_oem_4_150.jpg
“,
),
Car(
“Nissan”,
“GTR”,
“Https://media.ed.edmunds-media.com/nissan/gt-r/2018/oem/2018_nissan_gtr_coupe_nismo_fq_oem_1_150.jpg
Https://media.ed.edmunds-media.com/nissan/gt-r/2018/oem/2018_nissan_gtr_coupe_nismo_fq_oem_1_150.jpg
”,
),
Car(
“Nissan”,
“Sentra”,
“Https://media.ed.edmundsmedia.com/nissan/sentra/2017/oem/2017_nissan_sentra_sedan_srturbo_fq_oem_4_150.jpg
Https://media.ed.edmundsmedia.com/nissan/sentra/2017/oem/2017_nissan_sentra_sedan_srturbo_fq_oem_4_150.jpg
”,
)
];
}
@override
Widget build(BuildContext context) {
List<CarWidget> carWidgets = _cars.map((Car car) {
return CarWidget(car);
}).toList();
return new Scaffold(
appBar: new AppBar(
title: new Text(_title),
),
body: new ListView(children: carWidgets));
}
}
class CarWidget extends StatelessWidget {
CarWidget(this._car) : super();
final Car _car;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(20.0),
child: Container(
decoration: BoxDecoration(border: Border.all()),
padding: EdgeInsets.all(20.0),
child: Center(
child: Column(children: <Widget>[
Text(‘${_car._make} ${_car._model}’,
style: TextStyle(fontSize: 24.0)),
Padding(
padding: EdgeInsets.only(top: 20.0),
child: Image.network(_car._imageSrc))
]))));
}
}
Step 3 – Open Emulator & Run
Follow the instructions in Open Android Emulator & Run Your First App You should get something like the following as it is somewhat similar to the previous example:
Summary
The MyApp & Material App Widgets are unchanged. We declare a new class called Car.
This will store information about each car: its make, model and image.
Note that the ‘==’ operator is overloaded so it considers two Cars equal if they have the same make and model.
The MyHomePage Stateless Widget has become two different widgets instead:
MyHomePage StatefulWidget
MyHomePageState State Object
This holds the App Bar title and the list of Car objects.
These are initiated in the constructor.
The State object contains the ‘build’ method that converts the list of Car objects into a list of CarWidgets.
Then it returns a Scaffold containing the AppBar and a ListView containing the list of CarWidgets.
CarWidget
This displays a car’s make, model and image.
Notice that it now accepts a Car object in the constructor. This gives it all the info to display a car’s make, model and image.
Step 4– Add Car Selection
This is going to be achieved by holding state in the MyHomePage state object.
This state is going to be set by a method. This method is going to be passed to each Car Widget so it can be invoked by the Car Widget
when the user taps on it.
Modify MyHomePageState
We add variable ‘_selectedCar’ to store which car is selected. We add a method ‘_selectionHandler’ to handle car selection.This provides an inline JavaScript function that sets the variables ‘_title’ and ‘_selectedCar’.
This inline JavaScript function is passed to setState. Using ‘setState’ tells Flutter that the state of this object has changed and that this Widget will need to be re- endered. We change the code that constructs the CarWidgets to include 2 additional constructor arguments:
A boolean indicating if the car is the selected car.
The selection handler method that handles the car selection in this class.
class MyHomePageState extends State<MyHomePage> {
String _title;
List<Car> _cars;
Car _selectedCar;
MyHomePageState(this._title) {
_cars = [
Car(
“Bmw”,
“M3",
“Https://media.ed.edmundsmedia.com/bmw/m3/2018/oem/2018_bmw_m3_sedan_base_fq_oem_4_150.jpg“,
),
Car(
“Nissan”,
“GTR”,
“Https://media.ed.edmunds-media.com/nissan/gt-r/2018/oem/2018_nissan_gtr_coupe_nismo_fq_oem_1_150.jpg”,
),
Car(
“Nissan”,
“Sentra”,
“Https://media.ed.edmundsmedia.com/nissan/sentra/2017/oem/2017_nissan_sentra_sedan_srturbo_fq_oem_4_150.jpg”,
)
];
}
void _selectionHandler(Car selectedCar) {
setState(() {
_title = ‘Selected ${selectedCar._make} ${selectedCar._model}’;
_selectedCar = selectedCar;
});
}
@override
Widget build(BuildContext context) {
List<CarWidget> carWidgets = _cars.map((Car car) {
return CarWidget(car, car == _selectedCar, _selectionHandler);
}).toList();
return new Scaffold(
appBar: new AppBar(
title: new Text(_title),
),
body: new ListView(children: carWidgets));
}
}
Modify CarWidget
We add instance variable ‘_isSelected’ to store if this car is selected
or not. We add instance variable ‘_parentSelectionHandler’ to store the
selection handler method from the parent MyHomePageState class. We modify the constructor to accept & set these two instance
variables. We add a new method ‘_handleTap’ to handle the ‘onTap’ event
from the GestureDetector. This method invokes the
‘_parentSelectionHandler’ from the parent MyHomePageState
class. We modify the ‘build’ method. We wrap the Container with a GestureDetector. This is so we
can listen for the ‘onTap’ event. We modify the ‘BoxDecoration’ to set the background color
according to if the instance variable ‘isSelected’ is set to true
or false. If true the background color is set to blue, otherwise
white.
class CarWidget extends StatelessWidget {
CarWidget(this._car, this._isSelected, this._parentSelectionHandler)
: super();
final Car _car;
final bool _isSelected;
final ValueChanged<Car> _parentSelectionHandler;
void _handleTap() {
_parentSelectionHandler(_car);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(20.0),
child: GestureDetector(
onTap: _handleTap,
child: Container(
decoration: BoxDecoration(
color: _isSelected ? Colors.blue : Colors.white,
border: Border.all()),
padding: EdgeInsets.all(20.0),
child: Center(
child: Column(children: <Widget>[
Text(‘${_car._make} ${_car._model}’,
style: TextStyle(fontSize: 24.0)),
Padding(
padding: EdgeInsets.only(top: 20.0),
child: Image.network(_car._imageSrc))
])))));
}
}