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

Session/8 #21

Merged
merged 34 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
15f36d9
Add riverpod related packages
Sep 13, 2023
97eba52
Wrap with ProviderScope
Sep 13, 2023
65d4fe1
Add lint rule: prefer absolute path
Sep 15, 2023
e0fd43e
Enable riverpod_lint
Sep 15, 2023
965cb3a
Implement YumemiWeather DI
Sep 15, 2023
6215fd3
Update fetchWeather reference
Sep 15, 2023
cb4d385
Create UiState
Sep 15, 2023
02ecb06
Update weather_page.dart based on UiState
Sep 15, 2023
70b4d65
Update weather_forecast.dart based on UiState
Sep 15, 2023
57d2d55
Add build procedure on README.md
Sep 18, 2023
4146055
Rename provider and file name: UiState(ui_state.dart) -> WeatherState…
Sep 18, 2023
9b39dc4
Create ARCHITECTURE.md
Sep 18, 2023
1d0133d
Fix lint warnings
Sep 18, 2023
464db20
Edit image size
Sep 18, 2023
1bb46f6
Edit ARCHITECTURE.md
Sep 18, 2023
52de150
Update doc/ARCHITECTURE.md
mqkotoo Sep 20, 2023
7d4a11f
Edit lint rule
Sep 20, 2023
4338e84
Fix: ref.read -> ref.watch
Sep 20, 2023
8533bd3
Fix: throw UnimplementedError -> return YumemiWeather()
Sep 20, 2023
8ad3a27
Update README.md
Sep 20, 2023
9a1e8a0
Add graph.md
Sep 20, 2023
b91f7ce
Rename: WeatherState -> WeatherStateNotifier
Sep 22, 2023
f349515
Rename: WeatherData -> WeatherForecast(model)
Sep 22, 2023
40bd88f
Rename: WeatherForecast -> WeatherForecastPanel(component) to avoid n…
Sep 22, 2023
2a40bf0
Update ARCHITECTURE.md
Sep 22, 2023
b35e4a4
Add duplicate_ignore to fix warnings
Sep 26, 2023
516fe44
Move YumemiWeatherClientProvider to weather_service.dart
Sep 26, 2023
c9e20c5
Rename file: weather_state.dart -> weather_state_notifier.dart
Sep 26, 2023
4391026
Update ARCHITECTURE.md
Sep 26, 2023
2494991
Update graph.md
Sep 26, 2023
1f88e72
Rename: weatherData -> weatherState
Sep 26, 2023
2de8a51
Delete graph.md
Sep 27, 2023
d297f58
Update ARCHITECTURE.md
Sep 27, 2023
177b379
Update ARCHITECTURE.md
Sep 27, 2023
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
44 changes: 42 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,43 @@
# flutter_training
# ゆめみ Flutter研修課題

A new Flutter project.
テンプレート

- https://github.com/yumemi-inc/flutter-training-template

## 環境構築

*リポジトリをクローン

```
git clone https://github.com/mqkotoo/flutter_training.git
```

* 作業ディレクトリを変更する

```
cd flutter_training
```

* fvmに指定されたバージョンのFlutterをインストールする

```
fvm install
```

* 依存パッケージをインストールする

```
fvm flutter pub get
```

* freezed等のコード生成

trm11tkr marked this conversation as resolved.
Show resolved Hide resolved
```
fvm flutter pub run build_runner build
```

* ビルドラン

```
fvm flutter run
```
5 changes: 5 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ include: package:blendthink_lints/recommended.yaml
linter:
rules:
sort_pub_dependencies: false
always_use_package_imports: true
trm11tkr marked this conversation as resolved.
Show resolved Hide resolved

analyzer:
plugins:
- custom_lint
trm11tkr marked this conversation as resolved.
Show resolved Hide resolved
48 changes: 48 additions & 0 deletions doc/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## アーキテクチャ
trm11tkr marked this conversation as resolved.
Show resolved Hide resolved

