Frameworks are the cornerstone of modern application development, and among them, Flutter stands out for both mobile and desktop apps. Understanding the Flutter application layer architecture fosters a smooth development process, thereby giving Flutter developers an edge.
In this guide, we'll scrutinize the architecture of typical Flutter applications, enabling you to write efficient custom code for your Flutter app. Let's venture on this journey to a better comprehension of this robust cross-platform framework.
Software architecture underscores the blueprint of your app's system, depicting the structure of the system's components, their properties, and how they interact. In simple words, the architecture mirrors the "skeleton" of any software system, paving the way for better app scalability, performance, and maintainability.
Speaking of app architecture, Flutter shines due to its unique architectural design. The crux of this Flutter app architecture is its layered approach, twigged from the underlying operating system to the user interface of the Flutter app.
Now, let's take a closer look at the Flutter application layer architecture and how this special setup plays a major role in the successful execution of both mobile and desktop apps.
1void main() { 2 runApp(MyApp()); 3} 4 5class MyApp extends StatelessWidget{ 6 @override 7 Widget build(BuildContext context){ 8 return MaterialApp( 9 title: 'Flutter App', 10 home: Scaffold( 11 appBar: AppBar( 12 title: Text('Flutter Application Layer Architecture'), 13 ), 14 body: Center( 15 child: Text('Hello World'), 16 ), 17 ), 18 ); 19 } 20}
Above is a basic example of a Flutter app with a simple UI structure. It acts as a glorious testament to the rich architecture of Flutter.
Flutter app architecture is a masterstroke incorporating Dart, a statically-typed language innovated by Google. You can utilize Dart to fashion high-quality, mission-critical Flutter apps for iOS, Android, and the web. Dart is well-suited for Flutter due to its ease of learning, scalability, and excellent support for asynchronous operations.
Coming to the structure of Flutter, it takes on a layered architecture. From the underlying operating system to the high-level Flutter apps, these layers play a vital role. The two major parts of Flutter are the Flutter engine, written predominantly in C++, and the Flutter framework, which provides a collection of reusable UI elements called widgets.
Each Flutter application starts with a runApp() function, which inflates the passed widget and attaches it to the screen creating a root widget. This approach of having a single root widget and building the entire UI as a tree of widgets is fundamental to Flutter and sets it apart from other cross-platform frameworks.
1void main() { 2 runApp(MyApp()); 3} 4 5class MyApp extends StatelessWidget { 6 @override 7 Widget build(BuildContext context) { 8 return MaterialApp( 9 title: 'Flutter App', 10 home: Home(), 11 ); 12 } 13} 14 15class Home extends StatelessWidget{ 16 @override 17 Widget build(BuildContext context){ 18 return Scaffold( 19 appBar: AppBar( 20 title: Text('Home Page'), 21 ), 22 body: Center( 23 child: Text('Welcome to the application'), 24 ), 25 ); 26 } 27}
This example illustrates how you can construct your own widget, Home, to represent the home page of the application. With these powerful building blocks at your disposal, you can develop complex Flutter applications with diverse functions and features.
Flutter's core principle is "Everything is a Widget", and this principle is rooted deeply in Flutter's Application Layer Architecture.
In a Flutter app, we build the UI by composing multiple widget trees, enfolded within the root widget. These widgets are categorized into two types: stateless and stateful. Stateless widgets are immutable, i.e., once you instantiate them, you can't change their properties. On the other hand, stateful widgets can change dynamically – these widgets can 'mutate' over time, making them essential for a UI that changes in response to events.
For instance, consider a login button that changes its text post-click. Since the button text changes based on user interaction, it'd be categorized as a Stateful widget.
1class LoginButton extends StatefulWidget { 2 @override 3 _LoginButtonState createState() => _LoginButtonState(); 4} 5 6class _LoginButtonState extends State<LoginButton> { 7 bool _isLoggedIn = false; 8 9 @override 10 Widget build(BuildContext context) { 11 return RaisedButton( 12 onPressed: () { 13 setState(() { 14 _isLoggedIn = !_isLoggedIn; 15 }); 16 }, 17 child: Text(_isLoggedIn ? 'Logout' : 'Login'), 18 ); 19 } 20}
The diagram below serves as a visual guide to the widget hierarchy in a Flutter app:
1- MyApp 2 - MaterialApp 3 - Scaffold 4 - AppBar 5 - Text 6 - Center 7 - Text
A profound understanding of Flutter's architecture helps developers better structure their Flutter app, making it easier to test, maintain, and scale. With the Flutter architecture, you can evolve your app architecture, business logic, and data layer in an efficient and manageable way.
The choice of state management strategies also plays an essential role in shaping your Flutter app architecture. From Provider and Riverpod to Bloc and Redux, you can choose the one that fits your app requirements and your development team's familiarity.
At the core of every Flutter app are Widgets. Widgets exist in a hierarchical order within Flutter applications, and they constitute the primary building blocks of the user interface. Each widget nests within its parent and can receive a constant flow of immutable configurations from the parent widget.
To create an interactive user interface, Flutter relies on the concept of "declarative UIs". This revolves around expressing how the UI should look based on given states rather than dictating a series of procedural steps to create and manage UIs. Thus, you describe the UI in terms of stateful widgets and stateless widgets under different conditions (or states), and the framework dynamically rebuilds the widget tree when there are state changes.
In our login button case, we used a stateful widget:
1class LoginButton extends StatefulWidget { 2 @override 3 _LoginButtonState createState() => _LoginButtonState(); 4} 5 6class _LoginButtonState extends State<LoginButton> { 7 bool _isLoggedIn = false; 8 9 @override 10 Widget build(BuildContext context) { 11 return RaisedButton( 12 onPressed: () { 13 setState(() { 14 _isLoggedIn = !_isLoggedIn; 15 }); 16 }, 17 child: Text(_isLoggedIn ? 'Logout' : 'Login'), 18 ); 19 } 20}
In this piece of Flutter code, the LoginButton widget extends the StatefulWidget class, which makes it stateful. Its mutable state, LoginButtonState, inherits from the State class and is associated with LoginButton for the whole lifetime of the LoginButton widget. Whenever there is a state change, i.e., the isLoggedIn variable toggles between true and false, the build method is triggered, leading to the UI's updating.
Understanding the mechanics of Flutter's rendering process provides greater clarity when creating custom UIs.
In the Flutter application layer architecture, rendering involves painting the widget hierarchy onto the rendering surfaces of the screen in response to various triggers including user events or system updates.
When the state of a widget changes, for instance, a setState function call on a stateful widget, the framework marks that widget as dirty. Marking the widget as dirty tells the Framework that the widget needs to be re-rendered during the next frame.
During the next frame, which runs at up to 60 times per second, Flutter's rendering engine walks the widget tree and render object tree, calling the build method for each dirty widget to generate a new widget tree. These render objects form part of the render tree and are taken from Flutter's lower level rendering layer.
Each render object knows its parent, its size and position, and a few other things, and can also reference its corresponding widget. Every frame, Flutter walks the render tree, calling the layout and paint methods on each render object to display them on the screen.
Here's a trivial example of a "Counter" app that demonstrates state and re-rendering:
1class Counter extends StatefulWidget { 2 @override 3 _CounterState createState() => _CounterState(); 4} 5 6class _CounterState extends State<Counter> { 7 int _counter = 0; 8 9 void _incrementCounter() { 10 setState(() { 11 _counter++; 12 }); 13 } 14 15 @override 16 Widget build(BuildContext context) { 17 return Scaffold( 18 appBar: AppBar( 19 title: Text('Counter'), 20 ), 21 body: Center( 22 child: Column( 23 mainAxisAlignment: MainAxisAlignment.center, 24 children: <Widget>[ 25 Text( 26 'You have pushed the button this many times:', 27 ), 28 Text( 29 '$_counter', 30 style: Theme.of(context).textTheme.headline4, 31 ), 32 ], 33 ), 34 ), 35 floatingActionButton: FloatingActionButton( 36 onPressed: _incrementCounter, 37 tooltip: 'Increment', 38 child: Icon(Icons.add), 39 ), 40 ); 41 } 42}
This creates a "Counter" app, where the counter text is updated each time the FloatingActionButton is pressed.
In Flutter, state represents data that can change over time and can influence the app's behavior or UI. State management refers to the technique for tracking and updating this mutable state.
Even though Flutter does not enforce any specific pattern for state management, it does provide some lower-level utilities like setState that you can use to update state and trigger rerender of your widgets.
However, as your Flutter apps grow in complexity, you may realize that setState and hierarchy-based communication using Widget, BuildContext, and InheritedWidget are insufficient and error-prone.
This is when you may look towards more sophisticated state management strategies. For instance, libraries such as Provider, Riverpod, Bloc, MobX, and Redux can offer fine-grained and robust state management. Choosing the right state management strategy will depend on your app's complexity, your team's familiarity with those libraries, and even your personal preference.
Below is a counter application utilizing Provider for state management:
1class Counter with ChangeNotifier { 2 int _value = 0; 3 4 int get value => _value; 5 6 void increment() { 7 _value++; 8 notifyListeners(); 9 } 10} 11 12class MyApp extends StatelessWidget { 13 @override 14 Widget build(BuildContext context) { 15 return ChangeNotifierProvider( 16 create: (context) => Counter(), 17 child: MaterialApp( 18 home: Scaffold( 19 appBar: AppBar(title: Text('Provider Demo')), 20 body: Center(child: MyDisplayWidget()), 21 floatingActionButton: MyIncrementButton(), 22 ), 23 ), 24 ); 25 } 26} 27 28class MyDisplayWidget extends StatelessWidget { 29 @override 30 Widget build(BuildContext context) { 31 final counter = Provider.of<Counter>(context); 32 return Text('Button pressed ${counter.value} times'); 33 } 34} 35 36class MyIncrementButton extends StatelessWidget { 37 @override 38 Widget build(BuildContext context) { 39 final counter = Provider.of<Counter>(context, listen: false); 40 return FloatingActionButton( 41 onPressed: counter.increment, 42 tooltip: 'Increment', 43 child: Icon(Icons.add), 44 ); 45 } 46}
In this code, we use ChangeNotifier, a type of Listenable, to have Counter inform the framework when changes occur. Then, we use ChangeNotifierProvider to make Counter available to MyDisplayWidget and MyIncrementButton.
Handling screens or pages in Flutter apps is essential and integral in Flutter application layer architecture. This is where navigation and routing feature prominently. Users expect modern apps to adhere to their platform's navigation guidelines seamlessly, and luckily, Flutter's navigation and routing mechanism allows us to achieve this.
A route in a Flutter app is just a widget that we display because of a certain action performed by the user. When we move from one route or screen to another, we're performing a transition.
In Flutter, routes are created from Route or PageRoute objects via the Navigator widget. When we ask Navigator to push a route, that route goes to the top of the stack and is displayed. When we pop a route, it's removed from the stack and the previous route is shown.
Here is an example of basic routing:
1void main() { 2 runApp(MaterialApp( 3 title: 'Navigation and Routing in Flutter', 4 home: FirstRoute(), 5 )); 6} 7 8class FirstRoute extends StatelessWidget { 9 @override 10 Widget build(BuildContext context) { 11 return Scaffold( 12 appBar: AppBar( 13 title: Text('First Route'), 14 ), 15 body: Center( 16 child: ElevatedButton( 17 child: Text('Open route'), 18 onPressed: () { 19 Navigator.push( 20 context, 21 MaterialPageRoute(builder: (context) => SecondRoute()), 22 ); 23 }, 24 ), 25 ), 26 ); 27 } 28} 29 30class SecondRoute extends StatelessWidget { 31 @override 32 Widget build(BuildContext context) { 33 return Scaffold( 34 appBar: AppBar( 35 title: Text("Second Route"), 36 ), 37 body: Center( 38 child: ElevatedButton( 39 onPressed: () { 40 Navigator.pop(context); 41 }, 42 child: Text('Go back!'), 43 ), 44 ), 45 ); 46 } 47}
In the above Flutter code, Navigator.push is used to navigate from the FirstRoute to the SecondRoute and Navigator.pop is used to navigate back to the FirstRoute.
Testing is an integral part of the development process to ensure the quality and correctness of the code. Since most components in a Flutter app are widgets, and Flutter has a fully equipped testing framework, it becomes easier to write widget tests to verify that the UI appears and operates as expected.
Flutter supports three types of tests: unit tests, widget tests, and integration tests. Each test type checks different aspects of an app.
However, Flutter also ensures easy debugging, providing tools for visual debugging, including the Flutter inspector. The Flutter inspector uses a technology called Widget Inspector, which examines the widget hierarchy visual trees, verifying the properties of each Flutter widget during runtime.
Here is an example of a basic unit test for our Increment Counter:
1void main() { 2 test('Counter increments smoke test', () { 3 final counter = Counter(); 4 5 counter.increment(); 6 7 expect(counter.value, 1); 8 }); 9}
In the Flutter unit test example above, a Counter instance is created, the increment method is called, and the new value is then confirmed to be 1.
The debugging and testing capabilities of Flutter's architecture simplify the app development process, bringing us closer to developing intuitive and quality applications.
In real scenarios, we tend to build applications that are complex, having more than one functionality. They have multiple pages and might be interacting with an API or database, which drives us to use an optimal state management strategy. Let's consider an e-commerce app, where users can view product listings, add products to the cart, and place orders.
In creating such an application, our approach would majorly focus on widgets and how we can organize them functionally across several pages (or routes). An e-commerce application might have the following structure:
Considering Flutter's ability to create custom widgets, Dart's async features for APIS or database interactions, and a chosen state management solution, you can build the application as per your specifications.
1void main() { 2 runApp(ECommerceApp()); 3} 4 5class ECommerceApp extends StatelessWidget { 6 @override 7 Widget build(BuildContext context) { 8 return MaterialApp( 9 title: 'E-Commerce Flutter App', 10 home: ProductList()); 11 } 12} 13 14class Product { 15 //... Product fields go here 16} 17 18class ProductList extends StatelessWidget { 19 @override 20 Widget build(BuildContext context) { 21 // List of Products as Widgets 22 } 23} 24 25class ProductDetail extends StatelessWidget { 26 final Product product; 27 28 ProductDetail({Key key, @required this.product}) : super(key: key); 29 30 @override 31 Widget build(BuildContext context) { 32 // Display Product Details 33 } 34} 35 36class Cart extends StatelessWidget { 37 @override 38 Widget build(BuildContext context) { 39 // Display Cart 40 } 41} 42 43class OrderConfirmationPage extends StatelessWidget { 44 @override 45 Widget build(BuildContext context) { 46 // Display Order Confirmation 47 } 48}
This is a simple example, just to provide an idea of how we can structure our app. In reality, an e-commerce app architecture with Flutter would be more comprehensive, involving usage of state management strategies, network requests, error handling, saving preferences and much more.
We have progressively unwrapped the various components and concepts of the Flutter Application Layer Architecture, providing insights into how we can navigate the Flutter framework to design effective mobile and desktop apps.
Today, we dissected the core abstraction at the heart of every Flutter app — widgets. We deliberated how to classify widgets into stateless and stateful categories based on their behavior and, in effect, construct the user interface. We also pondered over the Widget Tree and RenderObjectTree, the two crucial elements to understanding the Flutter rendering process.
Furthermore, we delved into the domain of state management, which is critical in orchestrating the user impact on widget states and successfully mirroring responses in the UI. Equipped with this knowledge, we can now capably design apps with dynamic, engaging user interfaces that offer a seamless user experience.
Lastly, we detailed the navigation and routing segment of Flutter apps, glorifying how screens flow based on user actions. We concluded with a real-world case study, where we developed a scheme for building an e-commerce app flexing various components of the Flutter application layer architecture that we explored throughout the blog.
Indeed, understanding this architecture arms us with the competence to construct complicated Flutter apps that run smoothly and resonate with our audiences on both mobile and desktop platforms.
Now, developers reading this blog, you'll naturally have developed a firmer footing into Flutter app architecture, unlocking the opportunity to build robust, scalable, and maintainable Flutter applications. It's time to let your creativity run wild and take full advantage of Flutter's Application Layer Architecture!
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.