Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
27d300c
checkin initial expansion list implementation
ericwindmill Dec 5, 2024
d7833c5
Merge branch 'main' of https://github.com/flutter/website into design…
ericwindmill Dec 5, 2024
b9cc497
compleye expansion list scss
ericwindmill Dec 5, 2024
c01a730
finish draft
ericwindmill Dec 5, 2024
27f49ea
fix jank
ericwindmill Dec 6, 2024
6bb2626
remove commented code
ericwindmill Dec 6, 2024
62e1f38
finish data yml file and tags html
ericwindmill Dec 6, 2024
9c05942
add three more icons
ericwindmill Dec 6, 2024
f213dd3
add result and command
ericwindmill Dec 6, 2024
2ed01e3
checkin updated figma file
ericwindmill Dec 6, 2024
beae98a
fix sql icon
ericwindmill Dec 6, 2024
bbca37a
fix link
ericwindmill Dec 6, 2024
5e13792
Merge branch 'main' into design-pattern-index
ericwindmill Dec 8, 2024
d8f7cc4
Merge branch 'main' of https://github.com/flutter/website into design…
ericwindmill Dec 9, 2024
2ffea51
replace data file with collections feature
ericwindmill Dec 9, 2024
e061967
Merge branch 'design-pattern-index' of https://github.com/flutter/web…
ericwindmill Dec 9, 2024
8e86881
clean up
ericwindmill Dec 9, 2024
cdb6ff8
more cleanup
ericwindmill Dec 9, 2024
c970b2b
more cleanup
ericwindmill Dec 9, 2024
62fb9d3
Merge branch 'main' into design-pattern-index
ericwindmill Dec 10, 2024
0b50b92
sort recipes
ericwindmill Dec 10, 2024
8a29475
Update design-patterns.md
ericwindmill Dec 10, 2024
40fe7cc
Add action to page description
parlough Dec 11, 2024
85165a5
Minor style cleanup and fixes
parlough Dec 11, 2024
3c286c7
Slightly increase collapse duration
parlough Dec 11, 2024
40ce1d4
Adjust naming of data
parlough Dec 11, 2024
bfdd563
move design patterns
ericwindmill Dec 11, 2024
9837485
fix links
ericwindmill Dec 11, 2024
18bc3fd
move code excerpts out of cookbook
ericwindmill Dec 11, 2024
6e724a5
fix link typo
ericwindmill Dec 11, 2024
7313cb4
fix utils path in code excerpts
ericwindmill Dec 11, 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 modified diagrams/app-architecture/architecture-docs.fig
Binary file not shown.
120 changes: 120 additions & 0 deletions src/_data/cookbook/design_patterns.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
- title: "Persistent storage architecture: Key-value data"
tags:
- data
- shared-preferences
- dark mode
description: Save application data to a user's on-device key-value store.
path: /cookbook/architecture/key-value-data
img: /assets/images/docs/app-architecture/design-patterns/kv-store-icon.svg
body: |
Most Flutter applications, no matter how small or big they are,
require storing data on the user’s device at some point,
such as API keys, user preferences or data that should be available offline.

In this recipe, you will learn how to integrate persistent storage for key-value
data in a Flutter application by implementing a switchable light-mode and dark-mode theme
in an application and storing the users preference.
- title: "Persistent storage architecture: SQL"
tags:
- data
- SQL
description: Save complex application data to a user's device with SQL.
path: /cookbook/architecture/sql
img: /assets/images/docs/app-architecture/design-patterns/sql-icon.svg
body: |
Most Flutter applications, no matter how small or big they are,
might require storing data on the user’s device at some point.
For example, API keys, user preferences or data that should be available offline.

In this recipe, you will learn how to integrate persistent storage
for complex data using SQL by implementing a todo-list app that
follows our recommended Flutter app architecture.
- title: Offline-first support
description: Implement offline-first support for one feature in an application.
tags:
- data
- user experience
- repository pattern
path: /cookbook/architecture/offline-first
img: /assets/images/docs/app-architecture/design-patterns/offline-first-icon.svg
body: |
An offline-first application is an app capable of offering most or all of its
functionality while being disconnected from the internet.
Offline-first applications usually rely on stored data to offer users temporary access
to data that would otherwise only be available online.

Some offline-first applications combine local and remote data seamlessly, while other applications
inform the user when the application is using cached data. In the same way,
some applications synchronize data in the background while others require the
user to explicitly synchronize it. It all depends on the application requirements and
the functionality it offers, and it’s up to the developer to decide which implementation fits their needs.