今回のプロジェクトではあえて、いわゆるアーキテクチャというような大掛かりな設計はせずに、[`WeatherState`](../lib/state/weather_state.dart)
という、天気の取得操作、結果を管理するプロバイダーを作成し、Viewレイヤとやりとりするだけのシンプルな設計にしました。

### ルール

* Sateレイヤでなんらかの状態を操作して保管する。
mqkotoo marked this conversation as resolved.
Show resolved Hide resolved
* Viewレイヤから`ref.read`してStateの操作、`ref.watch`してStateの表示を行う。
* Stateレイヤのロジックが肥大する場合はServiceレイヤに移動させる。

上記のルールに基づいて開発を進めました。

### モチベーション

* 今回のような規模が小さいプロジェクトに、たくさんレイヤーがあるアーキテクチャを採用すると無駄なレイヤーが増え、複雑化する。
* 無理やり一定のアーキテクチャの型にはめると、個人的にはまだアーキテクチャの知識が浅いので、実際の必要性を感じることなく形だけのアーキテクチャになる可能性がある。
* 簡潔でシンプルでわかりやすい。(主観になりますが。。)

<img height="700" src="image/architecture.jpg"></img>

## View

### WeatherPage

* アプリのメイン画面
* `Reload`ボタンを押して、`weatherStateProvider`をreadして、天気の取得処理を行う。
* `weatherStateProvider`で天気取得に失敗した場合は、エラーメッセージを表示する。

### WeatherForecast

* 天気の情報、気温等を表示しているコンポネント。
* `weatherStateProvider`をwatchして、天気の状態を表示する。

## State

### WeatherState

* 天気データの取得、管理するプロバイダー
* 天気の取得が成功したらそのデータを格納し、失敗した場合は、`onError`関数を引数で受け取る。

## Service

* `WeatherState`での処理が肥大化したので天気を取得する処理はここに移動した。

## Model

* 天気情報やリクエストを送るモデルを定義
Binary file added doc/image/architecture.jpg
trm11tkr marked this conversation as resolved.
Show resolved Hide resolved
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 15 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_training/utils/provider/yumemi_weather_client.dart';
import 'package:flutter_training/view/launch_page.dart';
import 'package:yumemi_weather/yumemi_weather.dart';

void main() {
runApp(const MainApp());
WidgetsFlutterBinding.ensureInitialized();
runApp(
ProviderScope(
overrides: [
//YumemiWeatherクライアントを注入
yumemiWeatherClientProvider.overrideWithValue(
YumemiWeather(),
),
],
child: const MainApp(),
),
);
}

