Improving Flutter Performance with Provider

Photo by Jean Gerber / Unsplash

Introduction

Every mobile developer knows the importance of delivering a buttery-smooth, 60fps (and increasingly 120fps) app. Flutter prides itself by being performant by default, but sometimes extra steps are needed to optimize particularly heavy UIs. We’ll take a look at once such example and solve it using the provider package.

The code for this article can be found in the repository below:

GitHub - MagsMagnoli/flutter-provider-optimization
Contribute to MagsMagnoli/flutter-provider-optimization development by creating an account on GitHub.
class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int counter = 0;
  int items = 10;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: SingleChildScrollView(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              children: [
                Text('$counter', style: TextStyle(fontSize: 20)),
                SizedBox(height: 16),
                Wrap(
                  children: Iterable.generate(items)
                      .map((e) => ExpensiveItem(
                            position: e + 1,
                            counter: counter,
                          ))
                      .toList(),
                )
              ],
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          setState(() {
            counter += 1;
          });
        },
      ),
    );
  }
}

Each item initializes with a loading spinner to simulate being expensive to create. If the number contained in the list item is a factor of the counter then its background will be randomly assigned a color, otherwise it will be white.

Currently when the counter is increased the call to setState causes every list item to be rebuilt. This is unnecessary for items that will have the same state for consecutive values of the counter. In the next section we’ll see how the provider package can help us optimize the rebuilding of these widgets.

Optimizing With Provider

Install The Provider Package

Add the provider package to your pubspec.yaml file

Optimize The List

The first thing we’ll do is extract the list to its own widget with a const constructor so it won’t be rebuilt every time we call setState. This is because in Dart a const variable becomes a compile-time constant.

Now we’ll wrap this new ExpensiveItemList widget with a Provider widget and pass it the counter variable. Doing so makes counter available to widgets further down the tree.

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: SingleChildScrollView(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              children: [
                Text('$counter', style: TextStyle(fontSize: 20)),
                SizedBox(height: 16),
                Provider<int>.value(
                    value: counter,
                    builder: (_, __) => const ExpensiveItemList())
              ],
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          setState(() {
            counter += 1;
          });
        },
      ),
    );
  }
}

class ExpensiveItemList extends StatelessWidget {
  final items = 10;

  const ExpensiveItemList();

  @override
  Widget build(BuildContext context) {
    return Wrap(
      children: Iterable.generate(items)
          .map((e) => ExpensiveItem(
                position: e + 1,
              ))
          .toList(),
    );
  }
}

Optimize The List Item

The ExpensiveItem Widget no longer receives the counter variable in its constructor but it still needs access to it in order to know when to rebuild. We’ll use the select extension on BuildContext which provider gives us to only listen to when the item switches from not being a factor to being a factor, and vice versa.

class ExpensiveItem extends StatelessWidget {
  final int position;

  const ExpensiveItem({Key key, this.position}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final size = (MediaQuery.of(context).size.width / 5) - 32 / 5;
    final isFactor = context
        .select<int, bool>((value) => value > 0 && position % value == 0);
    ...
  }
}

Results

After implementing the changes, re-run the app and you’ll notice that only the items that need to update their UI are being rebuilt!

Conclusion

We successfully used the provider package to optimize our Flutter app. Congratulations! 🥳

Note that, as is usually the case, this isn’t the only way we could have done this. There are other packages and state management solutions we could have used instead. What matters is that we chose a solution that got the job done in a reasonable amount of time.

The key takeaway here is this:

When you have a Widget that is particularly expensive to create, isolate!
Mags
Founder of Mags.ai. Building an open community of motivated learners