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

Publish app architecture 'case-study' pages #11414

Merged
merged 51 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 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
28ef5b1
checkin
ericwindmill Oct 24, 2024
4df3e47
Merge branch 'main' of https://github.com/flutter/website into ew-app…
ericwindmill Nov 19, 2024
48e1492
add pages, complete index page
ericwindmill Nov 19, 2024
9cf4f15
complete ui-layer page
ericwindmill Nov 19, 2024
20150e3
complete data layer page
ericwindmill Nov 19, 2024
9c8ea79
complete dep injection page
ericwindmill Nov 19, 2024
0697d05
complete testing page
ericwindmill Nov 19, 2024
00001bb
fix some links
ericwindmill Nov 19, 2024
802b1ee
Merge branch 'main' into ew-app-architecture-case-study
ericwindmill Nov 21, 2024
5ea4022
fix some links
ericwindmill Nov 21, 2024
1fe773d
fix remaining links
ericwindmill Nov 21, 2024
19b287f
fix typo
ericwindmill Nov 21, 2024
ab06e15
Merge branch 'main' into ew-app-architecture-case-study
ericwindmill Nov 21, 2024
566144a
Merge branch 'main' of https://github.com/flutter/website into ew-app…
ericwindmill Nov 26, 2024
b687eea
Apply suggestions from code review
ericwindmill Nov 26, 2024
9ea2852
Merge branch 'ew-app-architecture-case-study' of https://github.com/f…
ericwindmill Nov 26, 2024
0282f4c
use markdown image syntax and add titles to code fences
ericwindmill Nov 26, 2024
c7213a9
use highlightLines
ericwindmill Nov 26, 2024
124da10
address feedback
ericwindmill Nov 26, 2024
0080d24
add figma source file
ericwindmill Nov 26, 2024
59c46bc
Merge branch 'main' of https://github.com/flutter/website into ew-app…
ericwindmill Nov 27, 2024
eb068de
fix typo in diagram
ericwindmill Nov 27, 2024
442becf
Apply suggestions from @parlough code review
ericwindmill Nov 27, 2024
fdb19e1
remove Result section and update all code comments
ericwindmill Nov 27, 2024
083bf61
address all data layer page feedback
ericwindmill Nov 27, 2024
cd9d2ba
Addresses feedback on dep injection page
ericwindmill Nov 27, 2024
59e7b4a
sentence case ViewModel
ericwindmill Nov 27, 2024
5c7d0cf
address remaining review feedback for @parlough
ericwindmill Nov 27, 2024
68bc87b
Merge branch 'main' into ew-app-architecture-case-study
parlough Dec 1, 2024
19cbce9
Fix broken links
parlough Dec 1, 2024
481f636
Use a few more site variables
parlough Dec 1, 2024
4fac627
More consistent package links
parlough Dec 1, 2024
49f2cc5
Consistent GitHub capitalization
parlough Dec 1, 2024
dc74cff
Some minor formatting and typo fixes
parlough Dec 1, 2024
d46134f
Fix spelling of Dismissible
parlough Dec 1, 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
Binary file added diagrams/app-architecture/architecture-docs.fig
Binary file not shown.
15 changes: 15 additions & 0 deletions src/_data/sidenav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,21 @@
permalink: /app-architecture/concepts
- title: Guide to app architecture
permalink: /app-architecture/guide
- title: Architecture case study
permalink: /app-architecture/case-study
children:
- title: Overview
permalink: /app-architecture/case-study
ericwindmill marked this conversation as resolved.
Show resolved Hide resolved
match-page-url-exactly: true
- title: UI layer
permalink: /app-architecture/case-study/ui-layer
- title: Data layer
permalink: /app-architecture/case-study/data-layer
- title: Dependency injection
permalink: /app-architecture/case-study/dependency-injection
- title: Testing each layer
permalink: /app-architecture/case-study/testing