In this guide, you will learn how to implement different approaches to offline-first
in Flutter applications that follow our recommended architecture guidelines.
- title: Optimistic state
description: Improve the perception of responsiveness of an application by implementing optimistic state.
tags:
- user experience
- asynchronous dart
path: /cookbook/architecture/optimistic-state
img: /assets/images/docs/app-architecture/design-patterns/optimistic-state-icon.svg
body: |
When building user experiences, the perception of performance is sometimes just as
important as the actual performance of the code. In general, users don’t like
waiting for an action to finish to see the result, and anything that takes more
than a few milliseconds could be considered “slow” or “unresponsive” from the user’s perspective.

Developers can help mitigate this negative perception by presenting a successful UI
state before the background task is fully completed. An example of this would be
tapping a “Subscribe” button, and seeing it change to “Subscribed” instantly, even if the
background call to the subscription API is still running.

This technique is known as Optimistic State, Optimistic UI or Optimistic User Experience.
In this recipe, you will implement optimistic state for a feature in an application that follows our
recommended architecture guidelines.
- title: The command pattern
description: Simplify view model logic by implementing a Command class.
tags:
- mvvm
- asynchronous dart
- state
path: /cookbook/architecture/command
img: /assets/images/docs/app-architecture/design-patterns/command-icon.svg
body: |
A command is a class that wraps a method and helps to handle the different
states of that method, such as running, complete, and error.

View models can use commands to handle interaction and run actions.
They can also be used to display different UI states,
like loading indicators when an action is running,
or an error dialog when an action failed.

View models can become very complex as an application grows
and features become bigger.
Commands can help to simplify view models and reuse code.

In this guide, you will learn how to use the command pattern
to improve your view models.
- title: Better error handling
description: Improve error handling across classes with Result objects.
tags:
- error handling
- services
path: /cookbook/architecture/result
img: /assets/images/docs/app-architecture/design-patterns/result-icon.svg
body: |
Dart provides a built-in error handling mechanism
with the ability to throw and catch exceptions.

Dart’s exceptions are unhandled exceptions.
This means that methods that throw exceptions don’t need to declare them,
and calling methods aren't required to catch them either.

This can lead to situations where exceptions are not handled properly.
In large projects, developers might forget to catch exceptions,
and the different application layers and components
could throw exceptions that aren’t documented.
This can lead to errors and crashes.

In this guide, you will learn about this limitation
and how to mitigate it using the result pattern.
12 changes: 1 addition & 11 deletions src/_data/sidenav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -439,20 +439,10 @@
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
permalink: /cookbook/architecture
children:
- title: Optimistic state
permalink: /cookbook/architecture/optimistic-state
- title: "Persistent storage architecture: Key-value data"
permalink: /cookbook/architecture/key-value-data
- title: "Persistent storage architecture: SQL"
permalink: /cookbook/architecture/sql
- title: Offline-first
permalink: /cookbook/architecture/offline-first
permalink: /app-architecture/design-patterns

- title: Platform integration
permalink: /platform-integration
Expand Down
54 changes: 54 additions & 0 deletions src/_includes/expansion-list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{%- comment -%}
This component expects a list of article or page objects.
Each article should have the following attributes:
- title: String - name of the article
- description: String - 1-2 sentence description of the article
- tags: List<String> - A short list of items that describe what the reader can expect from the content or format.
i.e. data, user-experience OR tutorial, 10 minute read
- body: String - This is the text body of the expandable portion of expansion panel.
- img: String - the path to an image that is shown in next to the title
- path: String - Path to the article or page
{%- endcomment -%}

<div class="expansion-panel-list">
{% for item in list -%}
{% assign id = base_id | append: '-expansion-' | append: forloop.index -%}
{% if item.expanded -%}
{% assign expanded = 'true' -%}
{% assign show = 'show' -%}
{% else -%}
{% assign class = 'collapsed' -%}
{% assign expanded = 'false' -%}
{% assign show = '' -%}
{% endif -%}
<div class="expansion-panel">
<a class="{{class}} collapsible"
data-toggle="collapse"
href="#{{id}}"
role="button"
aria-expanded="{{expanded}}"
aria-controls="{{id}}">
<div class="expansion-panel-title">
<div class="expansion-panel-title-leading">
<img src="{{item.img}}" alt="Alt text" />
</div>
<div class="expansion-panel-title-content">
<p class="content-title">{{item.title}}</p>
<ul class="content-tags">
{% for tag in item.tags -%}
<li class="tag">{{tag}}</li>
{% endfor -%}
</ul>
<p class="content-description">{{item.description}}</p>
</div>
</div>
</a>
<div class="expansion-panel-body collapse {{show}}" id="{{id}}">
<p>{{item.body}}</p>
<a href="{{item.path}}">Read full article</a>
<!-- Required to add "margin" that doesn't cause expansion jank -->
<div class="separator"></div>
</div>
</div>
{% endfor -%}
</div>
2 changes: 2 additions & 0 deletions src/_sass/base/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ $site-color-sub-grey: #8d9399;
$site-color-nav-links: #6E7274;
$site-color-body: #212121; // Poor contrast with links
$site-color-body-light: color.scale($site-color-body, $lightness: 20%);
$site-color-body-caption: color.scale($site-color-body, $lightness: 30%);
$site-color-footer: #303c42;
$site-color-primary: $flutter-color-blue-500;
$twitter-color: #60CAF6;
$site-color-panel-background: color.scale($site-color-primary, $lightness: 95%);

