Reducing Flutter App size

BTLA Tech Team
7 min readApr 4, 2024

--

Contributors:

Divyasourabh, Utkarsh Dixit, Rachit Goyal

This is the fifth blog of this series of blog posts highlighting our experiences with flutter and what’s next for mobile apps.

App size reduction holds paramount importance, particularly in the realm of cross-platform app development. In our pursuit of optimizing the size of our applications, we have utilized these below methods:

Method 1: Asset Resolver

In our codebase, we extensively leverage dynamic assets including webp, png, jpeg, gif, JSON content, .otf fonts, and more. This approach is driven by our goal to deliver personalised content tailored to each user cohort. Traditionally, embedding these assets directly into the codebase significantly inflates the download size of our application, as each asset contributes to the overall package size. To address this challenge, we devised a strategic solution we named as Asset Resolver.

The Asset Resolver strategy involves leveraging the capabilities of AWS S3 Bucket to store our assets on Amazon servers. Rather than embedding assets directly into the codebase, we store them remotely in the cloud. At runtime, when the application requires access to these assets, it dynamically retrieves them from the server. This approach effectively optimizes the size of our assets, as they are fetched on-demand during runtime instead of being bundled with the initial download package.

By adopting the Asset Resolver strategy with AWS S3 Bucket integration, we achieve several key benefits.

  • Firstly, this strategic approach significantly reduces the initial download size of the application, as only essential code and minimal resources are included in the package.
  • Secondly, efficient asset management is ensured, as assets can be updated and modified on the server without requiring app updates or redistributions.
  • Additionally, this approach enhances overall scalability and flexibility, allowing for efficient management of dynamic assets across various platforms and application versions.

Overall, the Asset Resolver strategy enables us to use cohort wise assets optimize the size of our application while maintaining seamless access to dynamic assets, ultimately enhancing user experience and minimizing resource overhead.

Method 2: Code Optimisation

  • Minification and Obfuscation: Leverage Flutter’s built-in support for code minification and obfuscation to reduce the size of your app’s executable.
  • Tree Shaking: Utilize only the necessary packages and libraries in your app by removing any unused dependencies. This can significantly reduce the final app size.

Method 3: Dependency Management

  • Choose Lightweight Dependencies: When selecting third-party packages, opt for those that are well-maintained, actively developed, and have a minimal impact on your app’s size.
  • Avoid Unnecessary Imports: Import only the specific parts of libraries that you need rather than importing the entire library to minimize unnecessary code inclusion.

Method 4: Optimize Fonts

  • Use System Fonts: Whenever possible, utilize system fonts instead of custom fonts to reduce the size of your app. System fonts are already installed on users’ devices, eliminating the need to bundle them with your app.
  • Subset Custom Fonts: If custom fonts are required, consider subsetting them to include only the characters needed by the app. This reduces the font file size while still preserving the necessary glyphs.

Method 5: Deferred components

Deferred components in Flutter refer to a technique where certain parts of the user interface (UI) or application logic are loaded lazily, only when they are needed. This approach can greatly enhance the performance, download size and efficiency of Flutter applications, particularly for large or complex projects.

Here are some key points about deferred components and their uses:

  • Reduced App Download Size: Deferred components contribute to reducing the initial download size of Flutter applications. By loading components dynamically, the initial package size of the app can be smaller, as less code and resources are bundled upfront. This is particularly advantageous for users with limited storage space or those on slower network connections, as it ensures quicker installation and reduces the barrier to entry for trying out the application.
  • Modularisation and Code Splitting: Deferred components promote modularisation and code splitting, enabling developers to organize their codebase more effectively. This can lead to better maintainability, scalability, and collaboration among team members working on different parts of the application.
  • Progressive Loading: Deferred components can be loaded progressively, allowing the application to display meaningful content to users while additional resources are loaded in the background. This helps to maintain user engagement and satisfaction, even during loading periods.

By strategically implementing deferred loading techniques, developers can create more efficient and responsive apps.

In Flutter, we can defer both a module and a Dart file, enabling efficient loading of components based on runtime conditions or user interactions.

Steps to Create Deferred Components in Flutter

Step 1

Add the Play core library to the android/app/build.gradle file of the root project.

dependencies {
implementation "com.google.android.play:core:1.8.0"
}

Step 2

Enable SplitCompat in your Flutter app by adding the following line to the application element in android/app/src/main/AndroidManifest.xml.

android:name="io.flutter.embedding.android.FlutterPlayStoreSplitApplication"

Step 3

Enable deferred components in your Flutter app by adding the following to the pubspec.yaml file located at the root of your project.

# The following section is specific to Flutter packages.

flutter:
deferred-components:

Step 4