- title: Recommendations
permalink: /app-architecture/recommendations
- title: Design patterns
Expand Down
242 changes: 242 additions & 0 deletions src/content/app-architecture/case-study/data-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
---
title: Data layer
short-title: Data layer
description: >-
A walk-through of the data layer of an app that implements MVVM architecture.
prev:
title: UI layer
path: /app-architecture/case-study/ui-layer
next:
title: Dependency Injection
path: /app-architecture/case-study/dependency-injection
---


The data layer of an application, known as the *model* in MVVM terminology,
is the source of truth for all application data. As the source of truth,
it's the only place that application data should be updated.

It's responsible for consuming data from various external APIs,
exposing that data to the UI,
handling events from the UI that require data to be updated,
and sending update requests to those external APIs as needed.

The data layer in this guide has two main components,
[repositories][] and [services][].

![A diagram that highlights the data layer components of an application.](/assets/images/docs/app-architecture/guide/feature-architecture-simplified-Data-highlighted.png)

* **Repositories** are the source of the truth for application data, and contain
logic that relates to that data, like updating the data in response to new
user events or polling for data from services. Repositories are responsible
for synchronizing the data when offline capabilities are supported, managing
retry logic, and caching data.
* **Services** are stateless Dart classes that interact with APIs, like HTTP
servers and platform plugins. Any data that your application needs that isn't
created inside the application code itself should be fetched from within
service classes.

## Define a service

A service class is the least ambiguous of all the architecture components.
It's stateless, and its functions don't have side effects.
Its only job is to wrap an external API.
There's generally one service class per data source,
such as a client HTTP server or a platform plugin.

ericwindmill marked this conversation as resolved.
Show resolved Hide resolved

![A diagram that shows the inputs and outputs of service objects.](/assets/images/docs/app-architecture/case-study/mvvm-case-study-services-architecture.png)

In the Compass app, for example, there's an [`APIClient`][] service that
handles the CRUD calls to the client-facing server.

```dart title=api_client.dart
class ApiClient {
// Some code omitted for demo purposes.

Future<Result<List<ContinentApiModel>>> getContinents() async {...}

Future<Result<List<DestinationApiModel>>> getDestinations() async {...}

Future<Result<List<ActivityApiModel>>> getActivityByDestination(String ref) async {...}

Future<Result<List<BookingApiModel>>> getBookings() async {...}

Future<Result<BookingApiModel>> getBooking(int id) async {...}

Future<Result<BookingApiModel>> postBooking(BookingApiModel booking) async {...}

Future<Result<void>> deleteBooking(int id) async {...}

Future<Result<UserApiModel>> getUser() async {...}
}
```

The service itself is a class,
where each method wraps a different API endpoint and
exposes asynchronous response objects.
Continuing the earlier example of deleting a saved booking,
the `deleteBooking` method returns a `Future<Result<void>>`.

:::note
Some methods return data classes that are
specifically for raw data from the API,
such as the `BookingApiModel` class.
As you'll soon see, repositories extract data and
expose it to s in a different format.
:::


## Define a repository

A repository's sole responsibility is to manage application data.
A repository is the source of truth for a single type of application data,
and it should be the only place where that data type is mutated.
The repository is responsible for polling new data from external sources,
handling retry logic, managing cached data,
and transforming raw data into domain models.

![A diagram that highlights the repository component of an application.](/assets/images/docs/app-architecture/guide/feature-architecture-simplified-Repository-highlighted.png)

You should have a separate repository for each different type of
data in your application. For example,
the Compass app has repositories called `UserRepository`,
`BookingRepository`, `AuthRepository`, `DestinationRepository`, and more.

The following example is the `BookingRepository` from the Compass app,
and shows the basic structure of a repository.

```dart title=booking_repository_remote.dart
class BookingRepositoryRemote implements BookingRepository {
BookingRepositoryRemote({
required ApiClient apiClient,
}) : _apiClient = apiClient;

final ApiClient _apiClient;
List<Destination>? _cachedDestinations;

Future<Result<void>> createBooking(Booking booking) async {...}
Future<Result<Booking>> getBooking(int id) async {...}
Future<Result<List<BookingSummary>>> getBookingsList() async {...}
Future<Result<void>> delete(int id) async {...}
}
```

