Cleaned up code, implemented message queue to notify subscribers of document updates.

This commit is contained in:
Anton Stubenbord
2023-02-06 01:04:13 +01:00
parent 337c178be8
commit 4d7fab1839
111 changed files with 1412 additions and 1029 deletions

View File

@@ -1,30 +1,30 @@
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:rxdart/subjects.dart';
///
/// Base repository class which all repositories should implement
///
abstract class BaseRepository<State extends RepositoryState, Type>
extends Cubit<State> with HydratedMixin {
final State _initialState;
abstract class BaseRepository<T> extends Cubit<IndexedRepositoryState<T>>
with HydratedMixin {
final IndexedRepositoryState<T> _initialState;
BaseRepository(this._initialState) : super(_initialState) {
hydrate();
}
Stream<State?> get values =>
Stream<IndexedRepositoryState<T>?> get values =>
BehaviorSubject.seeded(state)..addStream(super.stream);
State? get current => state;
IndexedRepositoryState<T>? get current => state;
bool get isInitialized => state.hasLoaded;
Future<Type> create(Type object);
Future<Type?> find(int id);
Future<Iterable<Type>> findAll([Iterable<int>? ids]);
Future<Type> update(Type object);
Future<int> delete(Type object);
Future<T> create(T object);
Future<T?> find(int id);
Future<Iterable<T>> findAll([Iterable<int>? ids]);
Future<T> update(T object);
Future<int> delete(T object);
@override
Future<void> clear() async {

View File

@@ -3,10 +3,8 @@ import 'dart:async';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
class CorrespondentRepositoryImpl
extends LabelRepository<Correspondent, CorrespondentRepositoryState> {
class CorrespondentRepositoryImpl extends LabelRepository<Correspondent> {
final PaperlessLabelsApi _api;
CorrespondentRepositoryImpl(this._api)
@@ -15,7 +13,7 @@ class CorrespondentRepositoryImpl
@override
Future<Correspondent> create(Correspondent correspondent) async {
final created = await _api.saveCorrespondent(correspondent);
final updatedState = {...state.values}
final updatedState = {...state.values ?? {}}
..putIfAbsent(created.id!, () => created);
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
return created;
@@ -24,7 +22,7 @@ class CorrespondentRepositoryImpl
@override
Future<int> delete(Correspondent correspondent) async {
await _api.deleteCorrespondent(correspondent);
final updatedState = {...state.values}
final updatedState = {...state.values ?? {}}
..removeWhere((k, v) => k == correspondent.id);
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
return correspondent.id!;
@@ -34,7 +32,7 @@ class CorrespondentRepositoryImpl
Future<Correspondent?> find(int id) async {
final correspondent = await _api.getCorrespondent(id);
if (correspondent != null) {
final updatedState = {...state.values}..[id] = correspondent;
final updatedState = {...state.values ?? {}}..[id] = correspondent;
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
return correspondent;
}
@@ -44,7 +42,7 @@ class CorrespondentRepositoryImpl
@override
Future<Iterable<Correspondent>> findAll([Iterable<int>? ids]) async {
final correspondents = await _api.getCorrespondents(ids);
final updatedState = {...state.values}
final updatedState = {...state.values ?? {}}
..addEntries(correspondents.map((e) => MapEntry(e.id!, e)));
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
return correspondents;
@@ -53,7 +51,8 @@ class CorrespondentRepositoryImpl
@override
Future<Correspondent> update(Correspondent correspondent) async {
final updated = await _api.updateCorrespondent(correspondent);
final updatedState = {...state.values}..update(updated.id!, (_) => updated);
final updatedState = {...state.values ?? {}}
..update(updated.id!, (_) => updated);
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
return updated;
}
@@ -64,7 +63,7 @@ class CorrespondentRepositoryImpl
}
@override
Map<String, dynamic> toJson(CorrespondentRepositoryState state) {
Map<String, dynamic> toJson(covariant CorrespondentRepositoryState state) {
return state.toJson();
}
}

View File

@@ -1,10 +1,8 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
class DocumentTypeRepositoryImpl
extends LabelRepository<DocumentType, DocumentTypeRepositoryState> {
class DocumentTypeRepositoryImpl extends LabelRepository<DocumentType> {
final PaperlessLabelsApi _api;
DocumentTypeRepositoryImpl(this._api)
@@ -13,7 +11,7 @@ class DocumentTypeRepositoryImpl
@override
Future<DocumentType> create(DocumentType documentType) async {
final created = await _api.saveDocumentType(documentType);
final updatedState = {...state.values}
final updatedState = {...state.values ?? {}}
..putIfAbsent(created.id!, () => created);
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
return created;
@@ -22,7 +20,7 @@ class DocumentTypeRepositoryImpl
@override
Future<int> delete(DocumentType documentType) async {
await _api.deleteDocumentType(documentType);
final updatedState = {...state.values}
final updatedState = {...state.values ?? {}}
..removeWhere((k, v) => k == documentType.id);
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
return documentType.id!;
@@ -32,7 +30,7 @@ class DocumentTypeRepositoryImpl
Future<DocumentType?> find(int id) async {
final documentType = await _api.getDocumentType(id);
if (documentType != null) {
final updatedState = {...state.values}..[id] = documentType;
final updatedState = {...state.values ?? {}}..[id] = documentType;
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
return documentType;
}
@@ -42,7 +40,7 @@ class DocumentTypeRepositoryImpl
@override
Future<Iterable<DocumentType>> findAll([Iterable<int>? ids]) async {
final documentTypes = await _api.getDocumentTypes(ids);
final updatedState = {...state.values}
final updatedState = {...state.values ?? {}}
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e)));
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
return documentTypes;
@@ -51,7 +49,8 @@ class DocumentTypeRepositoryImpl
@override
Future<DocumentType> update(DocumentType documentType) async {
final updated = await _api.updateDocumentType(documentType);
final updatedState = {...state.values}..update(updated.id!, (_) => updated);
final updatedState = {...state.values ?? {}}
..update(updated.id!, (_) => updated);
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
return updated;
}
@@ -62,7 +61,7 @@ class DocumentTypeRepositoryImpl
}
@override
Map<String, dynamic> toJson(DocumentTypeRepositoryState state) {
Map<String, dynamic> toJson(covariant DocumentTypeRepositoryState state) {
return state.toJson();
}
}

View File

@@ -10,7 +10,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
@override
Future<SavedView> create(SavedView object) async {
final created = await _api.save(object);
final updatedState = {...state.values}
final updatedState = {...state.values ?? {}}
..putIfAbsent(created.id!, () => created);
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
return created;
@@ -19,7 +19,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
@override
Future<int> delete(SavedView view) async {
await _api.delete(view);
final updatedState = {...state.values}..remove(view.id);
final updatedState = {...state.values ?? {}}..remove(view.id);
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
return view.id!;
}
@@ -27,7 +27,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
@override
Future<SavedView?> find(int id) async {
final found = await _api.find(id);
final updatedState = {...state.values}
final updatedState = {...state.values ?? {}}
..update(id, (_) => found, ifAbsent: () => found);
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
return found;
@@ -37,7 +37,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
final found = await _api.findAll(ids);
final updatedState = {
...state.values,
...state.values ?? {},
...{for (final view in found) view.id!: view},
};
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
@@ -56,7 +56,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
}
@override
Map<String, dynamic> toJson(SavedViewRepositoryState state) {
Map<String, dynamic> toJson(covariant SavedViewRepositoryState state) {
return state.toJson();
}
}

View File

@@ -3,8 +3,7 @@ import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
class StoragePathRepositoryImpl
extends LabelRepository<StoragePath, StoragePathRepositoryState> {
class StoragePathRepositoryImpl extends LabelRepository<StoragePath> {
final PaperlessLabelsApi _api;
StoragePathRepositoryImpl(this._api)
@@ -13,7 +12,7 @@ class StoragePathRepositoryImpl
@override
Future<StoragePath> create(StoragePath storagePath) async {
final created = await _api.saveStoragePath(storagePath);
final updatedState = {...state.values}
final updatedState = {...state.values ?? {}}
..putIfAbsent(created.id!, () => created);
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
return created;
@@ -22,7 +21,7 @@ class StoragePathRepositoryImpl
@override
Future<int> delete(StoragePath storagePath) async {
await _api.deleteStoragePath(storagePath);
final updatedState = {...state.values}
final updatedState = {...state.values ?? {}}
..removeWhere((k, v) => k == storagePath.id);
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
return storagePath.id!;
@@ -32,7 +31,7 @@ class StoragePathRepositoryImpl
Future<StoragePath?> find(int id) async {
final storagePath = await _api.getStoragePath(id);
if (storagePath != null) {
final updatedState = {...state.values}..[id] = storagePath;
final updatedState = {...state.values ?? {}}..[id] = storagePath;
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
return storagePath;
}
@@ -42,7 +41,7 @@ class StoragePathRepositoryImpl
@override
Future<Iterable<StoragePath>> findAll([Iterable<int>? ids]) async {
final storagePaths = await _api.getStoragePaths(ids);
final updatedState = {...state.values}
final updatedState = {...state.values ?? {}}
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e)));
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
return storagePaths;
@@ -51,7 +50,8 @@ class StoragePathRepositoryImpl
@override
Future<StoragePath> update(StoragePath storagePath) async {
final updated = await _api.updateStoragePath(storagePath);
final updatedState = {...state.values}..update(updated.id!, (_) => updated);
final updatedState = {...state.values ?? {}}
..update(updated.id!, (_) => updated);
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
return updated;
}
@@ -62,7 +62,7 @@ class StoragePathRepositoryImpl
}
@override
Map<String, dynamic> toJson(StoragePathRepositoryState state) {
Map<String, dynamic> toJson(covariant StoragePathRepositoryState state) {
return state.toJson();
}
}

View File

@@ -1,10 +1,8 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
class TagRepositoryImpl extends LabelRepository<Tag> {
final PaperlessLabelsApi _api;
TagRepositoryImpl(this._api) : super(const TagRepositoryState());
@@ -12,7 +10,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
@override
Future<Tag> create(Tag object) async {
final created = await _api.saveTag(object);
final updatedState = {...state.values}
final updatedState = {...state.values ?? {}}
..putIfAbsent(created.id!, () => created);
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
return created;
@@ -21,7 +19,8 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
@override
Future<int> delete(Tag tag) async {
await _api.deleteTag(tag);
final updatedState = {...state.values}..removeWhere((k, v) => k == tag.id);
final updatedState = {...state.values ?? {}}
..removeWhere((k, v) => k == tag.id);
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
return tag.id!;
}
@@ -30,7 +29,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
Future<Tag?> find(int id) async {
final tag = await _api.getTag(id);
if (tag != null) {
final updatedState = {...state.values}..[id] = tag;
final updatedState = {...state.values ?? {}}..[id] = tag;
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
return tag;
}
@@ -40,7 +39,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
@override
Future<Iterable<Tag>> findAll([Iterable<int>? ids]) async {
final tags = await _api.getTags(ids);
final updatedState = {...state.values}
final updatedState = {...state.values ?? {}}
..addEntries(tags.map((e) => MapEntry(e.id!, e)));
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
return tags;
@@ -49,7 +48,8 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
@override
Future<Tag> update(Tag tag) async {
final updated = await _api.updateTag(tag);
final updatedState = {...state.values}..update(updated.id!, (_) => updated);
final updatedState = {...state.values ?? {}}
..update(updated.id!, (_) => updated);
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
return updated;
}
@@ -60,7 +60,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
}
@override
Map<String, dynamic>? toJson(TagRepositoryState state) {
Map<String, dynamic>? toJson(covariant TagRepositoryState state) {
return state.toJson();
}
}

View File

@@ -1,8 +1,7 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/base_repository.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
abstract class LabelRepository<T extends Label, State extends RepositoryState>
extends BaseRepository<State, T> {
LabelRepository(State initial) : super(initial);
abstract class LabelRepository<T extends Label> extends BaseRepository<T> {
LabelRepository(IndexedRepositoryState<T> initial) : super(initial);
}

View File

@@ -17,20 +17,16 @@ class LabelRepositoriesProvider extends StatelessWidget {
return MultiRepositoryProvider(
providers: [
RepositoryProvider(
create: (context) => context.read<
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
create: (context) => context.read<LabelRepository<Correspondent>>(),
),
RepositoryProvider(
create: (context) => context.read<
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
create: (context) => context.read<LabelRepository<DocumentType>>(),
),
RepositoryProvider(
create: (context) => context
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
create: (context) => context.read<LabelRepository<StoragePath>>(),
),
RepositoryProvider(
create: (context) =>
context.read<LabelRepository<Tag, TagRepositoryState>>(),
create: (context) => context.read<LabelRepository<Tag>>(),
),
],
child: child,

View File

@@ -1,8 +1,8 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/base_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/saved_view_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
abstract class SavedViewRepository
extends BaseRepository<SavedViewRepositoryState, SavedView> {
abstract class SavedViewRepository extends BaseRepository<SavedView> {
SavedViewRepository(super.initialState);
}

View File

@@ -1,13 +1,13 @@
import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
part 'correspondent_repository_state.g.dart';
@JsonSerializable()
class CorrespondentRepositoryState
extends RepositoryState<Map<int, Correspondent>> {
extends IndexedRepositoryState<Correspondent> {
const CorrespondentRepositoryState({
super.values = const {},
super.hasLoaded,

View File

@@ -20,6 +20,6 @@ CorrespondentRepositoryState _$CorrespondentRepositoryStateFromJson(
Map<String, dynamic> _$CorrespondentRepositoryStateToJson(
CorrespondentRepositoryState instance) =>
<String, dynamic>{
'values': instance.values.map((k, e) => MapEntry(k.toString(), e)),
'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)),
'hasLoaded': instance.hasLoaded,
};

View File

@@ -1,20 +1,21 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:json_annotation/json_annotation.dart';
part 'document_type_repository_state.g.dart';
@JsonSerializable()
class DocumentTypeRepositoryState
extends RepositoryState<Map<int, DocumentType>> {
class DocumentTypeRepositoryState extends IndexedRepositoryState<DocumentType> {
const DocumentTypeRepositoryState({
super.values = const {},
super.hasLoaded,
});
@override
DocumentTypeRepositoryState copyWith(
{Map<int, DocumentType>? values, bool? hasLoaded}) {
DocumentTypeRepositoryState copyWith({
Map<int, DocumentType>? values,
bool? hasLoaded,
}) {
return DocumentTypeRepositoryState(
values: values ?? this.values,
hasLoaded: hasLoaded ?? this.hasLoaded,

View File

@@ -20,6 +20,6 @@ DocumentTypeRepositoryState _$DocumentTypeRepositoryStateFromJson(
Map<String, dynamic> _$DocumentTypeRepositoryStateToJson(
DocumentTypeRepositoryState instance) =>
<String, dynamic>{
'values': instance.values.map((k, e) => MapEntry(k.toString(), e)),
'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)),
'hasLoaded': instance.hasLoaded,
};

View File

@@ -1,11 +1,11 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:json_annotation/json_annotation.dart';
part 'saved_view_repository_state.g.dart';
@JsonSerializable()
class SavedViewRepositoryState extends RepositoryState<Map<int, SavedView>> {
class SavedViewRepositoryState extends IndexedRepositoryState<SavedView> {
const SavedViewRepositoryState({
super.values = const {},
super.hasLoaded = false,

View File

@@ -20,6 +20,6 @@ SavedViewRepositoryState _$SavedViewRepositoryStateFromJson(
Map<String, dynamic> _$SavedViewRepositoryStateToJson(
SavedViewRepositoryState instance) =>
<String, dynamic>{
'values': instance.values.map((k, e) => MapEntry(k.toString(), e)),
'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)),
'hasLoaded': instance.hasLoaded,
};

View File

@@ -1,20 +1,21 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:json_annotation/json_annotation.dart';
part 'storage_path_repository_state.g.dart';
@JsonSerializable()
class StoragePathRepositoryState
extends RepositoryState<Map<int, StoragePath>> {
class StoragePathRepositoryState extends IndexedRepositoryState<StoragePath> {
const StoragePathRepositoryState({
super.values = const {},
super.hasLoaded = false,
});
@override
StoragePathRepositoryState copyWith(
{Map<int, StoragePath>? values, bool? hasLoaded}) {
StoragePathRepositoryState copyWith({
Map<int, StoragePath>? values,
bool? hasLoaded,
}) {
return StoragePathRepositoryState(
values: values ?? this.values,
hasLoaded: hasLoaded ?? this.hasLoaded,

View File

@@ -20,6 +20,6 @@ StoragePathRepositoryState _$StoragePathRepositoryStateFromJson(
Map<String, dynamic> _$StoragePathRepositoryStateToJson(
StoragePathRepositoryState instance) =>
<String, dynamic>{
'values': instance.values.map((k, e) => MapEntry(k.toString(), e)),
'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)),
'hasLoaded': instance.hasLoaded,
};

View File

@@ -1,18 +1,21 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
part 'tag_repository_state.g.dart';
@JsonSerializable()
class TagRepositoryState extends RepositoryState<Map<int, Tag>> {
class TagRepositoryState extends IndexedRepositoryState<Tag> {
const TagRepositoryState({
super.values = const {},
super.hasLoaded = false,
});
@override
TagRepositoryState copyWith({Map<int, Tag>? values, bool? hasLoaded}) {
TagRepositoryState copyWith({
Map<int, Tag>? values,
bool? hasLoaded,
}) {
return TagRepositoryState(
values: values ?? this.values,
hasLoaded: hasLoaded ?? this.hasLoaded,

View File

@@ -18,6 +18,6 @@ TagRepositoryState _$TagRepositoryStateFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$TagRepositoryStateToJson(TagRepositoryState instance) =>
<String, dynamic>{
'values': instance.values.map((k, e) => MapEntry(k.toString(), e)),
'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)),
'hasLoaded': instance.hasLoaded,
};

View File

@@ -0,0 +1,16 @@
abstract class IndexedRepositoryState<T> {
final Map<int, T>? values;
final bool hasLoaded;
const IndexedRepositoryState({
required this.values,
this.hasLoaded = false,
}) : assert(!(values == null) || !hasLoaded);
IndexedRepositoryState.loaded(this.values) : hasLoaded = true;
IndexedRepositoryState<T> copyWith({
Map<int, T>? values,
bool? hasLoaded,
});
}

View File

@@ -1,16 +0,0 @@
abstract class RepositoryState<T> {
final T values;
final bool hasLoaded;
const RepositoryState({
required this.values,
this.hasLoaded = false,
});
RepositoryState.loaded(this.values) : hasLoaded = true;
RepositoryState<T> copyWith({
T? values,
bool? hasLoaded,
});
}