DEV Community

Edo Lubis for Tentang Anak Tech Team

Posted on

5 Tips to Improve Your Flutter Performance

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(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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'),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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),
              ),
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

the memory usage around is 30-40 MB

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]);
        },
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

the memory usage is around 7-10 MB

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",
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

If the image dimensions exceed what is suitable for the widget, Flutter will generate an error when debugInvertOversizedImages is set to true.

warning error memory

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,
          ),
Enter fullscreen mode Exit fullscreen mode

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();  
  }  
}
Enter fullscreen mode Exit fullscreen mode

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),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)