This article aims to provide a high-level overview of the architecture of Flutter, including the core principles and concepts that underlie its design.
Flutter is a cross-platform UI toolkit designed to allow code reuse across operating systems such as iOS and Android, as well as allowing applications to dock directly with underlying platform services. The goal is to enable developers to deliver high-performance applications that feel natural across platforms, embracing differences while sharing as much code as possible.
During development, the Flutter application runs in a virtual machine that provides stateful thermal overloading without the need for a full recompilation. When released, Flutter applications are compiled directly into machine code, either Intel X64, ARM instructions, or JavaScript if targeted to the Web. The framework is open source, under permitted BSD licenses, and has a thriving ecosystem of third-party packages that complement the core library functionality.
This overview is divided into several parts.
- Layer Model: Part that builds a Flutter.
- Reactive User interfaces: The core concept of Lutter’s user interface development.
- Widgets: The basic building blocks of the Lutter user interface.
- How Lutter converts UI code into pixels.
- Platform Embedders: Code for mobile and desktop operating systems to execute Flutter applications.
- Including Flutter with other code: information about the different technologies available for Flutter applications.
- Support for the Web: a summary of features of Flutter in the browser environment.
Achitectural layers
Flutter is designed to be an extensible, layered system. It exists as a series of independent inventories, each dependent on the underlying layer. Neither layer has privileged access to the one below, and each part of the framework layer is designed to be selectable and replaceable.
To the underlying operating system, Flutter applications are packaged in the same way as other native applications. A platform-specific embed provides an entry point; Coordinate with the underlying operating system to access services such as render surfaces, accessibility, and input; And manage the message event loop. The embeders are written in platform-appropriate languages: Currently Java and C++ for Android, Objective-C/Objective-C++ for iOS and macOS, and C++ for Windows and Linux. Using an embed, the Flutter code can be integrated into an existing application as a module or as the entire content of the application. Flutter contains many embeds for common target platforms, but others exist as well.
The core of Flutter is the Flutter engine, which is mainly written in C++ and supports all the basic elements required for Flutter applications. The engine is responsible for rasterizing the composite scene whenever a new frame needs to be drawn. It provides a low-level implementation of the Flutter core API, including graphics (via Skia), text layout, file and network I/O, accessibility support, plug-in architecture, and Dart runtime and compilation toolchains.
The engine is exposed to the Flutter framework via dart: UI, which encapsulates the underlying C++ code in dart classes. This library exposes the lowest level primitives, such as the classes used to drive the input, graphics, and text rendering subsystems.
Typically, developers interact with Flutter through the Flutter framework, which provides a modern, reactive framework written in the Dart language. It includes a rich set of platforms, layouts, and infrastructure libraries, consisting of a series of layers. From the bottom to the top, we have:
- Base classes and component services, such as animation, painting, and gestures, provide common abstractions on top of the underlying base.
- The render layer provides an abstraction for handling the layout. From this layer, you can build a tree of renderable objects. You can manipulate these objects dynamically, and the tree automatically updates the layout to reflect your changes.
- The Widgets layer is a constituent abstraction. Each render object in the render layer has a corresponding class in the Widgets layer. In addition, the Widgets layer allows you to define combinations of classes that can be reused. This is the layer that introduces the reactive programming model.
- The Material and Cupertino libraries provide a comprehensive set of controls that use composite primitives from the Widget layer to implement the Material or iOS design language.
The Flutter frame is relatively small; Many of the higher-level features that developers are likely to use are implemented in packages, including platform plug-ins like cameras and WebView, as well as platform-independent features like character, HTTP, and animation, which are built on top of the core Dart and Flutter libraries. Some of these packages come from the broader ecosystem, covering services such as in-app payments, Apple authentication and animation.
The rest of this review starts with the reactive paradigm of UI development and walks through the layers. We then described how to put widgets together and convert them into objects that can be rendered as part of your application. We describe how Flutter interacts with other code at the platform level, then briefly summarize how Flutter’s Web support differs from other targets.
Reactive user interfaces
Flutter is a passive, pseudo-declarative UI framework that provides a mapping from the application state to the interface state. The framework updates the interface at runtime when the application state changes. This model was inspired by Facebook’s work on its React framework, which included a rethinking of many traditional design principles.
In most traditional UI frameworks, the initial state of the user interface is described once, then updated separately by user code at run time in response to events. One challenge with this approach is that as the complexity of the application increases, developers need to be aware of how state changes link up throughout the UI. For example, consider the following UI.
There are many places to change state: color boxes, tone sliders, radio buttons. As users interact with the user interface, changes must be reflected everywhere else. Worse, unless careful, a small change to one part of the user interface can have a ripple effect on seemingly unrelated code.
One solution is an approach like MVC, where the controller pushes data changes to the model, which then pushes new state to the view through the controller. However, this is problematic because creating and updating UI elements are two separate steps that can easily get out of sync.
Flutter, like other reactive frameworks, takes another approach to this problem by explicitly decoupling the user interface from its underlying state. With the React style API, you only need to create the UI description, and the framework is responsible for creating and/or updating the user interface appropriately using this configuration.
In Flutter, widgets (similar to those in React) are represented by immutable classes that are used to configure the object tree. These widgets are used to manage individual object trees for layout, and then to manage individual object trees for composition. At the heart of Flutter is a set of mechanisms for efficiently walking the modified parts of the tree, converting the object tree to a lower-level object tree, and propagating changes through those trees.
A widget declares its user interface by overriding the build() method, which is a function that converts state to UI.
UI = f(state)
Copy the code
The build() method is designed to execute quickly and should have no side effects, allowing the framework to call it whenever it needs to (possibly once per render frame).
This approach relies on certain features of the language runtime (especially fast object instantiation and deletion). Fortunately, Dart is uniquely suited to this task.
Widgets
As mentioned earlier, Flutter emphasizes that the widget is a constituent unit. Widgets are components of the user interface that Flutter applies. Each Widget is an immutable statement that is part of the user interface.
The widgets form a composition-based hierarchy. Each widget is nested within its parent and can receive context from the parent. This structure continues down to the root widget (the container that hosts the Flutter application, usually MaterialApp or CupertinoApp), as shown in this trivial example.
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(title: Text('My Home Page')), body: Center( child: Builder( builder: (BuildContext context) { return Column( children: [ Text('Hello World'), SizedBox(height: 20), RaisedButton( onPressed: () { print('Click!'); }, child: Text('A button'), ), ], ); },),),),); }}Copy the code
In the previous code, all instantiated classes are widgets.
The application updates the user interface in response to events, such as user interactions, by telling the framework to replace one widget in the hierarchy with another. The framework then compares the old and new widgets and effectively updates the user interface.
Flutter has its own implementation for each UI control, rather than being subject to system-provided controls: for example, both the iOS Switch control and its Android counterpart have a pure Dart implementation.
This approach offers several benefits:
- Provides unlimited scalability. Developers who want a variation of the Switch control can create one in any way they want, not limited to the extension points provided by the operating system.
- Significant performance bottlenecks are avoided by allowing the Flutter to synthesize the entire scene at once, without transitioning back and forth between the Flutter code and the platform code.
- Decouple application behavior from any operating system dependencies. Applications look and feel the same on all versions of the operating system, even if the operating system changes the implementation of its controls.
Composition
Widgets are typically made up of many other small, single-purpose widgets that can be combined to produce powerful effects.
Where possible, the number of design concepts is kept to a minimum while allowing the total vocabulary to be large. For example, in the Widgets layer, Flutter uses the same core concepts (a Widget) to represent drawing to the screen, layout (positioning and size), user interaction, state management, themes, animation, and navigation. At the animation level, the concepts Animations and Tweens cover most of the design space. In the rendering layer, RenderObjects are used to describe layout, drawing, hit testing, and accessibility. In each case, the corresponding vocabulary ends up being large: there are hundreds of widgets and render objects, and dozens of animation and Tweens types.
The hierarchy of classes is deliberately shallow and wide to maximize the number of possible combinations, focusing on small, composable widgets that each do one thing well. The core features are abstract, and even basic features like padding and align are implemented as separate components rather than built into the core. (This is also in stark contrast to traditional apis, where features like padding are built into the common core of each layout component.) . So, for example, to Center a widget, instead of adjusting a nominal Align attribute, you can wrap it in a Center widget.
There are widgets for padding, alignment, rows, columns, and grids. These layout pieces have no visual representation of their own. Instead, their sole purpose is to control some aspect of the layout of another component. Flutter also includes utility parts that take advantage of this combination method.
For example, a Container, a commonly used widget, is made up of several widgets that are responsible for layout, painting, positioning, and size. Specifically, the Container is made up of LimitedBox, ConstrainedBox, Align, Padding, DecoratedBox, and Transform widgets, which you can see by reading its source code. One of the defining features of Flutter is that you can drill into the source of any widget and inspect it. So, you can combine it with other simple widgets in novel ways, or create a new widget directly using Container as inspiration, rather than subclassing Container to create custom effects.
Building widgets
As mentioned earlier, you determine the visual appearance of the widget by overloading the build() function to return a new tree of elements. This tree represents the part of the widget in the user interface in a more concrete way. For example, a toolbar widget might have a builder function that returns some text and a horizontal layout of various buttons. As needed, the framework recursively asks each widget to build until the tree is completely described by concrete renderable objects. The framework then stitches these renderable objects together into a tree of renderable objects.
A widget builder should have no side effects. Every time the function is asked to build, the widget should return a new Widgets tree 1, regardless of what the widget previously returned. The framework does the heavy lifting, deciding which build methods need to be called based on the render object tree (more on that later). More information about this process can be found in the Inside Flutter theme.
On each render frame, Flutter can recreate only those parts of the UI whose state has changed by calling the Widget’s build() method. Therefore, it is important that the build method should return quickly and that the recalculation work should be done in some asynchronous fashion and then stored as part of the state for use by the build method.
Although this automatic comparison approach is naive, it is quite effective and enables high-performance, interactive applications. Furthermore, the design of the build function simplifies your code by focusing on declaring what a widget is made of, rather than the complexity of updating the user interface from one state to another.
Widget state
The framework introduces two broad categories of widgets: stateful and stateless.
Many widgets do not have a mutable state: they do not have any properties that change over time (for example, an icon or a label). These widgets are subclasses of StatelessWidgets.
However, a widget is stateful if its unique features need to change based on user interaction or other factors. For example, if a widget has a counter that increments each time the user clicks a button, the value of the counter is the widget’s state. When this value changes, the widget needs to be rebuilt to update its UI portion. These widgets are subclasses of StatefulWidgets, and (because widgets themselves are immutable) they store mutable State in a separate State subclass. StatefulWidgets have no build method; Instead, their user interfaces are built through State objects.
Whenever you mutate a State object (for example, by incrementing a counter), you must call setState() to signal to the framework to update the user interface by calling State’s build method again.
Having separate state and widget objects lets other widgets treat stateless and stateful widgets exactly the same without fear of losing state. Instead of holding onto a child object to preserve its state, a parent object can create a new instance of a child object at any time without losing its persistent state. The framework does all the work of finding and reusing existing state objects when appropriate.
State management
So, if many widgets can contain state, how do you manage that state and pass it around the system?
Just like any other class, you can use constructors in a widget to initialize its data, so the build() method ensures that any child widget has the data it needs when it is instantiated.
@override
Widget build(BuildContext context) {
return ContentWidget(importantState);
}
Copy the code
However, as the widget tree grows deeper, passing state information up and down the tree hierarchy becomes cumbersome. Therefore, a third widget type, InheritedWidget, provides an easy way to grab data from shared ancestors. You can use an InheritedWidget to create a state widget that wraps a common ancestor in the widget tree, as shown in this example.
Whenever an ExamWidget or GradeWidget object needs data from StudentState, it can now access it with a command, for example
final studentState = StudentState.of(context);
Copy the code
The of(context) call takes the build context (a handle to the current widget location) and returns the closest ancestor in the tree that matches the StudentState type. The InheritedWidgets also provide an updateShouldNotify() method that a Flutter calls to determine whether a state change should trigger a rebuild of the child widgets that use it.
Flutter itself makes extensive use of inheritedWidgets as part of a shared state framework, such as the app’s visual theme, which includes properties such as colors and type styles that are common throughout the app. The MaterialApp Build () method inserts a topic into the tree when it is built, and then, in a deeper hierarchy, a widget can use the.of() method to find relevant topic data, for example.
Container(
color: Theme.of(context).secondaryHeaderColor,
child: Text(
'Text with a background color',
style: Theme.of(context).textTheme.headline6,
),
);
Copy the code
This approach is also used in Navigator, which provides page routing, and MediaQuery, which provides access to screen metrics such as orientation, size, and brightness.
As applications grow, more advanced state management approaches, which reduce the ritual of creating and using stateful widgets, become more attractive. Many Flutter applications use utility packages like providers, which provide a wrapper around the InheritedWidget. The layered architecture of Flutter also enables other methods to implement state-to-UI transitions, such as the Flutter_hooks package.
Rendering and layout
This section introduces the render pipeline, which is a series of steps that Flutter takes to transform the hierarchy of widgets into actual pixels to paint on the screen.
Flutter ‘s rendering model
You may be wondering: If Flutter is a cross-platform framework, how can it provide the same performance as a single-platform framework?
It’s useful to start thinking about how traditional Android apps work. When drawing, you first call the Java code of the Android framework. The Android system library provides a component that takes care of its own drawing and converts it to a Canvas object, which Android can then render using Skia, a graphics engine written in C/C++ that calls the CPU or GPU to do the drawing on the device.
Cross-platform frameworks typically work by creating an abstraction layer on top of the underlying native Android and iOS UI libraries, trying to smooth out inconsistencies in each platform’s presentation. App code is often written in interpreted languages such as JavaScript, which in turn must interact with java-based Android or Objective-C-based iOS libraries to display the UI. All of this adds a lot of overhead, especially where there is a lot of interaction between the UI and the application logic.
Flutter, by contrast, minimizes these abstractions, bypassing the system UI widget library and using its own set of widgets. The Dart code that draws the Flutter visual effect is compiled into native code and rendered using Skia. Flutter also embedded its own copy of Skia as part of the engine, allowing developers to upgrade their apps to keep up with the latest performance improvements, even if the phone has not been updated with the new Android version. The same goes for Flutter on other original platforms, such as iOS, Windows or macOS.
From user input to the GPU
The first principle that Flutter applies to its rendering pipeline is: simple is fast. Flutter has a straightforward pipeline for how data flows through the system, as shown in the following sequence.
Let’s look at some of the details of these stages.
Build: from Widget to Element
Consider this simple code snippet that demonstrates a simple widget hierarchy.
Container(
color: Colors.blue,
child: Row(
children: [
Image.network('https://www.example.com/1.png'),
Text('A'),
],
),
);
Copy the code
When Flutter needs to render the fragment, it calls the Build () method, which returns a subtree of widgets and renders the UI based on the current application state. In this process, the build() method can introduce new widgets as needed, depending on their state. As a simple example, in the previous code snippet, the Container has colors and child attributes. Looking at the source code of the Container, you can see that if the color is not null, it inserts a ColoredBox for the color.
if (color ! = null) current = ColoredBox(color: color, child: current);Copy the code
Correspondingly, image and text widgets may be inserted into the build process as subwidgets, such as RawImage and RichText. As a result, the resulting widget hierarchy may be deeper than the code indicates, as in example 2.
This explains why when you examine the tree through a debugging tool such as the Flutter inspector, part of Dart DevTools, you may see a deeper structure than your original code.
During the construction phase, Flutter translates the widgets expressed in the code into the corresponding tree of elements, with one element for each widget. Each element represents a concrete instance of a widget at a specific location in the tree hierarchy. There are two basic types of elements.
- ComponentElement is a host for other elements
- RenderObjectElement, an element that participates in the layout or drawing stage.
RenderObjectElements are intermediaries between their widget analogy and the underlying RenderObject, which we’ll cover later.
The element of any widget can be referenced by its BuildContext, which is a handle to the widget’s location in the tree. This is the context in a function call, such as theme.of (context), and is provided as an argument to the build() method.
Because widgets are immutable, including parent/child relationships between nodes, any change to the widget tree (such as changing Text(‘A’) to Text(‘B’) in the previous example) results in A new set of widget objects being returned. But this does not mean that the underlying representation has to be rebuilt. The element tree is persistent from frame to frame and thus plays a key performance role, allowing Flutter to cache its underlying representation just as the widget hierarchy is fully tractable. Flutter can rebuild only the parts of the element tree that need to be reconfigured by walking through only the widgets that have changed.
Layout and rendering
This will be a very rare application that draws just one widget. Therefore, an important part of any UI framework is the ability to efficiently lay out a hierarchy of widgets, determining the size and location of each element before rendering on the screen.
The base class for each node in the render tree is RenderObject, which defines an abstract model of layout and drawing. This is extremely general: it does not promise a fixed number of dimensions, or even cartesian coordinates (as demonstrated by this polar coordinate example). Each RenderObject knows its parents, but little about its children other than how to access them and their constraints. This provides the RenderObject with enough abstraction to handle a variety of use cases.
In the construction phase, a Flutter creates or updates an object inherited from the RenderObject for each RenderObjectElement in the element tree. RenderObjects are primitives. RenderParagraph renders the text, RenderImage renders the image, and RenderTransform applies a transform before drawing its child elements.
Most Flutter widgets are rendered by an object derived from a RenderBox subclass that represents a fixed size RenderObject in 2D Cartesian space. RenderBox provides the basis for a box constraint model, establishing a minimum and maximum width and height for each widget to be rendered.
To perform layout, Flutter traverses the render tree depth-first and passes size constraints from parent to child. In determining its size, the offspring must respect the constraints imposed on it by its parent. The child responds by passing dimensions up under the constraints established by the parent.
At the end of the single-pass tree, each object has a defined size within its parent constraint and is ready to be drawn by calling the paint() method.
The box constraint model is very powerful as a way to lay out objects in O(n) time.
- A parent object can determine the size of a child object by setting the maximum and minimum constraints to the same value. For example, the top render object in a mobile application constrains its children to the size of the screen. Child objects can choose how to use that space. For example, they might just put what they want to render in the center and keep it within the prescribed constraints).
- Parents can specify the width of their child but give the child a high degree of flexibility (or specify the height but provide a flexible width). An example in the real world is streaming text, which may have to fit a horizontal constraint but varies vertically depending on the number of texts.
This model works even when a child object needs to know how much free space it has to decide how to render its content. By using the LayoutBuilder widget, a child object can examine the constraints passed down and use them to decide how to use them, for example.
Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth < 600) { return OneColumnLayout(); } else { return TwoColumnLayout(); }});Copy the code
More information about constraints and layout systems, as well as working examples, can be found in the understanding Constraints topic.
The root of all RenderObjects is the RenderView, which represents the total output of the render tree. The compositeFrame() method, which is part of the RenderView object at the base of the render tree, is called when the platform requires a new frame to be rendered (for example, because of vsync or because of texture decompression/upload completion). This will create a SceneBuilder to trigger the update of the scene. When the scene is complete, the RenderView object passes the synthesized scene to the window.render () method in dart: UI, which passes control to the GPU to render it.
Further details on the synthesis and rasterization stages of the tubes are beyond the scope of this advanced article, but more information can be found in this talk on Flutter rendering tubes.
Platform embedding
As we can see, the user interface of Flutter is not translated into equivalent operating system widgets, but is built, laid out, synthesized, and drawn by Flutter itself. Depending on the platform’s unique concerns, the mechanisms for fetching textures and participating in the application lifecycle of the underlying operating system inevitably differ. The engine is platform independent and presents a stable ABI (Application binary Interface) that provides a way for platform interleavers to set up and use Flutter.
The platform embedder is the native operating system application that hosts all Flutter content and acts as the glue between the host operating system and Flutter. When you launch a Flutter application, the embed provides entry points, initializes the Flutter engine, obtains the UI and rasterized threads, and creates textures that Flutter can write to. The embedder is also responsible for the application lifecycle, including input gestures (such as mouse, keyboard, touch), window sizes, thread management, and platform messages. Flutter includes platform embeds for Android, iOS, Windows, macOS and Linux. You can also create a custom platform embed like this one that supports remote Flutter sessions via a VNC-style framebuffer, or this one for Raspberry Pi.
Each platform has its own set of apis and constraints. Some brief notes on the platform.
- On iOS and macOS, Flutter is loaded into an embed as a UIViewController or AN NSViewController, respectively. The platform embed creates a FlutterEngine, which acts as a host for the Dart virtual machine and your Flutter runtime, and a FlutterViewController, which connects to the FlutterEngine and passes UIKit or Cocoa input events to Flutter, And display FlutterEngine rendered frames using Metal or OpenGL.
- On Android, Flutter is loaded into an embed by default as an Activity. The view is controlled by FlutterView, which renders the Flutter contents as views or textures according to their composition and Z-ordering requirements.
- On Windows, Flutter is hosted in a traditional Win32 application and renders content using ANGLE, a library that converts OpenGL API calls into DirectX 11 equivalents. Efforts are under way to provide Windows inserts using the UWP application model and to replace ANGLE with a more direct path through DirectX 12.
Integrating with other code
Flutter provides a variety of interoperability mechanisms, whether you want to access code or apis written in languages like Kotlin or Swift, call native C-based apis, embed native controls in Flutter applications, or embed Flutter in existing applications.
Platform channels
For mobile and desktop applications, Flutter allows you to invoke custom code via platform channels. This is a simple mechanism for communicating between your Dart code and the platform-specific code of your host application. By creating a common channel (wrapper name and codec), you can send and receive messages between Dart and platform components written in languages like Kotlin or Swift. Data is serialized from Dart types like Map to standard formats, and then deserialized to equivalent representations in Kotlin (like HashMap) or Swift (like Dictionary).
Here is a simple platform channel example of a Dart call receiving event handler in Kotlin (Android) or Swift (iOS).
// Dart side
const channel = MethodChannel('foo');
final String greeting = await channel.invokeMethod('bar', 'world');
print(greeting);
Copy the code
// Android (Kotlin)
val channel = MethodChannel(flutterView, "foo")
channel.setMethodCallHandler { call, result ->
when (call.method) {
"bar" -> result.success("Hello, ${call.arguments}")
else -> result.notImplemented()
}
}
Copy the code
// iOS (Swift)
let channel = FlutterMethodChannel(name: "foo", binaryMessenger: flutterView)
channel.setMethodCallHandler {
(call: FlutterMethodCall, result: FlutterResult) -> Void in
switch (call.method) {
case "bar": result("Hello, \(call.arguments as! String)")
default: result(FlutterMethodNotImplemented)
}
}
Copy the code
More examples of using platform channels, including macOS examples, can be found in the Flutter/Plugins repository 3. Additionally, Flutter already has thousands of plug-ins that cover many common scenarios, from Firebase to ads to device hardware like cameras and Bluetooth.
Foreign Function Interface
For C-based apis, including those that can be generated for code written in modern languages like Rust or Go, Dart provides a straightforward mechanism to bind native code using the Dart: FFI library. The external function interface (FFI) model can be much faster than platform channels because no serialization is required to pass data. Instead, the Dart runtime provides the ability to allocate memory on the heap supported by Dart objects and make calls to statically or dynamically linked libraries. FFI works on all platforms except the Web, where the JS package is equally useful.
To use FFI, you create a typedef for each Dart and unmanaged method signature and instruct the Dart VM to map between them. As a simple example, here is a snippet that calls the traditional Win32 MessageBox()API.
typedef MessageBoxNative = Int32 Function(
IntPtr hWnd, Pointer<Utf16> lpText, Pointer<Utf16> lpCaption, Int32 uType);
typedef MessageBoxDart = int Function(
int hWnd, Pointer<Utf16> lpText, Pointer<Utf16> lpCaption, int uType);
final user32 = DynamicLibrary.open('user32.dll');
final MessageBox =
user32.lookupFunction<MessageBoxNative, MessageBoxDart>('MessageBoxW');
final result = MessageBox(
0, // No owner window
Utf16.toUtf16('Test message'), // Message
Utf16.toUtf16('Window caption'), // Window title
0 // OK button only
);
Copy the code
Rendering native controls in a Flutter app
Because the content of a Flutter is drawn on a texture, and its widget tree is entirely internal, there is no place for things like Android views in the internal model of a Flutter, nor for staggered rendering in the Flutter widgets. This is a problem for developers who want to include existing platform components, such as browser controls, in their Flutter applications.
Flutter solves this problem by introducing platform view widgets (AndroidView and UiKitView) that allow you to embed this content on each platform. Platform views can be integrated with other Flutter content 4. Each of these widgets acts as an intermediary to the underlying operating system. For example, on Android, AndroidView has three main functions.
- Make a copy of the graphics texture rendered by the native view and render it to Flutter for composition as part of the surface rendered by Flutter each time the frame is framed.
- Respond to click tests and input gestures, and translate those gestures into equivalent native input.
- Create a simulation of the accessibility tree and pass commands and responses between the native and Flutter layers.
Inevitably, there is some overhead associated with this synchronization. Therefore, in general, this approach works best for complex controls like Google Maps, and re-implementing Flutter is not practical.
Normally, the Flutter application instantiates these widgets in the Build () method based on platform tests. As an example, from the Google_maps_flutter plugin.
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'plugins.flutter.io/google_maps',
onPlatformViewCreated: onPlatformViewCreated,
gestureRecognizers: gestureRecognizers,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
return UiKitView(
viewType: 'plugins.flutter.io/google_maps',
onPlatformViewCreated: onPlatformViewCreated,
gestureRecognizers: gestureRecognizers,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
}
return Text(
'$defaultTargetPlatform is not yet supported by the maps plugin');
}
Copy the code
Communicating with native code underlying AndroidView or UiKitView usually uses the platform channel mechanism, as described earlier.
Platform view is not currently available for desktop platforms, but this is not an architectural limitation and support may be added in the future.
Hosting Flutter content in a parent app
The opposite of the previous scenario is to embed the Flutter widget in an existing Android or iOS application. As described in the previous section, the newly created Flutter application running on a mobile device is hosted in an Android activity or an iOS UIViewController. Flutter content can be embedded into existing Android or iOS applications using the same embed API.
The Flutter module template is designed for easy embedding; You can embed them as source dependencies into existing Gradle or Xcode build definitions, or compile them into Android Archive or iOS Framework binaries for use, without requiring every developer to install Flutter.
The Flutter engine takes a very short time to initialize because it needs to load the Flutter shared library, initialize the Dart runtime, create and run Dart isolation, and attach the render surface to the UI. To minimize UI latency when rendering Flutter content, it is best to initialize the Flutter engine in the overall application initialization sequence, or at least before the first Flutter screen, so that the user does not experience a sudden pause when loading the first Flutter code. In addition, separating the Flutter engine allows it to be reused across multiple Flutter screens and share the memory overhead involved in loading the necessary libraries.
More information about how Flutter loads into existing Android or iOS apps can be found in the loading order, performance, and memory topics.
Flutter web support
While general architectural concepts apply to all platforms that Flutter supports, the Web support for Flutter has some unique features worth commenting on.
Dart has been written into JavaScript for as long as the language has existed, and its toolchain is optimized for development and production purposes. Many important applications are compiled into JavaScript from Dart and run in production today, including advertiser tools for Google Ads. Because the Flutter framework is written in Dart, compiling into JavaScript is relatively simple.
However, the Flutter engine, written in C++, is designed to interface with the underlying operating system rather than a web browser. Therefore, a different approach is required. On the web, Flutter provides a re-implementation of the engine on top of the standard browser API. Currently, we have two options for rendering Flutter content on the web. HTML and WebGL. In HTML mode, Flutter uses HTML, CSS, Canvas, and SVG. To render to WebGL, Flutter uses a version of Skia compiled into WebAssembly called CanvasKit. While HTML mode provides the best code size features, CanvasKit provides the fastest path to the browser’s graphics stack and offers some higher graphics fidelity with local mobile target 5.
The architecture layer diagram for the web version is shown below.
Perhaps the most significant difference compared to other platforms on which Flutter runs is that Flutter does not need to provide a Dart runtime. Instead, the Flutter framework (and any code you write) is compiled into JavaScript. It’s worth noting that Dart has very few language semantic differences (JIT versus AOT, native versus Web compilation) across all modes, and most developers will never write a line of code that encounters such differences.
At the time of development, Flutter Web uses DartDevc, a compiler that supports incremental compilation, thus allowing hot reloads of applications (although not yet hot reloads). Instead, when you are ready to create a production application for the Web, use Dart 2JS, Dart’s highly optimized production JavaScript compiler, to package the Flutter core and framework with your application into a minimal source file that can be deployed to any Web server. The code can be provided in a single file, or it can be split into multiple files through deferred import.
Further information
For those interested in internal information about Flutter, the Inside Flutter whitepaper provides a useful guide to the design philosophy of the framework.
1 Although the constructor will return a fresh tree, you only need to return something different if there is some new configuration to add. If the configuration is the same, you can return the same widget.
This is a slight simplification to make it easier to read. In practice, this tree may be more complex.
3 Although work is in progress on Linux and Windows, examples of these platforms can be found in the Flutter desktop embedded library. As development on these platforms matures, this content will gradually migrate to the Flutter main library.
4 This approach has some limitations. For example, transparency is not synthesized in the same way as other Flutter widgets for platform views.
5 An example is shadows, which must be approximated by DOM equivalent primitives at the cost of sacrificing some fidelity.
Cultivate immortality
Since the Flutter Dojo is open source, it has been enjoyed by many Flutter learners and enthusiasts. More and more people have joined the Flutter study, so I started a Flutter community, but there were too many people. Therefore, there are three groups of “Guide to Flutter” [North] [East]. If you are interested in Flutter, you can add my wechat account and indicate that you want to join the Flutter group, or directly follow my wechat public account [Android Group English].
If you are interested, please add me to wechat [Tomcat_xu] and I will join you in the group.
Project Address:
Github.com/xuyisheng/f…
Translation via www.DeepL.com/Translator (free version)