Advanced Flutter Interview: Part two

Advanced Flutter Interview: Part two

This is the second article in a big series on advanced questions. I feel like it will be a permanent section on my blog ๐Ÿ˜. If you haven't read the first part, I'd recommend going there first. Subscribe to my telegram channel and LinkedIn to not to miss exciting things about Dart & Flutter.

Does Dart use GC? Describe the techniques used.

Dart is a language that incorporates a garbage collection (GC) mechanism, offering an automated memory management approach that mitigates the need for manual memory allocation and deallocation - a stark contrast to languages like C or C++, where developers must manage memory explicitly.

The Dart runtime comes with a built-in garbage collector alongside other critical utilities. But what exactly is a garbage collector? In essence, it serves as an intermediary between your code and the memory heap. When an object is instantiated, it's allocated memory space. If at any point this object becomes unused or 'dead', the garbage collector steps in to reclaim the memory it had occupied.

Young Scavenger
Dart's garbage collection utilizes a two-generation system to optimize its performance. This system is made up of the "new" or "young" generation and the "old" generation.

Objects in Dart are initially allocated in the new generation, which is a small region of memory. Dart uses a technique known as "scavenging" for this space. Scavenging is a fast, efficient process due to the new generation's relatively small size.

The underlying principle is that many objects 'die young', i.e., they become unreachable quickly after their creation. Hence, frequently running the garbage collector in the new space can be very efficient as it helps to quickly clean up memory that is no longer in use.

When objects survive garbage collection in the new generation (they are still reachable after a garbage collection cycle), they are then moved to the old generation. The old generation is typically larger and garbage collection is performed less frequently in this space because objects in the old generation are likely to stay alive longer.

Scheduling
GC provides hooks to Flutter Engine which are called when Flutter determines that it is currently idling. This minimizes the effects of garbage collection and gives the GC the windows to perform its work.

Mark sweep
Objects that survive past a certain age are shifted to a new memory region handled by the second-generation garbage collector. This collector uses a mark-sweep method, which consists of two phases: marking the objects still in use and sweeping (reclaiming) the unmarked ones.

This method can momentarily freeze the UI thread during the marking phase, causing an app to pause. However, this is rare, as Dart's young scavenger typically handles short-lived objects, and Flutter's scheduling helps reduce these pauses.

Still, if an app doesn't adhere to the idea that most objects die young (the weak generational hypothesis), these collections could happen more often, although it's unlikely due to Flutter's widget implementation.

What is pattern matching?

What patterns do

In general, a pattern may match a value, destructure a value, or both, depending on the context and shape of the pattern.

First, pattern matching allows you to check whether a given value:

  • Has a certain shape.
  • Is a certain constant.
  • Is equal to something else.
  • Has a certain type.

Then, pattern destructuring provides you with a convenient declarative syntax to break that value into its constituent parts. The same pattern can also let you bind variables to some or all of those parts in the process.

Read more:

Patterns
Summary of patterns in Dart.

Tell about isolates and isolate groups. What is heap?

Isolates: In Dart, concurrency is achieved through isolates, which are independent workers similar to threads in other languages. However, unlike threads, isolates don't share memory. This means each isolate has its own memory and runs in its own separate execution thread. Because of this separation, you avoid potential issues with shared state that you might encounter with traditional threads. Each isolate has event loop to process async events.

  • Advantages: Since isolates don't share memory, there's no need for locks or other synchronization primitives. This can lead to fewer bugs related to concurrency.
  • Communication: Isolates communicate by sending and receiving asynchronous messages. For example, you might send a message from one isolate to another asking it to compute something, then receive a message back with the result.

Isolate Groups: While each isolate has its own separate memory, Dart 2.15 introduced the concept of "Isolate Groups". An isolate group is a collection of isolates that share a common, garbage-collected heap. Isolates within the same group can create and share certain immutable objects directly without copying. This enables more efficient communication between isolates in the same group.

Heap: In computing, a heap usually refers to an area of dynamically-allocated memory used for variable storage during runtime. Variables created at runtime (as opposed to compile time) are often stored in the heap.

In the context of Dart and isolates:

  • Each isolate has its own heap, meaning it has its own area of memory where it manages its variables and objects, but with the introduction of isolate groups, isolates within the same group can have a shared heap, allowing them to interact with certain objects more efficiently.

Describe different ways of creating an isolate