Create a Deferred Component, which can be either a simple Flutter widget or an entire module.

Entry point of Deferred Module:

import 'package:demo_module/hello_screen.dart';
import 'package:flutter/material.dart';


class DemoModuleHome extends StatefulWidget {
const DemoModuleHome({super.key});


@override
State<DemoModuleHome> createState() => _DemoModuleHomeState();
}


class _DemoModuleHomeState extends State<DemoModuleHome> {
@override
Widget build(BuildContext context) {
return HelloScreen();
}
}
import 'package:demo_module/screen_1.dart';
import 'package:demo_module/screen_2.dart';
import 'package:flutter/material.dart';


class HelloScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Hello Screen'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Hello',
style: TextStyle(fontSize: 24),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ScreenOne()),
);
},
child: Text('Open Screen One'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ScreenTwo()),
);
},
child: Text('Open Screen Two'),
),
],
),
),
);
}
}
import 'package:flutter/material.dart';

class ScreenOne extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Screen One'),
),
body: Center(
child: Text(
'This is Screen One',
style: TextStyle(fontSize: 24),
),
),
);
}
}
import 'package:flutter/material.dart';

class ScreenTwo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Screen Two'),
),
body: Center(
child: Text(
'This is Screen Two',
style: TextStyle(fontSize: 24),
),
),
);
}
}

Step 5

Using a Deferred Component.

To use the deferred component in your app, you can use a FutureBuilder to load the component asynchronously and display a loading indicator while the component is being loaded.

import 'package:flutter/material.dart';
import 'package:demo_module/main.dart' deferred as deferredBox;
class HomeWidget extends StatefulWidget {
const HomeWidget({super.key});


@override
State<HomeWidget> createState() => _HomeWidgetState();
}


class _HomeWidgetState extends State<HomeWidget> {
late Future<void> _libraryFuture;


@override
void initState() {
// TODO: implement initState
super.initState();
_libraryFuture = deferredBox.loadLibrary();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: _libraryFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return deferredBox.DemoModuleHome();
}
return const CircularProgressIndicator();
},
);
}
}

Step 6

To build your app with deferred components, run the following command.

 flutter build appbundle

Step 7

On the first run, the Flutter validator may fail with issues that need to be addressed. The tool will provide recommendations for how to set up the project and fix these issues. It will also generate files that need to be manually moved or overridden in the android directory. Follow the tool’s recommendations and move or override the generated files as needed to properly set up the project.

<projectDirectory>/deferred_components_loading_units.yaml

You will need to copy the loading units from this file and paste them into the pubspec.yaml file, as well as mention the component in the settings.gradle file located in the android directory.

flutter:
deferred-components:
- name: sampleComponent
libraries:
- package:deferred_component_poc/demo_module/
include ':app', ':sampleComponent'

In the same way, update each file in the android folder.

strings.xml 
loc: <projectDir>/android/app/src/main/res/values/strings.xml

android_deferred_components_setup_files
loc: <projectDir>/android/<componentName>

AndroidManifest.xml
loc: <projectDir>/android/app/src/main/AndroidManifest.xml

Step 8

Re-run the Command

flutter build appbundle

After executing the build appbundle command, review the terminal output for the presence of the message “Deferred components prebuild validation passed”. This confirmation signifies that the prebuilt components included in the .aab file have undergone successful validation and are primed for utilization.

If the aforementioned message does not appear, it could indicate an issue with the prebuilt components. In such instances, you might need to rebuild either the app or the prebuilt components to rectify the problem. Upon resolving the issue, rerun the build appbundle command to produce a fresh .aab file containing properly validated prebuilt components.

Step 9

Running the Application

flutter run

Impact on app size with Deferred Component

In our comparative analysis, we embarked on creating two distinct projects to explore the impact of deferred components within Flutter applications. Each project was equipped with assets totalling ~4 MB in size. Upon thorough examination, we observed a notable difference in download sizes between the project incorporating deferred components and the one without such optimisation.

The project integrated with deferred components exhibited a download size approximately 5.3 MB smaller than its counterpart lacking this feature.

This significant reduction in download size underscores the effectiveness of employing deferred components in mitigating the overall footprint of Flutter applications, thus enhancing user accessibility and improving the user experience, particularly for users with limited bandwidth or storage constraints.

Continuing Our Quest: Advancing Performance through Deferred Components in Flutter

In conclusion, while we have made significant strides in optimising asset management with the Asset Resolver strategy, our commitment to enhancing performance and minimising app size remains steadfast. We continue our efforts to explore and implement additional strategies for reducing and optimising app metrics.

This blog is a work in progress, and we plan to incorporate more insights and updates in the future.

Reference Link

--

--

No responses yet