late Initialization in Dart

late Initialization in Dart

#17DaysOfFlutter Day 17

When you check the title of the blog post, you might have thought "Did he make a mistake?", "Shouldn't L be capital?" The answer is no. I wrote it on purpose and yes "late" was on purpose.

From Dart 2.12, Dart language started to support late initiazaliton of variables. Main use cases for the late initialization is:

  • Declaring a non-nullable variable that is going to be initialized in another event

  • Lazily initializing a variable (duh!)

Let's see them in detail with a proper use-cases.

Declaring a non-nullable variable that is going to be initialized in another event

Imagine you have an AnimationController, for your application. In the documentation of it, it is advised to create the controller in the initState and dispose it in dispose function.

An AnimationController should be disposed when it is no longer needed. This reduces the likelihood of leaks. When used with a StatefulWidget, it is common for an AnimationController to be created in the State.initState method and then disposed in the State.dispose method.

import 'package:flutter/material.dart';
class Foo extends StatefulWidget {
  const Foo({ super.key });

  @override
  State<Foo> createState() => _FooState();
}

class _FooState extends State<Foo> with SingleTickerProviderStateMixin {
  AnimationController? _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
    );
  }

  @override
  void dispose() {
    _controller?.dispose();
    super.dispose();
  }
}

In the code above, you need to make the controller variable nullable to assign in the correct places. However, being nullable requires putting question mark before each instance function usage.

Also, if you try to follow along the immutability and try to put final in front of the controller, you can see that it is going to start shouting that something is wrong.

For fixing this, late is our savior. This will also help us to get rid of the nullability side of things as well.

class _FooState extends State<Foo> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
    );
  }

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

Lazily initializing a variable (duh!)

Another usage of late keyword is, in the scenario of a variable might not be needed, and/or initializing it is costly. One advantage of late keyword is the lazy initialization. When you assign the object with the late keyword, it is going to do the assigning when the call happens to the object reference.

Let's see an example of how it can affect our workflow. Check out the numbered comments and explains of it below:

class MyHomePage extends StatefulWidget {
  const MyHomePage({
    Key? key,
  }) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // (1)
  late final elements = loadAHugeList();

  bool _isHugeDataLoaded = false;

  void _triggerLoadingData() {
    setState(() {
      if (!_isHugeDataLoaded) {
        _isHugeDataLoaded = true;
      }
    });
  }
  // (2)
  List<String> loadAHugeList() {
    // (3)
    print('isLoaded');
    return List.generate(1000000, (i) => '$i');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (_isHugeDataLoaded)
              ListView.builder(
                // (4)
                itemCount: elements.length,
                itemBuilder: (context, index) {
                  return Text(elements[index]);
                },
              )
            else
              // (5)
              const Text(
                'You have not loaded data yet.',
              ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton.extended(
        label: const Text('Load huge data'),
        onPressed: _triggerLoadingData,
        tooltip: 'Load Huge Data',
        icon: const Icon(Icons.add),
      ),
    );
  }
}
  • (1): The late variable "elements" is defined but not assigned yet because the value is not called.

  • (2): The function to assign values is creating a list with one million items to it

  • (3): Only with a click on Floation Action Button you can see a message in your logs.

  • (4): The elements is only used for the first time when we use it in the list. But the list is not visible when the app is loaded for the first time.

  • (5): The initial phase of the app will show this text.

Pros/Cons

Pros:

  • late initialization was designed to prevent unnecessary initialization of objects.

  • Your variable will not be initialized unless you use it.

  • It is initialized only once. Next time when you use it, you get the value from cache memory. thanks to final and non-nullable combination.

  • Initializing an instance variable with access to current instance (aka this).

Cons

  • Forgotten variables can cause runtimes erros

  • There is no way to know programmatically if the initialization happened or not

Conclusion

Dart language provides us a lot of features for us. Taking advantage of them as developers is upto us and our decision to learn programming better. With this brand new information go ahead and share with me where you use late keyword. If you have any further questions, drop me a DM.