134 lines
3.8 KiB
Dart
134 lines
3.8 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
import 'package:lemmy_api_client/v3.dart';
|
|
import 'package:mobx/mobx.dart';
|
|
|
|
import '../l10n/l10n_from_string.dart';
|
|
|
|
part 'async_store.freezed.dart';
|
|
part 'async_store.g.dart';
|
|
|
|
/// [AsyncState] but observable with helper methods/getters
|
|
class AsyncStore<T> = _AsyncStore<T> with _$AsyncStore<T>;
|
|
|
|
abstract class _AsyncStore<T> with Store {
|
|
@observable
|
|
AsyncState<T> asyncState = AsyncState<T>.initial();
|
|
|
|
@computed
|
|
bool get isLoading => asyncState is AsyncStateLoading<T>;
|
|
|
|
@computed
|
|
String? get errorTerm => asyncState.whenOrNull<String?>(
|
|
error: (errorTerm) => errorTerm,
|
|
data: (data, errorTerm) => errorTerm,
|
|
);
|
|
|
|
/// sets data in asyncState
|
|
@action
|
|
void setData(T data) => asyncState = AsyncState.data(data);
|
|
|
|
/// reset an asyncState to its initial one
|
|
@action
|
|
void reset() => asyncState = AsyncState<T>.initial();
|
|
|
|
/// runs some async action and reflects the progress in [asyncState].
|
|
/// If successful, the result is returned, otherwise null is returned.
|
|
/// If this [AsyncStore] is already running some action, it will exit immediately and do nothing
|
|
///
|
|
/// When [refresh] is true and [asyncState] is [AsyncStateData], then the data state is persisted and
|
|
/// errors are not fatal but stored in [AsyncStateData]
|
|
@action
|
|
Future<T?> run(AsyncValueGetter<T> callback, {bool refresh = false}) async {
|
|
if (isLoading) return null;
|
|
|
|
final data = refresh ? asyncState.mapOrNull(data: (data) => data) : null;
|
|
|
|
if (data == null) {
|
|
asyncState = AsyncState<T>.loading();
|
|
}
|
|
|
|
try {
|
|
final result = await callback();
|
|
|
|
asyncState = AsyncState.data(result);
|
|
|
|
return result;
|
|
} on SocketException {
|
|
if (data != null) {
|
|
asyncState = data.copyWith(errorTerm: L10nStrings.network_error);
|
|
} else {
|
|
asyncState = const AsyncState.error(L10nStrings.network_error);
|
|
}
|
|
} catch (err) {
|
|
if (data != null) {
|
|
asyncState = data.copyWith(errorTerm: err.toString());
|
|
} else {
|
|
asyncState = AsyncState<T>.error(err.toString());
|
|
}
|
|
rethrow;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// [run] but specialized for a [LemmyApiQuery].
|
|
/// Will catch [LemmyApiException] and map to its error term.
|
|
@action
|
|
Future<T?> runLemmy(
|
|
String instanceHost,
|
|
LemmyApiQuery<T> query, {
|
|
bool refresh = false,
|
|
}) async {
|
|
try {
|
|
return await run(
|
|
() => LemmyApiV3(instanceHost).run(query),
|
|
refresh: refresh,
|
|
);
|
|
} on LemmyApiException catch (err) {
|
|
final data = refresh ? asyncState.mapOrNull(data: (data) => data) : null;
|
|
if (data != null) {
|
|
asyncState = data.copyWith(errorTerm: err.message);
|
|
} else {
|
|
asyncState = AsyncState<T>.error(err.message);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// helper function for mapping [asyncState] into 3 variants
|
|
U map<U>({
|
|
required U Function() loading,
|
|
required U Function(String errorTerm) error,
|
|
required U Function(T data) data,
|
|
}) {
|
|
return asyncState.when<U>(
|
|
initial: loading,
|
|
data: (value, errorTerm) => data(value),
|
|
loading: loading,
|
|
error: error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// State in which an async action can be
|
|
@freezed
|
|
class AsyncState<T> with _$AsyncState<T> {
|
|
/// async action has not yet begun
|
|
const factory AsyncState.initial() = AsyncStateInitial<T>;
|
|
|
|
/// async action completed successfully with [T]
|
|
/// and possibly an error term after a refresh
|
|
const factory AsyncState.data(T data, [String? errorTerm]) =
|
|
AsyncStateData<T>;
|
|
|
|
/// async action is running at the moment
|
|
const factory AsyncState.loading() = AsyncStateLoading<T>;
|
|
|
|
/// async action failed with a translatable error term
|
|
const factory AsyncState.error(String errorTerm) = AsyncStateError<T>;
|
|
}
|