Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor ListState and paginated screens #334

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
49 changes: 20 additions & 29 deletions lib/blocs/album_list_cubit.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import 'dart:async';

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:reaxit/api/api_repository.dart';
import 'package:reaxit/api/exceptions.dart';
import 'package:reaxit/blocs/list_state.dart';
import 'package:reaxit/config.dart' as config;
import 'package:reaxit/blocs.dart';
import 'package:reaxit/models.dart';
import 'package:reaxit/ui/widgets.dart';

typedef AlbumListState = ListState<ListAlbum>;

class AlbumListCubit extends Cubit<AlbumListState> {
static const int firstPageSize = 60;
static const int pageSize = 30;

class AlbumListCubit extends PaginatedCubit<ListAlbum> {
final ApiRepository api;

/// The last used search query. Can be set through `this.search(query)`.
Expand All @@ -27,10 +22,10 @@ class AlbumListCubit extends Cubit<AlbumListState> {
/// The offset to be used for the next paginated request.
int _nextOffset = 0;

AlbumListCubit(this.api) : super(const AlbumListState.loading(results: []));
AlbumListCubit(this.api) : super(firstPageSize: 60, pageSize: 30);

@override
Future<void> load() async {
emit(state.copyWith(isLoading: true));
try {
final query = _searchQuery;
final albumsResponse = await api.getAlbums(
Expand All @@ -49,30 +44,29 @@ class AlbumListCubit extends Cubit<AlbumListState> {

if (albumsResponse.results.isEmpty) {
if (query?.isEmpty ?? true) {
emit(const AlbumListState.failure(message: 'There are no albums.'));
emit(const ErrorListState('There are no albums.'));
} else {
emit(AlbumListState.failure(
message: 'There are no albums found for "$query".',
));
emit(ErrorListState('There are no albums found for "$query".'));
}
} else {
emit(AlbumListState.success(
results: albumsResponse.results,
isDone: isDone,
));
emit(ResultsListState.withDone(albumsResponse.results, isDone));
}
} on ApiException catch (exception) {
emit(AlbumListState.failure(message: exception.message));
emit(ErrorListState(exception.message));
}
}

@override
Future<void> more() async {
// Ignore calls to `more()` if there is no data, or already more coming.
final oldState = state;
if (oldState is! ResultsListState ||
oldState is LoadingMoreListState ||
oldState is DoneListState) return;

// Ignore calls to `more()` if there is no data, or already more coming.
if (oldState.isDone || oldState.isLoading || oldState.isLoadingMore) return;
final resultsState = oldState as ResultsListState<ListAlbum>;

emit(oldState.copyWith(isLoadingMore: true));
emit(LoadingMoreListState.from(resultsState));
try {
final query = _searchQuery;

Expand All @@ -87,17 +81,14 @@ class AlbumListCubit extends Cubit<AlbumListState> {
// changed since the request was made.
if (query != _searchQuery) return;

final albums = state.results + albumsResponse.results;
final albums = resultsState.results + albumsResponse.results;
final isDone = albums.length == albumsResponse.count;

_nextOffset += pageSize;

emit(AlbumListState.success(
results: albums,
isDone: isDone,
));
emit(ResultsListState.withDone(albums, isDone));
} on ApiException catch (exception) {
emit(AlbumListState.failure(message: exception.message));
emit(ErrorListState(exception.getMessage()));
}
}

Expand All @@ -110,7 +101,7 @@ class AlbumListCubit extends Cubit<AlbumListState> {
_searchDebounceTimer?.cancel();
if (query?.isEmpty ?? false) {
/// Don't get results when the query is empty.
emit(const AlbumListState.loading(results: []));
emit(const LoadingListState());
} else {
_searchDebounceTimer = Timer(config.searchDebounceTime, load);
}
Expand Down
83 changes: 81 additions & 2 deletions lib/blocs/calendar_cubit.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import 'dart:async';

import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:reaxit/api/api_repository.dart';
import 'package:reaxit/api/exceptions.dart';
import 'package:reaxit/blocs.dart';
import 'package:reaxit/config.dart' as config;
import 'package:reaxit/models.dart';

Expand Down Expand Up @@ -103,7 +103,86 @@ class CalendarEvent {
);
}

typedef CalendarState = ListState<CalendarEvent>;
/// Generic class to be used as state for paginated lists.
class CalendarState extends Equatable {
/// The results to be shown. These are outdated if `isLoading` is true.
final List<CalendarEvent> results;

/// A message describing why there are no results.
final String? message;

/// Different results are being loaded. The results are outdated.
final bool isLoading;

/// More of the same results are being loaded. The results are not outdated.
final bool isLoadingMore;

/// The last results have been loaded. There are no more pages left.
final bool isDone;

bool get hasException => message != null;

const CalendarState({
required this.results,
required this.message,
required this.isLoading,
required this.isLoadingMore,
required this.isDone,
});

CalendarState copyWith({
List<CalendarEvent>? results,
String? message,
bool? isLoading,
bool? isLoadingMore,
bool? isDone,
}) =>
CalendarState(
results: results ?? this.results,
message: message ?? this.message,
isLoading: isLoading ?? this.isLoading,
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
isDone: isDone ?? this.isDone,
);

@override
List<Object?> get props => [
results,
message,
isLoading,
isLoadingMore,
isDone,
];

@override
String toString() {
return 'ListState<$CalendarEvent>(isLoading: $isLoading, isLoadingMore: $isLoadingMore,'
' isDone: $isDone, message: $message, ${results.length} ${CalendarEvent}s)';
}

const CalendarState.loading({required this.results})
: message = null,
isLoading = true,
isLoadingMore = false,
isDone = true;

const CalendarState.loadingMore({required this.results})
: message = null,
isLoading = false,
isLoadingMore = true,
isDone = true;

const CalendarState.success({required this.results, required this.isDone})
: message = null,
isLoading = false,
isLoadingMore = false;

const CalendarState.failure({required String this.message})
: results = const [],
isLoading = false,
isLoadingMore = false,
isDone = true;
}

class CalendarCubit extends Cubit<CalendarState> {
static const int firstPageSize = 20;
Expand Down
114 changes: 49 additions & 65 deletions lib/blocs/list_state.dart
Original file line number Diff line number Diff line change
@@ -1,82 +1,66 @@
import 'package:equatable/equatable.dart';

/// Generic class to be used as state for paginated lists.
class ListState<T> extends Equatable {
/// The results to be shown. These are outdated if `isLoading` is true.
final List<T> results;

/// A message describing why there are no results.
final String? message;
/// Generic type for states with a paginated list of results.
///
/// There are a number of subtypes:
/// * [ErrorListState] - indicates that there was an error.
/// * [LoadingListState] - indicates that we are loading.
/// * [ResultsListState] - indicates that there are results.
/// * [DoneListState] - indicates that there are no more results.
/// * [LoadingMoreListState] - indicates that we are loading more results.
abstract class ListState<T> extends Equatable {
const ListState();

/// Different results are being loaded. The results are outdated.
final bool isLoading;
/// A convenience method to get the results if they are available.
///
/// Returns `[]` if this state is not a (subtype of) [ResultsListState].
List<T> get results =>
this is ResultsListState ? (this as ResultsListState<T>).results : [];

/// More of the same results are being loaded. The results are not outdated.
final bool isLoadingMore;
/// A convenience method to get the error message if there is one.
///
/// Returns `null` iff this state is not a (subtype of) [ErrorListState].
String? get message =>
this is ErrorListState ? (this as ErrorListState<T>).message : null;

/// The last results have been loaded. There are no more pages left.
final bool isDone;
@override
List<Object?> get props => [];
}

bool get hasException => message != null;
class LoadingListState<T> extends ListState<T> {
const LoadingListState();
}

const ListState({
required this.results,
required this.message,
required this.isLoading,
required this.isLoadingMore,
required this.isDone,
});
class ErrorListState<T> extends ListState<T> {
@override
final String message;

ListState<T> copyWith({
List<T>? results,
String? message,
bool? isLoading,
bool? isLoadingMore,
bool? isDone,
}) =>
ListState<T>(
results: results ?? this.results,
message: message ?? this.message,
isLoading: isLoading ?? this.isLoading,
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
isDone: isDone ?? this.isDone,
);
const ErrorListState(this.message);

@override
List<Object?> get props => [
results,
message,
isLoading,
isLoadingMore,
isDone,
];
List<Object?> get props => [message];
}

class ResultsListState<T> extends ListState<T> {
@override
String toString() {
return 'ListState<$T>(isLoading: $isLoading, isLoadingMore: $isLoadingMore,'
' isDone: $isDone, message: $message, ${results.length} ${T}s)';
}
final List<T> results;

const ResultsListState(this.results);

factory ResultsListState.withDone(List<T> results, bool isDone) =>
isDone ? DoneListState(results) : ResultsListState(results);

const ListState.loading({required this.results})
: message = null,
isLoading = true,
isLoadingMore = false,
isDone = true;
@override
List<Object?> get props => [results];
}

const ListState.loadingMore({required this.results})
: message = null,
isLoading = false,
isLoadingMore = true,
isDone = true;
class LoadingMoreListState<T> extends ResultsListState<T> {
const LoadingMoreListState(super.results);

const ListState.success({required this.results, required this.isDone})
: message = null,
isLoading = false,
isLoadingMore = false;
factory LoadingMoreListState.from(ResultsListState<T> state) =>
LoadingMoreListState(state.results);
}

const ListState.failure({required String this.message})
: results = const [],
isLoading = false,
isLoadingMore = false,
isDone = true;
class DoneListState<T> extends ResultsListState<T> {
const DoneListState(super.results);
}
Loading