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:
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.
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.
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.
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]}'), );},);
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; } }
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
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.
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.
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 iOSSimilarly for iOS, you need to perform the .IPA
building steps as below:
.IPA
file. It will also generate an app thinning size report file.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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.