Supercharge Your Flutter Apps with Google’s App Architecture.

Henry Ifebunandu
10 min readSep 28, 2023

--

Image AI-Generated using DALL-E

I know I know… You’re thinking, "This is another long talk about architecture to confuse me further” (hopefully not 😅).

First off, Architecture is an essential aspect of software development because it provides a structured approach to building software that meets the needs of users, businesses, and other stakeholders. It defines the system’s structure and components, which helps developers understand how different parts of the system fit together and interact. Additionally, it facilitates communication and collaboration among team members, promotes consistency and standardization, enables flexibility and adaptability, and reduces risks and costs.

Nowadays, I’m not comfortable going over my old code bases because I don’t think I can go through the hell of trying to figure out what it does because there is no structure, seeing the flaws of my old apps should mean I’m doing something right, I mean, that’s growth, right?😏

Back to the matter, architecture is essential for building high-quality software that can evolve and meet changing user and business needs.

The whole gist sums up that, not applying architecture can lead to a variety of issues that can negatively impact the software’s quality, maintainability, scalability, security, and overall success. Therefore, it’s essential to prioritize architecture in software development to ensure that the software meets the needs of users, businesses, and other stakeholders.

For this reason, the software industry has seen a variety of proposed architectures that do the job with their pros and cons. Some examples are Clean Architecture, Domain-Driven Design, and Bloc.

If you study some of these architectures, certain similar attributes begin to emerge in terms of code structuring. A few of the important attributes:

  1. Separation of concerns: The architectures are designed to separate different concerns or aspects of the application into independent and modular components. This can be achieved using design patterns, such as Model-View-Controller (MVC) or Model-View-ViewModel (MVVM).
  2. Layering: The architectures are designed with a layered structure that allows the application to be divided into logical and independent layers. This can include a presentation layer (UI), a business logic layer, and a data access layer.
  3. Decoupling: The architecture is designed to minimize dependencies between components, making it easier to maintain, update, and extend the application over time. This can be achieved through the use of interfaces, abstraction, and dependency injection.
  4. Reusability: The architectures are designed to promote code reuse, allowing standard functionality to be abstracted into reusable components or libraries.
  5. Testability: The architectures are designed in a way that makes it easy to write automated tests, allowing developers to verify the behaviour of individual components or modules in isolation.
  6. Maintainability: The architecture should be designed to make it easy to maintain and update the application over time, with clear and consistent coding standards and well-defined interfaces between components.

It’s safe to say that whichever architecture we employ in our Flutter app should possess these (or some of these) attributes.

But you already know all that, so…

Enter Modern App Architecture

Google proposes “modern app architecture ”as their recommended architecture, it encompasses their best practices for building robust and high-quality apps (Android), and it is meant to be taken as a guide and adapted to your requirements, but you can apply the ideology to your Flutter apps, I use this personally.

Aside from the separation of concerns as one of the core principles, they state some other principles that make sense that we can adopt.

Drive UI from data models

According to modern app architecture, it’s important to use data models to drive the user interface (UI). Data models represent the information that your app uses, and they are separate from the UI and other components of your app.

  • Persistent models are particularly useful because they keep your data safe in situations where the app is removed from memory or when there are problems with network connections. This means that if your app is removed from memory, the data models will still exist and your users won’t lose their data.

By designing your app architecture around data model classes, you also make it easier to test and ensure that it works reliably.

Single source of truth

To keep your app’s data organized and consistent, it’s essential to have a “Single Source of Truth” (SSOT) for each type of data. This means that only one part of your app is responsible for modifying that data, and it exposes functions or events that other parts of the app can use to interact with it.

This pattern has several benefits: it makes it easier to track changes to the data, it prevents other parts of the app from accidentally or intentionally changing the data, and it centralizes all changes to that data in one place. For example, in an app that works offline, the source of truth for the data might be a database (local), while in other cases it might be a ViewModel or the UI itself.

Unidirectional Data Flow ⇄

The Unidirectional Data Flow (UDF) pattern is often used alongside the Single Source of Truth principle to manage app data. In this pattern, data flows in one direction, while events that modify the data flow in the opposite direction.

In Android, data typically flows from higher-level parts of the app to lower-level parts, while events that trigger changes flow in the opposite direction. For example, application data might flow from data sources to the UI, while user events like button presses flow from the UI to the Single Source of Truth for that data.

So what is this app architecture we’ve been talking about?

This architecture consists of two main layers and another optional layer, which are the UI layer, Data layer and optionally Domain Layer.

Diagram 1: App Architecture.

UI Layer

The UI is the user interface. It is responsible for displaying the application data on the screen and for allowing the user to interact with the application. The UI should update to reflect changes in the application data, either due to user interaction or external input. Effectively, the UI is a visual representation of the application state as retrieved from the data layer. The UI layer houses UI elements like your widgets, pages/screens and also your State Managers/State Holders.

