Sneh Pandya Exploring the horizon with a knack for product management. Co-host of the NinjaTalks podcast and community organizer at Google Developers Group. Explorer, adventurer, traveler.

Performance improvements for mobile apps in Flutter

7 min read 1961

Performance is a crucial factor for any app or product, and multiple factors impact it. Generally, when you build apps in Flutter, the performance results are good enough, but you may still face problems with the performance of your app.

That’s why you need to pay attention to the best practices and performance improvements for your Flutter app during development itself — to fix the issues ahead of time and deliver a flawless experience to your end-users.

The objective of this article is to walk you through the nitty-gritty best practices of performance improvements for Flutter apps. I’ll show you how to:

  1. Avoid re-building widgets
  2. Make use of constant widgets
  3. Load list items efficiently — and on-demand
  4. Make use of async/await
  5. Efficiently use operators
  6. Make use of interpolation techniques
  7. Reduce your app size

1. Avoid re-building widgets

One of the most common performance anti-patterns is using setState to rebuild StatefulWidgets. Every time a user interacts with the widget, the entire view is refreshed, affecting the scaffold, background widget, and the container — which significantly increases the load time of the app.

Only rebuilding what we have to update is a good strategy in this case. This can be achieved using the Bloc pattern in Flutter. Packages like flutter_bloc, MobX, and Provider are popular ones.

But, did you know this can be done without any external packages, too? Let’s have a look at the below example:

class _CarsListingPageState extends State<CarsListingPage> {
  final _carColorNotifier = ValueNotifier<CarColor>(Colors.red);
  Random _random = new Random();

  void _onPressed() {
    int randomNumber = _random.nextInt(10);
    _carColorNotifier.value =
        Colors.primaries[randomNumber % Colors.primaries.lengths];
  }

  @override
  void dispose() {
    _carColorNotifier.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    print('building `CarsListingPage`');
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: _onPressed,
        child: Icon(Icons.colorize),
      ),
      body: Stack(
        children: [
          Positioned.fill(
            child: BackgroundWidget(),
          ),
          Center(
            child: ValueListenableBuilder(
              valueListenable: _colorNotifier,
              builder: (_, value, __) => Container(
                height: 100,
                width: 100,
                color: value,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

The class _CarsListingPageState describes the behavior for possible actions based on the state, such as _onPressed. The framework’s build method is building an instance of the Widget based on the context supplied to the method. It creates an instance of floatingActionButton and specifies the properties such as color, height, and width.

When the user presses the FloatingActionButton on the screen, onPressed is called and invokes _onPressed from _CarsListingPageState. A random color is then assigned from the primary color palette, which is then returned via builder and the color is filled in the center of the screen.

Here, every time, the build method in the above code does not print the output building CarsListingPage on the console. This means that this logic works correctly — it is just building the widget we need.

2. Make use of constant widgets

What’s the difference between a normal widget and a constant one? Just as the definition suggests, applying const to the widget will initialize it at the compile time.

We made a custom demo for .
No really. Click here to check it out.

This means that declaring the widget as a constant will initialize the widget and all its dependents during compilation instead of runtime. This will also allow you to make use of widgets as much as possible while avoiding unnecessary rebuilds.

Below is an example of how to make use of a constant widget:

class _CarListingPageState extends State<CarListingPage> {
  int _counter = 0;

  void _onPressed() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: _onPressed,
        child: Icon(Icons.play_arrow),
      ),
      body: Stack(
        children: [
          Positioned.fill(
            child: const DemoWidget(),
          ),
          Center(
              child: Text(
                _counter.toString(),
              )),
        ],
      ),
    );
  }
}

class DemoWidget extends StatelessWidget {
  const DemoWidget();

  @override
  Widget build(BuildContext context) {
    print('building `DemoWidget`');
    return Image.asset(
      'assets/images/logo.jpg',
      width: 250,
    );
  }
}

The _CarListingPageState class specifies a state: _onPressed, which invokes setState and increases the value of _counter. The build method generates a FloatingActionButton and other elements in the tree. The first line inside DemoWidget creates a new instance and declares it a constant.

Every time the FloatingActionButton is pressed, the counter increases and the value of the counter is written inside the child item on the screen. During this execution, DemoWidget is reused and regeneration of the entire widget is skipped since it is declared as a constant widget.

As visible in the GIF below, the statement “building DemoWidget” is printed only once when the widget is built for the first time, and then it is reused.

However, every time you hot-reload or restart the app, you will see the statement “building DemoWidget” printed.

Constant widget demo

3. Load list items efficiently — and on-demand

When working with list items, developers generally use a combination of the widgets SingleChildScrollView and Column.

When working with large lists, things can get messy pretty quickly if you continue using this same set of widgets. This is because each item is attached to the list and then rendered on the screen, which increases the overall load on the system.

It is a good idea to use the ListView builder in such cases. This improves performance on a very high level. Let’s look at an example for a builder object:

ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('Row: ${items[index]}'),
);},);

