diff --git a/README.md b/README.md index f00242a..5e20429 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,19 @@ background. Inspired by [Revolut's RxData library](https://github.com/revolut-mo ```shell flutter pub add rxdata -flutter pub add flutter_bloc ``` ## Usage -First, define `DataDelagete` object and specify `Data` type and `Exception` type. +First, define `DataDelagete` object and specify `Data` type. ```dart final dataDelegate = DataDelegate( fromNetwork: () async* { + // [fromNetwork] can yield multiple values before closing. You can sequentially fetch data and + // and yield them step by step. final response = await getRequest(); - yield response; }, fromStorage: () async { @@ -38,19 +38,21 @@ You can also use `DataListener` and `DataConsumer`. class ExampleWidget extends StatelessWidget { const ExampleWidget({Key? key, required this.dataDelegate}) : super(key: key); - final DataDelegate dataDelegate; + final DataDelegate dataDelegate; @override Widget build(BuildContext context) { return Scaffold( - body: DataBuilder( + body: DataBuilder( bloc: dataDelegate, builder: (context, state) { - if (state.hasValue) { - return Text(state.value!.toString()); - } else { - return Text('No data'); - } + return Column( + children: [ + if (state.isLoading) const CircularProgressIndicator(), + if (state.hasError) Text(state.error!.toString()), + if (state.hasValue) Text(state.value!.toString()), + ], + ); }, ), ); @@ -68,4 +70,5 @@ class ExampleWidget extends StatelessWidget { You can then call `dataDelegate.reload()` to fetch data again. Delegate will handle caching by itself, provided that you specified your callbacks. -See `example` project for full usage. \ No newline at end of file +See [example project](https://github.com/vaetas/rxdata/blob/main/example/lib/main.dart) for full +usage. \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index e1bcc8c..af9c692 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -36,7 +36,7 @@ class HomeScreen extends StatefulWidget { } class _HomeScreenState extends State { - late final DataDelegate dataDelegate; + late final DataDelegate dataDelegate; bool wasErrorThrown = false; @@ -79,6 +79,9 @@ class _HomeScreenState extends State { ); }, fromStorage: () async { + // Uncomment line below to simulate storage IO failure. + // throw Exception('Failed to load from storage'); + print('[_HomeScreenState.initState] fromStorage'); final box = await Hive.openBox('storage'); final data = box.get('data'); @@ -103,7 +106,7 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: DataBuilder( + body: DataBuilder( bloc: dataDelegate, builder: (context, state) { return CustomScrollView( diff --git a/example/pubspec.lock b/example/pubspec.lock index c052b54..61dd85c 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -241,7 +241,7 @@ packages: path: ".." relative: true source: path - version: "0.2.2" + version: "0.2.1" sky_engine: dependency: transitive description: flutter diff --git a/lib/src/builder.dart b/lib/src/builder.dart index dff5c0b..7f6f897 100644 --- a/lib/src/builder.dart +++ b/lib/src/builder.dart @@ -3,22 +3,22 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rxdata/rxdata.dart'; /// Wrapper around [BlocBuilder] for shorter generics definition. -class DataBuilder extends BlocBuilder, Data> { +class DataBuilder extends BlocBuilder, Data> { const DataBuilder({ Key? key, - required BlocWidgetBuilder> builder, - DataDelegate? bloc, - BlocBuilderCondition>? buildWhen, + required BlocWidgetBuilder> builder, + DataDelegate? bloc, + BlocBuilderCondition>? buildWhen, }) : super(key: key, bloc: bloc, buildWhen: buildWhen, builder: builder); } /// Wrapper around [BlocListener] for shorter generics definition. -class DataListener extends BlocListener, Data> { +class DataListener extends BlocListener, Data> { const DataListener({ Key? key, - required BlocWidgetListener> listener, - DataDelegate? bloc, - BlocListenerCondition>? listenWhen, + required BlocWidgetListener> listener, + DataDelegate? bloc, + BlocListenerCondition>? listenWhen, Widget? child, }) : super( key: key, @@ -30,14 +30,14 @@ class DataListener extends BlocListener, Data> { } /// Wrapper around [BlocConsumer] for shorter generics definition. -class DataConsumer extends BlocConsumer, Data> { +class DataConsumer extends BlocConsumer, Data> { const DataConsumer({ Key? key, - required BlocWidgetListener> listener, - required BlocWidgetBuilder> builder, - DataDelegate? bloc, - BlocListenerCondition>? listenWhen, - BlocBuilderCondition>? buildWhen, + required BlocWidgetListener> listener, + required BlocWidgetBuilder> builder, + DataDelegate? bloc, + BlocListenerCondition>? listenWhen, + BlocBuilderCondition>? buildWhen, }) : super( key: key, listener: listener, diff --git a/lib/src/data.dart b/lib/src/data.dart index 5ae0ea1..f6d13ed 100644 --- a/lib/src/data.dart +++ b/lib/src/data.dart @@ -1,8 +1,5 @@ /// Wrapper around your data of type [V]. -/// -/// [E] is a type for a possible exceptions. You can either provide your own -/// exception class/enum or use [Object]. -class Data { +class Data { const Data({ this.value, this.error, @@ -13,7 +10,7 @@ class Data { final V? value; /// Current error - final E? error; + final Object? error; /// Whether you can except the data to refresh soon. Remember that you can /// have [isLoading] set to true and still have some value and/or error at the @@ -27,9 +24,9 @@ class Data { bool get hasError => error != null; /// Create copy of this [Data] object with new params. - Data copyWith({ + Data copyWith({ V? value, - E? error, + Object? error, bool? isLoading, }) { return Data( diff --git a/lib/src/delegate.dart b/lib/src/delegate.dart index 40e6ee5..bcde3fd 100644 --- a/lib/src/delegate.dart +++ b/lib/src/delegate.dart @@ -2,7 +2,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rxdata/src/data.dart'; /// Delegates fetching and caching behavior of specified [Data] object. -class DataDelegate extends Cubit> { +class DataDelegate extends Cubit> { DataDelegate({ required this.fromNetwork, this.fromMemory, @@ -10,7 +10,7 @@ class DataDelegate extends Cubit> { this.fromStorage, this.toStorage, this.onClearCache, - }) : super(Data(isLoading: true)) { + }) : super(Data(isLoading: true)) { _init(); } @@ -37,22 +37,29 @@ class DataDelegate extends Cubit> { bool _locked = false; Future _init() async { - final memoryValue = fromMemory?.call(); - if (memoryValue != null) { - emit(Data(value: memoryValue)); - } else { - await _loadFromStorage(); + try { + final memoryValue = fromMemory?.call(); + if (memoryValue != null) { + emit(Data(value: memoryValue)); + } else { + await _loadFromStorage(); + } + } catch (e, s) { + onError(e, s); } await _fetch(); } Future _loadFromStorage() async { - final value = await fromStorage?.call(); - - if (value != null) { - emit(state.copyWith(value: value)); - toMemory?.call(value); + try { + final value = await fromStorage?.call(); + if (value != null) { + emit(state.copyWith(value: value)); + toMemory?.call(value); + } + } catch (e, s) { + onError(e, s); } } @@ -74,24 +81,8 @@ class DataDelegate extends Cubit> { toMemory?.call(event); await toStorage?.call(event); } - } catch (e) { - if (e is E) { - // .copyWith throws a type error once in a time: - // type '_Exception' is not a subtype of type 'Null' of 'error' - - // emit(state.copyWith(error: e as E)); - emit( - Data( - value: state.value, - error: e as E, - ), - ); - } else { - throw ArgumentError( - 'Exception ${e.runtimeType} is not subtype of E in Data.' - 'Ensure you only throw exceptions of type E.', - ); - } + } catch (e, s) { + onError(e, s); } finally { emit(state.copyWith(isLoading: false)); } @@ -121,6 +112,13 @@ class DataDelegate extends Cubit> { Future clearCache() async { await onClearCache?.call(); } + + @override + void onError(Object error, StackTrace stackTrace) { + print('[DataDelegate.onError] ERROR: $error'); + emit(state.copyWith(error: error)); + super.onError(error, stackTrace); + } } typedef _FromNetwork = Stream Function();