A complete guide to architect your Flutter application

Gaganpreet Singh
CodeX
Published in
6 min readApr 1, 2021

Introduction

Back in 2018, when I started learning Flutter, it took a lot of attention as I found it very useful and efficient to do cross-platform development. Coming from the Android app development background, initially, it took a while to understand the Flutter framework and how it manages the application states. But later on, I see a lot of potential in Flutter.

While working with Flutter, I have used a lot of state management approaches but since there is no official suggested approach, it becomes very hard to say which one is the best.

As mentioned earlier, coming from Android background, I found the Stacked plugin very easy to use to architect the Flutter application. In this article, I will show how one can design and architect Flutter applications using the Stacked framework.

Objective

In this article, we will see how to:

  • Setup base architecture of Flutter app using the Stacked plugin
  • Use dependency injection for layers separation
  • Code generator to generate boilerplate code for DI, routes, and JSON parsing
  • Make API calls using a Retrofit plugin (mostly Android developers might be familiar with it)

Apart from the basic architecture setup, this article also demonstrates

  • Project structure
  • Navigation using ViewModel (without context)
  • Easy data sharing between the screens
  • And a few more…

What is Stacked?

Stacked is a state management system for Flutter applications made developed by the FilledStacks community. It helps in managing the state and is an MVVM-style architecture. You can download this plugin from here and add it in your pubspec.yaml file like:

dependencies:
stacked: ^2.0.0

How does it work?

The architecture is very simple. It consists of 3 major pieces, everything else is up to your implementation style.

These pieces are:

  • View: Shows the UI to the user. Single widgets also qualify as views (for consistency in terminology) a view, in this case, is not a “Page” it’s just a UI representation.
  • ViewModel: Manages the state of the View, business logic, and any other logic as required from user interaction. It does this by making use of the services
  • Services: A wrapper of a single functionality/feature set. This is commonly used to wrap things like showing a dialog, wrapping database functionality, integrating an API, etc.

Like MVVM, each ViewModel belongs to a View. So either you can create a separate ViewModel or each View or you can re-use a ViewModel based on your requirement. Services, on the other side, are declared globally and can be accessed by any ViewModel in your application.

While designing applications, always keep these principles in mind:

  • Each layer component should know only about the component below it. This means, Views have information of ViewModels but ViewModel should have no information on Views.
  • Views should never MAKE USE of services directly. Services should be only accessible through ViewModel or other business layers
  • Views should contain zero to (preferred) no logic. If the logic is from UI-only items then we do the least amount of required logic and pass the rest to the ViewModel
  • Views should ONLY render the state in its ViewModel
  • ViewModels for widgets that represent page views are bound to a single View only
  • ViewModels may be re-used if the UI requires the same functionality
  • ViewModels should not know about other ViewModels

Separation of layers using DI (Dependency Injection)

The most important principle to follow is the separation of concerns. It’s a common mistake to write all your code in View or ViewModel. These UI-based classes should only contain logic that handles UI only.

Why DI?

As your app grows, at some point you will need to put your app’s logic in classes that are separated from your Widgets. Keeping your widgets from having direct dependencies makes your code better organized and easier to test and maintain.

For DI, we have used the get_it plugin which you can add it in your pubspec.yaml file like:

dependencies:
get_it: ^6.0.0

Code generator

Flutter community is so awesome that they have made a plugin (one of my favorite plugins :)) for code generation which reduces the effort to write the boilerplate code. The build_runner package provides a concrete way of generating files using Dart code. The files are always generated directly on disk, and rebuilds are incremental.

All the auto-generated files have these extensions:

  • .g.dart
  • .gr.dart
  • .config.dart

For code generation, you need to do few things:

  • Define the file name for the auto-generated code. It should have the original file name with a .g.dart extension
  • Define what code needs to be generated. In case of functions, it should have a $ prefix
  • Use the build_runner tool to generate code

Example: Let’s take an example of TodoEntity class. You need to write the part file name (that will be generated automatically) below imports:

part 'todo_entity.g.dart';

To generate code using the build_runner tool, run the below command on the terminal (path should be pointed to your project)

flutter pub run build_runner build

Note: You need to run the build_runner command for code generation every time when you make changes in the original file.

Retrofit for network calls

Initially, when I was dealing with http package for the network calls, I really missed the fun and easiness to make API calls that I used to do in Android using the Retrofit library. Luckily, there is Retrofit for Dart plugin (again thanks to the awesome Flutter community) available for the API calls. This is very similar to the Retrofit library available for Android. For the sample code, I used retrofit in the service layer to make API calls. Below is the sample code of TodoApiService class:

@RestApi()
abstract class TodoApiService {

factory TodoApiService(Dio dio, {String baseUrl}) = _TodoApiService;

@GET("todos")
Future<List<TodoEntity>> getTodos();

@GET("todos/{id}")
Future<TodoEntity> getTodoDetail(@Path("id") int todoId);

}

Like Retrofit for Android, the code-generation for this class is taken care by the Retrofit for Dart library. Pretty easy, right!

Project structure

To keep things simple and easy-to-understand, I have been using below project structure for my production applications:

Navigation using ViewModel (without Context)

One of the biggest concerns I had in Flutter is that we need to pass context reference to Navigator for navigation purposes. So, in code, I had to come back to View from ViewModel only to navigate to a different screen which I felt was not a good option. I was looking for something that I can use for navigation through ViewModel and found auto_route plugin. With the help of ExtendedNavigator, you can set all the routes of your application, and using the root context we can navigate to any desired screen. To simplify things, I created a NavigationService class to handle routing in my application. Below is the code I used in my ViewModel to navigate to TodoDetailScreen:

_navigationService.push(Routes.todoDetailScreen);

Final thoughts

Using the MVVM architecture pattern provided by the Stacked framework, things become very easy to write and understand. With DI, each and every layer can be re-used and tested easily. The project structure for small applications looks clean, however, for the big enterprise application, this might not be the optimal solution. In the next article, I will show how we can use uncle Bob’s Clean Architecture for architecting Flutter application.

--

--

Gaganpreet Singh
CodeX
Writer for

Tech enthusiast, Consultant, Android and Flutter developer