There are three possible approaches to spawn an isolate:

  1. Isolate.spawn - spawn an isolate from a function in the same isolate group. This approch is often used when you need a full duplex communication.
  2. Isolate.run - a wrapper around Isolate.spawn. Used for one-shoot computation. The result is returned in a Future.
  3. Isolate.spawnUri - ย spawn isolate from a snapshot in a new IsolateGroup. This takes URI to a precompiled platform-specific snapshot and executes its main function. Note, that this doesn't work in Flutter.

What is immutability?

Immutability refers to the inability to change an object after it has been created. In the context of computer science and programming, when an object (like a string or list) is immutable, it means that its state cannot be modified after it is created. If you want to change an immutable object, you have to create a new object with the desired changes. Here's a breakdown of immutability:

Characteristics:

  • Fixed State: Once the data is set, it can't be changed.
  • Predictability: Because the data can't change, it leads to safer code. You can be sure that the data will remain the same throughout its lifetime.
  • Thread-Safe: Immutable objects are inherently safe to use across multiple threads since there's no risk of one thread changing the data while another is reading it.

Benefits:

  • Reduction in Side Effects: Functions that operate on immutable objects can be more easily reasoned about since they donโ€™t cause side effects.
  • Concurrency: Immutable objects are inherently thread-safe, making them beneficial in concurrent and multi-threaded environments.
  • Integrity: It's easier to ensure data integrity since data can't be changed unexpectedly.

Errors vs Exceptions

Both error and exception classes are used to indicate failure. However, there is a fundamental difference.

Exceptions are designed to be caught so that the program can process them and display a snackbar with a reason, for example. It is also quite common to receive a 401 REST Exception and perform some OAuth-related logic. In contrast, errors are intended to be detrimental. Errors are meant to cause damage. If an error occurs during runtime, something has gone wrong. For example, an error could be: StackOverflowError. This causes a crash.

This only works if the author uses them correctly. For instance, the Dio package has had an exception named DioError for several years.

What is a widget?

Widgets are immutable configurations used to describe certain parts of the user interface. Note that the Widgets layer cannot be considered the UI layer because it does not draw anything. Furthermore, you have the ability to manage dependencies, init controllers or dispose them. As a result, it is more appropriate to refer to it as a Configuration Layer.

The mutable components, namely the Element and Render Object, respond to the immutable configuration. Element manages the life cycle and work with widgets. The Render Object is responsible for painting widgets onto the canvas.

Rebuilding is triggered by a change in state or inherited widget. Any element that depends on that state is marked as dirty, meaning it should be rebuilt. Rebuilding is only performed for the dirty elements. The newly returned Widget from the build method is compared to the previous one. If there is no change, the Element and its descendants are skipped during updating.

Related optimizations exist. For example, use the const keyword to create canonical instances so element can recognize them as non-changed, thereby skipping some work.

Tell about Inherited Widget

Inherited widget is a type of widget that efficiently propagates itself down the tree. All widgets down the tree can get the inherited widget via O(1) as all copies are stored in the HashTable.

Each element maintains a hash table of inherited elements. When Element is inserted into the tree, it simply stores the reference to a parent's map (see https://github.com/flutter/flutter/blob/3.10.7/packages/flutter/lib/src/widgets/framework.dart?ref=lazebny.io#L5508-L5512). In fact, Inherited Element uses the same method, but instead it copies the map so that the original cannot be modified, and puts itself to it. Then all descendants will be able to access the new inherited element.

In fact, this is the main reason why inherited widgets are so incredible! They offer O(1) time complexity and are a great way to provide something down the tree. You can find a bunch of examples in the framework code (FocusScope, MediaQuery).

