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 = _AsyncStore with _$AsyncStore; abstract class _AsyncStore with Store { @observable AsyncState asyncState = AsyncState.initial(); @computed bool get isLoading => asyncState is AsyncStateLoading; @computed String? get errorTerm => asyncState.whenOrNull( error: (errorTerm) => errorTerm, data: (data, errorTerm) => errorTerm, ); /// sets data in asyncState @action void setData(T data) => asyncState = AsyncState.data(data); /// 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 run(AsyncValueGetter callback, {bool refresh = false}) async { if (isLoading) return null; final data = refresh ? asyncState.mapOrNull(data: (data) => data) : null; if (data == null) { asyncState = AsyncState.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.error(err.toString()); } rethrow; } } /// [run] but specialized for a [LemmyApiQuery]. /// Will catch [LemmyApiException] and map to its error term. @action Future runLemmy( String instanceHost, LemmyApiQuery 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.error(err.message); } } } /// helper function for mapping [asyncState] into 3 variants U map({ required U Function() loading, required U Function(String errorTerm) error, required U Function(T data) data, }) { return asyncState.when( initial: loading, data: (value, errorTerm) => data(value), loading: loading, error: error, ); } } /// State in which an async action can be @freezed class AsyncState with _$AsyncState { /// async action has not yet begun const factory AsyncState.initial() = AsyncStateInitial; /// async action completed successfully with [T] /// and possibly an error term after a refresh const factory AsyncState.data(T data, [String? errorTerm]) = AsyncStateData; /// async action is running at the moment const factory AsyncState.loading() = AsyncStateLoading; /// async action failed with a translatable error term const factory AsyncState.error(String errorTerm) = AsyncStateError; }