25 changed files with 284 additions and 117 deletions
			
			
		| @ -1,5 +1,6 @@ | |||||||
| /.dart_tool/ | /.dart_tool/ | ||||||
| /pubspec.lock | /pubspec.lock | ||||||
|  | packages/**/coverage | ||||||
| 
 | 
 | ||||||
| # Melos reccomends not adding them to vcs but we need them as we don't use melos in CI | # Melos reccomends not adding them to vcs but we need them as we don't use melos in CI | ||||||
| # **/pubspec_overrides.yaml | # **/pubspec_overrides.yaml | ||||||
|  | |||||||
| @ -1,43 +1,71 @@ | |||||||
| part of '../../neon.dart'; | part of '../../neon.dart'; | ||||||
| 
 | 
 | ||||||
|  | @immutable | ||||||
| class Result<T> { | class Result<T> { | ||||||
|   Result( |   const Result( | ||||||
|     this.data, |     this.data, | ||||||
|     this.error, { |     this.error, { | ||||||
|     required this.loading, |     required this.isLoading, | ||||||
|     required this.cached, |     required this.isCached, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   factory Result.loading() => Result( |   factory Result.loading() => const Result( | ||||||
|         null, |         null, | ||||||
|         null, |         null, | ||||||
|         loading: true, |         isLoading: true, | ||||||
|         cached: false, |         isCached: false, | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|   factory Result.success(final T data) => Result( |   factory Result.success(final T data) => Result( | ||||||
|         data, |         data, | ||||||
|         null, |         null, | ||||||
|         loading: false, |         isLoading: false, | ||||||
|         cached: false, |         isCached: false, | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|   factory Result.error(final Object error) => Result( |   factory Result.error(final Object error) => Result( | ||||||
|         null, |         null, | ||||||
|         error, |         error, | ||||||
|         loading: false, |         isLoading: false, | ||||||
|         cached: false, |         isCached: false, | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|   final T? data; |   final T? data; | ||||||
|   final Object? error; |   final Object? error; | ||||||
|   final bool loading; |   final bool isLoading; | ||||||
|   final bool cached; |   final bool isCached; | ||||||
| 
 | 
 | ||||||
|   Result<R> transform<R>(final R? Function(T data) call) => Result( |   Result<R> transform<R>(final R? Function(T data) call) => Result( | ||||||
|         data != null ? call(data as T) : null, |         data != null ? call(data as T) : null, | ||||||
|         error, |         error, | ||||||
|         loading: loading, |         isLoading: isLoading, | ||||||
|         cached: cached, |         isCached: isCached, | ||||||
|       ); |       ); | ||||||
|  | 
 | ||||||
|  |   Result<T> asLoading() => Result( | ||||||
|  |         data, | ||||||
|  |         error, | ||||||
|  |         isLoading: true, | ||||||
|  |         isCached: isCached, | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |   bool get hasError => error != null; | ||||||
|  | 
 | ||||||
|  |   bool get hasData => data != null; | ||||||
|  |   bool get hasUncachedData => hasData && !isCached; | ||||||
|  | 
 | ||||||
|  |   T get requireData { | ||||||
|  |     if (hasData) { | ||||||
|  |       return data!; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     throw StateError('Result has no data'); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   bool operator ==(final Object other) => | ||||||
|  |       other is Result && other.isLoading == isLoading && other.data == data && other.error == error; | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   int get hashCode => Object.hash(data, error, isLoading, isCached); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,28 +1,51 @@ | |||||||
| part of '../../neon.dart'; | part of '../../neon.dart'; | ||||||
| 
 | 
 | ||||||
| class ResultBuilder<R> extends StatelessWidget { | typedef ResultWidgetBuilder<T> = Widget Function(BuildContext context, Result<T> snapshot); | ||||||
|  | 
 | ||||||
|  | class ResultBuilder<T> extends StreamBuilderBase<Result<T>, Result<T>> { | ||||||
|   const ResultBuilder({ |   const ResultBuilder({ | ||||||
|     required this.stream, |  | ||||||
|     required this.builder, |     required this.builder, | ||||||
|  |     this.initialData, | ||||||
|  |     super.stream, | ||||||
|     super.key, |     super.key, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   final Stream<Result<R>?>? stream; |   ResultBuilder.behaviorSubject({ | ||||||
|  |     required this.builder, | ||||||
|  |     BehaviorSubject<Result<T>>? super.stream, | ||||||
|  |     super.key, | ||||||
|  |   }) : initialData = stream?.valueOrNull; | ||||||
|  | 
 | ||||||
|  |   final ResultWidgetBuilder<T> builder; | ||||||
|  |   final Result<T>? initialData; | ||||||
| 
 | 
 | ||||||
|   final Widget Function(BuildContext, Result<R>) builder; |   @override | ||||||
|  |   Result<T> initial() => initialData?.asLoading() ?? Result<T>.loading(); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Widget build(final BuildContext context) => StreamBuilder( |   Result<T> afterData(final Result<T> current, final Result<T> data) { | ||||||
|         stream: stream, |     // prevent rebuild when only the cache state cahnges | ||||||
|         builder: (final context, final snapshot) { |     if (current == data) { | ||||||
|           if (snapshot.hasError) { |       return current; | ||||||
|             return builder(context, Result.error(snapshot.error!)); |     } | ||||||
|  | 
 | ||||||
|  |     return data; | ||||||
|   } |   } | ||||||
|           if (snapshot.hasData) { | 
 | ||||||
|             return builder(context, snapshot.data!); |   @override | ||||||
|  |   Result<T> afterError(final Result<T> current, final Object error, final StackTrace stackTrace) { | ||||||
|  |     if (current.hasError) { | ||||||
|  |       return current; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|           return builder(context, Result.loading()); |     return Result( | ||||||
|         }, |       current.data, | ||||||
|  |       error, | ||||||
|  |       isLoading: false, | ||||||
|  |       isCached: false, | ||||||
|     ); |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Widget build(final BuildContext context, final Result<T> currentSummary) => builder(context, currentSummary); | ||||||
| } | } | ||||||
|  | |||||||
| @ -0,0 +1,84 @@ | |||||||
|  | import 'package:neon/neon.dart'; | ||||||
|  | import 'package:test/test.dart'; | ||||||
|  | 
 | ||||||
|  | void main() { | ||||||
|  |   group('Result', () { | ||||||
|  |     test('Equality', () { | ||||||
|  |       const data = 'someData'; | ||||||
|  | 
 | ||||||
|  |       const a = Result( | ||||||
|  |         data, | ||||||
|  |         null, | ||||||
|  |         isLoading: true, | ||||||
|  |         isCached: false, | ||||||
|  |       ); | ||||||
|  |       const b = Result( | ||||||
|  |         data, | ||||||
|  |         null, | ||||||
|  |         isLoading: true, | ||||||
|  |         isCached: true, | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |       expect(a, equals(a), reason: 'identical'); | ||||||
|  |       expect(a, equals(b), reason: 'ignore cached state in equality'); | ||||||
|  | 
 | ||||||
|  |       expect(a.hashCode, equals(a.hashCode), reason: 'identical'); | ||||||
|  |       expect(a.hashCode, isNot(equals(b.hashCode)), reason: 'hashcode should respect the cached state'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('Transform to loading', () { | ||||||
|  |       const data = 'someData'; | ||||||
|  | 
 | ||||||
|  |       final a = Result.success(data); | ||||||
|  |       const b = Result( | ||||||
|  |         data, | ||||||
|  |         null, | ||||||
|  |         isLoading: true, | ||||||
|  |         isCached: false, | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |       expect(a, isNot(equals(b))); | ||||||
|  |       expect(a.asLoading(), equals(b)); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('data check', () { | ||||||
|  |       const data = 'someData'; | ||||||
|  | 
 | ||||||
|  |       final a = Result.loading(); | ||||||
|  |       final b = Result.success(data); | ||||||
|  |       const c = Result( | ||||||
|  |         data, | ||||||
|  |         null, | ||||||
|  |         isLoading: false, | ||||||
|  |         isCached: true, | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |       expect(a.hasData, false); | ||||||
|  |       expect(b.hasData, true); | ||||||
|  | 
 | ||||||
|  |       expect(() => a.requireData, throwsStateError); | ||||||
|  |       expect(b.requireData, equals(data)); | ||||||
|  | 
 | ||||||
|  |       expect(b.hasUncachedData, true); | ||||||
|  |       expect(c.hasUncachedData, false); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('error check', () { | ||||||
|  |       const error = 'someError'; | ||||||
|  | 
 | ||||||
|  |       final a = Result.error(error); | ||||||
|  | 
 | ||||||
|  |       expect(a.hasError, true); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('transform', () { | ||||||
|  |       const data = 1; | ||||||
|  | 
 | ||||||
|  |       final a = Result.success(data); | ||||||
|  | 
 | ||||||
|  |       String transformer(final int data) => data.toString(); | ||||||
|  | 
 | ||||||
|  |       expect(a.transform(transformer), equals(Result.success(data.toString()))); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
					Loading…
					
					
				
		Reference in new issue