There are also a few derives of InheritedWidget: InheritedModel and InheritedNotifier. InheritedModel is used when you have multiple aspects in a widget. If you use plain InheritedWidget then when you update field A then all the descendants will be notified (event if they don't really depend on it). This is where InheritedModel comes in! If some element depends on field B of InheritedModel, then there is no point in notifying that widget when field A changes. So we can specify one aspect and that element will only depend on field B.

InheritedNotifier takes listenable as a parameter and listens to it. When there is a notification from the listenable the inherited notifier updates its dependencies.

State & Widget lookup

Flutter elements are organised in a tree data structure, and each node in that tree has access to both ancestors and descendants. This gives that node the ability to traverse both upstream and downstream (which all elements do).

When an element is injected into the tree, it is given a link to its parent. If that element has any children, they are also maintained. That's what actually drives all the lookup methods on [BuildContext].

You can easily find an ancestor state of a certain type. Under the hood, Flutter just goes through this chain of elements until it finds the matching StatefulElement. This applies to findAncestorWidget and findAncestorRenderObject: Flutter traverses the tree of elements (each element has a link to the widget) and finds an element with the link to the needed widget and returns it.

What are the keys in Flutter?

New instances of widgets are created during the build process, but attached elements remain the same. Thus, there is a need to find a way of matching the old and new generation. Flutter has a straightforward static method on Widget that compares new and old elements. If the runtime type and keys are alike, the element could be updated with the new configuration. When there is no key, only types are compared, which can sometimes result in unpredictable behaviour.

Flutter provides various types of keys. We begin by looking at the LocalKey subclasses: These subclasses include ObjectKey, ValueKey, and UniqueKey. Each of these keys are used to uniquely identify widgets. Let's consider a basic example that involves two coloured containers that swap when a button is tapped.

And create a screen which will hold the logic of swapping:

Is this going to work? Will these widgets actually be swapped? It may seem so. After clicking the button, the widgets replaced each other. It would be beneficial to explore the internal workings of the framework in greater depth and gain a deeper understanding.

When the framework inflates the widget, it creates the corresponding element. In our scenario, the structure will appear like this, keeping in mind that it was simplified.

Please note that widget 'a' and widget 'b' are independent instances of the same class that lack the key. Upon calling the setState method, a new build is activated, composing new widgets in different 'slots' (also called locations). However, let's examine [Widget.canUpdate].

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

Will the [Widget.canUpdate] method return true if the old widget is Widget A and the new widget is Widget B? Yes, it will. The framework will notify the element about new configuration coming. See what's happening:

The StatelessElement that previously contained widget "A" has now been updated with widget "B", and the element that contained widget "B" is now updated with widget "A". Frequently, this implies that the renderObject has been updated with the latest parameters. In fact, you may already grasp the problem at this point, but let us move ahead and alter that Item into a Stateful Widget. It will change the provided color when it is clicked.

Well... I made those changes, would you believe it? The colours do not change!

0:00
/

This is because [State] lives inside an [Element]. It's not attached to the widget in any way. Obviously, the problem above is caused by new widgets being linked to the wrong elements. If you want to know more about an algorithm for updating elements, see the documentation notes at https://github.com/flutter/flutter/blob/stable/packages/flutter/lib/src/widgets/framework.dart#L3773 and https://github.com/flutter/flutter/blob/stable/packages/flutter/lib/src/widgets/framework.dart#L3786-L3815.

At the moment, we can help the framework to match old and new widgets. It is necessary to add a key to each widget. You can add either UniqueKey or ObjectKey or ValueKey to [Item2], or even implement your own if you need to, but the only requirement here would be that it is persistent across rebuilds (the same key is equivalent to another instance of such a key). Now look at how our items are now matched:

Now the underlying system can see that the widgets have keys, and it is now possible to correctly associate elements with the new widgets.

Key mistakes

Please bear in mind the requirement that I mentioned: "Keys must remain persistent across rebuilds." Let's examine it more closely. What happens when the widget is rebuilt? New instances are created, except when you use const. This implies that new instances of keys are also created, and if the previous instance does not match the new one, it will disrupt the matching process (in case it was not anticipated). This is another aspect to consider when working with keys.


Using an incorrect key could cause the [Element] to be recreated. Let us review this example:

The unique keys are not consistent across rebuilds and hence won't match. As a result, the elements and state will be re-instantiated causing the loss of all the ephemeral states stored. This can be easily resolved by adding the ValueKey as on the pic.

What the hell is a global key?

GlobalKey has some new features. GlobalKeys are designed to be associated with an [Element]. They must be unique throughout the application because they are stored in a global map where GlobalKey is a key and [Element] is a value.

When the item with the global key is mounted, it is added to a global map (see https://github.com/flutter/flutter/blob/stable/packages/flutter/lib/src/widgets/framework.dart#L3975). And when unmounted - removed from it.

The interesting thing here is that the [Element] associated with the global key can change parents, so it can be deactivated in one place in the tree and activated in another without losing its state. This can only happen within the same frame. You can start the journey here: https://github.com/flutter/flutter/blob/stable/packages/flutter/lib/src/widgets/framework.dart#L4081

Thanks for reading me!

The second part is over and I hope you enjoyed it. I believe these questions are unique content and really useful to you. If so, please consider supporting me by subscribing to my telegram channel and LinkedIn to not to miss exciting things about Dart & Flutter.

Read more