class MainApp extends StatelessWidget {
Expand Down
10 changes: 10 additions & 0 deletions lib/service/weather_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@ import 'dart:convert';
import 'package:flutter_training/model/weather_data.dart';
import 'package:flutter_training/model/weather_request.dart';
import 'package:flutter_training/utils/api/result.dart';
import 'package:flutter_training/utils/provider/yumemi_weather_client.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:yumemi_weather/yumemi_weather.dart';

part 'weather_service.g.dart';

@riverpod
WeatherService weatherService(WeatherServiceRef ref) {
return WeatherService(ref.read(yumemiWeatherClientProvider));
trm11tkr marked this conversation as resolved.
Show resolved Hide resolved
}

class WeatherService {
WeatherService(this._client);

final YumemiWeather _client;

/// Get weather information
///
/// If successful, the value is stored in [Success],
/// if unsuccessful, the error message is stored in [Failure].

Expand Down
25 changes: 25 additions & 0 deletions lib/service/weather_service.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions lib/state/weather_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:flutter_training/model/weather_data.dart';
import 'package:flutter_training/model/weather_request.dart';
import 'package:flutter_training/service/weather_service.dart';
import 'package:flutter_training/utils/api/result.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'weather_state.g.dart';

@riverpod
class WeatherState extends _$WeatherState {
trm11tkr marked this conversation as resolved.
Show resolved Hide resolved
@override
WeatherData? build() => null;

void getWeather({
required WeatherRequest request,
required void Function(String error) onError,
}) {
return switch (ref.read(weatherServiceProvider).fetchWeather(request)) {
Success(value: final value) => state = value,
Failure(exception: final error) => onError.call(error),
};
}
trm11tkr marked this conversation as resolved.
Show resolved Hide resolved
}
25 changes: 25 additions & 0 deletions lib/state/weather_state.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions lib/utils/provider/yumemi_weather_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:yumemi_weather/yumemi_weather.dart';

part 'yumemi_weather_client.g.dart';

//初回に上書きしてキャッシュして使うので、直接アクセスはできないようにする
trm11tkr marked this conversation as resolved.
Show resolved Hide resolved
@riverpod
YumemiWeather yumemiWeatherClient(YumemiWeatherClientRef ref) {
throw UnimplementedError();
}
trm11tkr marked this conversation as resolved.
Show resolved Hide resolved
26 changes: 26 additions & 0 deletions lib/utils/provider/yumemi_weather_client.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions lib/view/weather_view/component/weather_forecast.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_training/model/weather_condition.dart';
import 'package:flutter_training/model/weather_data.dart';
import 'package:flutter_training/state/weather_state.dart';

class WeatherForecast extends StatelessWidget {
const WeatherForecast({super.key, required this.weatherData});

final WeatherData? weatherData;
class WeatherForecast extends ConsumerWidget {
const WeatherForecast({super.key});

@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final textTheme = Theme.of(context).textTheme;
final weatherData = ref.watch(weatherStateProvider);
return Column(
children: [
//Weather conditions
Expand Down
51 changes: 23 additions & 28 deletions lib/view/weather_view/weather_page.dart
Original file line number Diff line number Diff line change
@@ -1,45 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_training/model/weather_data.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_training/model/weather_request.dart';
import 'package:flutter_training/service/weather_service.dart';
import 'package:flutter_training/utils/api/result.dart';
import 'package:flutter_training/state/weather_state.dart';
import 'package:flutter_training/view/weather_view/component/weather_forecast.dart';
import 'package:yumemi_weather/yumemi_weather.dart';

class WeatherPage extends StatefulWidget {
class WeatherPage extends ConsumerWidget {
const WeatherPage({super.key});

@override
State<WeatherPage> createState() => _WeatherPageState();
}

class _WeatherPageState extends State<WeatherPage> {
WeatherData? weatherData;
final service = WeatherService(YumemiWeather());
Widget build(BuildContext context, WidgetRef ref) {
void onReloaded(WeatherRequest request) {
ref.read(weatherStateProvider.notifier).getWeather(
request: request,
onError: (errorMessage) => showDialog<void>(
barrierDismissible: false,
context: context,
builder: (_) => _ErrorDialog(errorMessage),
),
);
}

void _onReloaded() {
//fetchWeatherの結果がSuccessかFailureかで処理を分ける
return switch (service
.fetchWeather(WeatherRequest(area: 'Aichi', date: DateTime.now()))) {
Success(value: final value) => setState(() => weatherData = value),
Failure(exception: final error) => showDialog<void>(
barrierDismissible: false,
context: context,
builder: (_) => _ErrorDialog(error),
),
};
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FractionallySizedBox(
widthFactor: 0.5,
child: Column(
children: [
const Spacer(),
WeatherForecast(weatherData: weatherData),
const WeatherForecast(),
Flexible(
child: Column(
children: [
Expand All @@ -54,7 +42,14 @@ class _WeatherPageState extends State<WeatherPage> {
),
Expanded(
child: TextButton(
onPressed: _onReloaded,
onPressed: () {
onReloaded(
WeatherRequest(
area: 'Nagoya',
date: DateTime.now(),
),
);
},
child: const Text('Reload'),
),
),
Expand Down
Loading
Loading