Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial pages for Architecture guidance #11300

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0a00b84
initial
ericwindmill Oct 22, 2024
0b99acd
add guide page
ericwindmill Oct 22, 2024
6949530
fix table
ericwindmill Oct 22, 2024
5f201ca
cleanup
ericwindmill Oct 22, 2024
2b7fe00
Merge branch 'main' of https://github.com/flutter/website into ew-app…
ericwindmill Oct 22, 2024
2ac9295
fix table
ericwindmill Oct 23, 2024
24fe609
Merge branch 'main' of https://github.com/flutter/website into ew-app…
ericwindmill Oct 23, 2024
406f4f1
initial
ericwindmill Oct 23, 2024
d82d5f8
replace quotes and apostrophes with monospace
ericwindmill Oct 23, 2024
fb96050
checkin
ericwindmill Oct 23, 2024
9b5ca69
Merge branch 'ew-app-architecture-recs' into ew-app-architecture-first
ericwindmill Oct 24, 2024
77df4d3
Merge branch 'main' of https://github.com/flutter/website into ew-app…
ericwindmill Oct 24, 2024
6984dcc
Update _pill.scss comments
ericwindmill Oct 24, 2024
6727bf2
Fix typos
ericwindmill Oct 24, 2024
9b95804
fix formatting of arch recs yml file
ericwindmill Oct 24, 2024
2d7a35b
fix html formatting
ericwindmill Oct 24, 2024
5bfa848
adds resources to recommendations page
ericwindmill Oct 24, 2024
af154f3
fix link
ericwindmill Oct 24, 2024
cccfc72
add links to intro page
ericwindmill Oct 24, 2024
e00f9a1
Merge branch 'main' into ew-app-architecture-first
ericwindmill Oct 25, 2024
69fe237
Merge branch 'main' of https://github.com/flutter/website into ew-app…
ericwindmill Nov 5, 2024
82c10ee
fix hero-image
ericwindmill Nov 5, 2024
c20f173
address PR feedback
ericwindmill Nov 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions src/_data/architecture_recommendations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
- category: Separation of concerns
description: |
You should separate your app into a UI layer and a data layer. Within those layers, you should further separate logic into classes by responsibility.
recommendations:
- recommendation: Use clearly defined data and UI layers.
description: |
Separation of concerns is the most important architectural principle.
The data layer exposes application data to the rest of the app, and contains most of the business logic in your application.
The UI layer displays application data and listens for user events from users. The UI layer contains separate classes for UI logic and widgets.
confidence: strong

- recommendation: Use the repository pattern in the data layer.
description: |
The repository pattern is a software design pattern that isolates the data access logic from the rest of the application.
It creates an abstraction layer between the application's business logic and the underlying data storage mechanisms (databases, APIs, file systems, etc.).
In practice, this means creating Repository classes and Service classes.
confidence: strong

- recommendation: Use ViewModels and Views in the UI layer. (MVVM)
description: |
Separation of concerns is the most important architectural principle.
This particular separation makes your code much less error prone because your widgets remain "dumb".
confidence: strong

- recommendation: Use `ChangeNotifiers` and `Listenables` to handle widget updates.
description: |
The `ChangeNotifier` API is part of the Flutter SDK, and is a convenient way to have your widgets observe changes in your ViewModels.
confidence: conditional
confidence-description: |
There are many options to handle state-management, and ultimately the decision comes down to personal preference.
Read about [our ChangeNotifier recommendation][] or [other popular options][].

- recommendation: Do not put logic in widgets.
description: |
Logic should be encapsulated in methods on the ViewModel. The only logic a view should contain is:
* Simple if-statements to show and hide widgets based on a flag or nullable field in the ViewModel
* Animation logic that relies on the widget to calculate
* Layout logic based on device information, like screen size or orientation.
* Simple routing logic
confidence: strong

- recommendation: Use a domain layer.
description: |
A domain layer is only needed if your application has exceeding complex logic that crowds your ViewModels,
or if you find yourself repeating logic in ViewModels.
In very large apps, use-cases are useful, but in most apps they add unnecessary overhead.
confidence: conditional
confidence-description: |
Use in apps with complex logic requirements.

- category: Handling data
description: |
Handling data with care makes your code easier to understand, less error prone, and
prevents malformed or unexpected data from being created.
recommendations:
- recommendation: Use unidirectional data flow.
description: |
Data updates should only flow from the data layer to the UI layer.
Interactions in the UI layer are sent to the data layer where they're processed.
confidence: strong

- recommendation: Use `Commands` to handle events from user interaction.
description: |
Commands prevent rendering errors in your app, and standardize how the UI layer sends events to the data layer.
Read about commands in the [architecture case study][].
confidence: recommend

- recommendation: Use immutable data models.
description: |
Immutable data is crucial in making sure that data only updates in the model.
confidence: strong