Diagram 2: UI Layer (Blown)

Domain Layer

The domain layer is optional but recommended if you need to encapsulate complex business logic or simple business logic reused by multiple ViewModels or State holders. It is also recommended if you want to favour reusability. This means that the domain layer can be used to avoid code duplication and to make it easier to maintain your code. The domain layer is optional because not all apps will have these requirements. If your app is simple and does not require complex business logic or code reuse, then you may not need to use the domain layer. However, if your app is complex or requires code reuse, then the domain layer can be valuable. The Domain layer houses the Use Case(s).

For those coming from the Clean architecture paradigm for mobile apps, if asked to explain what the domain side (use cases) does in your app, What would you be your response? (answers in the comments)

Diagram 3: Domain Layer (Blown)

Data Layer

This layer handles all the data storage and retrieval tasks. It includes the app’s repositories, which provide a single interface for accessing data from multiple sources, such as a database, local storage or a network API. The repository is the part that reconciles which data source (Local or Remote) is the source of truth. The data layer communicates with the domain layer to provide data, and it may also include caching and data synchronization functionality.

Diagram 4: Data Layer (Blown)

You can learn more about Modern App Architecture here.

I discovered the modern app architecture that piqued my interest, and I decided to put it to the test in my Flutter apps with a unique twist: combining the App architecture with the Feature-First approach.

Now how do they all come together? But first…

What is the Feature-First approach?

A feature-first approach to architecture is a methodology where the design of the software architecture is driven by the features or requirements of the system, This is not a new concept, it’s used in the industry a lot. In this approach, the focus is on identifying and defining the key features or capabilities that the system must have to meet the needs of the users.

What is a feature?

In the words of Andrea Bizzotto; “ a feature is a functional requirement that helps the user complete a given task” or

We can say a feature is a distinct and valuable functionality or characteristic that enhances the capabilities of a software product.

Once the key features are identified, the architecture is designed and developed to support those features. This often involves breaking down the system into smaller, more manageable components, each responsible for delivering a specific feature or set of features.

The feature-first approach is often used in agile software development methodologies, focusing on delivering working software quickly and continuously. By prioritizing features and designing the architecture to support those features, developers can deliver software that meets the needs of the users more efficiently and effectively.

Say you wanted to design a Loan App, an app that grants loans to users who request them, potential features of such an app could include:

  • Authentication
  • User Verification
  • Loans (Request & Repay)
  • And so on…

You get the point.

Each of these features would have the different layers we previously mentioned. So for example our Authentication would have a UI Layer, Domain Layer, and Data Layer (your feature might not need all layers it all depends on your use case, keep calm though, we’ll get to that in the coming parts).

The UI layer of our Authentication feature would contain all views about Authentication, for example, this is where you put your Login and Signup screens and view models(state managers or state holders). You might also decide to split it further making Login and Sign up separate features, which works too.

The Data layer of our Authentication would contain repositories and data sources specifically needed for Authentication. The optional Domain layer can contain our slightly complex use cases, an example; let’s say our UI layer needs data from multiple repository sources instead of writing complex code in your UI layer (in the state holder) or maybe manipulating your repository to accommodate for that, you could create a domain layer that could contain that specific use case.

Diagram 5: Feature — App Architecture

With that said, we should take a look at how a typical folder structure might look in our project:

> lib
> app
> feature
> auth ------> feature 1
> loan ------> feature 2
> user-verification ------> feature 3

...
> shared ------> shared across features
> data
> domain
> ui

> core -----> shared across the entire app
> di (dependency injection)
> navigation or routing
> utils
...

We can directly see our feature folder housing the features our app possesses. The shared folder contains code that can be shared across our features, it also houses our various layers (Data, Domain, UI) but only if necessary because it is not always the case.

Now, take a look at our feature folder:

> feature
> auth ------> feature 1
> data
> data-source
> local -----> local data source
> remote -----> remote data source (live data)
> repository
> model
> domain
- example_usecase.dart
> ui
> provider or bloc (state managers)
> view (ui elements)
> loan ------> feature 2
> user-verification ------> feature 3
...

As I previously stated, in each of our features we have the Data, Domain, UI. You can clearly see our Data layer housing data source, which houses local and remote data sources and a repository folder which reconciles how we use data from either our local or remote data sources and handles calls to our data sources. We can also see the domain side which houses our use cases and our UI layer which houses the state managers (provider, bloc or whatever state manager you use) and UI elements (widgets, screens).

I should emphasize that this folder structure should merely be taken as a guideline and not a must-have, you should use it only if you need it. Just as not every feature needs all layers, folders should only contain what you need. For example: Not all data sources must have both a local and remote data source.

What's Next?

Again, if you want to learn more about the architecture, you can read it here.

Please clap and share to support me and follow for more. Comment to share your opinions, I really want to hear them. Cheers!

--

--