4. Make use of async/await

When writing your execution flow, it is important to determine whether the code is allowed to run synchronously or asynchronously. Async code is more difficult to debug and improve, but there are still a few ways you can write async code in Flutter, which includes using Future, async/await, and others.

When combined with async, the code readability improves because the structure and pattern of writing code are followed. On the other hand, overall execution performance improves due to its ability to entertain fail-safe strategies where needed — in this case, try ... catch. Let’s look at the example below:

// Inappropriate
Future<int> countCarsInParkingLot() {
  return getTotalCars().then((cars) {


    return cars?.length ?? 0;

  }).catchError((e) {
    log.error(e);
    return 0;
  });
}

// Appropriate
Future<int> countCarsInParkingLot() async {     // use of async
  try {
    var cars = await getTotalCars();


    return cars?.length ?? 0;


  } catch (e) {
    log.error(e);
    return 0;
  }
}

5. Efficiently use operators

Flutter is packed with language-specific features. One of them is operators.

Null-check operators, nullable operators, and other appropriate ones are recommended if you want to reduce development time, write robust code to avoid logical errors, and also improve the readability of the code.

Let’s look at some examples below:

car = van == null ? bus : audi;         // Old pattern

car = audi ?? bus;                      // New pattern

car = van == null ? null : audi.bus;    // Old pattern

car = audi?.bus;                        // New pattern

(item as Car).name = 'Mustang';         // Old pattern

if (item is Car) item.name = 'Mustang'; // New pattern

6. Make use of interpolation techniques

It is a common practice to perform string operations and chaining using the operator +. Instead of doing that, we’ll make use of string interpolation, which improves the readability of your code and reduces the chances of errors.

// Inappropriate
var discountText = 'Hello, ' + name + '! You have won a brand new ' + brand.name + 'voucher! Please enter your email to redeem. The offer expires within ' + timeRemaining.toString() ' minutes.';

// Appropriate
var discountText = 'Hello, $name! You have won a brand new ${brand.name} voucher! Please enter your email to redeem. The offer expires within ${timeRemaining} minutes.';

As specified, accessing variables inline improves the readability of specified text with values, and the code becomes less error-prone because the string is divided into fewer pieces.

7. Reduce your app size

It is really easy to add a ton of packages to your code during your development process. As you’re probably aware, this can turn into bloatware.

Let’s use an Android app as an example. You can use Gradle, a powerful open-source build tool which comes with a plethora of configuration options, to reduce the app’s size.

You can also generate Android app bundles, which are a new packaging system introduced by Google.

App bundles are efficient in multiple ways. Only the code necessary for a specific target device is downloaded from the Google Play Store. This is made possible as the Google Play Store repacks and ships only the necessary files and resources for the target device’s screen density, platform architecture, supporting hardware features, and so on.

