Skip to content

Commit

Permalink
Merge pull request #196 from kosukesaigusa/fix/pagination
Browse files Browse the repository at this point in the history
fix: fix firestore pagination bug
  • Loading branch information
kosukesaigusa committed Sep 21, 2023
2 parents f2cd602 + 7c81cdd commit 74940eb
Show file tree
Hide file tree
Showing 13 changed files with 86 additions and 88 deletions.
32 changes: 16 additions & 16 deletions .github/workflows/flutter_ci_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 の値を使う
Expand All @@ -66,51 +66,51 @@ 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 に表示する。
- name: Report Test mottai_flutter_app
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
reporter: flutter-json

# 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}}
Expand All @@ -124,15 +124,15 @@ 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
reporter: flutter-json

# 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}}
Expand All @@ -146,15 +146,15 @@ 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
reporter: flutter-json

# 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}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class SignInButtonBuilder extends StatelessWidget {
separator!,
SizedBox(
width: separatorSpaceRight,
)
),
],
Text(
text,
Expand Down
2 changes: 1 addition & 1 deletion packages/mottai_flutter_app/lib/chat/chat_room.dart
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ class ChatRoomStateNotifier extends StateNotifier<ChatRoomState> {
state = state.copyWith(
readChatMessages: [
...state.newReadChatMessages,
...state.pastReadChatMessages
...state.pastReadChatMessages,
],
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class DisableUserAccountRequestController {
Navigator.pop(context);
},
child: const Text('戻る'),
)
),
],
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class _FirebaseStorageSampleState
else
const Center(
child: Text('未アップロード'),
)
),
],
),
);
Expand Down
4 changes: 2 additions & 2 deletions packages/mottai_flutter_app/lib/host/ui/host_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ class HostFormState extends ConsumerState<HostForm> {
color: Theme.of(context).colorScheme.error,
),
),
)
),
],
),
),
Expand Down Expand Up @@ -578,7 +578,7 @@ class _TextInputSection<T extends dynamic> extends StatelessWidget {
],
if (child != null) ...[
child!,
]
],
],
),
);
Expand Down
2 changes: 1 addition & 1 deletion packages/mottai_flutter_app/lib/map/ui/map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ class MapPageState extends ConsumerState<MapPage> {
);
},
icon: const Icon(Icons.near_me),
)
),
],
),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';

// TODO: AsyncNotifier で書き換える
/// Firestore のパジネーションを行うための [StateNotifier].
class FirestorePaginationStateNotifier<T>
extends StateNotifier<AsyncValue<List<T>>> {
FirestorePaginationStateNotifier({
required this.fetch,
required this.idFromObject,
int initialPerPage = 10,
}) : super(AsyncValue<List<T>>.data(const [])) {
_initialize();
_initialize(perPage: initialPerPage);
}

/// データを取得する関数。
Expand All @@ -25,23 +27,23 @@ class FirestorePaginationStateNotifier<T>
/// 次のページがあるかどうか。
bool _hasNext = true;

/// リクエスト一回あたりの取得件数のデフォルト値。
static const _defaultPerPage = 10;

/// 取得処理中かどうか。
bool get isFetching => _isFetching;

/// 次のページがあるかどうか。
bool get hasNext => _hasNext;

/// 初期化処理。
Future<List<T>> _initialize({int perPage = _defaultPerPage}) async {
Future<List<T>> _initialize({required int perPage}) async {
state = AsyncValue<List<T>>.loading();
_isFetching = true;
try {
final items = await fetch(perPage, null);
state = AsyncValue.data(items);
_hasNext = items.isNotEmpty;
state = AsyncValue<List<T>>.data(items);
if (items.isNotEmpty) {
_lastFetchedId = idFromObject(items.last);
}
} on Exception catch (e, s) {
state = AsyncValue<List<T>>.error(e, s);
} finally {
Expand All @@ -51,38 +53,25 @@ class FirestorePaginationStateNotifier<T>
}

/// 次のページを取得する。
Future<void> fetchNext({int perPage = _defaultPerPage}) async {
Future<void> 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 ?? <T>[], ...result];
final items = await fetch(perPage, _lastFetchedId);
final newItems = [...state.valueOrNull ?? <T>[], ...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<List<T>>.error(e, stackTrace);
return;
} finally {
_isFetching = false;
}
}

/// リフレッシュする。
Future<void> refresh({int perPage = _defaultPerPage}) async {
_isFetching = true;
_lastFetchedId = null;
try {
final items = await fetch(perPage, null);
_hasNext = items.isNotEmpty;
state = AsyncValue<List<T>>.data(items);
} on Exception catch (e, s) {
state = AsyncValue<List<T>>.error(e, s);
} finally {
_isFetching = false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';

import '../firestore_pagination.dart';

/// [FirestorePaginationStateNotifier] を使用した無限スクロールの [ListView] UI.
class FirestorePaginationListView<T> extends ConsumerWidget {
const FirestorePaginationListView({
/// [FirestorePaginationStateNotifier] を使用したパジネーション UI.
class FirestorePaginationView<T> 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,
});
Expand All @@ -20,13 +23,19 @@ class FirestorePaginationListView<T> extends ConsumerWidget {
AsyncValue<List<T>>> stateNotifierProvider;

/// [ListView.builder] で表示する各要素のビルダー関数。
final Widget Function(BuildContext, T) itemBuilder;
final Widget Function(BuildContext, List<T>) 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) {
Expand All @@ -40,18 +49,12 @@ class FirestorePaginationListView<T> 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(
Expand All @@ -71,15 +74,14 @@ class FirestorePaginationListView<T> extends ConsumerWidget {
}
}

// TODO: あとで消す。
/// 開発時のみ表示する、無限スクロールのデバッグ用ウィジェット。
class _DebugIndicator<T> extends ConsumerWidget {
const _DebugIndicator({
required this.notifier,
required this.items,
});

/// [FirestorePaginationStateNotifier]
/// [FirestorePaginationStateNotifier] インスタンス。
final FirestorePaginationStateNotifier<T> notifier;

/// 取得したアイテム一覧。
Expand Down
Loading

0 comments on commit 74940eb

Please sign in to comment.