Nikolas Rimikis
1 year ago
committed by
GitHub
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