Whether you're building apps or preparing for interviews, understanding flutter architecture is essential. That's why I spent 20+ hours simplifying Flutter's complexities to make it straightforward for you. This is your step-by-step guide, crafted for both beginners and seasoned developers, focusing on clarity and simplicity.
In this blog, we'll cover important aspects of Flutter architecture, breaking them down into easy-to-understand sections:
The Layer Model: We'll explore the layers that make up Flutter, helping you understand how it's built.
Reactive User Interfaces: Learn about this key concept in Flutter UI development and how it makes your interfaces more dynamic.
Flutter Widgets: Get to know widgets, the core building blocks of any Flutter UI.
The Rendering Process: Understand how Flutter transforms your UI code into visuals on the screen.
Platform Embedders Overview: Discover how Flutter runs on different devices, including mobile and desktop.
Integrating Flutter with Other Code: We'll discuss the techniques for blending Flutter with existing codebases.
Support for the Web: Concluding with insights into Flutter's capabilities and performance in web applications.
Not only will this guide enhance your understanding of Flutter, but it will also equip you with the knowledge to confidently tackle Flutter architecture questions in interviews. You'll learn how to explain these concepts in a clear and concise manner, a key skill for impressing potential employers.
💡 This guide is highly inspired by official flutter documentation. If you want, you can read those for more details with technical jargon. But this guide is easier to understand, especially if you're just starting or want to avoid lots of complicated terms.
If you like to learn or upgrade your skills in Flutter, consider checking out: Flutter University.
Let's dive into Flutter architecture together, making it simple and interview-ready, one concept at a time.
The Layer Model
Flutter is a popular tool for building applications. It's unique because it uses a layered architecture. This architecture is made up of different parts that work together to make Flutter flexible and powerful.
The Basics of Flutter's Layers
At its core, Flutter is made up of different layers. Each layer depends on the one below it, but none of them have special control over another. This design means you can change parts of Flutter without affecting the whole system.
Architecture layers diagram from flutter's official documentation.
The Bottom Layer: Embedder
The first layer is the Embedder. This is where Flutter connects with the device's operating system. Whether you're using an Android, iOS, Windows, or another type of device, the Embedder helps Flutter work with it. It handles things like showing things on the screen, getting input from users, and other basic tasks. Different programming languages are used for different devices, like Java and C++ for Android, and Objective-C for iOS.
The Engine: Flutter's Powerhouse
Above the Embedder is the Flutter Engine. Mostly written in C++, this engine is the heart of Flutter. It's responsible for important tasks like drawing images and text on the screen, handling files and internet connections, and running Dart code (the programming language used in Flutter).
This engine uses a library called dart:ui to connect with the Flutter framework. This library turns the complex C++ code into something easier to use in Dart.
The Core Framework: Where Developers Work
Now we reach the Flutter Framework. This is where most developers spend their time. It's written in Dart and provides tools to build your app.
Building Blocks
At the lowest level of the Framework, there are basic classes and services. These include things like animations, graphics, and ways to recognize user gestures.
Rendering Layer
Next up is the rendering layer. This is about arranging things on the screen. You create a tree of objects that can change dynamically, and Flutter updates the screen to show these changes.
Widgets Layer
Above that is the widgets layer. Widgets are the basic building blocks in Flutter apps. Each thing you see on the screen, like a button or a text field, is a widget. This layer lets you combine widgets to make more complex designs.
Material and Cupertino Libraries
Finally, there are the Material and Cupertino libraries. They provide ready-to-use widgets that follow specific design styles – Material for Android and Cupertino for iOS.
Packages: Expanding Flutter's Abilities
Beyond the core framework, Flutter can be extended with packages. These are additional tools and features created by the Flutter community. They cover a wide range of functionalities, from camera integration to web support.
Anatomy of a Flutter App
When you create a Flutter app, it's made up of several parts:
Dart App: This is where your app's interface and logic live. You use widgets to build the UI and write Dart code for what your app does.
Framework (source code): This provides higher-level tools to build your app, like gesture detection and text input.
Engine (source code): It does the heavy lifting of drawing your app on the screen.
Embedder (source code): This connects Flutter to the device's operating system.
Runner: This combines everything into a package that can run on your device.
The Build Process: From Code to Application
When you're ready to release your app, the Flutter build process takes over. Here's what happens:
Compilation: Your Dart code is compiled into either JIT (Just In Time) for debugging or AOT (Ahead Of Time) for release.
Asset Bundling: All resources and assets, such as images and fonts, are bundled into the app package.
Native Packaging: The AOT compiled app and the bundled assets are packaged into a native container, such as an APK for Android or an IPA for iOS.
Signing: The package is signed with a developer certificate, which is necessary for distributing the app on platforms like the Google Play Store or Apple App Store.
With these processes, your Flutter app is transformed into an executable package that can run smoothly on your target platform.
Reactive User Interfaces
Flutter's UI is "reactive." This means the UI changes automatically when the app's data changes. Imagine a music app showing a play button. When you press play, the app's state changes and the button changes to a pause button. Flutter handles these changes smoothly.
This reactive nature comes from the "React" framework by Facebook. In older frameworks, changing the UI was harder. Developers had to manually update the UI for each data change. This was complex and error-prone. Flutter makes this easier.
How Flutter Manages UI
In Flutter, everything on the screen is a widget. Widgets are like building blocks. You use them to describe what the UI looks like. Flutter then takes care of showing it on the screen.
Widgets in Flutter are immutable. This means once you create a widget, you can't change it. If you want a different widget, you create a new one. This may sound inefficient, but Dart, the language used in Flutter, handles this very well.
Flutter separates the creation of UI from its underlying state. This separation is key to Flutter's reactive nature. You define UI as a function of the state:
UI = f(state)
Whenever the state changes, Flutter calls the build()
method of the widget. This method describes how the UI looks based on the current state. Since this method can be called often, it's designed to be fast and free of side effects.
Flutter Widgets
Imagine widgets as the building blocks of a Flutter app's user interface. Everything you see in a Flutter app, from buttons to padding and even the app itself, is a widget. Widgets describe what their view should look like with their current configuration and state.
How Widgets Organize Themselves
Widgets in Flutter are organized in a tree-like hierarchy. This means every widget nests inside a parent widget, receiving a context that helps it know where it is within the overall widget tree. The tree starts with a root widget, which could be a MaterialApp
or CupertinoApp
widget, setting the stage for the entire application.
Example of a Simple Widget Tree
Consider this basic Flutter app structure and it's widget tree:
Code:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('My Home Page'),
),
body: Center(
child: Column(
children: [
const Text('Hello World'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
print('Click!');
},
child: const Text('A button'),
),
],
),
),
),
);
}
}
Widget Tree:
This code snippet introduces us to a simple Flutter app containing a MaterialApp
with a Scaffold
, displaying a text and a button on the screen. Each element, from the MaterialApp
to the Text
widget, plays a specific role, contributing to the app's overall UI.
Updating the UI
Flutter apps respond to events like user interactions by replacing widgets in the UI with new widget instances. Flutter's engine then compares the new widgets with the old ones and updates the UI where necessary. This efficient method ensures smooth and fast app performance.
Flutter's Approach to UI
Flutter does not rely on the native UI components of the platform it runs on. Instead, it uses its own set of widgets. This approach has several benefits:
Customization: Developers can easily customize widgets to fit their needs.
Performance: Drawing the UI directly in Flutter removes the need to communicate with the platform's native components, leading to better performance.
Consistency: The app's appearance remains consistent across all platforms.
Widget Composition
In Flutter, complex widgets are built by combining many simpler, single-purpose widgets. This compositional approach encourages reusability and simplifies the process of creating custom widgets. For instance, the Container
widget, which is commonly used for decoration and alignment, is composed of several simpler widgets like Padding
, Align
, and DecoratedBox
.
Building Widgets
Widgets are defined by their build()
method, which describes how the widget looks and functions. Stateless widgets are simple and immutable, while Stateful widgets can change over time, responding to user input or other factors.
Stateless vs. Stateful Widgets
Stateless Widgets: Do not change over time. Examples include text labels and icons.
Stateful Widgets: Can change based on interactions or internal state changes. They use the
setState()
method to trigger a rebuild with new data.
Managing State
Managing state is crucial in Flutter apps. Local state management is straightforward with stateful widgets, but as apps grow, global state management becomes necessary. Flutter offers several ways to manage state effectively, including using the Provider
package, InheritedWidget
, and other state management solutions like flutter_bloc
. This is a huge topic in itself, we'll cover this in a separate article.
The Rendering Process
Flutter transforms widgets into actual visuals on your screen through several steps. This process is called the rendering pipeline:
Build Phase
This initial phase involves constructing the widget tree. Widgets are defined in the code and represent the desired appearance and structure of the app's UI. The build()
method plays a crucial role here, creating and returning a hierarchy of widgets based on the app's current state (more about widgets & widget tree).
Layout Phase
After the widget tree is built, Flutter enters the layout phase, where it calculates the size and position of each widget. This phase ensures that every widget is allocated the correct amount of space, based on the constraints provided by its parent widgets.
Paint Phase
The final phase is where the magic happens—Flutter paints the widgets onto the screen. Using its rendering engine (Skia for most platforms and Impeller for iOS), Flutter draws the widgets according to the calculations made during the layout phase.
The Advantages of Flutter's Rendering Model
Flutter's widget-centric model offers several significant benefits for app development:
Cross-Platform Consistency: Flutter's use of its own widget set, instead of relying on native UI components, ensures that your app looks and behaves consistently across all platforms. Flutter uses Skia to draw graphics, which means it can quickly turn your designs into pixels on the screen.
High Performance: By compiling directly to native code and leveraging its embedded rendering engine, Flutter achieves performance that rivals native app development.
Efficient Updates: Flutter's intelligent rendering pipeline updates only the widgets that need changing, rather than the entire UI, making it highly efficient.
From Widget to Element to RenderObject
Flutter's rendering pipeline transforms widgets into elements and then into RenderObjects:
Widgets are the blueprint, defining what the UI should look like.
Elements are the instances of widgets, representing them in the widget tree.
RenderObjects are the actual objects that get painted onto the screen, handling layout and rendering.
This process ensures that the app's UI is efficiently updated and rendered, maintaining smooth performance even as the complexity of the interface grows.
Platform Embedders Overview
At its core, platform embedding is about how Flutter interacts with the underlying operating system to display your app and handle its lifecycle. Unlike other frameworks that might convert UI elements into the operating system's widgets, Flutter does all the UI rendering itself.
This means Flutter needs a way to communicate with the host operating system to display its content, process user inputs, and manage app states. That's where platform embedding comes into play.
The platform embedder is essentially a bridge between Flutter and the native OS. It's responsible for initializing the Flutter engine, which runs your app, and creating a window or a texture where Flutter can draw its UI. This setup allows your Flutter app to run on Android, iOS, Windows, macOS, and Linux.
How Does It Work on Different Platforms?
Android and iOS
On Android, Flutter typically runs inside an Activity as a FlutterView. This FlutterView is where your Flutter UI is rendered. It can work as a view or a texture, based on your app's needs. For iOS, Flutter uses a UIViewController to integrate with the system. Both platforms use their respective APIs to handle app lifecycle events, input gestures, and other system services.
Desktop (Windows, macOS, Linux)
For desktop platforms, Flutter integrates differently. On Windows, it's hosted in a traditional Win32 application, with content rendered using ANGLE to translate OpenGL calls to DirectX. macOS uses an NSViewController for integration, while Linux details can vary based on the specific setup.
Custom Platform Embedders
One of the powerful features of Flutter is the ability to create custom platform embedders (checkout). This means if the default embedders don't meet your needs, you can build your own. Examples include creating an embedder for a Raspberry Pi or for systems that support VNC-style remote sessions.
Integrating with Other Code
Flutter stands out for creating visually appealing apps across multiple platforms with a single codebase. However, developers often face situations where Flutter needs to interact with platform-specific code or native libraries.
This integration is crucial for accessing device capabilities or incorporating existing native code into Flutter apps. Let's explore how Flutter achieves this seamless integration.
Platform Channels: Bridging Dart and Native Code
Platform channels are the primary way Flutter communicates with native code. This method is essential when your app needs to use device-specific features not available in Flutter's framework, such as accessing battery level, using Bluetooth, or implementing a native third-party SDK.
How Platform Channels Work
Platform channels use a simple yet powerful system for message passing. You define a channel in your Dart code and specify how it communicates with the native side of your app, whether it's written in Kotlin/Java for Android or Swift/Objective-C for iOS.
Dart Side:
You initiate communication with a method call over the channel, specifying the method name and any arguments.
const channel = MethodChannel('com.example/battery');
final batteryLevel = await channel.invokeMethod('getBatteryLevel');
Native Side (Kotlin for Android example):
You respond to these method calls on the native side, implementing the logic required and returning the result back to Dart.
MethodChannel(flutterEngine.dartExecutor, "com.example/battery").setMethodCallHandler {
call, result ->
if (call.method == "getBatteryLevel") {
val batteryLevel = getBatteryLevel()
result.success(batteryLevel)
} else {
result.notImplemented()
}
}
This two-way communication channel allows Flutter apps to utilize native features and libraries seamlessly.
Foreign Function Interface (FFI): Direct Native Code Invocation
For direct interaction with C-based libraries, Flutter offers the Foreign Function Interface (FFI). This interface is more direct and often faster than platform channels because it allows Dart code to call C functions directly, without the need for message serialization.
C Function Example
Imagine we have a C function that multiplies two numbers.
// multiply.c
#include <stdint.h>
int32_t multiply(int32_t x, int32_t y) {
return x * y;
}
Compiling C Code to a Library
You compile this C code into a shared library (libmultiply.so
or libmultiply.dylib
), depending on your operating system.
Dart FFI with Typedef
In Dart, you use dart:ffi
to interoperate with this C function. First, define the typedef
s matching the C function signature, then load the library and call the function.
import 'dart:ffi';
import 'package:ffi/ffi.dart';
// Typedefs for the C function
typedef MultiplyC = Int32 Function(Int32 x, Int32 y);
typedef MultiplyDart = int Function(int x, int y);
void main() {
// Load the shared library
final dylib = DynamicLibrary.open('libmultiply.so'); // Adjust the library name as needed
// Get a reference to the multiply function
final MultiplyDart multiply = dylib
.lookupFunction<MultiplyC, MultiplyDart>('multiply');
// Call the C function from Dart
final result = multiply(4, 5);
print('4 * 5 = $result');
}​
In this streamlined example, DynamicLibrary.open
loads the compiled C library, lookupFunction
maps the multiply
C function to a callable Dart function, and then you can use multiply
just like any Dart function. This demonstrates the basic steps for integrating C code into a Dart (Flutter) application using FFI and typedef
for clear and type-safe function signatures.
This approach is particularly useful for CPU-intensive tasks like image processing, cryptographic operations, or direct interaction with native system APIs.
Rendering Native Controls within Flutter Widgets
In some cases, you might want to display native Android or iOS controls within your Flutter app. Flutter accommodates this through platform views (AndroidView
and UiKitView
), allowing you to embed native components directly within your Flutter widget tree.
Implementing Platform Views
You can use AndroidView
or UiKitView
widgets to embed native components. This method is typically used for complex controls that would be impractical to recreate in Flutter, such as a web view or a map view.
Widget build(BuildContext context) {
return Platform.isAndroid
? AndroidView(viewType: 'native-view')
: UiKitView(viewType: 'native-view');
}
While powerful, platform views should be used judiciously due to their performance implications, as they introduce overhead in rendering and event handling.
Embedding Flutter in Existing Applications
Conversely, Flutter allows embedding its content into existing Android or iOS applications. This feature is invaluable for gradually migrating an existing app to Flutter or adding new Flutter-based features to an established app.
Integrating Flutter Modules
To integrate Flutter into an existing app, you create a Flutter module and include it in your Android or iOS project. This process involves configuring the native project to host Flutter content, ensuring a smooth user experience and performance.
// Android example
val flutterFragment = FlutterFragment.createDefault()
supportFragmentManager
.beginTransaction()
.add(R.id.flutter_container, flutterFragment)
.commit()
This flexibility enables developers to leverage Flutter's productivity and UI capabilities without rewriting their entire application.
Support for the Web
Flutter has revolutionized the way developers build cross-platform applications, extending its capabilities beyond mobile to web platforms. This article aims to understand Flutter's web support, breaking down the complex architecture into easy-to-understand concepts for beginners.
Introduction to Flutter Web Support
Flutter is a powerful framework that allows developers to create beautiful, natively compiled applications from a single codebase. It is known for its ability to run on various platforms, including iOS, Android, and now, the web. The integration of Flutter with web technologies marks a significant milestone, offering developers the tools to craft high-performance, rich web applications.
Dart
At the core of Flutter's web support is Dart, a modern programming language developed by Google. Dart is unique because it can compile into JavaScript, the language of the web. This capability is crucial for Flutter's operation on web platforms, as it allows Dart code to run in web browsers seamlessly. Dart's design focuses on both development and production needs, ensuring that apps are not only easy to build but also optimized for performance.
Flutter Engine and Web Browsers
The Flutter engine, originally written in C++, is designed to interact with the underlying operating system of a device. However, this approach does not directly apply to web browsers. To bridge this gap, Flutter introduces a web-specific engine implementation that leverages standard browser APIs. This adaptation enables Flutter apps to run on the web without losing their rich features and responsive design.
Rendering Options: HTML and WebGL
Flutter offers two modes for rendering content on the web:
HTML Mode: Utilises familiar web technologies like HTML, CSS, Canvas, and SVG. This mode is optimised for code size, ensuring quicker load times and efficient performance on web platforms.
WebGL Mode (CanvasKit): Employs a WebAssembly-compiled version of Skia (CanvasKit) to achieve direct access to the browser's graphics capabilities. Although this mode results in larger app sizes, it delivers superior graphics performance and fidelity, closely mirroring the experience on native platforms.
Development and Production Workflow
Flutter simplifies the development process with tools tailored for both stages:
Development: During the build phase, Flutter uses the
dartdevc
compiler, which supports incremental compilation. This feature enables the hot restart of web apps, facilitating rapid development cycles.Production: For deploying apps to the web, Flutter switches to the
dart2js
compiler. This highly optimized compiler packages the Flutter framework, application code, and dependencies into a compact, minified JavaScript file. This process ensures that the final product is ready for efficient deployment and performance on web servers.
Before we go...
If you're reading till now, you're awesome, thanks for reading!
If you loved this, consider following me and maybe sharing a word about Flutter University with others 😄
Got any doubt or wanna chat? React out to me on twitter or linkedin.
Top comments (0)