:::note Development versus staging environments
The class in the previous example is `BookingRepositoryRemote`,
which extends an abstract class called `BookingRepository`.
This base class is used to create repositories for different environments.
For example, the compass app also has a class called `BookingRepositoryLocal`,
which is used for local development.

You can see the differences between the
[`BookingRepository` classes on GitHub][].
:::


The `BookingRepository` takes the `ApiClient` service as an input,
which it uses to get and update the raw data from the server.
It's important that the service is a private member,
so that the UI layer can't bypass the repository and call a service directly.

With the `ApiClient` service,
the repository can poll for updates to a user's saved bookings that
might happen on the server, and make `POST` requests to delete saved bookings.

The raw data that a repository transforms into application models can come from
multiple sources and multiple services,
and therefore repositories and services have a many-to-many relationship.
A service can be used by any number of repositories,
and a repository can use more than one service.

![A diagram that highlights the data layer components of an application.](/assets/images/docs/app-architecture/guide/feature-architecture-simplified-Data-highlighted.png)

### Domain models

The `BookingRepository` outputs `Booking` and `BookingSummary` objects,
which are *domain models*. All repositories output corresponding domain models.
These data models differ from API models in that they only contain the data
needed by the rest of the app.
API models contain raw data that often needs to be filtered,
combined, or deleted to be useful to the app's s.
The repo refines the raw data and outputs it as domain models.

In the example app, domain models are exposed through
return values on methods like `BookingRepository.getBooking`.
The `getBooking` method is responsible for getting the raw data from
the `ApiClient` service, and transforming it into a `Booking` object.
It does this by combining data from multiple service endpoints.

```dart title=booking_repository_remote.dart highlightLines=14-21
// This method was edited for brevity.
Future<Result<Booking>> getBooking(int id) async {
try {
// Get the booking by ID from server.
final resultBooking = await _apiClient.getBooking(id);
if (resultBooking is Error<BookingApiModel>) {
return Result.error(resultBooking.error);
}
final booking = resultBooking.asOk.value;

final destination = _apiClient.getDestination(booking.destinationRef);
final activities = _apiClient.getActivitiesForBooking(
booking.activitiesRef);

return Result.ok(
Booking(
startDate: booking.startDate,
endDate: booking.endDate,
destination: destination,
activity: activities,
),
);
} on Exception catch (e) {
return Result.error(e);
}
}
```

:::note
In the Compass app, service classes return `Result` objects.
`Result` is a utility class that wraps asynchronous calls and
makes it easier to handle errors and manage UI state that relies
on asynchronous calls.

This pattern is a recommendation, but not a requirement.
The architecture recommended in this guide can be implemented without it.

You can learn about this class in the [Result cookbook recipe][].
:::

### Complete the event cycle

Throughout this page, you've seen how a user can delete a saved booking,
starting with an event—a user swiping on a `Dismissible` widget.
The view model handles that event by delegating
the actual data mutation to the `BookingRepository`.
The following snippet shows the `BookingRepository.deleteBooking` method.

```dart title=booking_repository_remote.dart
Future<Result<void>> delete(int id) async {
try {
return _apiClient.deleteBooking(id);
} on Exception catch (e) {
return Result.error(e);
}
}
```

The repository sends a `POST` request to the API client with
the `_apiClient.deleteBooking` method,
and returns a `Result`. The `HomeViewModel` consumes the `Result,
and the data it contains, and ultimately calls `notifyListeners`,
completing the cycle.

[repositories]: /app-architecture/guide#repositories
[services]: /app-architecture/guide#services
[`APIClient`]: https://github.com/flutter/samples/blob/main/compass_app/app/lib/data/services/api/api_client.dart
[`sealed`]: {{site.dart-site}}/language/class-modifiers#sealed
[`BookingRepository` classes on GitHub]: https://github.com/flutter/samples/tree/main/compass_app/app/lib/data/repositories/booking
[Result cookbook recipe]: /cookbook/architecture

[//]: # (todo ewindmill@ - update Result link after #11444 lands)
Loading
Loading