- recommendation: Use freezed or built_value to generate immutable data models.
description: |
You can use packages to help generate useful functionality in your data models, [freezed][] or [built_value][].
These can generate common model methods like JSON ser/des, deep equality checking and copy methods.
These code generation packages can add significant build time to your applications if you have a lot of models.
confidence: recommend

- recommendation: Create separate API models and domain models.
description: |
Using separate models adds verbosity, but prevents complexity in ViewModels and use-cases.
confidence: conditional
confidence-description: Use in large apps.

- category: App structure
description: |
Well organized code benefits both the health of the app itself, and the team working on the code.
recommendations:

- recommendation: Use dependency injection.
description: |
Dependency injection prevents your app from having globally accessible objects, which makes your code less error prone.
We recommend you use the [provider](https://pub.dev/packages/provider) package to handle dependency injection.
confidence: strong

- recommendation: Use [go_router](https://pub.dev/packages/go_router) for navigation.
description: |
Go_router is the preferred way to write 90% of Flutter applications.
There are some specific use-cases that go_router doesn't solve,
in which case you can use the [Flutter Navigator API][] directly or try other packages found on [pub.dev][].
confidence: recommend

- recommendation: Use standardized naming conventions for classes, files and directories.
description: |
We recommend naming classes for the architectural component they represent.
For example, you may have the following classes:

* HomeViewModel
* HomeScreen
* UserRepository
* ClientApiService

For clarity, we do not recommend using names that can be confused with objects from the Flutter SDK.
For example, you should put your shared widgets in a directory called `ui/core/`,
rather than a directory called `/widgets`.
confidence: recommend

- recommendation: Use abstract repository classes
description: |
Repository classes are the sources of truth for all data in your app,
and facilitate communication with external APIs.
Creating abstract repository classes allows you to create different implementations,
which can be used for different app environments, such as "development" and "staging".
confidence: strong

- category: Testing
description: |
Good testing practices makes your app flexible.
It also makes it straightforward and low risk to add new logic and new UI.
recommendations:

- recommendation: Test architectural components separately, and together.
description: |
* Write unit tests for every service, repository and ViewModel class. These tests should test the logic of every method individually.
* Write widget tests for views. Testing routing and dependency injection are particularly important.
confidence: strong

- recommendation: Make fakes for testing (and write code that takes advantage of fakes.)
description: |
Fakes aren't concerned with the inner workings of any given method as much
as they're concerned with inputs and outputs. If you have this in mind while writing application code,
you're forced to write modular, lightweight functions and classes with well defined inputs and outputs.
confidence: strong
27 changes: 27 additions & 0 deletions src/_sass/components/_pill.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@use '../base/variables' as *;
@use '../vendor/bootstrap';
@use 'sass:color';


.rrec-pill {
border-radius: 8px;
margin: $site-spacer / 2;
padding: $site-spacer / 2;
text-align: center;

// Grey by default
background: $site-color-light-grey;
color: $site-color-body-light;

// Green
&.success {
background: darken($alert-success-bg, 10%);
color: $alert-success-fg;
}

// Blue
&.info {
background: darken($alert-info-bg, 10%);
color: darken($alert-info-bg, 60%);
}
}
1 change: 1 addition & 0 deletions src/_sass/site.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
@use 'components/header';
@use 'components/juicy-button';
@use 'components/next-prev-nav';
@use 'components/pill';
@use 'components/sidebar';
@use 'components/toc';

Expand Down
170 changes: 170 additions & 0 deletions src/content/app-architecture/concepts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
---
title: Common architecture concepts
short-title: Architecture concepts
description: >
Learn about common architecture concepts in application design,
and how they apply to Flutter.
prev:
title: Architecting Flutter apps
path: /app-architecture
next:
title: Guide to app architecture
path: /app-architecture/guide
---

In this section, you'll find tried and true principles that guide architectural
decisions in the larger world of app development,
as well as information about how they fit into Flutter specifically.
It's a gentle introduction to vocabulary and concepts related to
the recommended architecture and best practices,
so they can be explored in more detail throughout this guide.

## Separation of concerns

[Separation-of-concerns][] is a core principle in app development that
promotes modularity and maintainability by dividing an application's
functionality into distinct, self-contained units. From a high-level,
this means separating your UI logic from your business logic.
This is often described as *layered* architecture.
Within each layer, you should further separate your application by
feature or functionality. For example, your application's authentication logic
should be in a different class than the search logic.

In Flutter, this applies to widgets in the UI layer as well. You should write
reusable, lean widgets that hold as little logic as possible.

## Layered architecture

Flutter applications should be written in *layers*. Layered architecture is a
software design pattern that organizes an application into distinct layers, each
with specific roles and responsibilities. Typically, applications are separated
into 2 to 3 layers, depending on complexity.

<img src='/assets/images/docs/app-architecture/common-architecture-concepts/horizontal-layers-with-icons.png'
alt="The three common layers of app architecture, the UI layer, logic layer,
and data layer.">

* **UI layer** - Displays data to the user that is exposed by the business logic
layer, and handles user interaction. This is also commonly referred to as the
‘presentation layer'.
* **Logic layer** - Implements core business logic, and facilitates interaction
between the data layer and UI layer. Commonly known as the ‘domain layer'.
The logic layer is optional, and only needs to be implemented if your
application has complex business logic that happens on the client.
Many apps are only concerned with presenting data to a user and
allowing the user to change that data (colloquially known as CRUD apps).
These apps might not need this optional layer.
* **Data layer** - Manages interactions with data sources, such as databases or
platform plugins. Exposes data and methods to the business logic layer.

These are called ‘layers' because each layer can only communicate with the
layers directly below or above it. The UI layer shouldn't know that the data
layer exists, and vice versa.

## Single source of truth

Every data type in your app should have a [single source of truth][] (SSOT).
The source of truth is responsible for representing local or remote state.
If the data can be modified in the app,
the SSOT class should be the only class that can do so.

This can dramatically reduce the number of bugs in your application,
and it can simply code because you'll only ever have one copy of the same data.

Generally, the source of truth for any given type of data in your application is
held in a class called a **Repository**, which is part of the data layer.
There is typically one repository class for each type of data in your app.

This principle can be applied across layers and components in your application
as well as within individual classes. For example,
a Dart class might use [getters][] to derive values from an SSOT field
(instead of having multiple fields that need to be updated independently)
or a list of [records][] to group related values
(instead of parallel lists whose indices might get out of sync).

## Unidirectional data flow

[Unidirectional data flow][] (UDF) refers to a design pattern that helps
decouple state from the UI that displays that state. In the simplest terms,
state flows from the data layer through the logic layer and eventually to the
widgets in the UI layer.
Events from user-interaction flow the opposite direction,
from the presentation layer back through the logic layer and to the data layer.

<img src='/assets/images/docs/app-architecture/common-architecture-concepts/horizontal-layers-with-UDF.png'
alt="The three common layers of app architecture, the UI layer, logic layer,
and data layer, and the flow of state from the data layer to the UI layer.">

In UDF, the update loop from user interaction to re-rendering the UI looks like
this:

1. [UI layer] An event occurs due to user interaction, such as a button being
clicked. The widget's event handler callback invokes a method exposed by a
class in the logic layer.
2. [Logic layer] The logic class calls methods exposed by a repository that
know how to mutate the data.
3. [Data layer] The repository updates data (if necessary) and then provides the
new data to the logic class.
4. [Logic layer] The logic class saves its new state, which it sends to the UI.
5. [UI layer] The UI displays the new state of the ViewModel.

New data can also start at the data layer.
For example, a repository might poll an HTTP server for new data.
In this case, the data flow only makes the second half of the journey.
The most important idea is that data changes always happen
in the [SSOT][], which is the data layer.
This makes your code easier to understand, less error prone, and
prevents malformed or unexpected data from being created.


## UI is a function of (immutable) state

Flutter is declarative,
meaning that it builds its UI to reflect the current state of your app.
When state changes,
your app should trigger a rebuild of the UI that depends on that state.
In Flutter, you'll often hear this described as "UI is a function of state".

<img src='/assets/images/docs/app-architecture/common-architecture-concepts/ui-f-state.png'
style="width:50%; margin:auto; display:block"
alt="UI is a function of state.">

It's crucial that your data drive your UI, and not the other way around.
Data should be immutable and persistent,
and views should contain as little logic as possible.
This minimizes the possibility of data being lost when an app is closed,
and makes your app more testable and resilient to bugs.

## Extensibility

Each piece of architecture should have a well defined list of inputs and outputs.
For example, a view model in the logic layer should only
take in data sources as inputs, such as repositories,
and should only expose commands and data formatted for views.

Using clean interfaces in this way allows you to swap out
concrete implementations of your classes without needing to
change any of the code that consumes the interface.

## Testability

The principals that make software extensible also make software easier to test.
For example, you can test the self-contained logic of a view model by mocking a
repository.
The view model tests don't require you to mock other parts of your application,
and you can test your UI logic separate from Flutter widgets themselves.

Your app will also be more flexible.
It will be straightforward and low risk to add new logic and new UI.
For example, adding a new view model cannot break any logic
from the data or business logic layers.

The next section explains the idea of inputs and outputs for any given component
in your application's architecture.

[Separation-of-concerns]: https://en.wikipedia.org/wiki/Separation_of_concerns
[single source of truth]: https://en.wikipedia.org/wiki/Single_source_of_truth
[SSOT]: https://en.wikipedia.org/wiki/Single_source_of_truth
[getters]: {{site.dart-site}}/effective-dart/design#do-use-getters-for-operations-that-conceptually-access-properties
[records]: {{site.dart-site}}/language/records
[Unidirectional data flow]: https://en.wikipedia.org/wiki/Unidirectional_Data_Flow_(computer_science)
Loading
Loading