25 changed files with 284 additions and 117 deletions
			
			
		| @ -1,5 +1,6 @@ | ||||
| /.dart_tool/ | ||||
| /pubspec.lock | ||||
| packages/**/coverage | ||||
| 
 | ||||
| # Melos reccomends not adding them to vcs but we need them as we don't use melos in CI | ||||
| # **/pubspec_overrides.yaml | ||||
|  | ||||
| @ -1,43 +1,71 @@ | ||||
| part of '../../neon.dart'; | ||||
| 
 | ||||
| @immutable | ||||
| class Result<T> { | ||||
|   Result( | ||||
|   const Result( | ||||
|     this.data, | ||||
|     this.error, { | ||||
|     required this.loading, | ||||
|     required this.cached, | ||||
|     required this.isLoading, | ||||
|     required this.isCached, | ||||
|   }); | ||||
| 
 | ||||
|   factory Result.loading() => Result( | ||||
|   factory Result.loading() => const Result( | ||||
|         null, | ||||
|         null, | ||||
|         loading: true, | ||||
|         cached: false, | ||||
|         isLoading: true, | ||||
|         isCached: false, | ||||
|       ); | ||||
| 
 | ||||
|   factory Result.success(final T data) => Result( | ||||
|         data, | ||||
|         null, | ||||
|         loading: false, | ||||
|         cached: false, | ||||
|         isLoading: false, | ||||
|         isCached: false, | ||||
|       ); | ||||
| 
 | ||||
|   factory Result.error(final Object error) => Result( | ||||
|         null, | ||||
|         error, | ||||
|         loading: false, | ||||
|         cached: false, | ||||
|         isLoading: false, | ||||
|         isCached: false, | ||||
|       ); | ||||
| 
 | ||||
|   final T? data; | ||||
|   final Object? error; | ||||
|   final bool loading; | ||||
|   final bool cached; | ||||
|   final bool isLoading; | ||||
|   final bool isCached; | ||||
| 
 | ||||
|   Result<R> transform<R>(final R? Function(T data) call) => Result( | ||||
|         data != null ? call(data as T) : null, | ||||
|         error, | ||||
|         loading: loading, | ||||
|         cached: cached, | ||||
|         isLoading: isLoading, | ||||
|         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'; | ||||
| 
 | ||||
| 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({ | ||||
|     required this.stream, | ||||
|     required this.builder, | ||||
|     this.initialData, | ||||
|     super.stream, | ||||
|     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; | ||||
| 
 | ||||
|   @override | ||||
|   Result<T> initial() => initialData?.asLoading() ?? Result<T>.loading(); | ||||
| 
 | ||||
|   @override | ||||
|   Result<T> afterData(final Result<T> current, final Result<T> data) { | ||||
|     // prevent rebuild when only the cache state cahnges | ||||
|     if (current == data) { | ||||
|       return current; | ||||
|     } | ||||
| 
 | ||||
|     return data; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Result<T> afterError(final Result<T> current, final Object error, final StackTrace stackTrace) { | ||||
|     if (current.hasError) { | ||||
|       return current; | ||||
|     } | ||||
| 
 | ||||
|   final Widget Function(BuildContext, Result<R>) builder; | ||||
|     return Result( | ||||
|       current.data, | ||||
|       error, | ||||
|       isLoading: false, | ||||
|       isCached: false, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(final BuildContext context) => StreamBuilder( | ||||
|         stream: stream, | ||||
|         builder: (final context, final snapshot) { | ||||
|           if (snapshot.hasError) { | ||||
|             return builder(context, Result.error(snapshot.error!)); | ||||
|           } | ||||
|           if (snapshot.hasData) { | ||||
|             return builder(context, snapshot.data!); | ||||
|           } | ||||
| 
 | ||||
|           return builder(context, Result.loading()); | ||||
|         }, | ||||
|       ); | ||||
|   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