In this post, you’ll learn more about Layering the Flutter Application Architecture.
Structuring code is crucial for building large-scale applications that are maintainable and extendable. Poorly structured code can make it difficult to add new features or make changes to existing functionality, leading to bugs and technical debt that can accumulate over time.
What is app architecture?
The architecture of an application determines how its components interact with each other, how data is stored and retrieved, and how the application responds to user input. A well-structured architecture should be modular, with clear separation of concerns between different parts of the codebase. This allows developers to work on different parts of the codebase independently, without fear of breaking other parts of the application.
There are several popular architectural patterns that can be used to structure code, such as Model-View-Controller (MVC), Model-View-ViewModel (MVVM), and Clean Architecture. Each pattern has its own strengths and weaknesses, and the choice of pattern will depend on the specific needs of the application.
Some common components of an application architecture include:
- User interface (UI). The interface that users interact with to use the application.
- Data storage. The technology used to store data, such as a database or file system.
- Backend. The server-side components that handle business logic and data processing.
- Networking. The protocols and technologies used to communicate between the frontend and backend of the application.
- Security. The mechanisms used to protect the application from unauthorized access and attacks.
A few words about layers
Layers refer to different levels of abstraction or responsibility in a system. Each layer has a specific function and interacts with the layers above and below it to provide a cohesive and functional system.
The layers typically include:
- Presentation Layer: This layer is responsible for presenting the data to the user in a way that is easy to understand and interact with. It includes user interface components such as screens, forms, and dialogs.
- Business Layer: This layer contains the application logic and business rules that process and manipulate data. It handles data validation, workflows, and business logic rules that govern how data is processed.
- Data Access Layer: This layer is responsible for accessing data from a database or other data storage mechanism. It includes data access components such as data mappers, repositories, and data contexts.
- Infrastructure Layer: This layer contains the technical infrastructure components that support the application. It includes components such as logging, configuration, and security.
By separating an application into layers, each layer can be developed and tested independently, making the application more maintainable and scalable. It also allows for more flexibility in the development process, as changes can be made to one layer without affecting the others.
The layered architecture pattern is just one example of how layers can be used in software development. Other examples include the OSI model for networking and the protocol stack used in communications.
The Single Responsibility Principle
The Single Responsibility Principle (SRP) is a software development principle when each class or module should have only one responsibility or job to perform.
The SRP is one of the SOLID principles;it helps to ensure that code is easy to understand, modify, and test.
When a class or module has only one responsibility, it is easier to understand its purpose and to modify it without affecting other parts of the codebase. This also helps to reduce the likelihood of introducing bugs or errors when making changes.
For example, a class that handles user authentication should not also be responsible for managing user profiles. Instead, these responsibilities should be separated into two separate classes, each with its own responsibility.
Here are some examples of responsibilities that software components may have:
- User interface (UI) components are responsible for displaying information to the user and capturing user input.
- Business logic components are responsible for implementing the business rules and processes that govern the behavior of an application.
- Data access components are responsible for accessing and manipulating data in a database or other data storage mechanism.
- Networking components are responsible for managing communication between different parts of an application or between different applications.
- Security components are responsible for ensuring that an application is secure and protected from unauthorized access or attacks.
Architectural patterns
Some common architectural choices include:
- Monolithic architecture involves building a single, self-contained application that handles all aspects of the system’s functionality. This can be easier to develop and deploy, but may become unwieldy as the system grows larger and more complex.
- Microservices architecture implies breaking the system down into small, independent services that communicate with each other over a network. This can make the system more scalable and easier to maintain, but may introduce additional complexity in terms of service discovery, communication protocols, and deployment.
- Event-driven architecture is about building the system around a set of events or triggers, such as user input or external messages. This can make the system more responsive and adaptable to changing conditions, but may require additional overhead in terms of event management and processing.
- Service-oriented architecture (SOA) is for building the system around a set of loosely-coupled, reusable services that can be easily combined and composed to create more complex functionality. This can make the system more flexible and adaptable, but may require additional effort in terms of service design and coordination.
When making architectural choices, consider a variety of factors, including the requirements of the system, the available technology stack, the skills and experience of the development team, and the expected future growth and evolution of the system.
Define your layers
You can create different layers in your application to perform the three main duties. Layers need to be defined carefully so that they guarantee convenient operation and without compromising work.
Good architecture: what is it?
At TBR Group, we follow an architecture pattern that has four layers.
- Data Layer. This layer is responsible for interacting with the API.
- Domain Layer. This is the one responsible for transforming the data coming from the data layer.
- Business logic layer manages the state (usually using flutter_bloc).
- Finally, the presentation layer. It renders UI components based on the state.
Each layer contains multiple neurons, or nodes, which perform mathematical operations on the input data and pass the output to the next layer. The first layer, the input layer, receives the raw input data and passes it to the first hidden layer. The output layer produces the final output of the network, which could be a prediction or classification based on the input.
The layer responsible for domain-specific data modeling is known as the domain layer, which transforms unprocessed data into models specific to the domain that are then used by the business logic layer. The business logic layer maintains an unalterable state of the domain models supplied by the repository. Furthermore, this layer responds to user inputs from the UI and interacts with the repository when modifications are required based on the current state.
Say NO to mixed responsibilities
Very often, mixed responsibilities cause chaos in various projects. Without a clear division of duties and priorities, it is very easy to get confused and make a lot of mistakes that are then difficult to find and correct.
import 'package:firebase_auth/firebase_auth.dart';
class StartPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.userChanges(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return const ProfilePage();
}
return const SignInPage();
},
);
}
}
Be consistent (always)
When working according to a specific template for projects, we advise you not to use labels with layers. Each layer should have a chain of relationships that will prevent interaction between layers that are not directly related. For example, the presentation layer should not call or interact in any way directly with the data-tier APIs.
Establish a consistent naming convention and adhere to it throughout the development process. In the case of a project with a data layer that is a dependency of the domain layer, it would be beneficial to name the data layer accordingly, such as “data” or “repository”.
├── lib
| ├── posts
│ │ ├── bloc
│ │ │ └── post_bloc.dart
| | | └── post_event.dart
| | | └── post_state.dart
| | └── models
| | | └── models.dart
| | | └── post.dart
│ │ └── view
│ │ | ├── posts_page.dart
│ │ | └── posts_list.dart
| | | └── view.dart
| | └── widgets
| | | └── bottom_loader.dart
| | | └── post_list_item.dart
| | | └── widgets.dart
│ │ ├── posts.dart
│ ├── app.dart
│ ├── simple_bloc_observer.dart
│ └── main.dart
├── pubspec.lock
├── pubspec.yaml
Reusability in mind
The abstract declaration and concrete implementation approach used in the flutter_todos example from the bloc library is a good way to implement this type of solution. The abstract declaration provides a blueprint for the functionality needed, while the concrete implementation provides the specific details for how it is implemented. This allows for flexibility in adapting the functionality to different use cases while maintaining a consistent API interface.
Balance abstraction
Choosing between creating a repository with a direct dependency or creating an API wrapper for an external package will depend on various factors, including the complexity of the external package’s API, the number of dependencies it requires, and how well it meets the specific needs of the project.
If the external package has a clean and simple API that meets the needs of the project without requiring too many dependencies or configuration, then creating a repository with the package as a direct dependency may be the best approach. This can simplify the codebase and make it easier to maintain, as well as potentially provide better performance.
The chat_location application provides a good example of how to use this package with a repository, demonstrating how the package can be integrated into the app’s architecture while keeping the repository code clean and maintainable.
In cases where an external package’s API is more complex, creating an API package with a class that wraps that dependency can be a better approach. This can provide a more streamlined interface for the specific needs of the project and abstract away some of the implementation details.
The spacex_demo application is a good example of how to create a custom API package, providing a wrapper for the SpaceX API. This approach allows for a more tailored interface for the project’s specific needs, while also providing flexibility and isolation from any potential issues with the external API.
Don’t over-engineer
Over-abstracting can increase complexity and development time without providing any real benefit.
If the project primarily uses JSON as its data format, then abstracting the base properties of the model to accommodate other formats may not be necessary. In this case, it may be more efficient to simply create the serialization/deserialization functionality for JSON and avoid over-abstracting the model.
However, if there is a possibility of the data format changing in the future, or if there are other reasons for needing a more abstracted model, then it may be worthwhile to invest the time and effort into creating a more flexible model architecture.
Packages: yes or no?
The decision to use packages should be based on the specific needs and constraints of the project. For a small project with a small team, the added complexity of using packages may not be worth the benefits. In this case, it may be more efficient to handle migrations and updates manually.
However, as the project grows and the team expands, the benefits of using packages become more apparent. Component isolation becomes more important as the project becomes more complex, and having different team members working on different parts of the project simultaneously becomes more common.
You can have one teammate migrating the accounting repository to Very Good Analysis 2.4.0 and another migrating the retail API to dio 4.0.0 at the same time, so you can tackle fixing lints and null safety refactors without stepping on each other’s toes.
Choose right tools
When choosing packages or tools, it’s important to have a set of criteria to evaluate them against:
- Activity on the GitHub repository: a package that is actively being developed and maintained is more likely to be reliable and up-to-date.
- pub.dev metrics: pub.dev provides useful information such as the number of downloads, the number of dependent packages, and the percentage of code coverage. This information can help you evaluate a package’s popularity and usefulness.
- Test coverage: packages with high test coverage are more likely to be reliable and less prone to bugs.
- Who’s maintaining it: knowing who is responsible for maintaining a package can give you an idea of its reliability and longevity.
- Other factors that are important to you and your team: this could include factors such as documentation, community support, or compatibility with other tools or packages you’re already using.
Follow best practices
Studying other developers’ repositories is an excellent way to learn and improve. Many open-source projects have an active community of contributors who can help you with any questions or issues you may encounter.
Refactor
Refactoring is a natural part of the development process, and it’s important to recognize when something needs to be improved and not be afraid to make changes. Additionally, it’s important to maintain a positive attitude towards mistakes and failures, as they provide valuable learning experiences that can lead to growth and improvement. As the saying goes, “fail fast, fail often, fail forward.”
Welcome to our team!
If you are reading this article, then you are interested in the topic of structuring code. If you are ambitious, not afraid of difficulties, you have a lot of ideas, but a little lack of confidence and experience in your business – come to our team!