setState … setState … setState … Once you code an app of medium complexity, it becomes very important to think about the performance impact of your Flutter widgets.
Thinking about app performance is about being a good app citizen. Not only the app will be smoother for the user, but also it will drain less battery. And we all know battery is still the single point of failure for smartphones!
The official Stateful Widget doc has some very helpful information on performance considerations. Here, I will focus on the first item: “Push the state to the leaves”.
In this code tutorial, we will set up a screen with a list of shopping items. Each item displays a title and a picture. Some items actually have several pictures available, so we will loop through them in the display, with the picture changing for that item every 2 seconds.
Setting up the app
To follow the code tutorial, create a new app as follows.
1 |
flutter create splitwidgetsexample |
If you’re unsure how to set up a Flutter app, check out Getting started with Flutter official tutorial.
Firstly, we create a Material app in main.dart, which will launch the HomePage widget.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import 'package:flutter/material.dart'; import 'home_page.dart'; void main() { runApp(new MyApp()); } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return new MaterialApp( title: 'Split Widgets Example', theme: new ThemeData( primaryColor: const Color(0xFF43a047), accentColor: const Color(0xFFffcc00), primaryColorBrightness: Brightness.dark, ), home: new HomePage(), ); } } |
Secondly, we create home_page.dart. It will eventually show a list, but it is not implemented yet.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import 'package:flutter/material.dart'; class HomePage extends StatefulWidget { HomePage({Key key}) : super(key: key); @override _HomePageState createState() => new _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text("Split Widgets Example"), ), body: _buildList(), ); } Widget _buildList() { return new Text('Not implemented yet'); } } |
Setting up the app architecture
[mvp]
So let’s set up our contract for this feature, by creating a new file home_contract.dart.
1 2 3 4 5 6 7 8 9 10 11 |
abstract class View { } abstract class Model { } abstract class Presenter { } |
The only user action per se is the user starting the app to show the screen, ie view displayed. This is async, because it will update the view based on async data responses from the Model.
1 2 3 4 5 6 7 8 9 |
import 'dart:async'; [...] abstract class Presenter { Future viewDisplayed(); } |
In terms of data, we are concerned with 2 things: getting the list of items (ie titles), and getting an image to show for each item.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import 'dart:typed_data'; [...] abstract class Model { Future<List<Item>> getItems(); Future<Item> getImageForItem(Item item); } class Item { final int id; final String title; final int numImages; Uint8List currentImage; int currentImageNumber; Item(this.id,this.title, this.numImages); } |
Finally, the view has 3 possible states: loading of items in progress, display items, and display images (per item). We ignore error scenarios for this code tutorial. The first state is the initial state of the view.
1 2 3 4 5 6 7 |
abstract class View { void showItems(List<Item> items); void showImageForItem(Item item); } |
Implementing the MVP contract
Now, we create Model and Presenter classes that implement the abstract classes. The View abstract class is implemented in HomePage.
Firstly, let’s start with home_presenter.dart. The Presenter has a reference to the View and the Model, as it acts as a “coordinator” between View and Model.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import 'home_contract.dart'; import 'dart:async'; class HomePresenter implements Presenter { Model _model; View _view; HomePresenter(this._model, this._view); @override Future viewDisplayed() async { List<Item> items = await _model.getItems(); _view.showItems(items); for (var item in items) { _view.showImageForItem(await _model.getImageForItem(item)); } } } |
Secondly, let’s set up home_model.dart. For this code tutorial, we will load the images from assets, and we hard code the list of items.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
import 'dart:async'; import 'dart:typed_data'; import 'home_contract.dart'; import 'package:flutter/services.dart' show rootBundle; class HomeModel implements Model { List<Item> _items; Map<Item,Map<int, Uint8List>> _images; @override Future<Item> getImageForItem(Item item) async { // For this tutorial, we retrieve images from assets Map<int, Uint8List> imagesForItem = new Map<int, Uint8List>(); if (item.numImages > 0) { // For now, we get the first image only Uint8List firstImageForItem = (await rootBundle.load('assets/1.png')).buffer.asUint8List(); imagesForItem.putIfAbsent(1, () => firstImageForItem); _images.putIfAbsent(item, () => imagesForItem); item.currentImage = firstImageForItem; item.currentImageNumber = 1; } return item; } @override Future<List<Item>> getItems() async { // For this tutorial, we simulate a delay as you would get when calling a backend await new Future.delayed(new Duration(seconds: 2)); // For this tutorial, we use hard coded data _items = new List<Item>(); _items.add(new Item(1, "Item 1", 3)); _items.add(new Item(2, "Item 2", 1)); _items.add(new Item(3, "Item 3", 0)); _items.add(new Item(4, "Item 4", 1)); _items.add(new Item(5, "Item 5", 4)); _items.add(new Item(6, "Item 6", 2)); _items.add(new Item(7, "Item 7", 5)); _items.add(new Item(8, "Item 8", 1)); _items.add(new Item(9, "Item 9", 1)); _items.add(new Item(10, "Item 10", 1)); _items.add(new Item(11, "Item 11", 1)); _items.add(new Item(12, "Item 12", 0)); _items.add(new Item(13, "Item 13", 4)); _items.add(new Item(14, "Item 14", 2)); _items.add(new Item(15, "Item 15", 1)); _images = new Map<Item, Map<int, Uint8List>>(); return _items; } } |
To load images from assets, we create a new folder assets (on same level as lib folder). We then add 5 images files to it: 1 2 3 4 5 (click on each link, and right click on picture to save it in your newly created assets folder). We also need to amend the pubspec.yaml file.
1 2 3 4 5 6 7 8 9 10 11 |
[...] flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true assets: - assets/ |
Finally, HomePage itself will implement the View, and will create the Presenter when initialised.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
import 'home_contract.dart'; import 'home_model.dart'; import 'home_presenter.dart'; [...] class _HomePageState extends State<HomePage> implements View { List<Item> _items; HomePresenter _presenter; bool _loadingInProgress; @override void initState() { super.initState(); _loadingInProgress = true; _presenter = new HomePresenter(new HomeModel(), this); _presenter.viewDisplayed(); } [...] Widget _buildList() { if (_loadingInProgress) { return new Center( child: new CircularProgressIndicator(), ); } else { return new ListView( scrollDirection: Axis.vertical, shrinkWrap: true, children: _items.map((Item item) { return _buildListRow(item); }).toList(), ); } } Widget _buildListRow(Item item) { return new Container( margin: new EdgeInsets.all(8.0), child: new Row( children: <Widget>[ new Expanded(child: new Text(item.title)), new Container( width: 100.0, height: 100.0, child: item.currentImage != null? new Image.memory(item.currentImage) :new Center(child: new Text('Image not available'))) ], ) ); } @override void showImageForItem(Item item) { setState(() { for (var it in _items) { if (it.id == item.id) { it.currentImage = item.currentImage; } } _loadingInProgress = false; }); } @override void showItems(List<Item> items) { setState(() { _items = items; _loadingInProgress = false; }); } } |
At this point, the app shows the list of items, and, when loaded, the first image for each item. But we already notice an issue: the whole page gets rebuilt every time an item loads its image. From a user point of view, this is the right thing to do: we show images as soon as they are available. But for performance, it’s clearly wasteful. And we’re not even feature complete! It’s going to get worse…
Looping through the images
To loop through the image, we create a new method in the presenter, and we amend the model to return the next image instead of the first image.
The new presenter method _loopThroughImages() waits for 2 seconds, calls the model to get the item image for each item, updates the view for each item, then calls itself. As we now add a loop in the presenter, we need to add a way to stop it when disposing of the view.
Firstly, we amend home_contract.dart.
1 2 3 4 5 6 7 |
abstract class Presenter { Future viewDisplayed(); void viewDisposed(); } |
Secondly, we amend home_presenter.dart.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import 'home_contract.dart'; import 'dart:async'; class HomePresenter implements Presenter { Model _model; View _view; bool _active; HomePresenter(this._model, this._view); @override Future viewDisplayed() async { _active = true; List<Item> items = await _model.getItems(); _view.showItems(items); for (var item in items) { _view.showImageForItem(await _model.getImageForItem(item)); } _loopThroughImages(); } Future _loopThroughImages() async { await new Future.delayed(new Duration(seconds: 2)); List<Item> items = await _model.getItems(); for (var item in items) { Item updatedItem = await _model.getImageForItem(item); if (_active) { _view.showImageForItem(updatedItem); } else { return; } } _loopThroughImages(); } @override void viewDisposed() { _active = false; } } |
Finally, we amend home_model.dart.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
[...] class HomeModel implements Model { [...] @override Future<Item> getImageForItem(Item item) async { // For this tutorial, we retrieve images from assets if (item.numImages > 0) { // Initialise at first image int imageNumberToGet = 1; // If we already have a current image in memory, get the next one available for the item, or go back to the first one if (item.currentImageNumber != null) { imageNumberToGet = item.currentImageNumber + 1; if (imageNumberToGet > item.numImages) { imageNumberToGet = 1; } } // Do we have image already? if (_images.containsKey(item) && _images[item].containsKey(imageNumberToGet)) { item.currentImage = _images[item][imageNumberToGet]; item.currentImageNumber = imageNumberToGet; } else { // Get image from assets Uint8List imageForItem = (await rootBundle.load('assets/' + imageNumberToGet.toString() + '.png')).buffer.asUint8List(); if (_images.containsKey(item)) { _images[item].putIfAbsent(imageNumberToGet, () => imageForItem); } else { Map<int, Uint8List> imagesForItem = new Map<int, Uint8List>(); imagesForItem.putIfAbsent(imageNumberToGet, () => imageForItem); _images.putIfAbsent(item, () => imagesForItem); } item.currentImage = imageForItem; item.currentImageNumber = imageNumberToGet; } } return item; } @override Future<List<Item>> getItems() async { if (_items != null) { return _items; } // For this tutorial, we simulate a delay as you would get when calling a backend [...] } } |
OK, it works, but it’s messy. The whole view gets rebuilt every time one item image is displayed.
This is the kind of implementation we may write as a first stab, when prototyping a UI for example, but the HomePage gets rebuilt way more often than is necessary. So let’s push the state (the images) to the leaves (to their own stateful widget).
Refactoring widget to avoid rebuilding the whole page each time we loop through images
When we stop to think about when the HomePage widget gets rebuilt, we reach the conclusion that we should have a separate widget for each item view. But all the data comes from the same repository/model, so how do we achieve this?
Let’s go back to our contract, and split the view into 2: ItemsListView and ItemView. We amend home_contract.dart, by replacing abstract class View with the code below.
1 2 3 4 5 6 7 8 9 10 11 |
abstract class ItemsListView { void showItems(List<Item> items); } abstract class ItemRowView { void showImageForItem(Item item); } |
Then, we amend home_presenter.dart. It is now a singleton that will be used by several views, and it has methods for views to add themselves to it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
[...] import 'home_model.dart'; final HomePresenter homePresenter = new HomePresenter._private(); class HomePresenter implements Presenter { Model _model; ItemsListView _itemsListView; Map<int, ItemRowView> _itemViews = new Map<int, ItemRowView>(); bool _active; HomePresenter._private() { _model = new HomeModel(); } void setItemsListView(ItemsListView view) { _itemsListView = view; } void setRowView(Item item, ItemRowView view) { if (_itemViews.containsKey(item.id)) { _itemViews.remove(item.id); } _itemViews.putIfAbsent(item.id, () => view); } @override Future viewDisplayed() async { _active = true; List<Item> items = await _model.getItems(); _itemsListView.showItems(items); for (var item in items) { Item updatedItem = await _model.getImageForItem(item); if (_itemViews.containsKey(item.id)) { _itemViews[item.id].showImageForItem(updatedItem); } } _loopThroughImages(); } Future _loopThroughImages() async { await new Future.delayed(new Duration(seconds: 2)); List<Item> items = await _model.getItems(); for (var item in items) { Item updatedItem = await _model.getImageForItem(item); if (_active) { if (_itemViews.containsKey(item.id)) { _itemViews[item.id].showImageForItem(updatedItem); } } else { return; } } _loopThroughImages(); } @override void viewDisposed() { _active = false; } } |
Then, we create a new widget, in the file item_view.dart, and move across the _buildItemView() method from HomePage to it. This widget implements ItemRowView from the contract.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
import 'package:flutter/material.dart'; import 'home_contract.dart'; import 'home_presenter.dart'; class ItemView extends StatefulWidget { ItemView({Key key, this.item}) : super(key: key); Item item; @override _ItemViewState createState() => new _ItemViewState(); } class _ItemViewState extends State<ItemView> implements ItemRowView { @override void initState() { super.initState(); homePresenter.setRowView(widget.item, this); } @override Widget build(BuildContext context) { return new Container( margin: new EdgeInsets.all(8.0), child: new Row( children: <Widget>[ new Expanded(child: new Text(widget.item.title)), new Container( width: 100.0, height: 100.0, child: widget.item.currentImage != null? new Image.memory(widget.item.currentImage) :new Center(child: new Text('Image not available'))) ], ) ); } @override void showImageForItem(Item item) { if (mounted) { setState(() { widget.item = item; }); } } } |
Lastly, we amend home_page.dart to use the new widget, as well as implements the ItemsListView instead of View. We also delete methods _buildListRow and showImageForItem.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
[...] import 'item_view.dart'; [...] class _HomePageState extends State<HomePage> implements ItemsListView { List<Item> _items; bool _loadingInProgress; @override void initState() { super.initState(); _loadingInProgress = true; homePresenter.setItemsListView(this); homePresenter.viewDisplayed(); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text("Split Widgets Example"), ), body: _buildList(), ); } Widget _buildList() { if (_loadingInProgress) { return new Center( child: new CircularProgressIndicator(), ); } else { return new ListView( scrollDirection: Axis.vertical, shrinkWrap: true, children: _items.map((Item item) { return new ItemView(item: item); }).toList(), ); } } @override void showItems(List<Item> items) { setState(() { _items = items; _loadingInProgress = false; }); } } |
Performance profiling
All this theory about why we refactor is all nice and good, but have we really improved performance? Let’s check the Android Monitor.
The first thing to note is that performance stats vary quite a bit, on the same device, so it’s not an exact science.
But one thing is constant between all tests: CPU usage every 2 seconds (the loop) is higher before the refactoring. Not by a lot, or course, but it is around the 10% line much more often than after refactoring.
CPU before refactoring (10% line shown in grey):

CPU after refactoring (10% line shown in grey):

What next?
Generally speaking, if you remember that “calling setState will rebuild the whole widget”, you’re on a good path for improving your app performance. There are other considerations of course, such as making sure you use async code for I/O operations. For further help on improving or checking the performance of your app, check out the Performance Profiling section in the official doc.

Mobile app developer with 12 years experience. Started with the Android SDK in 2010 and switched to Flutter in 2017. Past apps range from start ups to large tech (Google) and non tech (British Airways) companies, in various sectors (transport, commercial operations, e-commerce).
Nice article, thank you =)
Great Work. Thank you !
How would you make it to were the app still looped through the list after you close the app and relaunch it, without having to re-run it in the editor