When developing mobile applications using Flutter, performance is crucial. A smoothly running application provides a better user experience, allowing users to explore the app without feeling annoyed or frustrated by slow startup times, crashes, or jank.
Optimizing application performance includes various aspects, such as app start times and efficient memory management. By minimizing the workload during initial launch, using efficient state management, and properly disposing of resources, developers can ensure a smoother user experience. Here are some ways to improve the performance of Flutter applications.
1. Avoid Unnecessary Initialization in the Main App
The main()
function is the entry point of a Flutter application. Keeping it clean and avoiding unnecessary initializations here is vital. Heavy operations should be deferred until they are needed, preferably in the appropriate widgets or services. And minimize the use of asynchronous functions in the main() function to ensure the first render time is as fast as possible. This helps to speed up the app's startup time, providing users with a quicker load time.
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
Additionally, we can also speed up the process by using Future.wait to perform multiple asynchronous operations concurrently. This technique allows us to initiate several tasks simultaneously and wait for all of them to complete, thereby optimizing the overall initialization time and enhancing the app's performance right from the start
void main() async {
await Future.wait([initFirebase(), initDatabase()]);
runApp(const MyApp());
}
And consider using a splash screen as the initial page to wait for the initiation to be completed.
void main() async {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool isSplashShow = true;
@override
void initState() {
super.initState();
_init();
}
void _init() async {
await Future.wait([_initFirebase(), _initDatabase()]);
_checkIsLoggedIn();
isSplashShow = false;
}
Future<void> _initFirebase() async {}
Future<void> _initDatabase() async {}
void _checkIsLoggedIn() {}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: isSplashShow
? const SplashPage()
: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
2. Prefer Using ListView
or CustomScrollView
Over SingleChildScrollView
When dealing with scrollable content, using ListView
or CustomScrollView
is more performance-efficient compared to SingleChildScrollView
. ListView
and CustomScrollView
are optimized for scrolling performance and memory usage, especially with large datasets, as they lazily build and dispose of widgets as they come into and out of the viewport.
For example, an application that displays a list using the SingleChildScrollView widget to show text from 1-999 uses approximately 30-40 MB of memory.
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final List<String> items = [];
@override
void initState() {
super.initState();
_init();
}
void _init() {
for (int i = 0; i < 9999; i++) {
items.add(i.toString());
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Title"),
),
body: SingleChildScrollView(
child: Column(
children: [
for (final item in items)
SizedBox(
width: double.infinity,
child: Text(item),
),
],
),
),
);
}
}
However, if the ListView widget is used to display text from 1-999, the memory usage is around 7-10 MB and also improve frame rendering time.
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final List<String> items = [];
@override
void initState() {
super.initState();
_init();
}
void _init() {
for (int i = 0; i < 9999; i++) {
items.add(i.toString());
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Title"),
),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return Text(items[index]);
},
),
);
}
}
3. Use cacheWidth
and cacheHeight
in Image
Displaying images in a Flutter application is a basic and easy task. However, many of us are unaware that displaying images whose sizes do not match what we want to display in a widget will cause our application to use unnecessary memory.
Flutter provides a way to detect oversized images by using debugInvertOversizedImages = true. This can alert developers if the displayed images are larger than desired.
void main() {
debugInvertOversizedImages = true;
return runApp(const MyApp());
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("counter"),
),
body: Column(
children: [
Image.network(
"https://images.unsplash.com/photo-1715196372160-31ba56b1a2f9",
),
],
),
);
}
}
If the image dimensions exceed what is suitable for the widget, Flutter will generate an error when debugInvertOversizedImages is set to true.
For this issue, we can use the cacheWidth and cacheHeight parameters in the Image widget. These parameters allow us to control memory usage by resizing the displayed image. Let's see how we can utilize them:
Image.network(
"https://images.unsplash.com/photo-1690906379371-9513895a2615",
height: 300,
width: 200,
cacheHeight: 300,
cacheWidth: 200,
),
By setting cacheWidth and cacheHeight to appropriate values, we can ensure that the image is displayed with the desired dimensions without unnecessarily consuming memory.
However, this solution is not perfect because each device has a different pixel ratio. Errors may not appear on our debug device but might on other devices with different pixel ratios.
Therefore, we can calculate cacheHeight and cacheWidth by multiplying with MediaQuery.of(context).devicePixelRatio.
For example:
extension ImageExtension on num {
int cacheSize(BuildContext context) {
return (this * MediaQuery.of(context).devicePixelRatio).round();
}
}
That extension allows us to easily calculate the cache size for our images and make the optimization process smoother:
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
debugInvertOversizedImages = false;
return Scaffold(
appBar: AppBar(
title: const Text("counter"),
),
body: Column(
children: [
Image.network(
"https://images.unsplash.com/photo-1690906379371-9513895a2615",
height: 300,
width: 200,
cacheHeight: 300.cacheSize(context),
cacheWidth: 200.cacheSize(context),
),
],
),
);
}
}
4. Dispose Unused Streams and Controller
When streams and controllers are no longer needed, failing to dispose of them can cause memory leaks. Memory leaks occur when memory that is no longer needed is not released, which over time can consume a significant portion of available memory, leading to poor performance and even application crash (out of memory).
To prevent memory leaks, it is important to dispose streams and controllers when they are no longer needed. This is typically done in the dispose
method of a StatefulWidget
.
late StreamSubscription _subscription;
late TextEditingController _textEditingController;
late ScrollController _scrollController;
@override
void initState() {
super.initState();
_subscription = counterBloc.counterStream.listen((data) {
// handle data
});
_textEditingController = TextEditingController();
_scrollController = ScrollController();
_textEditingController.addListener(() {
// handle data
});
_scrollController.addListener(() {
// handle data
});
}
@override
void dispose() {
_subscription.cancel();
_textEditingController.dispose();
_scrollController.dispose();
super.dispose();
}
5. Use const
and Prefer State Management Solutions
Using const
constructors for widgets whenever possible helps Flutter optimize the build process by reusing widgets rather than recreating them. Additionally, employing state management solutions like BLoC, Riverpod, or Provider can lead to better-organized code and more efficient state handling, ultimately improving performance.
final counterBloc = CounterBloc();
final counterProvider =
StateNotifierProvider.autoDispose<CounterController, int>(
(ref) => CounterController(),
);
class CounterController extends StateNotifier<int> {
CounterController() : super(0);
void increment() {
state = state + 1;
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("counter"),
),
body: Column(
children: const [
IncrementWidget(),
CounterWidget(),
],
),
);
}
}
class IncrementWidget extends ConsumerWidget {
const IncrementWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
return ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).increment();
},
child: const Text("counter"),
);
}
}
class CounterWidget extends ConsumerWidget {
const CounterWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
Top comments (0)