// Fonts
$font-size-base-weight: 400;
Expand Down
144 changes: 144 additions & 0 deletions src/_sass/components/_expansion-list.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
@use '../base/variables' as *;
@use '../vendor/bootstrap';

.expansion-panel-list {
background: $site-color-panel-background;
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 12px;
margin-top: 2rem;
margin-bottom: 2rem;

// Add padding on small screens, because images aren't displayed.
@include bootstrap.media-breakpoint-down(md) {
padding-left: 1rem;
}

// Rotates chevron
@mixin collapsible() {
display: flex;
align-items: center;
justify-content: space-between;
padding-right: 1rem;

&::after {
// Duplicated since Firefox doesn't support content alt text.
content: 'keyboard_arrow_down';
content: 'keyboard_arrow_down' / '';
font: $site-font-icon;
transition: transform .25s ease-in-out;
}

.collapsing {
transition-duration: 0.15s;
}

&:not(.collapsed) {
&::after {
content: 'keyboard_arrow_down' / '';
transform: rotate(180deg);
}
}
}

.expansion-panel {
> a {
// Adds display:flex, align:center, and justify:space-between.
@include collapsible();
}

a:hover {
text-decoration: none;
}

}

.expansion-panel-title {
display: flex;
flex-direction: row;
align-items: center;

.expansion-panel-title-leading {
height: 8rem;
width: 8rem;
margin: .5rem;
padding: 1rem;

// hide on small screens
display: none;
@include bootstrap.media-breakpoint-up(md) {
display: flex;
flex: 1 0 auto;
}

img {
margin: auto;
}
}

.expansion-panel-title-content {
.content-title {
color: $site-color-black;
font-size: 1rem;
font-weight: 500;
margin-bottom: .25rem;

@include bootstrap.media-breakpoint-up(md) {
font-size: 1.25rem;
}
}

.content-description {
color: $site-color-body;
margin-bottom: $site-spacer / 2;
}

.content-tags {
display: flex;
flex-direction: row;
list-style: none;
padding-left: 0;
margin-bottom: .75rem;

.tag {
color: $site-color-body-caption;
font-size: .8rem;
line-height: 1;
margin-bottom: 0;

&::after {
// Duplicated since Firefox doesn't support content alt text.
content: ' |\00a0';
content: ' |\00a0';
}
}

:last-child::after {
content: '';
content: '';
}
}
}
}

.expansion-panel-body {
margin: auto;
width: 90%;
border-top: .05rem solid rgba(0, 0, 0, 0.125);

p {
color: $site-color-body;
margin-top: 2rem;
}


.separator {
margin-bottom: 3rem;
}
}

:last-child {
.expansion-panel-body {
border-bottom: none;
}
}
}
1 change: 1 addition & 0 deletions src/_sass/site.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
@use 'components/content';
@use 'components/cookie-notice';
@use 'components/d2h';
@use 'components/expansion-list';
@use 'components/footer';
@use 'components/header';
@use 'components/juicy-button';
Expand Down
25 changes: 25 additions & 0 deletions src/content/app-architecture/design-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: Architecture design patterns
short-title: Design patterns
description: >
TODO
prev:
title: Recommendations
path: /app-architecture/recommendations
toc: false
---

If you've already read through the [architecture guide][] page,
or if you're comfortable with Flutter and the MVVM pattern,
the following articles are for you.

These articles aren't about high-level app architecture,
rather they're about solving specific design problems that improve your
application's code base regardless of how you've architected your app.
That said, the articles do assume the MVVM pattern laid out on the
previous pages in the code examples.

{% assign recipes = cookbook.design_patterns | sort: 'name' -%}
{% render expansion-list.html, list: recipes, base_id: 'design-patterns' %}

[architecture guide]: /app-architecture/guide
Loading
Loading