├── .fvm └── fvm_config.json ├── .gitignore ├── .metadata ├── .vscode ├── launch.json └── settings.json ├── README.md ├── all_lint_rules.yaml ├── analysis_options.yaml ├── lib ├── counter.dart ├── counter_controller.dart ├── counter_page.dart └── main.dart ├── pubspec.lock ├── pubspec.yaml ├── test └── counter_test.dart └── web ├── favicon.png ├── icons ├── Icon-192.png ├── Icon-512.png ├── Icon-maskable-192.png └── Icon-maskable-512.png ├── index.html └── manifest.json /.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "3.10.4", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | 46 | # FVM related 47 | .fvm/flutter_sdk 48 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 17 | base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 18 | - platform: web 19 | create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 20 | base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug (Chrome)", 6 | "program": "lib/main.dart", 7 | "request": "launch", 8 | "type": "dart", 9 | "args": ["--debug"] 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.flutterSdkPath": ".fvm/flutter_sdk", 3 | "dart.sdkPath": ".fvm/flutter_sdk/bin/cache/dart-sdk", 4 | "search.exclude": { 5 | "**/.fvm": true 6 | }, 7 | "files.watcherExclude": { 8 | "**/.fvm": true 9 | }, 10 | "dart.runPubGetOnPubspecChanges": "never" 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flutter_mvc_counter 2 | 3 | このリポジトリは近日公開予定の記事のサンプルアプリです。 4 | 5 | 以下に、記事と同様の内容を記載します。 6 | 7 | --- 8 | 9 | この章では、様々なアーキテクチャの最も基礎ともいえる MVC で作る方法を説明します。 10 | 11 | ## MVC とは 12 | 13 | MVC アーキテクチャの詳しい説明は、たとえば次のような文書: 14 | 15 | 16 | 17 | に任せることにして本書では省略しますが、MVC アーキテクチャは、プログラムを M (Model), V (View), C (Controller) の役割に分担して記述するアーキテクチャです。 18 | 19 | それぞれの役割を簡単に説明すると、以下の通りです。 20 | 21 | - Model: アプリケーションの振る舞いを表現するもの。View と Controller 以外のすべてのモジュールが該当する 22 | - View: ディスプレイに表示される見た目 23 | - Controller: ユーザーによる操作を解釈して Model を操作したり、モデルを UI に反映させたりするもの 24 | 25 | 大まかに格闘ゲームに例えるならば、 26 | 27 | - Model: 格闘ゲームの振る舞いを表現するもの(キャラクターの能力、キャラクターの各種操作内容、ダメージや吹っ飛び率の計算、残機数の計算...などすべて) 28 | - View: ゲームの画面に表示されるキャラクターやステージなどの見た目 29 | - Controller: ユーザーが手に持って操作するコントローラ 30 | 31 | といえるでしょう。 32 | 33 | View と Controller をあわせて UI(ユーザーインターフェース)といいます。その名の通り、ユーザーとアプリケーションの境界、両者をつなぐものです。 34 | 35 | 設計を考える際の最も大切な原則のひとつに、プレゼンテーションロジックとドメインロジックを分離する考え方 (PDS: Presentation Domain Separation) があります。これについても詳細は世の中の他の文書に任せますが、かんたんに言うと、ユーザーインターフェイスに関する実装と、アプリケーションの振る舞いを表現する実装とを分離することです。 36 | 37 | たとえば以下の文書: 38 | 39 | 40 | 41 | が説明するように、プレゼンテーションロジックとドメインロジックが分かれていると、 42 | 43 | - 同じプログラムを、重複コードなしに、複数の見た目に対応させすい 44 | - 一般にテストがやや難しいプレゼンテーションロジックを、ドメインロジックと分離することで、ドメインロジックをテスト可能に書きやすい 45 | 46 | などのメリットがあります。 47 | 48 | MVC アーキテクチャと対応させるならば、MVC アーキテクチャは UI (View + Controller) と アプリケーションの振る舞い・ロジック (Model) を分離して記述していく方針といえます。 49 | 50 | また、MVVM や本書で説明される他のいろいろなアーキテクチャパターンも、最も抽象的と言える MVC アーキテクチャを、それぞれの観点で切り口を変えたりレイヤー分けをしたりしたものと解釈することができます。 51 | 52 | ## MVC を Flutter に適用する 53 | 54 | この章で説明する MVC アーキテクチャによるカウンターアプリを以下のように解釈して Flutter フレームワークに適用し、その実装方針とします。 55 | 56 | なお、以下の解釈や実装方針は一定の一貫性や納得感を伴うものではありますが、唯一の最善の正解というわけではないことには注意してください。また、以下では説明のしやすさの観点から、M, V, C の順番を入れ替えています。 57 | 58 | ### View 59 | 60 | Flutter における View(ディスプレイに表示される見た目)は、ウィジェットが提供します。スマートフォン・タブレット、PC などの端末の画面上、または Web アプリならばそれらのブラウザ上に表示される見た目が View です。 61 | 62 | ### Controller 63 | 64 | Flutter で開発したアプリケーションは、スマートフォンやタブレットの画面を手で(またはマウス等のデバイスを通じて)操作します。 65 | 66 | Flutter アプリにおいて、格闘ゲームのコントローラに相当する、ユーザーが触って操作する対象は、たとえば `ElevatedButton` などのボタンや `SelectableText` などのウィジェット (`StatefulWidget`) が代表的です。 67 | 68 | `ElevatedButton` の `onPressed` のように「押したら(操作したら)どうなるか」という機能が記述されます。 69 | 70 | その `onPressed` の処理の中に、Controller の役割である「ユーザーによる操作を解釈して Model を操作」するプログラムを記述したり、同様の内容を `FooController` のようなコントローラクラスを定義して、そのメソッドとして記述し、たとえば [provider](https://pub.dev/packages/provider) パッケージの `Provider` を組み合わせて提供したりすると良いでしょう。 71 | 72 | コントローラは「ユーザーによる操作を解釈して Model を操作」するので、カウンターアプリならば、`CounterController` が `Counter` モデルのインスタンスを保持して、ユーザーからの操作を受けつつ、その操作に応じた指示を `Counter` モデルに対して行います。 73 | 74 | ### Model 75 | 76 | Model は View と Controller (UI) 以外のアプリケーションの振る舞いです。上述の通り、View と Controller は Flutter フレームワークに大きく依存しますが、Model は Flutter フレームワークや環境にできるだけ依存しないように記述して、ユニットテストを可能にする方針が望ましいです。 77 | 78 | Flutter フレームワークや外部の環境に依存せず、ピュアな Dart で記述できることが理想でしょう。そうすることでユニットテストもピュアな Dart で記述することができます。他のクラスのインスタンスに依存する場合には、コンストラクタインジェクションで依存性を注入するのが通例です。 79 | 80 | と言いつつ、本章で取り上げるカウンターアプリでは、モデルは Flutter の `ChangeNotifier` には依存することを認めることとします。しばしば「UI = f(state)」で説明される Model → View の関係と View の更新をかんたんに実装できる上、モデルのユニットテスト可能性には影響を及ぼさないからです(影響を及ぼさないように実装します)。 81 | 82 | ## 今回取り上げるカウンターアプリの要件 83 | 84 | ただ数字をカウントアップするよりは少し複雑な下記のようなカウンターアプリを取り上げます。 85 | 86 | - 「カウント」に数字が表示されている 87 | - 「カウントアップ」ボタンを押すと、「カウント」の数字に 1 が加算される 88 | - 「リストに追加」ボタンを押すと、現在のカウントの数字がリストに追加される 89 | - 「合計値」には、現在のリストの合計値が表示されている 90 | - リストに追加時に合計値が 5 の倍数になった場合には、「合計値が 5 の倍数です!」という `SnackBar` が表示される 91 | - 「クリア」ボタンを押すと、「カウント」が 0 に、「リスト」が空にリセットされる 92 | 93 | ![カウンターアプリ](https://github-production-user-asset-6210df.s3.amazonaws.com/13669049/248271618-19607def-ddad-4160-823a-c27e30b677d3.gif?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20230623%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20230623T131117Z&X-Amz-Expires=300&X-Amz-Signature=8828d061918d63785827835546df7461f2016124d613db20631653d73c480551&X-Amz-SignedHeaders=host&actor_id=13669049&key_id=0&repo_id=476170184) 94 | 95 | また、[provider](https://pub.dev/packages/provider) パッケージと、`ChangeNotifier`, `ChangeNotifierProvider` を使用し、Model に対するユニットテストを記述します。Flutter のウィジェットテストによる UI (View + Controller) に対するテストは省略します。 96 | 97 | ## カウンターアプリの実装 98 | 99 | ### Model の実装 100 | 101 | まずは、カウンターアプリの振る舞いを記述する `Counter` モデルを実装します。最終的には `ChangeNotifier` を継承し、Flutter フレームワークに依存しますが、途中までピュアな Dart のクラスとして書いてみましょう。 102 | 103 | `Counter` モデルが保持するのは、現在のカウント値と、カウント値のリストです。 104 | 105 | ```dart 106 | /// カウンターの振る舞いを表現するモデル。 107 | class Counter { 108 | /// カウント値。 109 | int _count = 0; 110 | 111 | /// カウント値のリスト。 112 | final List _counts = []; 113 | } 114 | ``` 115 | 116 | カウント値に 1 を加算する処理(振る舞い)を、次の `increment` メソッドとして定義します。 117 | 118 | ```dart 119 | /// カウント値に 1 を加算する。 120 | void increment() { 121 | _count++; 122 | } 123 | ``` 124 | 125 | 同様に、 126 | 127 | - カウント値をリストに追加する処理 128 | - カウント値とカウント値のリストをクリアする処理 129 | - カウント値のリストの合計を計算する処理 130 | - カウント値の合計が 5 の倍数であるかを判定する処理 131 | 132 | をそれぞれのメソッドとして定義すれば `Counter` モデルがほぼ完成です。ここまではピュアな Dart で何にも依存せずにカウンターの振る舞いを記述できています。 133 | 134 | ```dart 135 | /// カウンターの振る舞いを表現するモデル。 136 | class Counter { 137 | /// カウント値。 138 | int _count = 0; 139 | 140 | /// カウント値のリスト。 141 | final List _counts = []; 142 | 143 | /// カウント値に 1 を加算する。 144 | void increment() { 145 | _count++; 146 | } 147 | 148 | /// カウント値をリストに追加する。 149 | void append() { 150 | _counts.add(_count); 151 | } 152 | 153 | /// カウント値とカウント値のリストをクリアする。 154 | void clear() { 155 | _count = 0; 156 | _counts.clear(); 157 | } 158 | 159 | /// カウント値のリストの合計を計算する。 160 | int calculateTotal() { 161 | return _counts.fold(0, (a, b) => a + b); 162 | } 163 | 164 | /// カウント値の合計が 5 の倍数であるかを判定する。 165 | bool isTotalMultipleOfFive() => calculateTotal() % 5 == 0; 166 | } 167 | ``` 168 | 169 | 最後に `ChangeNotifier` を継承して、必要な箇所で `notifyListeners` メソッドをコールするよう書き換えて完成です。 170 | 171 | ```dart 172 | /// カウンターの振る舞いを表現するモデル。 173 | class Counter extends ChangeNotifier { 174 | int get count => _count; 175 | 176 | List get counts => _counts; 177 | 178 | /// カウント値。 179 | int _count = 0; 180 | 181 | /// カウント値のリスト。 182 | final List _counts = []; 183 | 184 | /// カウント値に 1 を加算する。 185 | void increment() { 186 | _count++; 187 | notifyListeners(); 188 | } 189 | 190 | /// カウント値をリストに追加する。 191 | void append() { 192 | _counts.add(_count); 193 | notifyListeners(); 194 | } 195 | 196 | /// カウント値とカウント値のリストをクリアする。 197 | void clear() { 198 | _count = 0; 199 | _counts.clear(); 200 | notifyListeners(); 201 | } 202 | 203 | /// カウント値のリストの合計を計算する。 204 | int calculateTotal() { 205 | return _counts.fold(0, (a, b) => a + b); 206 | } 207 | 208 | /// カウント値の合計が 5 の倍数であるかを判定する。 209 | bool isTotalMultipleOfFive() => calculateTotal() % 5 == 0; 210 | } 211 | ``` 212 | 213 | ### Model のユニットテストの実装 214 | 215 | Model が完成したので、View や Controller の実装に移る前に、Model のユニットテストを完成させてみます。 216 | 217 | Dart (Flutter) におけるユニットテストの書き方の詳細はここでは説明しませんが、`ChangeNotifier` にしか依存していない `Counter` モデルは、次のように簡単にユニットテストを書くことができ、その振る舞いを説明したり振る舞いの正しさを検査したりすることができます。 218 | 219 | ```dart 220 | void main() { 221 | late Counter counter; 222 | 223 | setUp(() { 224 | counter = Counter(); 225 | }); 226 | 227 | group('Counter', () { 228 | test('初期値は0である', () { 229 | expect(counter.count, 0); 230 | }); 231 | 232 | test('値が正しくインクリメントされる', () { 233 | counter.increment(); 234 | expect(counter.count, 1); 235 | }); 236 | 237 | test('値がリストに正しく追加される', () { 238 | counter.append(); 239 | expect(counter.counts.length, 1); 240 | expect(counter.counts[0], 0); 241 | 242 | counter.increment(); 243 | counter.append(); 244 | expect(counter.counts.length, 2); 245 | expect(counter.counts[1], 1); 246 | }); 247 | 248 | test('値とリストが正しくクリアされる', () { 249 | counter.increment(); 250 | counter.append(); 251 | counter.clear(); 252 | expect(counter.count, 0); 253 | expect(counter.counts.isEmpty, true); 254 | }); 255 | 256 | test('リストの値の合計が正しく計算される', () { 257 | counter.increment(); 258 | counter.append(); 259 | counter.increment(); 260 | counter.append(); 261 | expect(counter.calculateTotal(), 3); 262 | }); 263 | 264 | test('リストの値の合計が 5 の倍数である判定が正しくされる', () { 265 | counter.increment(); 266 | counter.increment(); 267 | counter.append(); 268 | expect(counter.isTotalMultipleOfFive(), false); 269 | counter.increment(); 270 | counter.append(); 271 | expect(counter.isTotalMultipleOfFive(), true); 272 | }); 273 | }); 274 | } 275 | ``` 276 | 277 | また、例えばカウントの値を何かしらの API と通信して送信し永続化するようなこともあるでしょう。そのような場合には、`Counter` クラスのコンストラクタで、リポジトリクラスや API クライアントのクラスをインジェクトします。 278 | 279 | ```dart 280 | class Counter extends ChangeNotifier { 281 | Counter(Repository repository): _repository = repository; 282 | 283 | /// リポジトリクラスのインスタンス。 284 | final Repository _repository; 285 | 286 | // ... 省略 287 | } 288 | ``` 289 | 290 | そうすることで、ユニットテストではそれをモックに置き換えることが容易にできます。 291 | 292 | ### Controller の実装 293 | 294 | コントローラは、`Counter` モデルを保持して、ユーザーによる操作を解釈してモデルを操作したり、UI に反映したりする役割を担います。 295 | 296 | コンストラクタで `Counter` モデルのインスタンスを渡す方法はシンプルで、コントローラのテストを書きたくなった場合にもモデルを容易にモックに置き換えることができるので良いでしょう。 297 | 298 | ユーザー操作に相当するコントローラの各メソッドが、ユーザーの操作を解釈しながら、対応するモデルのメソッドを呼ぶようなつくりになっています。 299 | 300 | `addToList` メソッドでは、合計値が 5 の倍数であるかどうかを判定して、そうである場合には、`SnackBar` を表示しています。これも Model を反映した View に反映するという意味で、コントローラの役割と捉えることができます。 301 | 302 | 他にも、例えばモデルで発生した例外を捕捉して、同様に `SnackBar` や `AlertDialog` を表示するような実装もコントローラに記述すると良いでしょう。 303 | 304 | Model と違って Flutter に依存することを認めており、例えば `ElevatedButton` の `onPressed` に直接記述しても差し支えないような処理なので、`BuildContext` をメソッドの引数として渡すことも、ここでは許容しています。 305 | 306 | ```dart 307 | /// ユーザーによる操作を解釈して [Counter] モデルを操作したり、モデルを UI に反映 308 | /// させたりするコントローラ。 309 | class CounterController { 310 | const CounterController(Counter counter) : _counter = counter; 311 | 312 | /// [CounterController] が保持・操作すべき [Counter] モデル。 313 | final Counter _counter; 314 | 315 | /// 「カウントアップボタンを押す。 316 | void countUp() => _counter.increment(); 317 | 318 | /// 「リストに追加」ボタンを押す。 319 | /// 合計値が 5 の倍数であった場合には [SnackBar] を表示する。 320 | void addToList(BuildContext context) { 321 | _counter.append(); 322 | final total = _counter.calculateTotal(); 323 | if (_counter.isTotalMultipleOfFive()) { 324 | ScaffoldMessenger.of(context).showSnackBar( 325 | SnackBar( 326 | content: Text('合計値 ($total) は 5 の倍数です!'), 327 | ), 328 | ); 329 | } 330 | } 331 | 332 | /// 「クリア」ボタンを押す。 333 | void clear() => _counter.clear(); 334 | } 335 | ``` 336 | 337 | ### View の実装 338 | 339 | さいごに View の実装を行います。ユーザーが見るべき画面を構成します。 340 | 341 | `context.watch()` によってモデルのインスタンスを監視し、モデルの変更が通知された際に画面が再描画されるようになっています。`ElevatedButton` の `onPressed` の処理では `context.read()` で参照した `CounterController` のインスタンスの各メソッドをコールしています。 342 | 343 | ```dart 344 | class CounterPage extends StatelessWidget { 345 | const CounterPage({super.key}); 346 | 347 | @override 348 | Widget build(BuildContext context) { 349 | final counter = context.watch(); 350 | return Scaffold( 351 | body: Center( 352 | child: Column( 353 | mainAxisAlignment: MainAxisAlignment.center, 354 | children: [ 355 | const Text('カウント'), 356 | Text(counter.count.toString()), 357 | const SizedBox(height: 16), 358 | const Text('リスト'), 359 | Text(counter.counts.toString()), 360 | const SizedBox(height: 16), 361 | const Text('合計値'), 362 | Text(counter.calculateTotal().toString()), 363 | const SizedBox(height: 16), 364 | ElevatedButton( 365 | onPressed: () => context.read().countUp(), 366 | child: const Text('カウントアップ'), 367 | ), 368 | const SizedBox(height: 16), 369 | ElevatedButton( 370 | onPressed: () => 371 | context.read().addToList(context), 372 | child: const Text('リストに追加'), 373 | ), 374 | const SizedBox(height: 16), 375 | ElevatedButton( 376 | onPressed: () => context.read().clear(), 377 | child: const Text('クリア'), 378 | ), 379 | ], 380 | ), 381 | ), 382 | ); 383 | } 384 | } 385 | ``` 386 | 387 | ## さいごに 388 | 389 | この章では、MVC アーキテクチャの概要を述べて、それをどのように解釈して Flutter に適用することができるか説明しました。 390 | 391 | サンプルアプリでは、その具体例を示しながら、モデルのユニットテストも記述しました。 392 | 393 | どのモジュールが他のどのモジュールに依存することは許して、反対にどのモジュールへの依存は許さないかを明確化し、依存させる場合にはどのように依存すると良いかの具体例を示すことで、テスト容易性、依存性の注入、PDS (Presentation Domain Separation) などの概念に関しても、意識したり学んだりするきっかけになるとも思います。 394 | 395 | MVC アーキテクチャは最も抽象的なアーキテクチャとして、その思想を学ぶことは、今後他の様々なアーキテクチャを学ぶ上でも重要です。それぞれのアーキテクチャのそれぞれのレイヤーや役割が MVC のどれに相当するのかを考えることで理解が深まるでしょう。 396 | 397 | また、ある程度大きい規模のアプリケーションであっても、MVC アーキテクチャによって十分に高い開発体験やテスト容易性を担保した開発を行うことができるはずです。 398 | 399 | 今後の Flutter の実装方針を考える参考にしてみてください。 400 | -------------------------------------------------------------------------------- /all_lint_rules.yaml: -------------------------------------------------------------------------------- 1 | linter: 2 | rules: 3 | - always_declare_return_types 4 | - always_put_control_body_on_new_line 5 | - always_put_required_named_parameters_first 6 | - always_require_non_null_named_parameters 7 | - always_specify_types 8 | - always_use_package_imports 9 | - annotate_overrides 10 | - avoid_annotating_with_dynamic 11 | - avoid_bool_literals_in_conditional_expressions 12 | - avoid_catches_without_on_clauses 13 | - avoid_catching_errors 14 | - avoid_classes_with_only_static_members 15 | - avoid_double_and_int_checks 16 | - avoid_dynamic_calls 17 | - avoid_empty_else 18 | - avoid_equals_and_hash_code_on_mutable_classes 19 | - avoid_escaping_inner_quotes 20 | - avoid_field_initializers_in_const_classes 21 | - avoid_final_parameters 22 | - avoid_function_literals_in_foreach_calls 23 | - avoid_implementing_value_types 24 | - avoid_init_to_null 25 | - avoid_js_rounded_ints 26 | - avoid_multiple_declarations_per_line 27 | - avoid_null_checks_in_equality_operators 28 | - avoid_positional_boolean_parameters 29 | - avoid_print 30 | - avoid_private_typedef_functions 31 | - avoid_redundant_argument_values 32 | - avoid_relative_lib_imports 33 | - avoid_renaming_method_parameters 34 | - avoid_return_types_on_setters 35 | - avoid_returning_null 36 | - avoid_returning_null_for_future 37 | - avoid_returning_null_for_void 38 | - avoid_returning_this 39 | - avoid_setters_without_getters 40 | - avoid_shadowing_type_parameters 41 | - avoid_single_cascade_in_expression_statements 42 | - avoid_slow_async_io 43 | - avoid_type_to_string 44 | - avoid_types_as_parameter_names 45 | - avoid_types_on_closure_parameters 46 | - avoid_unnecessary_containers 47 | - avoid_unused_constructor_parameters 48 | - avoid_void_async 49 | - avoid_web_libraries_in_flutter 50 | - await_only_futures 51 | - camel_case_extensions 52 | - camel_case_types 53 | - cancel_subscriptions 54 | - cascade_invocations 55 | - cast_nullable_to_non_nullable 56 | - close_sinks 57 | - combinators_ordering 58 | - comment_references 59 | - conditional_uri_does_not_exist 60 | - constant_identifier_names 61 | - control_flow_in_finally 62 | - curly_braces_in_flow_control_structures 63 | - depend_on_referenced_packages 64 | - deprecated_consistency 65 | - diagnostic_describe_all_properties 66 | - directives_ordering 67 | - discarded_futures 68 | - do_not_use_environment 69 | - empty_catches 70 | - empty_constructor_bodies 71 | - empty_statements 72 | - eol_at_end_of_file 73 | - exhaustive_cases 74 | - file_names 75 | - flutter_style_todos 76 | - hash_and_equals 77 | - implementation_imports 78 | - iterable_contains_unrelated_type 79 | - join_return_with_assignment 80 | - leading_newlines_in_multiline_strings 81 | - library_names 82 | - library_prefixes 83 | - library_private_types_in_public_api 84 | - lines_longer_than_80_chars 85 | - list_remove_unrelated_type 86 | - literal_only_boolean_expressions 87 | - missing_whitespace_between_adjacent_strings 88 | - no_adjacent_strings_in_list 89 | - no_default_cases 90 | - no_duplicate_case_values 91 | - no_leading_underscores_for_library_prefixes 92 | - no_leading_underscores_for_local_identifiers 93 | - no_logic_in_create_state 94 | - no_runtimeType_toString 95 | - non_constant_identifier_names 96 | - noop_primitive_operations 97 | - null_check_on_nullable_type_parameter 98 | - null_closures 99 | - omit_local_variable_types 100 | - one_member_abstracts 101 | - only_throw_errors 102 | - overridden_fields 103 | - package_api_docs 104 | - package_names 105 | - package_prefixed_library_names 106 | - parameter_assignments 107 | - prefer_adjacent_string_concatenation 108 | - prefer_asserts_in_initializer_lists 109 | - prefer_asserts_with_message 110 | - prefer_collection_literals 111 | - prefer_conditional_assignment 112 | - prefer_const_constructors 113 | - prefer_const_constructors_in_immutables 114 | - prefer_const_declarations 115 | - prefer_const_literals_to_create_immutables 116 | - prefer_constructors_over_static_methods 117 | - prefer_contains 118 | - prefer_double_quotes 119 | - prefer_equal_for_default_values 120 | - prefer_expression_function_bodies 121 | - prefer_final_fields 122 | - prefer_final_in_for_each 123 | - prefer_final_locals 124 | - prefer_final_parameters 125 | - prefer_for_elements_to_map_fromIterable 126 | - prefer_foreach 127 | - prefer_function_declarations_over_variables 128 | - prefer_generic_function_type_aliases 129 | - prefer_if_elements_to_conditional_expressions 130 | - prefer_if_null_operators 131 | - prefer_initializing_formals 132 | - prefer_inlined_adds 133 | - prefer_int_literals 134 | - prefer_interpolation_to_compose_strings 135 | - prefer_is_empty 136 | - prefer_is_not_empty 137 | - prefer_is_not_operator 138 | - prefer_iterable_whereType 139 | - prefer_mixin 140 | - prefer_null_aware_method_calls 141 | - prefer_null_aware_operators 142 | - prefer_relative_imports 143 | - prefer_single_quotes 144 | - prefer_spread_collections 145 | - prefer_typing_uninitialized_variables 146 | - prefer_void_to_null 147 | - provide_deprecation_message 148 | - public_member_api_docs 149 | - recursive_getters 150 | - require_trailing_commas 151 | - secure_pubspec_urls 152 | - sized_box_for_whitespace 153 | - sized_box_shrink_expand 154 | - slash_for_doc_comments 155 | - sort_child_properties_last 156 | - sort_constructors_first 157 | - sort_pub_dependencies 158 | - sort_unnamed_constructors_first 159 | - test_types_in_equals 160 | - throw_in_finally 161 | - tighten_type_of_initializing_formals 162 | - type_annotate_public_apis 163 | - type_init_formals 164 | - unawaited_futures 165 | - unnecessary_await_in_return 166 | - unnecessary_brace_in_string_interps 167 | - unnecessary_const 168 | - unnecessary_constructor_name 169 | - unnecessary_final 170 | - unnecessary_getters_setters 171 | - unnecessary_lambdas 172 | - unnecessary_late 173 | - unnecessary_new 174 | - unnecessary_null_aware_assignments 175 | - unnecessary_null_aware_operator_on_extension_on_nullable 176 | - unnecessary_null_checks 177 | - unnecessary_null_in_if_null_operators 178 | - unnecessary_nullable_for_final_variable_declarations 179 | - unnecessary_overrides 180 | - unnecessary_parenthesis 181 | - unnecessary_raw_strings 182 | - unnecessary_statements 183 | - unnecessary_string_escapes 184 | - unnecessary_string_interpolations 185 | - unnecessary_this 186 | - unnecessary_to_list_in_spreads 187 | - unreachable_from_main 188 | - unrelated_type_equality_checks 189 | - unsafe_html 190 | - use_build_context_synchronously 191 | - use_colored_box 192 | - use_decorated_box 193 | - use_enums 194 | - use_full_hex_values_for_flutter_colors 195 | - use_function_type_syntax_for_parameters 196 | - use_if_null_to_convert_nulls_to_bools 197 | - use_is_even_rather_than_modulo 198 | - use_key_in_widget_constructors 199 | - use_late_for_private_fields_and_variables 200 | - use_named_constants 201 | - use_raw_strings 202 | - use_rethrow_when_possible 203 | - use_setters_to_change_properties 204 | - use_string_buffers 205 | - use_string_in_part_of_directives 206 | - use_super_parameters 207 | - use_test_throws_matchers 208 | - use_to_and_as_if_applicable 209 | - valid_regexps 210 | - void_checks 211 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: all_lint_rules.yaml 2 | analyzer: 3 | language: 4 | strict-casts: true 5 | strict-inference: true 6 | strict-raw-types: true 7 | errors: 8 | included_file_warning: ignore 9 | invalid_use_of_visible_for_testing_member: error 10 | 11 | linter: 12 | rules: 13 | cascade_invocations: false 14 | prefer_final_parameters: false 15 | public_member_api_docs: false 16 | discarded_futures: false 17 | combinators_ordering: false 18 | eol_at_end_of_file: false 19 | no_leading_underscores_for_local_identifiers: false 20 | one_member_abstracts: false 21 | diagnostic_describe_all_properties: false 22 | prefer_double_quotes: false 23 | always_specify_types: false 24 | unnecessary_final: false 25 | prefer_expression_function_bodies: false 26 | always_put_required_named_parameters_first: false 27 | flutter_style_todos: false 28 | avoid_annotating_with_dynamic: false 29 | always_use_package_imports: false 30 | no_default_cases: false 31 | -------------------------------------------------------------------------------- /lib/counter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// カウンターの振る舞いを表現するモデル。 4 | class Counter extends ChangeNotifier { 5 | int get count => _count; 6 | 7 | List get counts => _counts; 8 | 9 | /// カウント値。 10 | int _count = 0; 11 | 12 | /// カウント値のリスト。 13 | final List _counts = []; 14 | 15 | /// カウント値に 1 を加算する。 16 | void increment() { 17 | _count++; 18 | notifyListeners(); 19 | } 20 | 21 | /// カウント値をリストに追加する。 22 | void append() { 23 | _counts.add(_count); 24 | notifyListeners(); 25 | } 26 | 27 | /// カウント値とカウント値のリストをクリアする。 28 | void clear() { 29 | _count = 0; 30 | _counts.clear(); 31 | notifyListeners(); 32 | } 33 | 34 | /// カウント値のリストの合計を計算する。 35 | int calculateTotal() { 36 | return _counts.fold(0, (a, b) => a + b); 37 | } 38 | 39 | /// カウント値の合計が 5 の倍数であるかを判定する。 40 | bool isTotalMultipleOfFive() => calculateTotal() % 5 == 0; 41 | } 42 | -------------------------------------------------------------------------------- /lib/counter_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'counter.dart'; 4 | 5 | /// ユーザーによる操作を解釈して [Counter] モデルを操作したり、モデルを UI に反映 6 | /// させたりするコントローラ。 7 | class CounterController { 8 | const CounterController(Counter counter) : _counter = counter; 9 | 10 | /// [CounterController] が保持・操作すべき [Counter] モデル。 11 | final Counter _counter; 12 | 13 | /// 「カウントアップボタンを押す。 14 | void countUp() => _counter.increment(); 15 | 16 | /// 「リストに追加」ボタンを押す。 17 | /// 合計値が 5 の倍数であった場合には [SnackBar] を表示する。 18 | void addToList(BuildContext context) { 19 | _counter.append(); 20 | final total = _counter.calculateTotal(); 21 | if (_counter.isTotalMultipleOfFive()) { 22 | ScaffoldMessenger.of(context).showSnackBar( 23 | SnackBar( 24 | content: Text('合計値 ($total) は 5 の倍数です!'), 25 | ), 26 | ); 27 | } 28 | } 29 | 30 | /// 「クリア」ボタンを押す。 31 | void clear() => _counter.clear(); 32 | } 33 | -------------------------------------------------------------------------------- /lib/counter_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | 4 | import 'counter.dart'; 5 | import 'counter_controller.dart'; 6 | 7 | class CounterPage extends StatelessWidget { 8 | const CounterPage({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | final counter = context.watch(); 13 | return Scaffold( 14 | body: Center( 15 | child: Column( 16 | mainAxisAlignment: MainAxisAlignment.center, 17 | children: [ 18 | const Text('カウント'), 19 | Text(counter.count.toString()), 20 | const SizedBox(height: 16), 21 | const Text('リスト'), 22 | Text(counter.counts.toString()), 23 | const SizedBox(height: 16), 24 | const Text('合計値'), 25 | Text(counter.calculateTotal().toString()), 26 | const SizedBox(height: 16), 27 | ElevatedButton( 28 | onPressed: () => context.read().countUp(), 29 | child: const Text('カウントアップ'), 30 | ), 31 | const SizedBox(height: 16), 32 | ElevatedButton( 33 | onPressed: () => 34 | context.read().addToList(context), 35 | child: const Text('リストに追加'), 36 | ), 37 | const SizedBox(height: 16), 38 | ElevatedButton( 39 | onPressed: () => context.read().clear(), 40 | child: const Text('クリア'), 41 | ), 42 | ], 43 | ), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | 4 | import 'counter.dart'; 5 | import 'counter_controller.dart'; 6 | import 'counter_page.dart'; 7 | 8 | void main() { 9 | runApp(const MyApp()); 10 | } 11 | 12 | class MyApp extends StatelessWidget { 13 | const MyApp({super.key}); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return MultiProvider( 18 | providers: [ 19 | ChangeNotifierProvider(create: (_) => Counter()), 20 | ProxyProvider( 21 | update: (_, counter, __) => CounterController(counter), 22 | ), 23 | ], 24 | child: MaterialApp( 25 | title: 'Flutter MVC Counter', 26 | debugShowCheckedModeBanner: false, 27 | theme: ThemeData( 28 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), 29 | useMaterial3: true, 30 | ), 31 | home: const CounterPage(), 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.11.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.1" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.3.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.1" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.17.1" 44 | cupertino_icons: 45 | dependency: "direct main" 46 | description: 47 | name: cupertino_icons 48 | sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.0.5" 52 | fake_async: 53 | dependency: transitive 54 | description: 55 | name: fake_async 56 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.3.1" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | js: 71 | dependency: transitive 72 | description: 73 | name: js 74 | sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 75 | url: "https://pub.dev" 76 | source: hosted 77 | version: "0.6.7" 78 | matcher: 79 | dependency: transitive 80 | description: 81 | name: matcher 82 | sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" 83 | url: "https://pub.dev" 84 | source: hosted 85 | version: "0.12.15" 86 | material_color_utilities: 87 | dependency: transitive 88 | description: 89 | name: material_color_utilities 90 | sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "0.2.0" 94 | meta: 95 | dependency: transitive 96 | description: 97 | name: meta 98 | sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" 99 | url: "https://pub.dev" 100 | source: hosted 101 | version: "1.9.1" 102 | nested: 103 | dependency: transitive 104 | description: 105 | name: nested 106 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 107 | url: "https://pub.dev" 108 | source: hosted 109 | version: "1.0.0" 110 | path: 111 | dependency: transitive 112 | description: 113 | name: path 114 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 115 | url: "https://pub.dev" 116 | source: hosted 117 | version: "1.8.3" 118 | provider: 119 | dependency: "direct main" 120 | description: 121 | name: provider 122 | sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f 123 | url: "https://pub.dev" 124 | source: hosted 125 | version: "6.0.5" 126 | sky_engine: 127 | dependency: transitive 128 | description: flutter 129 | source: sdk 130 | version: "0.0.99" 131 | source_span: 132 | dependency: transitive 133 | description: 134 | name: source_span 135 | sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 136 | url: "https://pub.dev" 137 | source: hosted 138 | version: "1.9.1" 139 | stack_trace: 140 | dependency: transitive 141 | description: 142 | name: stack_trace 143 | sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 144 | url: "https://pub.dev" 145 | source: hosted 146 | version: "1.11.0" 147 | stream_channel: 148 | dependency: transitive 149 | description: 150 | name: stream_channel 151 | sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" 152 | url: "https://pub.dev" 153 | source: hosted 154 | version: "2.1.1" 155 | string_scanner: 156 | dependency: transitive 157 | description: 158 | name: string_scanner 159 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "1.2.0" 163 | term_glyph: 164 | dependency: transitive 165 | description: 166 | name: term_glyph 167 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 168 | url: "https://pub.dev" 169 | source: hosted 170 | version: "1.2.1" 171 | test_api: 172 | dependency: transitive 173 | description: 174 | name: test_api 175 | sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb 176 | url: "https://pub.dev" 177 | source: hosted 178 | version: "0.5.1" 179 | vector_math: 180 | dependency: transitive 181 | description: 182 | name: vector_math 183 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 184 | url: "https://pub.dev" 185 | source: hosted 186 | version: "2.1.4" 187 | sdks: 188 | dart: ">=3.0.2 <4.0.0" 189 | flutter: ">=1.16.0" 190 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_mvc_counter 2 | description: A new Flutter project. 3 | publish_to: 'none' 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: '>=3.0.2 <4.0.0' 8 | 9 | dependencies: 10 | cupertino_icons: ^1.0.2 11 | flutter: 12 | sdk: flutter 13 | provider: ^6.0.5 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | 19 | flutter: 20 | uses-material-design: true 21 | -------------------------------------------------------------------------------- /test/counter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_mvc_counter/counter.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | late Counter counter; 6 | 7 | setUp(() { 8 | counter = Counter(); 9 | }); 10 | 11 | group('Counter', () { 12 | test('初期値は0である', () { 13 | expect(counter.count, 0); 14 | }); 15 | 16 | test('値が正しくインクリメントされる', () { 17 | counter.increment(); 18 | expect(counter.count, 1); 19 | }); 20 | 21 | test('値がリストに正しく追加される', () { 22 | counter.append(); 23 | expect(counter.counts.length, 1); 24 | expect(counter.counts[0], 0); 25 | 26 | counter.increment(); 27 | counter.append(); 28 | expect(counter.counts.length, 2); 29 | expect(counter.counts[1], 1); 30 | }); 31 | 32 | test('値とリストが正しくクリアされる', () { 33 | counter.increment(); 34 | counter.append(); 35 | counter.clear(); 36 | expect(counter.count, 0); 37 | expect(counter.counts.isEmpty, true); 38 | }); 39 | 40 | test('リストの値の合計が正しく計算される', () { 41 | counter.increment(); 42 | counter.append(); 43 | counter.increment(); 44 | counter.append(); 45 | expect(counter.calculateTotal(), 3); 46 | }); 47 | 48 | test('リストの値の合計が 5 の倍数である判定が正しくされる', () { 49 | counter.increment(); 50 | counter.increment(); 51 | counter.append(); 52 | expect(counter.isTotalMultipleOfFive(), false); 53 | counter.increment(); 54 | counter.append(); 55 | expect(counter.isTotalMultipleOfFive(), true); 56 | }); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kosukesaigusa/flutter-mvc-counter/bb826aa969dc9bd936b571bbe82fdf1f6bb3c5b9/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kosukesaigusa/flutter-mvc-counter/bb826aa969dc9bd936b571bbe82fdf1f6bb3c5b9/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kosukesaigusa/flutter-mvc-counter/bb826aa969dc9bd936b571bbe82fdf1f6bb3c5b9/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kosukesaigusa/flutter-mvc-counter/bb826aa969dc9bd936b571bbe82fdf1f6bb3c5b9/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kosukesaigusa/flutter-mvc-counter/bb826aa969dc9bd936b571bbe82fdf1f6bb3c5b9/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | flutter_mvc_counter 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter_mvc_counter", 3 | "short_name": "flutter_mvc_counter", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | --------------------------------------------------------------------------------