diff --git a/.github/workflows/flutter_ci_test.yaml b/.github/workflows/flutter_ci_test.yaml index e219c112..3194d118 100644 --- a/.github/workflows/flutter_ci_test.yaml +++ b/.github/workflows/flutter_ci_test.yaml @@ -38,20 +38,20 @@ jobs: changed_files=$(git diff $base_sha $current_sha --name-only | grep '\.dart$' || true) if [[ -n "$changed_files" ]]; then echo "Dart files changed." - echo "dart-changes=true" >> $GITHUB_ENV + echo "::set-output name=run-tests::true" else echo "No Dart files changed." - echo "dart-changes=false" >> $GITHUB_ENV + echo "::set-output name=run-tests::false" fi # fvm のバージョンとチャネルを環境変数に設定する - name: Check fvm - if: env.run-tests == 'true' + if: steps.dartfile.outputs.run-tests == 'true' uses: kuhnroyal/flutter-fvm-config-action@v1 # Flutter SDK の設定 - name: Setup Flutter SDK - if: env.run-tests == 'true' + if: steps.dartfile.outputs.run-tests == 'true' uses: subosito/flutter-action@v2 with: # バージョンとチャネルは fvm の値を使う @@ -66,35 +66,35 @@ jobs: # melosの設定 # デフォルトでmelos bootstrapコマンドが実行される - name: Setup melos - if: env.run-tests == 'true' + if: steps.dartfile.outputs.run-tests == 'true' uses: bluefireteam/melos-action@v1 with: run-bootstrap: false # melos bootstrap を実行する。 - name: melos bootstrap - if: env.run-tests == 'true' + if: steps.dartfile.outputs.run-tests == 'true' run: melos bootstrap --sdk-path=${{ runner.tool_cache }}/flutter # 依存関係を解決する。 - name: Install Flutter dependencies by melos - if: env.run-tests == 'true' + if: steps.dartfile.outputs.run-tests == 'true' run: melos pg --sdk-path=${{ runner.tool_cache }}/flutter # コードフォーマットを実行する。 # フォーマットの結果変更が発生した場合はエラー扱いにする。 - name: Run Flutter format - if: env.run-tests == 'true' + if: steps.dartfile.outputs.run-tests == 'true' run: melos exec --sdk-path=${{ runner.tool_cache }}/flutter -- "dart format --set-exit-if-changed ." # 静的解析を実行する。 - name: Analyze project source - if: env.run-tests == 'true' + if: steps.dartfile.outputs.run-tests == 'true' run: melos exec --sdk-path=${{ runner.tool_cache }}/flutter -- "flutter analyze ." # テストを実行する。 - name: Run-Flutter-Test - if: env.run-tests == 'true' + if: steps.dartfile.outputs.run-tests == 'true' run: melos exec --sdk-path=${{ runner.tool_cache }}/flutter -- "flutter test --machine --coverage > test-report.log" # mottai_flutter_app のテスト結果を GitHub Actions に表示する。 @@ -102,7 +102,7 @@ jobs: uses: dorny/test-reporter@v1 # テスト結果を表示するのでテストが失敗しても実行する。 # ただし、コードフォーマットや静的解析で CI が失敗した場合は処理を行わない。 - if: env.run-tests == 'true' && steps.Run-Flutter-Test.outputs.return-code == '0' + if: steps.dartfile.outputs.run-tests == 'true' && steps.Run-Flutter-Test.outputs.return-code == '0' with: name: Flutter Test Report mottai_flutter_app path: /home/runner/work/mottai-flutter-app/mottai-flutter-app/packages/mottai_flutter_app/test-report.log @@ -110,7 +110,7 @@ jobs: # Codecov に結果を送信する。 - name: Upload coverage to Codecov - if: env.run-tests == 'true' + if: steps.dartfile.outputs.run-tests == 'true' uses: codecov/codecov-action@v2 with: token: ${{secrets.CODECOV_TOKEN}} @@ -124,7 +124,7 @@ jobs: uses: dorny/test-reporter@v1 # テスト結果を表示するのでテストが失敗しても実行する。 # ただし、コードフォーマットや静的解析で CI が失敗した場合は処理を行わない。 - if: env.run-tests == 'true' && steps.Run-Flutter-Test.outputs.return-code == '0' + if: steps.dartfile.outputs.run-tests == 'true' && steps.Run-Flutter-Test.outputs.return-code == '0' with: name: Flutter Test Report mottai_flutter_app path: /home/runner/work/mottai-flutter-app/mottai-flutter-app/packages/firebase_common/test-report.log @@ -132,7 +132,7 @@ jobs: # Codecov に結果を送信する。 - name: Upload coverage to Codecov - if: env.run-tests == 'true' + if: steps.dartfile.outputs.run-tests == 'true' uses: codecov/codecov-action@v2 with: token: ${{secrets.CODECOV_TOKEN}} @@ -146,7 +146,7 @@ jobs: uses: dorny/test-reporter@v1 # テスト結果を表示するのでテストが失敗しても実行する。 # ただし、コードフォーマットや静的解析でCIが失敗した場合は処理を行わない。 - if: env.run-tests == 'true' && steps.Run-Flutter-Test.outputs.return-code == '0' + if: steps.dartfile.outputs.run-tests == 'true' && steps.Run-Flutter-Test.outputs.return-code == '0' with: name: Flutter Test Report dart_flutter_common path: /home/runner/work/mottai-flutter-app/mottai-flutter-app/packages/dart_flutter_common/test-report.log @@ -154,7 +154,7 @@ jobs: # Codecov に結果を送信する。 - name: Upload coverage to Codecov - if: env.run-tests == 'true' + if: steps.dartfile.outputs.run-tests == 'true' uses: codecov/codecov-action@v2 with: token: ${{secrets.CODECOV_TOKEN}} diff --git a/packages/mottai_flutter_app/lib/auth/ui/button_builder.dart b/packages/mottai_flutter_app/lib/auth/ui/button_builder.dart index 9ae1d45a..f627f2d9 100644 --- a/packages/mottai_flutter_app/lib/auth/ui/button_builder.dart +++ b/packages/mottai_flutter_app/lib/auth/ui/button_builder.dart @@ -134,7 +134,7 @@ class SignInButtonBuilder extends StatelessWidget { separator!, SizedBox( width: separatorSpaceRight, - ) + ), ], Text( text, diff --git a/packages/mottai_flutter_app/lib/chat/chat_room.dart b/packages/mottai_flutter_app/lib/chat/chat_room.dart index 3e6c4736..43243021 100644 --- a/packages/mottai_flutter_app/lib/chat/chat_room.dart +++ b/packages/mottai_flutter_app/lib/chat/chat_room.dart @@ -173,7 +173,7 @@ class ChatRoomStateNotifier extends StateNotifier { state = state.copyWith( readChatMessages: [ ...state.newReadChatMessages, - ...state.pastReadChatMessages + ...state.pastReadChatMessages, ], ); } diff --git a/packages/mottai_flutter_app/lib/development/disable_user_account_request/ui/disable_user_account_request_controller.dart b/packages/mottai_flutter_app/lib/development/disable_user_account_request/ui/disable_user_account_request_controller.dart index d6a12798..5ec1b03b 100644 --- a/packages/mottai_flutter_app/lib/development/disable_user_account_request/ui/disable_user_account_request_controller.dart +++ b/packages/mottai_flutter_app/lib/development/disable_user_account_request/ui/disable_user_account_request_controller.dart @@ -64,7 +64,7 @@ class DisableUserAccountRequestController { Navigator.pop(context); }, child: const Text('戻る'), - ) + ), ], ), ); diff --git a/packages/mottai_flutter_app/lib/development/firebase_storage/ui/firebase_storage.dart b/packages/mottai_flutter_app/lib/development/firebase_storage/ui/firebase_storage.dart index 98ac6408..e4fc76bb 100644 --- a/packages/mottai_flutter_app/lib/development/firebase_storage/ui/firebase_storage.dart +++ b/packages/mottai_flutter_app/lib/development/firebase_storage/ui/firebase_storage.dart @@ -116,7 +116,7 @@ class _FirebaseStorageSampleState else const Center( child: Text('未アップロード'), - ) + ), ], ), ); diff --git a/packages/mottai_flutter_app/lib/host/ui/host_form.dart b/packages/mottai_flutter_app/lib/host/ui/host_form.dart index e2a9251f..2ed016f0 100644 --- a/packages/mottai_flutter_app/lib/host/ui/host_form.dart +++ b/packages/mottai_flutter_app/lib/host/ui/host_form.dart @@ -268,7 +268,7 @@ class HostFormState extends ConsumerState { color: Theme.of(context).colorScheme.error, ), ), - ) + ), ], ), ), @@ -578,7 +578,7 @@ class _TextInputSection extends StatelessWidget { ], if (child != null) ...[ child!, - ] + ], ], ), ); diff --git a/packages/mottai_flutter_app/lib/map/ui/map.dart b/packages/mottai_flutter_app/lib/map/ui/map.dart index 441b5e78..f169fe81 100644 --- a/packages/mottai_flutter_app/lib/map/ui/map.dart +++ b/packages/mottai_flutter_app/lib/map/ui/map.dart @@ -257,7 +257,7 @@ class MapPageState extends ConsumerState { ); }, icon: const Icon(Icons.near_me), - ) + ), ], ), ), diff --git a/packages/mottai_flutter_app/lib/pagination/firestore_pagination.dart b/packages/mottai_flutter_app/lib/pagination/firestore_pagination.dart index 5db73e74..890cd8a1 100644 --- a/packages/mottai_flutter_app/lib/pagination/firestore_pagination.dart +++ b/packages/mottai_flutter_app/lib/pagination/firestore_pagination.dart @@ -1,13 +1,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +// TODO: AsyncNotifier で書き換える /// Firestore のパジネーションを行うための [StateNotifier]. class FirestorePaginationStateNotifier extends StateNotifier>> { FirestorePaginationStateNotifier({ required this.fetch, required this.idFromObject, + int initialPerPage = 10, }) : super(AsyncValue>.data(const [])) { - _initialize(); + _initialize(perPage: initialPerPage); } /// データを取得する関数。 @@ -25,9 +27,6 @@ class FirestorePaginationStateNotifier /// 次のページがあるかどうか。 bool _hasNext = true; - /// リクエスト一回あたりの取得件数のデフォルト値。 - static const _defaultPerPage = 10; - /// 取得処理中かどうか。 bool get isFetching => _isFetching; @@ -35,13 +34,16 @@ class FirestorePaginationStateNotifier bool get hasNext => _hasNext; /// 初期化処理。 - Future> _initialize({int perPage = _defaultPerPage}) async { + Future> _initialize({required int perPage}) async { state = AsyncValue>.loading(); _isFetching = true; try { final items = await fetch(perPage, null); + state = AsyncValue.data(items); _hasNext = items.isNotEmpty; - state = AsyncValue>.data(items); + if (items.isNotEmpty) { + _lastFetchedId = idFromObject(items.last); + } } on Exception catch (e, s) { state = AsyncValue>.error(e, s); } finally { @@ -51,18 +53,20 @@ class FirestorePaginationStateNotifier } /// 次のページを取得する。 - Future fetchNext({int perPage = _defaultPerPage}) async { + Future fetchNext({required int perPage}) async { final items = state; if (_isFetching || items.isRefreshing || !items.hasValue || !_hasNext) { return; } _isFetching = true; try { - final result = await fetch(perPage, _lastFetchedId); - final newItems = [...state.valueOrNull ?? [], ...result]; + final items = await fetch(perPage, _lastFetchedId); + final newItems = [...state.valueOrNull ?? [], ...items]; state = AsyncValue.data(newItems); - _hasNext = result.isNotEmpty; - _lastFetchedId = idFromObject(result.last); + _hasNext = items.isNotEmpty; + if (items.isNotEmpty) { + _lastFetchedId = idFromObject(items.last); + } } on Exception catch (e, stackTrace) { state = AsyncValue>.error(e, stackTrace); return; @@ -70,19 +74,4 @@ class FirestorePaginationStateNotifier _isFetching = false; } } - - /// リフレッシュする。 - Future refresh({int perPage = _defaultPerPage}) async { - _isFetching = true; - _lastFetchedId = null; - try { - final items = await fetch(perPage, null); - _hasNext = items.isNotEmpty; - state = AsyncValue>.data(items); - } on Exception catch (e, s) { - state = AsyncValue>.error(e, s); - } finally { - _isFetching = false; - } - } } diff --git a/packages/mottai_flutter_app/lib/pagination/ui/pagination_list_view.dart b/packages/mottai_flutter_app/lib/pagination/ui/pagination_list_view.dart index 2f3751a2..a73cdd9f 100644 --- a/packages/mottai_flutter_app/lib/pagination/ui/pagination_list_view.dart +++ b/packages/mottai_flutter_app/lib/pagination/ui/pagination_list_view.dart @@ -6,11 +6,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../firestore_pagination.dart'; -/// [FirestorePaginationStateNotifier] を使用した無限スクロールの [ListView] UI. -class FirestorePaginationListView extends ConsumerWidget { - const FirestorePaginationListView({ +/// [FirestorePaginationStateNotifier] を使用したパジネーション UI. +class FirestorePaginationView extends ConsumerWidget { + const FirestorePaginationView({ required this.stateNotifierProvider, - required this.itemBuilder, + required this.whenData, + required this.whenEmpty, + this.perPage = 10, + this.scrollValueThreshold = 0.8, this.showDebugIndicator = false, super.key, }); @@ -20,13 +23,19 @@ class FirestorePaginationListView extends ConsumerWidget { AsyncValue>> stateNotifierProvider; /// [ListView.builder] で表示する各要素のビルダー関数。 - final Widget Function(BuildContext, T) itemBuilder; + final Widget Function(BuildContext, List) whenData; - /// 開発時のみ表示する、無限スクロールのデバッグ用ウィジェットを表示するか。 - final bool showDebugIndicator; + /// 表示するものがないときに表示する内容 + final Widget Function(BuildContext) whenEmpty; + + /// 一度に取得する件数。 + final int perPage; /// 画面の何割をスクロールした時点で次の _limit 件のメッセージを取得するか。 - static const _scrollValueThreshold = 0.8; + final double scrollValueThreshold; + + /// デバッグ用のウィジェットを表示するかどうか。 + final bool showDebugIndicator; @override Widget build(BuildContext context, WidgetRef ref) { @@ -40,18 +49,12 @@ class FirestorePaginationListView extends ConsumerWidget { onNotification: (notification) { final metrics = notification.metrics; final scrollValue = metrics.pixels / metrics.maxScrollExtent; - if (scrollValue > _scrollValueThreshold) { - notifier.fetchNext(); + if (scrollValue > scrollValueThreshold) { + notifier.fetchNext(perPage: perPage); } return false; }, - child: ListView.builder( - itemCount: items.length, - itemBuilder: (context, index) { - final item = items[index]; - return itemBuilder(context, item); - }, - ), + child: whenData(context, items), ), if (kDebugMode && showDebugIndicator) Positioned( @@ -71,7 +74,6 @@ class FirestorePaginationListView extends ConsumerWidget { } } -// TODO: あとで消す。 /// 開発時のみ表示する、無限スクロールのデバッグ用ウィジェット。 class _DebugIndicator extends ConsumerWidget { const _DebugIndicator({ @@ -79,7 +81,7 @@ class _DebugIndicator extends ConsumerWidget { required this.items, }); - /// [FirestorePaginationStateNotifier] + /// [FirestorePaginationStateNotifier] インスタンス。 final FirestorePaginationStateNotifier notifier; /// 取得したアイテム一覧。 diff --git a/packages/mottai_flutter_app/lib/review/ui/reviews.dart b/packages/mottai_flutter_app/lib/review/ui/reviews.dart index 04aadbab..e0f60cff 100644 --- a/packages/mottai_flutter_app/lib/review/ui/reviews.dart +++ b/packages/mottai_flutter_app/lib/review/ui/reviews.dart @@ -21,25 +21,32 @@ class ReviewsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return FirestorePaginationListView( + return FirestorePaginationView( stateNotifierProvider: reviewsStateNotifierProvider, - itemBuilder: (context, review) => MaterialVerticalCard( - headerImageUrl: ref.watch(workerImageUrlProvider(review.workerId)), - header: ref.watch(workerDisplayNameProvider(review.workerId)), - subhead: review.createdAt?.formatRelativeDate(), - imageUrl: review.imageUrl, - title: review.title, - content: review.content, - menuButtonOnPressed: () {}, - actions: [ - ElevatedButton( - onPressed: () { - // TODO: 感想詳細ページへ遷移する - }, - child: const Text('もっと見る'), - ), - ], + whenData: (context, reviews) => ListView.builder( + itemCount: reviews.length, + itemBuilder: (context, index) { + final review = reviews[index]; + return MaterialVerticalCard( + headerImageUrl: ref.watch(workerImageUrlProvider(review.workerId)), + header: ref.watch(workerDisplayNameProvider(review.workerId)), + subhead: review.createdAt?.formatRelativeDate(), + imageUrl: review.imageUrl, + title: review.title, + content: review.content, + menuButtonOnPressed: () {}, + actions: [ + ElevatedButton( + onPressed: () { + // TODO: 感想詳細ページへ遷移する + }, + child: const Text('もっと見る'), + ), + ], + ); + }, ), + whenEmpty: (_) => const SizedBox(), ); } } diff --git a/packages/mottai_flutter_app/lib/root/ui/root.dart b/packages/mottai_flutter_app/lib/root/ui/root.dart index 1b4108ac..31d1e436 100644 --- a/packages/mottai_flutter_app/lib/root/ui/root.dart +++ b/packages/mottai_flutter_app/lib/root/ui/root.dart @@ -184,7 +184,7 @@ class _DrawerChild extends ConsumerWidget { leading: const Icon(Icons.login), title: const Text('サインイン(ソーシャル)'), onTap: () => context.router.pushNamed(SignInSamplePage.location), - ) + ), ], ListTile( leading: const Icon(Icons.notifications), diff --git a/packages/mottai_flutter_app/lib/user/ui/user_mode.dart b/packages/mottai_flutter_app/lib/user/ui/user_mode.dart index b28fa7bf..35ef3111 100644 --- a/packages/mottai_flutter_app/lib/user/ui/user_mode.dart +++ b/packages/mottai_flutter_app/lib/user/ui/user_mode.dart @@ -53,7 +53,7 @@ class UserModeSection extends ConsumerWidget { .read(userModeStateProvider.notifier) .update((_) => newSelection.first); }, - ) + ), ], ); }, diff --git a/packages/mottai_flutter_app/test/todo/ui/todos_test.dart b/packages/mottai_flutter_app/test/todo/ui/todos_test.dart index 9210e600..32b93e43 100644 --- a/packages/mottai_flutter_app/test/todo/ui/todos_test.dart +++ b/packages/mottai_flutter_app/test/todo/ui/todos_test.dart @@ -59,7 +59,7 @@ void main() { await tester.pumpWidget( ProviderScope( overrides: [ - todoRepositoryProvider.overrideWithValue(mockTodoRepository) + todoRepositoryProvider.overrideWithValue(mockTodoRepository), ], child: const _TodosPage(), ),