Google Play Console Stats show that the download size of the app is reduced by 40 to 60 percent in most cases when you choose app bundles over APKs.

The command to generate an app bundle is:

flutter build appbundle

To obfuscate the Dart language code, you need to use obfuscate and the --split-debug-info flag with the build command. The command looks like this:

flutter build apk --obfuscate --split-debug-info=/<project-name>/<directory>

The above command generates a symbol mapping file. This file is useful to de-obfuscate stack traces.

ProGuard and keep rules

Below is an example of app level build.gradle file with ProGuard and other configurations applied:

android {
    ...

    def proguard_list = [
            "../buildsettings/proguard/proguard-flutter.pro",
            "../buildsettings/proguard/proguard-firebase.pro",
            "../buildsettings/proguard/proguard-google-play-services.pro",
            ...
    ]

    buildTypes {
        release {
            debuggable false                // make app non-debuggable
            crunchPngs true                 // shrink images
            minifyEnabled true              // obfuscate code and remove unused code
            shrinkResources true            // shrink and remove unused resources
            useProguard true                // apply proguard
            proguard_list.each {            
               pro_guard -> proguardFile pro_guard
           }
            signingConfig signingConfigs.release
        }
    }

One of the best practices for reducing APK size is to apply ProGuard rules to your Android app. ProGuard applies rules that remove unused code from the final package generated. During the build generation process, the above code applies various configurations on code and resources using ProGuard from the specified location.

Below is an example of ProGuard rules specified for Firebase:

-keepattributes EnclosingMethod
-keepattributes InnerClasses
-dontwarn org.xmlpull.v1.**
-dontnote org.xmlpull.v1.**
-keep class org.xmlpull.** { *; }
-keepclassmembers class org.xmlpull.** { *; }

The above declarations are called keep rules. The keep rules are specified inside a ProGuard configuration file. These rules define what to do with the files, attributes, classes, member declarations and other annotations when the specified pattern of the keep rule matches during the code shrinking and obfuscation phase.

You can specify what to keep and what to ignore using the dash and declaration rule keyword, like so:

-keep class org.xmlpull.** { *; }

The above rule won’t remove the class or any of the class contents during the code shrinking phase when ProGuard is applied.

You still need to be cautious while using this because it can introduce errors if it’s not done properly. The reason for this is that, if you specify a rule that removes a code block, a class, or any members that are declared and used to run the code execution, the rule may introduce compile time errors, runtime errors, or even fatal errors such as null pointer exceptions.

You can learn more about how to implement the ProGuard rules the right way from the official Android developer community.

.IPA building steps for iOS

Similarly for iOS, you need to perform the .IPA building steps as below:

  1. Go to XCode and click on Distribute app in the right pane under the Archives section.
  2. After selecting the method of distribution, for example Development, and then click on Next button to go to the App Thinning section.Select a method of distribution menu in App Thinning
  3. In the App Thinning section, choose All compatible device variants.In development distribution options, choose All compatible device variants
  4. Then select Rebuild from Bitcode and Strip Swift symbols. Then sign and export the .IPA file. It will also generate an app thinning size report file.

Conclusion

In this article, we’ve discussed the techniques to improve performance for the apps made in Flutter. Although Flutter as a framework comes jam-packed with features and is constantly evolving with new updates, performance is always a key consideration.

App performance has been and will be a huge deciding factor when capturing the global market. When considering the different aspects of mobile apps such as app size, device resolution, execution speed of the code, and hardware capabilities, improving performance can make a huge difference, especially when targeting large audiences.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Sneh Pandya Exploring the horizon with a knack for product management. Co-host of the NinjaTalks podcast and community organizer at Google Developers Group. Explorer, adventurer, traveler.

Testing accessibility with Storybook

One big challenge when building a component library is prioritizing accessibility. Accessibility is usually seen as one of those “nice-to-have” features, and unfortunately, we’re...
Laura Carballo
4 min read

Leave a Reply