From 1ae2593d4e4a16302a902d1579f2b5ecfad284ae Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 13 Oct 2023 14:01:35 +0900 Subject: [PATCH 01/21] Change fetchWeather() -> syncFetchWeather() --- lib/service/weather_service.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/service/weather_service.dart b/lib/service/weather_service.dart index 00c0945..31e7a75 100644 --- a/lib/service/weather_service.dart +++ b/lib/service/weather_service.dart @@ -30,10 +30,12 @@ class WeatherService { /// If successful, the value is stored in [Success], /// if unsuccessful, the error message is stored in [Failure]. - Result fetchWeather(WeatherRequest request) { + Future> fetchWeather( + WeatherRequest request, + ) async { try { final jsonData = jsonEncode(request); - final resultJson = _client.fetchWeather(jsonData); + final resultJson = _client.syncFetchWeather(jsonData); final weatherData = jsonDecode(resultJson) as Map; final result = WeatherForecast.fromJson(weatherData); return Success(result); From 0d3f1028f1474a84dd24ca174351ce59da03f3e0 Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 13 Oct 2023 14:03:36 +0900 Subject: [PATCH 02/21] Update WeatherStateNotifier based on changes of fetchWeather() --- lib/state/weather_state_notifier.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/state/weather_state_notifier.dart b/lib/state/weather_state_notifier.dart index ef6e29a..5bc5c1c 100644 --- a/lib/state/weather_state_notifier.dart +++ b/lib/state/weather_state_notifier.dart @@ -11,11 +11,12 @@ class WeatherStateNotifier extends _$WeatherStateNotifier { @override WeatherForecast? build() => null; - void getWeather({ + Future getWeather({ required WeatherRequest request, required void Function(String error) onError, - }) { - return switch (ref.read(weatherServiceProvider).fetchWeather(request)) { + }) async { + return switch ( + await ref.read(weatherServiceProvider).fetchWeather(request)) { Success(value: final value) => state = value, Failure(exception: final error) => onError.call(error), }; From 9757bdae8f5b469e9efda5ffbfd93bab2ced121c Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 13 Oct 2023 14:05:33 +0900 Subject: [PATCH 03/21] Wrap syncFetchWeather() with compute --- lib/service/weather_service.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/service/weather_service.dart b/lib/service/weather_service.dart index 31e7a75..1908609 100644 --- a/lib/service/weather_service.dart +++ b/lib/service/weather_service.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:flutter/foundation.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'; @@ -35,7 +36,7 @@ class WeatherService { ) async { try { final jsonData = jsonEncode(request); - final resultJson = _client.syncFetchWeather(jsonData); + final resultJson = await compute(_client.syncFetchWeather, jsonData); final weatherData = jsonDecode(resultJson) as Map; final result = WeatherForecast.fromJson(weatherData); return Success(result); From 4449e5aa547b005cc16c335dbfda08833acf931a Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 13 Oct 2023 14:11:02 +0900 Subject: [PATCH 04/21] Create LoadingStateNotifier --- lib/service/weather_service.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/service/weather_service.dart b/lib/service/weather_service.dart index 1908609..006d7bb 100644 --- a/lib/service/weather_service.dart +++ b/lib/service/weather_service.dart @@ -21,6 +21,16 @@ WeatherService weatherService(WeatherServiceRef ref) { return WeatherService(ref.watch(yumemiWeatherClientProvider)); } +@riverpod +class LoadingStateNotifier extends _$LoadingStateNotifier { + @override + bool build() => false; + + void show() => state = true; + + void hide() => state = false; +} + class WeatherService { WeatherService(this._client); From 1a8437a9e2658457158adcd7593ba81352d553c2 Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 13 Oct 2023 14:12:43 +0900 Subject: [PATCH 05/21] Update generated files --- lib/service/weather_service.g.dart | 31 ++++++++++++++++--- lib/state/weather_state_notifier.g.dart | 9 ++++-- test/service/weather_service_test.mocks.dart | 3 -- .../weather_state_notifier_test.mocks.dart | 3 -- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/lib/service/weather_service.g.dart b/lib/service/weather_service.g.dart index 449c65c..20c9466 100644 --- a/lib/service/weather_service.g.dart +++ b/lib/service/weather_service.g.dart @@ -9,7 +9,7 @@ part of 'weather_service.dart'; // ************************************************************************** String _$yumemiWeatherClientHash() => - r'e40a0489f105c552873993d37c21849839ab751f'; + r'38eba946f0af47492e5740938eeccfd96ff4600a'; /// See also [yumemiWeatherClient]. @ProviderFor(yumemiWeatherClient) @@ -19,13 +19,13 @@ final yumemiWeatherClientProvider = AutoDisposeProvider.internal( debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$yumemiWeatherClientHash, - dependencies: null, - allTransitiveDependencies: null, + dependencies: const [], + allTransitiveDependencies: const {}, ); typedef YumemiWeatherClientRef = AutoDisposeProviderRef; -String _$weatherServiceHash() => r'36602ea8afd766fe0d1f565dda41a18117d1db1b'; +String _$weatherServiceHash() => r'0788d6f27a695455d6dcf3b0b46f0df60ce1dc7d'; /// See also [weatherService]. @ProviderFor(weatherService) @@ -35,10 +35,31 @@ final weatherServiceProvider = AutoDisposeProvider.internal( debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$weatherServiceHash, + dependencies: [yumemiWeatherClientProvider], + allTransitiveDependencies: { + yumemiWeatherClientProvider, + ...?yumemiWeatherClientProvider.allTransitiveDependencies + }, +); + +typedef WeatherServiceRef = AutoDisposeProviderRef; + +String _$loadingStateNotifierHash() => + r'6c22092104ce7a9a407ef4f8ea50dd23369cd34f'; + +/// See also [LoadingStateNotifier]. +@ProviderFor(LoadingStateNotifier) +final loadingStateNotifierProvider = + AutoDisposeNotifierProvider.internal( + LoadingStateNotifier.new, + name: r'loadingStateNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$loadingStateNotifierHash, dependencies: null, allTransitiveDependencies: null, ); -typedef WeatherServiceRef = AutoDisposeProviderRef; +typedef _$LoadingStateNotifier = 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/state/weather_state_notifier.g.dart b/lib/state/weather_state_notifier.g.dart index a6aaa83..945862f 100644 --- a/lib/state/weather_state_notifier.g.dart +++ b/lib/state/weather_state_notifier.g.dart @@ -9,7 +9,7 @@ part of 'weather_state_notifier.dart'; // ************************************************************************** String _$weatherStateNotifierHash() => - r'35c0e0b792c3a537c238cee332b4c4d384973a61'; + r'3daf445cbb8897226ac7c0abdd3b133f9dd280db'; /// See also [WeatherStateNotifier]. @ProviderFor(WeatherStateNotifier) @@ -20,8 +20,11 @@ final weatherStateNotifierProvider = AutoDisposeNotifierProvider< debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$weatherStateNotifierHash, - dependencies: null, - allTransitiveDependencies: null, + dependencies: [weatherServiceProvider], + allTransitiveDependencies: { + weatherServiceProvider, + ...?weatherServiceProvider.allTransitiveDependencies + }, ); typedef _$WeatherStateNotifier = AutoDisposeNotifier; diff --git a/test/service/weather_service_test.mocks.dart b/test/service/weather_service_test.mocks.dart index bdb77fd..6fdcbfe 100644 --- a/test/service/weather_service_test.mocks.dart +++ b/test/service/weather_service_test.mocks.dart @@ -30,7 +30,6 @@ class MockYumemiWeather extends _i1.Mock implements _i2.YumemiWeather { returnValue: '', returnValueForMissingStub: '', ) as String); - @override String fetchThrowsWeather(String? area) => (super.noSuchMethod( Invocation.method( @@ -40,7 +39,6 @@ class MockYumemiWeather extends _i1.Mock implements _i2.YumemiWeather { returnValue: '', returnValueForMissingStub: '', ) as String); - @override String fetchWeather(String? jsonString) => (super.noSuchMethod( Invocation.method( @@ -50,7 +48,6 @@ class MockYumemiWeather extends _i1.Mock implements _i2.YumemiWeather { returnValue: '', returnValueForMissingStub: '', ) as String); - @override String syncFetchWeather(String? jsonString) => (super.noSuchMethod( Invocation.method( diff --git a/test/state/weather_state_notifier_test.mocks.dart b/test/state/weather_state_notifier_test.mocks.dart index 9929d18..186a537 100644 --- a/test/state/weather_state_notifier_test.mocks.dart +++ b/test/state/weather_state_notifier_test.mocks.dart @@ -30,7 +30,6 @@ class MockYumemiWeather extends _i1.Mock implements _i2.YumemiWeather { returnValue: '', returnValueForMissingStub: '', ) as String); - @override String fetchThrowsWeather(String? area) => (super.noSuchMethod( Invocation.method( @@ -40,7 +39,6 @@ class MockYumemiWeather extends _i1.Mock implements _i2.YumemiWeather { returnValue: '', returnValueForMissingStub: '', ) as String); - @override String fetchWeather(String? jsonString) => (super.noSuchMethod( Invocation.method( @@ -50,7 +48,6 @@ class MockYumemiWeather extends _i1.Mock implements _i2.YumemiWeather { returnValue: '', returnValueForMissingStub: '', ) as String); - @override String syncFetchWeather(String? jsonString) => (super.noSuchMethod( Invocation.method( From b7c0aa0fbe1b8ec54f4e78538bc8d1253c001513 Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 13 Oct 2023 14:16:31 +0900 Subject: [PATCH 06/21] Implement loading UI --- lib/service/weather_service.dart | 10 ++- lib/view/weather_view/weather_page.dart | 89 +++++++++++++++---------- 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/lib/service/weather_service.dart b/lib/service/weather_service.dart index 006d7bb..e5f88fe 100644 --- a/lib/service/weather_service.dart +++ b/lib/service/weather_service.dart @@ -18,7 +18,7 @@ YumemiWeather yumemiWeatherClient(YumemiWeatherClientRef ref) { @riverpod WeatherService weatherService(WeatherServiceRef ref) { - return WeatherService(ref.watch(yumemiWeatherClientProvider)); + return WeatherService(ref.watch(yumemiWeatherClientProvider), ref); } @riverpod @@ -32,9 +32,10 @@ class LoadingStateNotifier extends _$LoadingStateNotifier { } class WeatherService { - WeatherService(this._client); + WeatherService(this._client, this._ref); final YumemiWeather _client; + final AutoDisposeProviderRef _ref; /// Get weather information /// @@ -45,12 +46,15 @@ class WeatherService { WeatherRequest request, ) async { try { + _ref.read(loadingStateNotifierProvider.notifier).show(); final jsonData = jsonEncode(request); final resultJson = await compute(_client.syncFetchWeather, jsonData); final weatherData = jsonDecode(resultJson) as Map; final result = WeatherForecast.fromJson(weatherData); + _ref.read(loadingStateNotifierProvider.notifier).hide(); return Success(result); } on YumemiWeatherError catch (e) { + _ref.read(loadingStateNotifierProvider.notifier).hide(); return switch (e) { YumemiWeatherError.invalidParameter => const Failure(ErrorMessage.invalidParameter), @@ -58,10 +62,12 @@ class WeatherService { const Failure(ErrorMessage.unknown) }; } on CheckedFromJsonException catch (_) { + _ref.read(loadingStateNotifierProvider.notifier).hide(); return const Failure( ErrorMessage.receiveInvalidData, ); } on FormatException catch (_) { + _ref.read(loadingStateNotifierProvider.notifier).hide(); return const Failure( ErrorMessage.receiveInvalidData, ); diff --git a/lib/view/weather_view/weather_page.dart b/lib/view/weather_view/weather_page.dart index 1de541f..d83048d 100644 --- a/lib/view/weather_view/weather_page.dart +++ b/lib/view/weather_view/weather_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.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/state/weather_state_notifier.dart'; import 'package:flutter_training/view/weather_view/component/weather_forecast_panel.dart'; @@ -25,50 +26,64 @@ class WeatherPage extends ConsumerWidget { ); } - return Scaffold( - body: Center( - child: FractionallySizedBox( - widthFactor: 0.5, - child: Column( - children: [ - const Spacer(), - const WeatherForecastPanel(), - Flexible( - child: Column( - children: [ - const SizedBox(height: 80), - Row( + final isLoading = ref.watch(loadingStateNotifierProvider); + + return Stack( + children: [ + Scaffold( + body: Center( + child: FractionallySizedBox( + widthFactor: 0.5, + child: Column( + children: [ + const Spacer(), + const WeatherForecastPanel(), + Flexible( + child: Column( children: [ - Expanded( - child: TextButton( - onPressed: () => Navigator.pop(context), - key: closeButtonKey, - child: const Text('Close'), - ), - ), - Expanded( - child: TextButton( - onPressed: () { - onReloaded( - WeatherRequest( - area: 'Nagoya', - date: DateTime.now(), - ), - ); - }, - key: reloadButton, - child: const Text('Reload'), - ), + const SizedBox(height: 80), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.pop(context), + key: closeButtonKey, + child: const Text('Close'), + ), + ), + Expanded( + child: TextButton( + onPressed: () { + onReloaded( + WeatherRequest( + area: 'Nagoya', + date: DateTime.now(), + ), + ); + }, + key: reloadButton, + child: const Text('Reload'), + ), + ), + ], ), ], ), - ], - ), + ), + ], ), - ], + ), ), ), - ), + // ローディングを表示 + if (isLoading) + const ColoredBox( + color: Colors.black26, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ], ); } } From 847982eee6a8b157610eff4b5e7aad18d4fc6edc Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 13 Oct 2023 14:21:17 +0900 Subject: [PATCH 07/21] Give key to CircularProgressIndicator widget --- lib/view/weather_view/weather_page.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/view/weather_view/weather_page.dart b/lib/view/weather_view/weather_page.dart index d83048d..f264b14 100644 --- a/lib/view/weather_view/weather_page.dart +++ b/lib/view/weather_view/weather_page.dart @@ -12,6 +12,8 @@ class WeatherPage extends ConsumerWidget { static final closeButtonKey = UniqueKey(); @visibleForTesting static final reloadButton = UniqueKey(); + @visibleForTesting + static final circularProgressIndicatorKey = UniqueKey(); @override Widget build(BuildContext context, WidgetRef ref) { @@ -77,10 +79,12 @@ class WeatherPage extends ConsumerWidget { ), // ローディングを表示 if (isLoading) - const ColoredBox( + ColoredBox( color: Colors.black26, child: Center( - child: CircularProgressIndicator(), + child: CircularProgressIndicator( + key: circularProgressIndicatorKey, + ), ), ), ], From 25bc04b55d4b679bea92e3fe3e839986175ca00b Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 13 Oct 2023 15:01:57 +0900 Subject: [PATCH 08/21] Update weather_service_test.dart based on changes of product files --- test/service/weather_service_test.dart | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/test/service/weather_service_test.dart b/test/service/weather_service_test.dart index ec4b590..1f8a7c3 100644 --- a/test/service/weather_service_test.dart +++ b/test/service/weather_service_test.dart @@ -34,10 +34,11 @@ void main() { container.dispose(); }); - test('success case', () { - when(mockClient.fetchWeather(any)).thenReturn(validJsonData); + test('success case', () async { + when(mockClient.syncFetchWeather(any)).thenReturn(validJsonData); - final result = container.read(weatherServiceProvider).fetchWeather(request); + final result = + await container.read(weatherServiceProvider).fetchWeather(request); expect( result, @@ -55,12 +56,12 @@ void main() { }); group('failure case', () { - test('invalidParameter error is thrown', () { - when(mockClient.fetchWeather(any)) + test('invalidParameter error is thrown', () async { + when(mockClient.syncFetchWeather(any)) .thenThrow(YumemiWeatherError.invalidParameter); final result = - container.read(weatherServiceProvider).fetchWeather(request); + await container.read(weatherServiceProvider).fetchWeather(request); expect( result, @@ -72,11 +73,12 @@ void main() { ); }); - test('unknown error is thrown', () { - when(mockClient.fetchWeather(any)).thenThrow(YumemiWeatherError.unknown); + test('unknown error is thrown', () async { + when(mockClient.syncFetchWeather(any)) + .thenThrow(YumemiWeatherError.unknown); final result = - container.read(weatherServiceProvider).fetchWeather(request); + await container.read(weatherServiceProvider).fetchWeather(request); expect( result, From 8944c59ec0dc5e3edc287460288547f115a63151 Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 13 Oct 2023 16:18:21 +0900 Subject: [PATCH 09/21] Remove circularProgressIndicatorKey --- lib/view/weather_view/weather_page.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/view/weather_view/weather_page.dart b/lib/view/weather_view/weather_page.dart index f264b14..d83048d 100644 --- a/lib/view/weather_view/weather_page.dart +++ b/lib/view/weather_view/weather_page.dart @@ -12,8 +12,6 @@ class WeatherPage extends ConsumerWidget { static final closeButtonKey = UniqueKey(); @visibleForTesting static final reloadButton = UniqueKey(); - @visibleForTesting - static final circularProgressIndicatorKey = UniqueKey(); @override Widget build(BuildContext context, WidgetRef ref) { @@ -79,12 +77,10 @@ class WeatherPage extends ConsumerWidget { ), // ローディングを表示 if (isLoading) - ColoredBox( + const ColoredBox( color: Colors.black26, child: Center( - child: CircularProgressIndicator( - key: circularProgressIndicatorKey, - ), + child: CircularProgressIndicator(), ), ), ], From 309f4a33b11240a637d29fa3bf565556cb8a2576 Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 20 Oct 2023 14:26:58 +0900 Subject: [PATCH 10/21] Update generated files based on session10 changes --- lib/service/weather_service.g.dart | 16 ++++++---------- lib/state/weather_state_notifier.g.dart | 9 +++------ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/lib/service/weather_service.g.dart b/lib/service/weather_service.g.dart index 20c9466..ff8c118 100644 --- a/lib/service/weather_service.g.dart +++ b/lib/service/weather_service.g.dart @@ -9,7 +9,7 @@ part of 'weather_service.dart'; // ************************************************************************** String _$yumemiWeatherClientHash() => - r'38eba946f0af47492e5740938eeccfd96ff4600a'; + r'e40a0489f105c552873993d37c21849839ab751f'; /// See also [yumemiWeatherClient]. @ProviderFor(yumemiWeatherClient) @@ -19,13 +19,13 @@ final yumemiWeatherClientProvider = AutoDisposeProvider.internal( debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$yumemiWeatherClientHash, - dependencies: const [], - allTransitiveDependencies: const {}, + dependencies: null, + allTransitiveDependencies: null, ); typedef YumemiWeatherClientRef = AutoDisposeProviderRef; -String _$weatherServiceHash() => r'0788d6f27a695455d6dcf3b0b46f0df60ce1dc7d'; +String _$weatherServiceHash() => r'41a00cbad069db0e4e14ae437ab2772c05c9916c'; /// See also [weatherService]. @ProviderFor(weatherService) @@ -35,15 +35,11 @@ final weatherServiceProvider = AutoDisposeProvider.internal( debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$weatherServiceHash, - dependencies: [yumemiWeatherClientProvider], - allTransitiveDependencies: { - yumemiWeatherClientProvider, - ...?yumemiWeatherClientProvider.allTransitiveDependencies - }, + dependencies: null, + allTransitiveDependencies: null, ); typedef WeatherServiceRef = AutoDisposeProviderRef; - String _$loadingStateNotifierHash() => r'6c22092104ce7a9a407ef4f8ea50dd23369cd34f'; diff --git a/lib/state/weather_state_notifier.g.dart b/lib/state/weather_state_notifier.g.dart index 945862f..ce6d8eb 100644 --- a/lib/state/weather_state_notifier.g.dart +++ b/lib/state/weather_state_notifier.g.dart @@ -9,7 +9,7 @@ part of 'weather_state_notifier.dart'; // ************************************************************************** String _$weatherStateNotifierHash() => - r'3daf445cbb8897226ac7c0abdd3b133f9dd280db'; + r'0bcde01f798d9a931796db0bfd80a05d15d30f94'; /// See also [WeatherStateNotifier]. @ProviderFor(WeatherStateNotifier) @@ -20,11 +20,8 @@ final weatherStateNotifierProvider = AutoDisposeNotifierProvider< debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$weatherStateNotifierHash, - dependencies: [weatherServiceProvider], - allTransitiveDependencies: { - weatherServiceProvider, - ...?weatherServiceProvider.allTransitiveDependencies - }, + dependencies: null, + allTransitiveDependencies: null, ); typedef _$WeatherStateNotifier = AutoDisposeNotifier; From e4142a9be0c65c81801c50bd87b4521e58eaa82c Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 20 Oct 2023 18:35:20 +0900 Subject: [PATCH 11/21] Update weather_state_notifier_test.dart, weather_state_notifier.dart --- lib/state/weather_state_notifier.dart | 4 +- lib/state/weather_state_notifier.g.dart | 2 +- test/state/weather_state_notifier_test.dart | 78 ++++++++++++--------- 3 files changed, 47 insertions(+), 37 deletions(-) diff --git a/lib/state/weather_state_notifier.dart b/lib/state/weather_state_notifier.dart index 5bc5c1c..73d1e56 100644 --- a/lib/state/weather_state_notifier.dart +++ b/lib/state/weather_state_notifier.dart @@ -15,8 +15,8 @@ class WeatherStateNotifier extends _$WeatherStateNotifier { required WeatherRequest request, required void Function(String error) onError, }) async { - return switch ( - await ref.read(weatherServiceProvider).fetchWeather(request)) { + final result = await ref.read(weatherServiceProvider).fetchWeather(request); + return switch (result) { 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 index ce6d8eb..7f76e2b 100644 --- a/lib/state/weather_state_notifier.g.dart +++ b/lib/state/weather_state_notifier.g.dart @@ -9,7 +9,7 @@ part of 'weather_state_notifier.dart'; // ************************************************************************** String _$weatherStateNotifierHash() => - r'0bcde01f798d9a931796db0bfd80a05d15d30f94'; + r'e88bef24a896e2c767d10d38fcc26fd39cbeca27'; /// See also [WeatherStateNotifier]. @ProviderFor(WeatherStateNotifier) diff --git a/test/state/weather_state_notifier_test.dart b/test/state/weather_state_notifier_test.dart index 6d48c60..e47c271 100644 --- a/test/state/weather_state_notifier_test.dart +++ b/test/state/weather_state_notifier_test.dart @@ -34,18 +34,22 @@ void main() { container.dispose(); }); - test('success case: getWeather()', () { - when(mockClient.fetchWeather(any)).thenReturn(validJsonData); + test('success case: getWeather()', () async { + when(mockClient.syncFetchWeather(any)).thenReturn(validJsonData); + + // 以下でgetWeather()してからweatherStateを読み取ってもkeepAliveじゃないので破棄される + // よってcontainer.listenを使ってwatchのように天気を監視する + // ref: https://riverpod.dev/ja/docs/essentials/testing#:~:text=notified%20of%20changes.-,%E6%B3%A8%E6%84%8F,-Be%20careful%20when + final weatherStateWatcher = + container.listen(weatherStateNotifierProvider, (_, __) {}); // 天気の取得処理を実行して、結果をstateに流す - container + await container .read(weatherStateNotifierProvider.notifier) .getWeather(request: request, onError: (error) {}); - final weatherState = container.read(weatherStateNotifierProvider); - expect( - weatherState, + weatherStateWatcher.read(), WeatherForecast( weatherCondition: WeatherCondition.cloudy, maxTemperature: 25, @@ -56,90 +60,96 @@ void main() { }); group('failure case: getWeather()', () { - test('an unknown error is thrown', () { - when(mockClient.fetchWeather(any)).thenThrow(YumemiWeatherError.unknown); + test('an unknown error is thrown', () async { + when(mockClient.syncFetchWeather(any)) + .thenThrow(YumemiWeatherError.unknown); + + final weatherStateWatcher = + container.listen(weatherStateNotifierProvider, (_, __) {}); // 表示されるエラーメッセージを格納 String? errorMessage; // 天気の取得処理を実行して、結果をstateに流す - container.read(weatherStateNotifierProvider.notifier).getWeather( + await container.read(weatherStateNotifierProvider.notifier).getWeather( request: request, onError: (e) { errorMessage = e; }, ); - final weatherState = container.read(weatherStateNotifierProvider); - - //取得はできてない - expect(weatherState, null); + // 失敗なので天気情報は取得できていないはず + expect(weatherStateWatcher.read(), null); expect(errorMessage, ErrorMessage.unknown); }); - test('invalidParameter error is thrown', () { - when(mockClient.fetchWeather(any)) + test('invalidParameter error is thrown', () async { + when(mockClient.syncFetchWeather(any)) .thenThrow(YumemiWeatherError.invalidParameter); + final weatherStateWatcher = + container.listen(weatherStateNotifierProvider, (_, __) {}); + // 表示されるエラーメッセージを格納 String? errorMessage; // 天気の取得処理を実行して、結果をstateに流す - container.read(weatherStateNotifierProvider.notifier).getWeather( + await container.read(weatherStateNotifierProvider.notifier).getWeather( request: request, onError: (e) { errorMessage = e; }, ); - final weatherState = container.read(weatherStateNotifierProvider); - - //取得はできてない - expect(weatherState, null); + // 失敗なので天気情報は取得できていないはず + expect(weatherStateWatcher.read(), null); expect(errorMessage, ErrorMessage.invalidParameter); }); - test('fromJson error case: CheckedFromJsonException should be thrown.', () { - when(mockClient.fetchWeather(any)) + test('fromJson error case: CheckedFromJsonException should be thrown.', + () async { + when(mockClient.syncFetchWeather(any)) .thenReturn(invalidJsonDataForCheckedFromJsonException); + final weatherStateWatcher = + container.listen(weatherStateNotifierProvider, (_, __) {}); + // 表示されるエラーメッセージを格納 String? errorMessage; // 天気の取得処理を実行して、結果をstateに流す - container.read(weatherStateNotifierProvider.notifier).getWeather( + await container.read(weatherStateNotifierProvider.notifier).getWeather( request: request, onError: (e) { errorMessage = e; }, ); - final weatherState = container.read(weatherStateNotifierProvider); - - //取得はできてない - expect(weatherState, null); + // 失敗なので天気情報は取得できていないはず + expect(weatherStateWatcher.read(), null); expect(errorMessage, ErrorMessage.receiveInvalidData); }); - test('received data is not in JSON format', () { - when(mockClient.fetchWeather(any)) + test('received data is not in JSON format', () async { + when(mockClient.syncFetchWeather(any)) .thenReturn(invalidJsonDataForFormatException); + final weatherStateWatcher = + container.listen(weatherStateNotifierProvider, (_, __) {}); + // 表示されるエラーメッセージを格納 String? errorMessage; // 天気の取得処理を実行して、結果をstateに流す - container.read(weatherStateNotifierProvider.notifier).getWeather( + await container.read(weatherStateNotifierProvider.notifier).getWeather( request: request, onError: (e) { errorMessage = e; }, ); - final weatherState = container.read(weatherStateNotifierProvider); - - //取得はできてない - expect(weatherState, null); + // 失敗なので天気情報は取得できていないはず + expect(weatherStateWatcher.read(), null); expect(errorMessage, ErrorMessage.receiveInvalidData); }); }); From 99581c4b43f42063f3e7c4eab9f90afd6915b8ff Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Mon, 23 Oct 2023 13:00:20 +0900 Subject: [PATCH 12/21] Replace fetchWeather with syncFetchWeather --- test/view/weather_page_test.dart | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test/view/weather_page_test.dart b/test/view/weather_page_test.dart index 2f25147..0991310 100644 --- a/test/view/weather_page_test.dart +++ b/test/view/weather_page_test.dart @@ -58,7 +58,7 @@ void main() { 'when reload button is pressed, ' 'cloudy weather and correct temperature should be displayed.', (tester) async { - when(mockClient.fetchWeather(any)).thenReturn(cloudyWeatherJsonData); + when(mockClient.syncFetchWeather(any)).thenReturn(cloudyWeatherJsonData); await tester.pumpWidget( ProviderScope( @@ -94,7 +94,7 @@ void main() { 'when reload button is pressed, ' 'sunny weather and correct temperature should be displayed.', (tester) async { - when(mockClient.fetchWeather(any)).thenReturn(sunnyWeatherJsonData); + when(mockClient.syncFetchWeather(any)).thenReturn(sunnyWeatherJsonData); await tester.pumpWidget( ProviderScope( @@ -130,7 +130,7 @@ void main() { 'when reload button is pressed, ' 'rainy weather and correct temperature should be displayed.', (tester) async { - when(mockClient.fetchWeather(any)).thenReturn(rainyWeatherJsonData); + when(mockClient.syncFetchWeather(any)).thenReturn(rainyWeatherJsonData); await tester.pumpWidget( ProviderScope( @@ -168,7 +168,7 @@ void main() { 'when fetchWeather() returns failure with invalidParameter error, ' 'error dialog and correct message should be visible. ' 'Then the dialog is closed by pressing the ok button.', (tester) async { - when(mockClient.fetchWeather(any)) + when(mockClient.syncFetchWeather(any)) .thenThrow(YumemiWeatherError.invalidParameter); await tester.pumpWidget( @@ -219,7 +219,8 @@ void main() { testWidgets( 'when fetchWeather() returns failure with unknown error, ' 'error dialog and correct message should be visible. ', (tester) async { - when(mockClient.fetchWeather(any)).thenThrow(YumemiWeatherError.unknown); + when(mockClient.syncFetchWeather(any)) + .thenThrow(YumemiWeatherError.unknown); await tester.pumpWidget( ProviderScope( @@ -258,7 +259,7 @@ void main() { testWidgets( 'when fetchWeather() returns failure with CheckedFromJsonException, ' 'error dialog and correct message should be visible. ', (tester) async { - when(mockClient.fetchWeather(any)) + when(mockClient.syncFetchWeather(any)) .thenReturn(invalidJsonDataForCheckedFromJsonException); await tester.pumpWidget( @@ -298,7 +299,7 @@ void main() { testWidgets( 'when fetchWeather() returns failure with FormatException, ' 'error dialog and correct message should be visible. ', (tester) async { - when(mockClient.fetchWeather(any)) + when(mockClient.syncFetchWeather(any)) .thenReturn(invalidJsonDataForFormatException); await tester.pumpWidget( From fc44afaadce1d8527c6e4d6a362be012d68986f7 Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Mon, 23 Oct 2023 14:42:31 +0900 Subject: [PATCH 13/21] Fix background color of CircularProgressIndicator --- lib/view/weather_view/weather_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/view/weather_view/weather_page.dart b/lib/view/weather_view/weather_page.dart index d83048d..03110b7 100644 --- a/lib/view/weather_view/weather_page.dart +++ b/lib/view/weather_view/weather_page.dart @@ -78,7 +78,7 @@ class WeatherPage extends ConsumerWidget { // ローディングを表示 if (isLoading) const ColoredBox( - color: Colors.black26, + color: Colors.black54, child: Center( child: CircularProgressIndicator(), ), From 2ce23698777a1da0d4821416cd6950a1a0a96b37 Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 27 Oct 2023 14:04:07 +0900 Subject: [PATCH 14/21] Move loading state notifier to state/, update how to handle loading view --- lib/service/weather_service.dart | 20 ++--------------- lib/service/weather_service.g.dart | 20 +---------------- lib/state/loading_state_notifier.dart | 13 +++++++++++ lib/state/loading_state_notifier.g.dart | 29 +++++++++++++++++++++++++ lib/state/weather_state_notifier.dart | 4 ++++ lib/state/weather_state_notifier.g.dart | 2 +- lib/view/weather_view/weather_page.dart | 2 +- 7 files changed, 51 insertions(+), 39 deletions(-) create mode 100644 lib/state/loading_state_notifier.dart create mode 100644 lib/state/loading_state_notifier.g.dart diff --git a/lib/service/weather_service.dart b/lib/service/weather_service.dart index e5f88fe..1908609 100644 --- a/lib/service/weather_service.dart +++ b/lib/service/weather_service.dart @@ -18,24 +18,13 @@ YumemiWeather yumemiWeatherClient(YumemiWeatherClientRef ref) { @riverpod WeatherService weatherService(WeatherServiceRef ref) { - return WeatherService(ref.watch(yumemiWeatherClientProvider), ref); -} - -@riverpod -class LoadingStateNotifier extends _$LoadingStateNotifier { - @override - bool build() => false; - - void show() => state = true; - - void hide() => state = false; + return WeatherService(ref.watch(yumemiWeatherClientProvider)); } class WeatherService { - WeatherService(this._client, this._ref); + WeatherService(this._client); final YumemiWeather _client; - final AutoDisposeProviderRef _ref; /// Get weather information /// @@ -46,15 +35,12 @@ class WeatherService { WeatherRequest request, ) async { try { - _ref.read(loadingStateNotifierProvider.notifier).show(); final jsonData = jsonEncode(request); final resultJson = await compute(_client.syncFetchWeather, jsonData); final weatherData = jsonDecode(resultJson) as Map; final result = WeatherForecast.fromJson(weatherData); - _ref.read(loadingStateNotifierProvider.notifier).hide(); return Success(result); } on YumemiWeatherError catch (e) { - _ref.read(loadingStateNotifierProvider.notifier).hide(); return switch (e) { YumemiWeatherError.invalidParameter => const Failure(ErrorMessage.invalidParameter), @@ -62,12 +48,10 @@ class WeatherService { const Failure(ErrorMessage.unknown) }; } on CheckedFromJsonException catch (_) { - _ref.read(loadingStateNotifierProvider.notifier).hide(); return const Failure( ErrorMessage.receiveInvalidData, ); } on FormatException catch (_) { - _ref.read(loadingStateNotifierProvider.notifier).hide(); return const Failure( ErrorMessage.receiveInvalidData, ); diff --git a/lib/service/weather_service.g.dart b/lib/service/weather_service.g.dart index ff8c118..34855e1 100644 --- a/lib/service/weather_service.g.dart +++ b/lib/service/weather_service.g.dart @@ -24,8 +24,7 @@ final yumemiWeatherClientProvider = AutoDisposeProvider.internal( ); typedef YumemiWeatherClientRef = AutoDisposeProviderRef; - -String _$weatherServiceHash() => r'41a00cbad069db0e4e14ae437ab2772c05c9916c'; +String _$weatherServiceHash() => r'36602ea8afd766fe0d1f565dda41a18117d1db1b'; /// See also [weatherService]. @ProviderFor(weatherService) @@ -40,22 +39,5 @@ final weatherServiceProvider = AutoDisposeProvider.internal( ); typedef WeatherServiceRef = AutoDisposeProviderRef; -String _$loadingStateNotifierHash() => - r'6c22092104ce7a9a407ef4f8ea50dd23369cd34f'; - -/// See also [LoadingStateNotifier]. -@ProviderFor(LoadingStateNotifier) -final loadingStateNotifierProvider = - AutoDisposeNotifierProvider.internal( - LoadingStateNotifier.new, - name: r'loadingStateNotifierProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$loadingStateNotifierHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$LoadingStateNotifier = 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/state/loading_state_notifier.dart b/lib/state/loading_state_notifier.dart new file mode 100644 index 0000000..141d048 --- /dev/null +++ b/lib/state/loading_state_notifier.dart @@ -0,0 +1,13 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'loading_state_notifier.g.dart'; + +@riverpod +class LoadingStateNotifier extends _$LoadingStateNotifier { + @override + bool build() => false; + + void show() => state = true; + + void hide() => state = false; +} diff --git a/lib/state/loading_state_notifier.g.dart b/lib/state/loading_state_notifier.g.dart new file mode 100644 index 0000000..5931935 --- /dev/null +++ b/lib/state/loading_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 'loading_state_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$loadingStateNotifierHash() => + r'6c22092104ce7a9a407ef4f8ea50dd23369cd34f'; + +/// See also [LoadingStateNotifier]. +@ProviderFor(LoadingStateNotifier) +final loadingStateNotifierProvider = + AutoDisposeNotifierProvider.internal( + LoadingStateNotifier.new, + name: r'loadingStateNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$loadingStateNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$LoadingStateNotifier = 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/state/weather_state_notifier.dart b/lib/state/weather_state_notifier.dart index 73d1e56..cc01b76 100644 --- a/lib/state/weather_state_notifier.dart +++ b/lib/state/weather_state_notifier.dart @@ -1,6 +1,8 @@ +// ignore_for_file: avoid_manual_providers_as_generated_provider_dependency 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/state/loading_state_notifier.dart'; import 'package:flutter_training/utils/api/result.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -15,7 +17,9 @@ class WeatherStateNotifier extends _$WeatherStateNotifier { required WeatherRequest request, required void Function(String error) onError, }) async { + ref.read(loadingStateNotifierProvider.notifier).show(); final result = await ref.read(weatherServiceProvider).fetchWeather(request); + ref.read(loadingStateNotifierProvider.notifier).hide(); return switch (result) { 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 index 7f76e2b..7e3ece1 100644 --- a/lib/state/weather_state_notifier.g.dart +++ b/lib/state/weather_state_notifier.g.dart @@ -9,7 +9,7 @@ part of 'weather_state_notifier.dart'; // ************************************************************************** String _$weatherStateNotifierHash() => - r'e88bef24a896e2c767d10d38fcc26fd39cbeca27'; + r'2df5143086d4af532805b5b42cec9c74709487cb'; /// See also [WeatherStateNotifier]. @ProviderFor(WeatherStateNotifier) diff --git a/lib/view/weather_view/weather_page.dart b/lib/view/weather_view/weather_page.dart index 03110b7..405db40 100644 --- a/lib/view/weather_view/weather_page.dart +++ b/lib/view/weather_view/weather_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.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/state/loading_state_notifier.dart'; import 'package:flutter_training/state/weather_state_notifier.dart'; import 'package:flutter_training/view/weather_view/component/weather_forecast_panel.dart'; From 44c86ec033e158c513f5d51dc4ab07f1f015ece6 Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 27 Oct 2023 14:11:08 +0900 Subject: [PATCH 15/21] Replace json dummy data with data of Result type. --- test/utils/dummy_data.dart | 52 ++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/test/utils/dummy_data.dart b/test/utils/dummy_data.dart index c4ffe60..0f6ba3a 100644 --- a/test/utils/dummy_data.dart +++ b/test/utils/dummy_data.dart @@ -1,3 +1,7 @@ +import 'package:flutter_training/model/weather_condition.dart'; +import 'package:flutter_training/model/weather_forecast.dart'; +import 'package:flutter_training/utils/api/result.dart'; + const validJsonData = ''' { "weather_condition": "cloudy", @@ -7,32 +11,32 @@ const validJsonData = ''' } '''; -const sunnyWeatherJsonData = ''' - { - "weather_condition": "sunny", - "max_temperature": 30, - "min_temperature": 0, - "date": "2023-09-19 10:24:31.877" - } - '''; +final sunnyWeatherData = Success( + WeatherForecast( + weatherCondition: WeatherCondition.sunny, + maxTemperature: 30, + minTemperature: 0, + date: DateTime(2023, 9, 19, 10, 24, 31, 877), + ), +); -const cloudyWeatherJsonData = ''' - { - "weather_condition": "cloudy", - "max_temperature": 25, - "min_temperature": 7, - "date": "2023-09-19 10:24:31.877" - } - '''; +final cloudyWeatherData = Success( + WeatherForecast( + weatherCondition: WeatherCondition.cloudy, + maxTemperature: 25, + minTemperature: 7, + date: DateTime(2023, 9, 19, 10, 24, 31, 877), + ), +); -const rainyWeatherJsonData = ''' - { - "weather_condition": "rainy", - "max_temperature": 44, - "min_temperature": -22, - "date": "2023-09-19 10:24:31.877" - } - '''; +final rainyWeatherData = Success( + WeatherForecast( + weatherCondition: WeatherCondition.rainy, + maxTemperature: 44, + minTemperature: -22, + date: DateTime(2023, 9, 19, 10, 24, 31, 877), + ), +); const invalidJsonDataForCheckedFromJsonException = ''' { From d0d261902d4fc42aada44a59e89c7df2cbc18ed4 Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 27 Oct 2023 14:18:52 +0900 Subject: [PATCH 16/21] Update weather_page_test.dart: mock WeatherService, add loading test --- test/view/weather_page_test.dart | 195 +++++++++++++++---------- test/view/weather_page_test.mocks.dart | 56 +++++++ 2 files changed, 174 insertions(+), 77 deletions(-) create mode 100644 test/view/weather_page_test.mocks.dart diff --git a/test/view/weather_page_test.dart b/test/view/weather_page_test.dart index 0991310..e2147a7 100644 --- a/test/view/weather_page_test.dart +++ b/test/view/weather_page_test.dart @@ -1,25 +1,30 @@ // ignore_for_file: scoped_providers_should_specify_dependencies +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_training/model/weather_condition.dart'; +import 'package:flutter_training/model/weather_forecast.dart'; import 'package:flutter_training/service/weather_service.dart'; +import 'package:flutter_training/utils/api/result.dart'; import 'package:flutter_training/utils/error/error_message.dart'; import 'package:flutter_training/view/weather_view/component/weather_forecast_panel.dart'; import 'package:flutter_training/view/weather_view/weather_page.dart'; +import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:yumemi_weather/yumemi_weather.dart'; -import '../service/weather_service_test.mocks.dart'; import '../utils/dummy_data.dart'; import '../utils/utils.dart'; +import 'weather_page_test.mocks.dart'; +@GenerateNiceMocks([MockSpec()]) void main() { setUp(setDisplaySize); tearDown(teardownDisplaySize); - final mockClient = MockYumemiWeather(); + final mockWeatherService = MockWeatherService(); testWidgets('initial screen', (tester) async { await tester.pumpWidget( @@ -32,11 +37,11 @@ void main() { // 気温のテキストの色を取得 final minTemp = - tester.widget(find.byKey(WeatherForecastPanel.minTempKey)) as Text; + tester.widget(find.byKey(WeatherForecastPanel.minTempKey)) as Text; final minTempColor = minTemp.style!.color; final maxTemp = - tester.widget(find.byKey(WeatherForecastPanel.maxTempKey)) as Text; + tester.widget(find.byKey(WeatherForecastPanel.maxTempKey)) as Text; final maxTempColor = maxTemp.style!.color; expect(find.byType(Placeholder), findsOneWidget); @@ -56,14 +61,21 @@ void main() { // cloudy testWidgets( 'when reload button is pressed, ' - 'cloudy weather and correct temperature should be displayed.', - (tester) async { - when(mockClient.syncFetchWeather(any)).thenReturn(cloudyWeatherJsonData); + 'cloudy weather and correct temperature should be displayed.', + (tester) async { + provideDummy>( + const Failure('初期値として仮のエラーを返します。'), + ); + + final fetchCompleter = Completer>(); + + when(mockWeatherService.fetchWeather(any)) + .thenAnswer((_) => fetchCompleter.future); await tester.pumpWidget( ProviderScope( overrides: [ - yumemiWeatherClientProvider.overrideWithValue(mockClient) + weatherServiceProvider.overrideWithValue(mockWeatherService) ], child: const MaterialApp( home: WeatherPage(), @@ -77,8 +89,10 @@ void main() { await tester.tap(find.byKey(WeatherPage.reloadButton)); await tester.pump(); - expect(find.byType(Placeholder), findsNothing); - expect(find.text('** ℃'), findsNothing); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + fetchCompleter.complete(cloudyWeatherData); + await tester.pump(); expect( find.bySemanticsLabel(WeatherSvgImage.cloudyLabel), @@ -89,17 +103,24 @@ void main() { expect(find.text('7 ℃'), findsOneWidget); }); - // sunny + //sunny testWidgets( 'when reload button is pressed, ' - 'sunny weather and correct temperature should be displayed.', - (tester) async { - when(mockClient.syncFetchWeather(any)).thenReturn(sunnyWeatherJsonData); + 'sunny weather and correct temperature should be displayed.', + (tester) async { + provideDummy>( + const Failure('初期値として仮のエラーを返します。'), + ); + + final fetchCompleter = Completer>(); + + when(mockWeatherService.fetchWeather(any)) + .thenAnswer((_) => fetchCompleter.future); await tester.pumpWidget( ProviderScope( overrides: [ - yumemiWeatherClientProvider.overrideWithValue(mockClient) + weatherServiceProvider.overrideWithValue(mockWeatherService) ], child: const MaterialApp( home: WeatherPage(), @@ -113,8 +134,13 @@ void main() { await tester.tap(find.byKey(WeatherPage.reloadButton)); await tester.pump(); - expect(find.byType(Placeholder), findsNothing); - expect(find.text('** ℃'), findsNothing); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + fetchCompleter.complete(sunnyWeatherData); + + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsNothing); expect( find.bySemanticsLabel(WeatherSvgImage.sunnyLabel), @@ -128,14 +154,21 @@ void main() { // rainy testWidgets( 'when reload button is pressed, ' - 'rainy weather and correct temperature should be displayed.', - (tester) async { - when(mockClient.syncFetchWeather(any)).thenReturn(rainyWeatherJsonData); + 'rainy weather and correct temperature should be displayed.', + (tester) async { + provideDummy>( + const Failure('初期値として仮のエラーを返します。'), + ); + + final fetchCompleter = Completer>(); + + when(mockWeatherService.fetchWeather(any)) + .thenAnswer((_) => fetchCompleter.future); await tester.pumpWidget( ProviderScope( overrides: [ - yumemiWeatherClientProvider.overrideWithValue(mockClient) + weatherServiceProvider.overrideWithValue(mockWeatherService) ], child: const MaterialApp( home: WeatherPage(), @@ -149,8 +182,13 @@ void main() { await tester.tap(find.byKey(WeatherPage.reloadButton)); await tester.pump(); - expect(find.byType(Placeholder), findsNothing); - expect(find.text('** ℃'), findsNothing); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + fetchCompleter.complete(rainyWeatherData); + + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsNothing); expect( find.bySemanticsLabel(WeatherSvgImage.rainyLabel), @@ -166,15 +204,17 @@ void main() { // invalidParameter testWidgets( 'when fetchWeather() returns failure with invalidParameter error, ' - 'error dialog and correct message should be visible. ' - 'Then the dialog is closed by pressing the ok button.', (tester) async { - when(mockClient.syncFetchWeather(any)) - .thenThrow(YumemiWeatherError.invalidParameter); + 'error dialog and correct message should be visible. ' + 'Then the dialog is closed by pressing the ok button.', (tester) async { + final fetchCompleter = Completer>(); + + when(mockWeatherService.fetchWeather(any)) + .thenAnswer((_) => fetchCompleter.future); await tester.pumpWidget( ProviderScope( overrides: [ - yumemiWeatherClientProvider.overrideWithValue(mockClient) + weatherServiceProvider.overrideWithValue(mockWeatherService) ], child: const MaterialApp( home: WeatherPage(), @@ -190,6 +230,18 @@ void main() { await tester.tap(find.byKey(WeatherPage.reloadButton)); await tester.pump(); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + fetchCompleter.complete( + const Failure( + ErrorMessage.invalidParameter, + ), + ); + + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsNothing); + expect( find.widgetWithText(AlertDialog, ErrorMessage.invalidParameter), findsOneWidget, @@ -218,14 +270,16 @@ void main() { // unknown testWidgets( 'when fetchWeather() returns failure with unknown error, ' - 'error dialog and correct message should be visible. ', (tester) async { - when(mockClient.syncFetchWeather(any)) - .thenThrow(YumemiWeatherError.unknown); + 'error dialog and correct message should be visible. ', (tester) async { + final fetchCompleter = Completer>(); + + when(mockWeatherService.fetchWeather(any)) + .thenAnswer((_) => fetchCompleter.future); await tester.pumpWidget( ProviderScope( overrides: [ - yumemiWeatherClientProvider.overrideWithValue(mockClient) + weatherServiceProvider.overrideWithValue(mockWeatherService) ], child: const MaterialApp( home: WeatherPage(), @@ -241,6 +295,18 @@ void main() { await tester.tap(find.byKey(WeatherPage.reloadButton)); await tester.pump(); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + fetchCompleter.complete( + const Failure( + ErrorMessage.unknown, + ), + ); + + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsNothing); + expect( find.widgetWithText(AlertDialog, ErrorMessage.unknown), findsOneWidget, @@ -255,17 +321,20 @@ void main() { ); }); - // CheckedFromJsonException - testWidgets( - 'when fetchWeather() returns failure with CheckedFromJsonException, ' - 'error dialog and correct message should be visible. ', (tester) async { - when(mockClient.syncFetchWeather(any)) - .thenReturn(invalidJsonDataForCheckedFromJsonException); + // CheckedFromJsonException,FormatException + testWidgets(''' + when fetchWeather() returns failure with CheckedFromJsonException or FormatException, + error dialog and correct message should be visible. ''', + (tester) async { + final fetchCompleter = Completer>(); + + when(mockWeatherService.fetchWeather(any)) + .thenAnswer((_) => fetchCompleter.future); await tester.pumpWidget( ProviderScope( overrides: [ - yumemiWeatherClientProvider.overrideWithValue(mockClient) + weatherServiceProvider.overrideWithValue(mockWeatherService) ], child: const MaterialApp( home: WeatherPage(), @@ -281,46 +350,18 @@ void main() { await tester.tap(find.byKey(WeatherPage.reloadButton)); await tester.pump(); - expect( - find.widgetWithText(AlertDialog, ErrorMessage.receiveInvalidData), - findsOneWidget, - ); + expect(find.byType(CircularProgressIndicator), findsOneWidget); - await tester.tap(find.text('OK')); - await tester.pump(); - - expect( - find.widgetWithText(AlertDialog, ErrorMessage.receiveInvalidData), - findsNothing, - ); - }); - - // FormatException - testWidgets( - 'when fetchWeather() returns failure with FormatException, ' - 'error dialog and correct message should be visible. ', (tester) async { - when(mockClient.syncFetchWeather(any)) - .thenReturn(invalidJsonDataForFormatException); - - await tester.pumpWidget( - ProviderScope( - overrides: [ - yumemiWeatherClientProvider.overrideWithValue(mockClient) - ], - child: const MaterialApp( - home: WeatherPage(), - ), + fetchCompleter.complete( + const Failure( + ErrorMessage.receiveInvalidData, ), ); - expect( - find.widgetWithText(AlertDialog, ErrorMessage.receiveInvalidData), - findsNothing, - ); - - await tester.tap(find.byKey(WeatherPage.reloadButton)); await tester.pump(); + expect(find.byType(CircularProgressIndicator), findsNothing); + expect( find.widgetWithText(AlertDialog, ErrorMessage.receiveInvalidData), findsOneWidget, @@ -331,8 +372,8 @@ void main() { expect( find.widgetWithText(AlertDialog, ErrorMessage.receiveInvalidData), - findsNothing, - ); - }); + findsNothing, + ); + }); }); } diff --git a/test/view/weather_page_test.mocks.dart b/test/view/weather_page_test.mocks.dart new file mode 100644 index 0000000..84b25a4 --- /dev/null +++ b/test/view/weather_page_test.mocks.dart @@ -0,0 +1,56 @@ +// Mocks generated by Mockito 5.4.2 from annotations +// in flutter_training/test/view/weather_page_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:flutter_training/model/weather_forecast.dart' as _i5; +import 'package:flutter_training/model/weather_request.dart' as _i6; +import 'package:flutter_training/service/weather_service.dart' as _i2; +import 'package:flutter_training/utils/api/result.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i7; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [WeatherService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWeatherService extends _i1.Mock implements _i2.WeatherService { + @override + _i3.Future<_i4.Result<_i5.WeatherForecast, String>> fetchWeather( + _i6.WeatherRequest? request) => + (super.noSuchMethod( + Invocation.method( + #fetchWeather, + [request], + ), + returnValue: _i3.Future<_i4.Result<_i5.WeatherForecast, String>>.value( + _i7.dummyValue<_i4.Result<_i5.WeatherForecast, String>>( + this, + Invocation.method( + #fetchWeather, + [request], + ), + )), + returnValueForMissingStub: + _i3.Future<_i4.Result<_i5.WeatherForecast, String>>.value( + _i7.dummyValue<_i4.Result<_i5.WeatherForecast, String>>( + this, + Invocation.method( + #fetchWeather, + [request], + ), + )), + ) as _i3.Future<_i4.Result<_i5.WeatherForecast, String>>); +} From 6a3cc99897a12f046829a2d6e76d65a93a08075c Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 27 Oct 2023 14:25:44 +0900 Subject: [PATCH 17/21] Add some comments --- test/view/weather_page_test.dart | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/test/view/weather_page_test.dart b/test/view/weather_page_test.dart index e2147a7..78f74c6 100644 --- a/test/view/weather_page_test.dart +++ b/test/view/weather_page_test.dart @@ -37,11 +37,11 @@ void main() { // 気温のテキストの色を取得 final minTemp = - tester.widget(find.byKey(WeatherForecastPanel.minTempKey)) as Text; + tester.widget(find.byKey(WeatherForecastPanel.minTempKey)) as Text; final minTempColor = minTemp.style!.color; final maxTemp = - tester.widget(find.byKey(WeatherForecastPanel.maxTempKey)) as Text; + tester.widget(find.byKey(WeatherForecastPanel.maxTempKey)) as Text; final maxTempColor = maxTemp.style!.color; expect(find.byType(Placeholder), findsOneWidget); @@ -61,8 +61,10 @@ void main() { // cloudy testWidgets( 'when reload button is pressed, ' - 'cloudy weather and correct temperature should be displayed.', - (tester) async { + 'cloudy weather and correct temperature should be displayed.', + (tester) async { + // 現状sealedクラスで定義された方をwhenで返すことができないので、provideDummyを使って型とその初期値を与える + // https://pub.dev/documentation/mockito/latest/mockito/provideDummy.html provideDummy>( const Failure('初期値として仮のエラーを返します。'), ); @@ -106,8 +108,8 @@ void main() { //sunny testWidgets( 'when reload button is pressed, ' - 'sunny weather and correct temperature should be displayed.', - (tester) async { + 'sunny weather and correct temperature should be displayed.', + (tester) async { provideDummy>( const Failure('初期値として仮のエラーを返します。'), ); @@ -154,8 +156,8 @@ void main() { // rainy testWidgets( 'when reload button is pressed, ' - 'rainy weather and correct temperature should be displayed.', - (tester) async { + 'rainy weather and correct temperature should be displayed.', + (tester) async { provideDummy>( const Failure('初期値として仮のエラーを返します。'), ); @@ -204,8 +206,8 @@ void main() { // invalidParameter testWidgets( 'when fetchWeather() returns failure with invalidParameter error, ' - 'error dialog and correct message should be visible. ' - 'Then the dialog is closed by pressing the ok button.', (tester) async { + 'error dialog and correct message should be visible. ' + 'Then the dialog is closed by pressing the ok button.', (tester) async { final fetchCompleter = Completer>(); when(mockWeatherService.fetchWeather(any)) @@ -270,7 +272,7 @@ void main() { // unknown testWidgets( 'when fetchWeather() returns failure with unknown error, ' - 'error dialog and correct message should be visible. ', (tester) async { + 'error dialog and correct message should be visible. ', (tester) async { final fetchCompleter = Completer>(); when(mockWeatherService.fetchWeather(any)) @@ -372,8 +374,8 @@ void main() { expect( find.widgetWithText(AlertDialog, ErrorMessage.receiveInvalidData), - findsNothing, - ); - }); + findsNothing, + ); + }); }); } From 3108815d5311e42b0db45c9883abf57f4994a7fe Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 27 Oct 2023 15:02:45 +0900 Subject: [PATCH 18/21] Fix comment space --- test/view/weather_page_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/view/weather_page_test.dart b/test/view/weather_page_test.dart index 78f74c6..893d4a7 100644 --- a/test/view/weather_page_test.dart +++ b/test/view/weather_page_test.dart @@ -105,7 +105,7 @@ void main() { expect(find.text('7 ℃'), findsOneWidget); }); - //sunny + // sunny testWidgets( 'when reload button is pressed, ' 'sunny weather and correct temperature should be displayed.', From 41f7c2a4886e6429e81597437a245ddeb0f9a509 Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Fri, 27 Oct 2023 16:29:36 +0900 Subject: [PATCH 19/21] Update provider dependencies --- doc/ARCHITECTURE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index 7c8a8be..8e461e3 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -61,6 +61,7 @@ * 天気の情報、気温等を表示しているコンポネント。 * `weatherStateNotifierProvider`をwatchして、天気の状態を表示する。 +## Providerの依存関係 ```mermaid flowchart TB subgraph Arrows @@ -82,12 +83,15 @@ flowchart TB end weatherStateNotifierProvider[["weatherStateNotifierProvider"]]; + loadingStateNotifierProvider[["loadingStateNotifierProvider"]]; weatherServiceProvider[["weatherServiceProvider"]]; yumemiWeatherClientProvider[["yumemiWeatherClientProvider"]]; WeatherForecastPanel((WeatherForecastPanel)); WeatherPage((WeatherPage)); weatherStateNotifierProvider ==> WeatherForecastPanel; + loadingStateNotifierProvider ==> WeatherPage; weatherStateNotifierProvider -.-> WeatherPage; yumemiWeatherClientProvider ==> weatherServiceProvider; + ``` \ No newline at end of file From 5762f078ac27f8a3d351678fbfabb681e7d4301c Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Sat, 28 Oct 2023 17:39:03 +0900 Subject: [PATCH 20/21] Update ARCHITECTURE.md --- doc/ARCHITECTURE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index 8e461e3..4691d52 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -41,11 +41,16 @@ ## State * `Service`からResultを取得し、取得した天気のデータを保持する +* ローディングの状態を通知する ### WeatherStateNotifier * 天気の取得が成功したらそのデータを格納し、失敗した場合は、`onError`関数を引数で受け取る。 +### LoadingStateNotifier + +* ローディングの状態を管理、`WeatherPage`に通知する + ## View * 天気の表示、エラーのダイアログを表示。 天気を取得するボタンを提供。 @@ -55,6 +60,7 @@ * アプリのメイン画面 * `Reload`ボタンを押して、`weatherStateNotifierProvider`をreadして、天気の取得処理を行う。 * `weatherStateNotifierProvider`で天気取得に失敗した場合は、エラーメッセージを表示する。 +* `LoadingStateNotifier`のローディング状態をウォッチしてローディングを表示する。 ### WeatherForecastPanel From fa2e29a7dfdb6a4473d3b5fd92621d87b9e12e2c Mon Sep 17 00:00:00 2001 From: mqkotoo Date: Mon, 30 Oct 2023 16:07:33 +0900 Subject: [PATCH 21/21] Delete unnecessary ignore --- lib/state/weather_state_notifier.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/state/weather_state_notifier.dart b/lib/state/weather_state_notifier.dart index cc01b76..cf0e0b0 100644 --- a/lib/state/weather_state_notifier.dart +++ b/lib/state/weather_state_notifier.dart @@ -1,4 +1,3 @@ -// ignore_for_file: avoid_manual_providers_as_generated_provider_dependency import 'package:flutter_training/model/weather_forecast.dart'; import 'package:flutter_training/model/weather_request.dart'; import 'package:flutter_training/service/weather_service.dart';