diff --git a/README.md b/README.md index bbf20ac..9497bcc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,37 @@ -# 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 +``` + +* ビルドラン + +``` +fvm flutter run +``` diff --git a/analysis_options.yaml b/analysis_options.yaml index 04f6aaf..62768d9 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -3,3 +3,10 @@ include: package:blendthink_lints/recommended.yaml linter: rules: sort_pub_dependencies: false + +analyzer: + errors: + # riverpod_generatorの影響でg.dartの末尾に不要な指定がつくので記載 + duplicate_ignore: ignore + plugins: + - custom_lint diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md new file mode 100644 index 0000000..7c8a8be --- /dev/null +++ b/doc/ARCHITECTURE.md @@ -0,0 +1,93 @@ +## アーキテクチャ + +今回のプロジェクトではあえて、いわゆるアーキテクチャというような大掛かりな設計はせずに、[`WeatherStateNotifier`](../lib/state/weather_state_notifier.dart) +という、天気の取得操作、結果を管理するプロバイダーを作成し、Viewレイヤとやりとりするだけのシンプルな設計にしました。 + +### ルール + +* Stateレイヤでなんらかの状態を操作して保管する。 +* Viewレイヤから`ref.read`してStateの操作、`ref.watch`してStateの表示を行う。 +* Stateレイヤのロジックが肥大する場合はServiceレイヤに移動させる。 + +上記のルールに基づいて開発を進めました。 + +### モチベーション + +* 今回のような規模が小さいプロジェクトに、たくさんレイヤーがあるアーキテクチャを採用すると無駄なレイヤーが増え、複雑化する。 +* 無理やり一定のアーキテクチャの型にはめると、個人的にはまだアーキテクチャの知識が浅いので、実際の必要性を感じることなく形だけのアーキテクチャになる可能性がある。 +* 簡潔でシンプルでわかりやすい。(主観になりますが。。) + +## Model + +* 天気情報やリクエストを送るモデルを定義、変換処理等を定義 + +### WeatherCondition + +* 天気のステータスを定義、取得した天気に応じてイメージを返すextensionを定義 + +### WeatherForecast + +* 天気の情報、最低気温、最高気温、日にちを定義 +* データの変換処理を定義 + +### WeatherRequest + +* リクエストを送るクラスを定義 + +## Service + +* 天気を取得して、[Result](../lib/utils/api/result.dart)型に変換するメソッドを提供する + +## State + +* `Service`からResultを取得し、取得した天気のデータを保持する + +### WeatherStateNotifier + +* 天気の取得が成功したらそのデータを格納し、失敗した場合は、`onError`関数を引数で受け取る。 + +## View + +* 天気の表示、エラーのダイアログを表示。 天気を取得するボタンを提供。 + +### WeatherPage + +* アプリのメイン画面 +* `Reload`ボタンを押して、`weatherStateNotifierProvider`をreadして、天気の取得処理を行う。 +* `weatherStateNotifierProvider`で天気取得に失敗した場合は、エラーメッセージを表示する。 + +### WeatherForecastPanel + +* 天気の情報、気温等を表示しているコンポネント。 +* `weatherStateNotifierProvider`をwatchして、天気の状態を表示する。 + +```mermaid +flowchart TB + subgraph Arrows + direction LR + start1[ ] -..->|read| stop1[ ] + style start1 height:0px; + style stop1 height:0px; + start2[ ] --->|listen| stop2[ ] + style start2 height:0px; + style stop2 height:0px; + start3[ ] ===>|watch| stop3[ ] + style start3 height:0px; + style stop3 height:0px; + end + subgraph Type + direction TB + ConsumerWidget((widget)); + Provider[[provider]]; + end + + weatherStateNotifierProvider[["weatherStateNotifierProvider"]]; + weatherServiceProvider[["weatherServiceProvider"]]; + yumemiWeatherClientProvider[["yumemiWeatherClientProvider"]]; + WeatherForecastPanel((WeatherForecastPanel)); + WeatherPage((WeatherPage)); + + weatherStateNotifierProvider ==> WeatherForecastPanel; + weatherStateNotifierProvider -.-> WeatherPage; + yumemiWeatherClientProvider ==> weatherServiceProvider; +``` \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 51589ea..f3952bc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_training/view/launch_page.dart'; void main() { - runApp(const MainApp()); + WidgetsFlutterBinding.ensureInitialized(); + runApp( + const ProviderScope( + child: MainApp(), + ), + ); } class MainApp extends StatelessWidget { diff --git a/lib/model/weather_data.dart b/lib/model/weather_forecast.dart similarity index 50% rename from lib/model/weather_data.dart rename to lib/model/weather_forecast.dart index d41eb0d..03afd4e 100644 --- a/lib/model/weather_data.dart +++ b/lib/model/weather_forecast.dart @@ -1,19 +1,19 @@ import 'package:flutter_training/model/weather_condition.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -part 'weather_data.freezed.dart'; +part 'weather_forecast.freezed.dart'; -part 'weather_data.g.dart'; +part 'weather_forecast.g.dart'; @freezed -class WeatherData with _$WeatherData { - const factory WeatherData({ +class WeatherForecast with _$WeatherForecast { + const factory WeatherForecast({ required WeatherCondition weatherCondition, required int maxTemperature, required int minTemperature, required DateTime date, - }) = _WeatherData; + }) = _WeatherForecast; - factory WeatherData.fromJson(Map json) => - _$WeatherDataFromJson(json); + factory WeatherForecast.fromJson(Map json) => + _$WeatherForecastFromJson(json); } diff --git a/lib/model/weather_data.freezed.dart b/lib/model/weather_forecast.freezed.dart similarity index 71% rename from lib/model/weather_data.freezed.dart rename to lib/model/weather_forecast.freezed.dart index c243c04..6e88740 100644 --- a/lib/model/weather_data.freezed.dart +++ b/lib/model/weather_forecast.freezed.dart @@ -3,7 +3,7 @@ // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark -part of 'weather_data.dart'; +part of 'weather_forecast.dart'; // ************************************************************************** // FreezedGenerator @@ -14,12 +14,12 @@ T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); -WeatherData _$WeatherDataFromJson(Map json) { - return _WeatherData.fromJson(json); +WeatherForecast _$WeatherForecastFromJson(Map json) { + return _WeatherForecast.fromJson(json); } /// @nodoc -mixin _$WeatherData { +mixin _$WeatherForecast { WeatherCondition get weatherCondition => throw _privateConstructorUsedError; int get maxTemperature => throw _privateConstructorUsedError; int get minTemperature => throw _privateConstructorUsedError; @@ -27,15 +27,15 @@ mixin _$WeatherData { Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) - $WeatherDataCopyWith get copyWith => + $WeatherForecastCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class $WeatherDataCopyWith<$Res> { - factory $WeatherDataCopyWith( - WeatherData value, $Res Function(WeatherData) then) = - _$WeatherDataCopyWithImpl<$Res, WeatherData>; +abstract class $WeatherForecastCopyWith<$Res> { + factory $WeatherForecastCopyWith( + WeatherForecast value, $Res Function(WeatherForecast) then) = + _$WeatherForecastCopyWithImpl<$Res, WeatherForecast>; @useResult $Res call( {WeatherCondition weatherCondition, @@ -45,9 +45,9 @@ abstract class $WeatherDataCopyWith<$Res> { } /// @nodoc -class _$WeatherDataCopyWithImpl<$Res, $Val extends WeatherData> - implements $WeatherDataCopyWith<$Res> { - _$WeatherDataCopyWithImpl(this._value, this._then); +class _$WeatherForecastCopyWithImpl<$Res, $Val extends WeatherForecast> + implements $WeatherForecastCopyWith<$Res> { + _$WeatherForecastCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; @@ -84,11 +84,11 @@ class _$WeatherDataCopyWithImpl<$Res, $Val extends WeatherData> } /// @nodoc -abstract class _$$_WeatherDataCopyWith<$Res> - implements $WeatherDataCopyWith<$Res> { - factory _$$_WeatherDataCopyWith( - _$_WeatherData value, $Res Function(_$_WeatherData) then) = - __$$_WeatherDataCopyWithImpl<$Res>; +abstract class _$$_WeatherForecastCopyWith<$Res> + implements $WeatherForecastCopyWith<$Res> { + factory _$$_WeatherForecastCopyWith( + _$_WeatherForecast value, $Res Function(_$_WeatherForecast) then) = + __$$_WeatherForecastCopyWithImpl<$Res>; @override @useResult $Res call( @@ -99,11 +99,11 @@ abstract class _$$_WeatherDataCopyWith<$Res> } /// @nodoc -class __$$_WeatherDataCopyWithImpl<$Res> - extends _$WeatherDataCopyWithImpl<$Res, _$_WeatherData> - implements _$$_WeatherDataCopyWith<$Res> { - __$$_WeatherDataCopyWithImpl( - _$_WeatherData _value, $Res Function(_$_WeatherData) _then) +class __$$_WeatherForecastCopyWithImpl<$Res> + extends _$WeatherForecastCopyWithImpl<$Res, _$_WeatherForecast> + implements _$$_WeatherForecastCopyWith<$Res> { + __$$_WeatherForecastCopyWithImpl( + _$_WeatherForecast _value, $Res Function(_$_WeatherForecast) _then) : super(_value, _then); @pragma('vm:prefer-inline') @@ -114,7 +114,7 @@ class __$$_WeatherDataCopyWithImpl<$Res> Object? minTemperature = null, Object? date = null, }) { - return _then(_$_WeatherData( + return _then(_$_WeatherForecast( weatherCondition: null == weatherCondition ? _value.weatherCondition : weatherCondition // ignore: cast_nullable_to_non_nullable @@ -137,15 +137,15 @@ class __$$_WeatherDataCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$_WeatherData implements _WeatherData { - const _$_WeatherData( +class _$_WeatherForecast implements _WeatherForecast { + const _$_WeatherForecast( {required this.weatherCondition, required this.maxTemperature, required this.minTemperature, required this.date}); - factory _$_WeatherData.fromJson(Map json) => - _$$_WeatherDataFromJson(json); + factory _$_WeatherForecast.fromJson(Map json) => + _$$_WeatherForecastFromJson(json); @override final WeatherCondition weatherCondition; @@ -158,14 +158,14 @@ class _$_WeatherData implements _WeatherData { @override String toString() { - return 'WeatherData(weatherCondition: $weatherCondition, maxTemperature: $maxTemperature, minTemperature: $minTemperature, date: $date)'; + return 'WeatherForecast(weatherCondition: $weatherCondition, maxTemperature: $maxTemperature, minTemperature: $minTemperature, date: $date)'; } @override bool operator ==(dynamic other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$_WeatherData && + other is _$_WeatherForecast && (identical(other.weatherCondition, weatherCondition) || other.weatherCondition == weatherCondition) && (identical(other.maxTemperature, maxTemperature) || @@ -183,26 +183,26 @@ class _$_WeatherData implements _WeatherData { @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$_WeatherDataCopyWith<_$_WeatherData> get copyWith => - __$$_WeatherDataCopyWithImpl<_$_WeatherData>(this, _$identity); + _$$_WeatherForecastCopyWith<_$_WeatherForecast> get copyWith => + __$$_WeatherForecastCopyWithImpl<_$_WeatherForecast>(this, _$identity); @override Map toJson() { - return _$$_WeatherDataToJson( + return _$$_WeatherForecastToJson( this, ); } } -abstract class _WeatherData implements WeatherData { - const factory _WeatherData( +abstract class _WeatherForecast implements WeatherForecast { + const factory _WeatherForecast( {required final WeatherCondition weatherCondition, required final int maxTemperature, required final int minTemperature, - required final DateTime date}) = _$_WeatherData; + required final DateTime date}) = _$_WeatherForecast; - factory _WeatherData.fromJson(Map json) = - _$_WeatherData.fromJson; + factory _WeatherForecast.fromJson(Map json) = + _$_WeatherForecast.fromJson; @override WeatherCondition get weatherCondition; @@ -214,6 +214,6 @@ abstract class _WeatherData implements WeatherData { DateTime get date; @override @JsonKey(ignore: true) - _$$_WeatherDataCopyWith<_$_WeatherData> get copyWith => + _$$_WeatherForecastCopyWith<_$_WeatherForecast> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/weather_data.g.dart b/lib/model/weather_forecast.g.dart similarity index 84% rename from lib/model/weather_data.g.dart rename to lib/model/weather_forecast.g.dart index 7096a77..460ab1c 100644 --- a/lib/model/weather_data.g.dart +++ b/lib/model/weather_forecast.g.dart @@ -2,18 +2,18 @@ // ignore_for_file: type=lint, implicit_dynamic_parameter, implicit_dynamic_type, implicit_dynamic_method, strict_raw_type -part of 'weather_data.dart'; +part of 'weather_forecast.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** -_$_WeatherData _$$_WeatherDataFromJson(Map json) => +_$_WeatherForecast _$$_WeatherForecastFromJson(Map json) => $checkedCreate( - r'_$_WeatherData', + r'_$_WeatherForecast', json, ($checkedConvert) { - final val = _$_WeatherData( + final val = _$_WeatherForecast( weatherCondition: $checkedConvert('weather_condition', (v) => $enumDecode(_$WeatherConditionEnumMap, v)), maxTemperature: $checkedConvert('max_temperature', (v) => v as int), @@ -29,7 +29,7 @@ _$_WeatherData _$$_WeatherDataFromJson(Map json) => }, ); -Map _$$_WeatherDataToJson(_$_WeatherData instance) => +Map _$$_WeatherForecastToJson(_$_WeatherForecast instance) => { 'weather_condition': _$WeatherConditionEnumMap[instance.weatherCondition]!, diff --git a/lib/service/weather_service.dart b/lib/service/weather_service.dart index 009074f..89f4be0 100644 --- a/lib/service/weather_service.dart +++ b/lib/service/weather_service.dart @@ -1,38 +1,52 @@ import 'dart:convert'; -import 'package:flutter_training/model/weather_data.dart'; +import 'package:flutter_training/model/weather_forecast.dart'; import 'package:flutter_training/model/weather_request.dart'; import 'package:flutter_training/utils/api/result.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 +YumemiWeather yumemiWeatherClient(YumemiWeatherClientRef ref) { + return YumemiWeather(); +} + +@riverpod +WeatherService weatherService(WeatherServiceRef ref) { + return WeatherService(ref.watch(yumemiWeatherClientProvider)); +} + 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]. - Result fetchWeather(WeatherRequest request) { + Result fetchWeather(WeatherRequest request) { try { final jsonData = jsonEncode(request); final resultJson = _client.fetchWeather(jsonData); final weatherData = jsonDecode(resultJson) as Map; - final result = WeatherData.fromJson(weatherData); - return Success(result); + final result = WeatherForecast.fromJson(weatherData); + return Success(result); } on YumemiWeatherError catch (e) { return switch (e) { YumemiWeatherError.invalidParameter => - const Failure('パラメータが有効ではありません。'), + const Failure('パラメータが有効ではありません。'), YumemiWeatherError.unknown => - const Failure('予期せぬエラーが発生しました。') + const Failure('予期せぬエラーが発生しました。') }; } on CheckedFromJsonException catch (_) { - return const Failure('不適切なデータを取得しました。'); + return const Failure('不適切なデータを取得しました。'); } on FormatException catch (_) { - return const Failure('不適切なデータを取得しました。'); + return const Failure('不適切なデータを取得しました。'); } } } diff --git a/lib/service/weather_service.g.dart b/lib/service/weather_service.g.dart new file mode 100644 index 0000000..449c65c --- /dev/null +++ b/lib/service/weather_service.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: type=lint, implicit_dynamic_parameter, implicit_dynamic_type, implicit_dynamic_method, strict_raw_type + +part of 'weather_service.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$yumemiWeatherClientHash() => + r'e40a0489f105c552873993d37c21849839ab751f'; + +/// See also [yumemiWeatherClient]. +@ProviderFor(yumemiWeatherClient) +final yumemiWeatherClientProvider = AutoDisposeProvider.internal( + yumemiWeatherClient, + name: r'yumemiWeatherClientProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$yumemiWeatherClientHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef YumemiWeatherClientRef = AutoDisposeProviderRef; + +String _$weatherServiceHash() => r'36602ea8afd766fe0d1f565dda41a18117d1db1b'; + +/// See also [weatherService]. +@ProviderFor(weatherService) +final weatherServiceProvider = AutoDisposeProvider.internal( + weatherService, + name: r'weatherServiceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$weatherServiceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef WeatherServiceRef = AutoDisposeProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/state/weather_state_notifier.dart b/lib/state/weather_state_notifier.dart new file mode 100644 index 0000000..ef6e29a --- /dev/null +++ b/lib/state/weather_state_notifier.dart @@ -0,0 +1,23 @@ +import 'package:flutter_training/model/weather_forecast.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_notifier.g.dart'; + +@riverpod +class WeatherStateNotifier extends _$WeatherStateNotifier { + @override + WeatherForecast? 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), + }; + } +} diff --git a/lib/state/weather_state_notifier.g.dart b/lib/state/weather_state_notifier.g.dart new file mode 100644 index 0000000..a6aaa83 --- /dev/null +++ b/lib/state/weather_state_notifier.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: type=lint, implicit_dynamic_parameter, implicit_dynamic_type, implicit_dynamic_method, strict_raw_type + +part of 'weather_state_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$weatherStateNotifierHash() => + r'35c0e0b792c3a537c238cee332b4c4d384973a61'; + +/// See also [WeatherStateNotifier]. +@ProviderFor(WeatherStateNotifier) +final weatherStateNotifierProvider = AutoDisposeNotifierProvider< + WeatherStateNotifier, WeatherForecast?>.internal( + WeatherStateNotifier.new, + name: r'weatherStateNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$weatherStateNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$WeatherStateNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/view/weather_view/component/weather_forecast.dart b/lib/view/weather_view/component/weather_forecast_panel.dart similarity index 63% rename from lib/view/weather_view/component/weather_forecast.dart rename to lib/view/weather_view/component/weather_forecast_panel.dart index 4c6744e..d49bf5b 100644 --- a/lib/view/weather_view/component/weather_forecast.dart +++ b/lib/view/weather_view/component/weather_forecast_panel.dart @@ -1,21 +1,21 @@ 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_notifier.dart'; -class WeatherForecast extends StatelessWidget { - const WeatherForecast({super.key, required this.weatherData}); - - final WeatherData? weatherData; +class WeatherForecastPanel extends ConsumerWidget { + const WeatherForecastPanel({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final textTheme = Theme.of(context).textTheme; + final weatherState = ref.watch(weatherStateNotifierProvider); return Column( children: [ //Weather conditions AspectRatio( aspectRatio: 1, - child: weatherData?.weatherCondition.svgImage ?? const Placeholder(), + child: weatherState?.weatherCondition.svgImage ?? const Placeholder(), ), const SizedBox(height: 16), //Temperature @@ -23,7 +23,7 @@ class WeatherForecast extends StatelessWidget { children: [ Expanded( child: Text( - '${weatherData?.minTemperature ?? '**'} ℃', + '${weatherState?.minTemperature ?? '**'} ℃', textAlign: TextAlign.center, style: textTheme.labelLarge!.copyWith( color: Colors.blue, @@ -32,7 +32,7 @@ class WeatherForecast extends StatelessWidget { ), Expanded( child: Text( - '${weatherData?.maxTemperature ?? '**'} ℃', + '${weatherState?.maxTemperature ?? '**'} ℃', textAlign: TextAlign.center, style: textTheme.labelLarge!.copyWith( color: Colors.red, diff --git a/lib/view/weather_view/weather_page.dart b/lib/view/weather_view/weather_page.dart index 2e920c3..978f147 100644 --- a/lib/view/weather_view/weather_page.dart +++ b/lib/view/weather_view/weather_page.dart @@ -1,37 +1,25 @@ 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/view/weather_view/component/weather_forecast.dart'; -import 'package:yumemi_weather/yumemi_weather.dart'; +import 'package:flutter_training/state/weather_state_notifier.dart'; +import 'package:flutter_training/view/weather_view/component/weather_forecast_panel.dart'; -class WeatherPage extends StatefulWidget { +class WeatherPage extends ConsumerWidget { const WeatherPage({super.key}); @override - State createState() => _WeatherPageState(); -} - -class _WeatherPageState extends State { - WeatherData? weatherData; - final service = WeatherService(YumemiWeather()); + Widget build(BuildContext context, WidgetRef ref) { + void onReloaded(WeatherRequest request) { + ref.read(weatherStateNotifierProvider.notifier).getWeather( + request: request, + onError: (errorMessage) => showDialog( + 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( - barrierDismissible: false, - context: context, - builder: (_) => _ErrorDialog(error), - ), - }; - } - - @override - Widget build(BuildContext context) { return Scaffold( body: Center( child: FractionallySizedBox( @@ -39,7 +27,7 @@ class _WeatherPageState extends State { child: Column( children: [ const Spacer(), - WeatherForecast(weatherData: weatherData), + const WeatherForecastPanel(), Flexible( child: Column( children: [ @@ -54,7 +42,14 @@ class _WeatherPageState extends State { ), Expanded( child: TextButton( - onPressed: _onReloaded, + onPressed: () { + onReloaded( + WeatherRequest( + area: 'Nagoya', + date: DateTime.now(), + ), + ); + }, child: const Text('Reload'), ), ), diff --git a/pubspec.lock b/pubspec.lock index 9d60dc7..efaf019 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d + url: "https://pub.dev" + source: hosted + version: "0.11.2" args: dependency: transitive description: @@ -129,6 +137,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" clock: dependency: transitive description: @@ -177,6 +201,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + custom_lint: + dependency: "direct dev" + description: + name: custom_lint + sha256: "837821e4619c167fd5a547b03bb2fc6be7e65b800ec75528848429705c31ceba" + url: "https://pub.dev" + source: hosted + version: "0.5.3" + custom_lint_builder: + dependency: transitive + description: + name: custom_lint_builder + sha256: "3537d50202568994a6f42b1f2953aed6292fc5ecf83e45237af73f64aff2be72" + url: "https://pub.dev" + source: hosted + version: "0.5.3" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: "3bdebdd52a42b4d6e5be9cd833ad1ecfbbc23e1020ca537060e54085497aea9c" + url: "https://pub.dev" + source: hosted + version: "0.5.3" dart_style: dependency: transitive description: @@ -246,6 +294,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "1bd39b04f1bcd217a969589777ca6bd642d116e3e5de65c3e6a8e8bdd8b178ec" + url: "https://pub.dev" + source: hosted + version: "2.4.0" flutter_svg: dependency: "direct main" description: @@ -299,6 +355,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: "728c0613556c1d153f7e7f4a367cffacc3f5a677d7f6497a1c2b35add4e6dacf" + url: "https://pub.dev" + source: hosted + version: "3.0.6" http_multi_server: dependency: transitive description: @@ -451,6 +515,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: a600120d6f213a9922860eea1abc32597436edd5b2c4e73b91410f8c2af67d22 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: aa216069d72f5478126029fa555874b4b38119f17e3f0f6c93fd63365f74502d + url: "https://pub.dev" + source: hosted + version: "0.3.3" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: "6294fe7e7d1875f32bdf04c8fce7620e718070273703097847df8f3bf16995ea" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: d132b1ccb476e60f99989caa6ba9b1c4d88409806c93d880d1633c60c382454d + url: "https://pub.dev" + source: hosted + version: "2.3.2" + riverpod_lint: + dependency: "direct dev" + description: + name: riverpod_lint + sha256: "74123a5f0a8e809ab80078dbc02061ecf02e09647a2c3231c87b7342bcf8d399" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" shelf: dependency: transitive description: @@ -504,6 +616,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.0" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: @@ -568,6 +688,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" vector_graphics: dependency: transitive description: @@ -600,6 +728,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + url: "https://pub.dev" + source: hosted + version: "11.10.0" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6150b08..14bc9c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,10 +10,12 @@ dependencies: flutter: sdk: flutter + flutter_riverpod: ^2.4.0 flutter_svg: ^2.0.7 freezed_annotation: ^2.4.1 json_annotation: ^4.8.1 + riverpod_annotation: ^2.1.5 yumemi_weather: git: url: https://github.com/yumemi-inc/flutter-training-template.git @@ -29,6 +31,9 @@ dev_dependencies: flutter_gen_runner: ^5.3.1 freezed: ^2.4.1 json_serializable: ^6.7.1 + riverpod_generator: ^2.3.2 + custom_lint: ^0.5.3 + riverpod_lint: ^2.0.4 flutter_gen: integrations: