Skip to content

Instantly share code, notes, and snippets.

@hachibeeDI
Created January 24, 2020 03:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hachibeeDI/a1d97fcec6ba4eb2d6a84e2f58a90a73 to your computer and use it in GitHub Desktop.
Save hachibeeDI/a1d97fcec6ba4eb2d6a84e2f58a90a73 to your computer and use it in GitHub Desktop.
import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
enum RemoteStatusType {
NotAsked,
Loading,
Succeed,
Failed,
}
@immutable
class RemoteStatus extends Equatable {
const RemoteStatus(this.type, [this.reason])
: assert(
!(type == RemoteStatusType.Failed && reason == null),
);
final RemoteStatusType type;
/// reason why the [RemoteStatusType] was [RemoteStatusType.Failed].
final String reason;
@override
List<Object> get props => [type, reason];
}
/// Mixin to be able to handle remote state for a BLoC.
/// You must call the method [closeController] when the BLoC was closed.
///
/// ```dart
/// class SampleBloc extends Bloc<State, Event> with RemoteStateHandlable {
/// final RemoteStateController remoteStateController;
/// SampleBloc(): remoteStateController = RemoteStateController();
///
/// @override
/// Future<void> close() {
/// // Do not forget to call it!
/// closeController();
/// super.close();
/// }
/// ```
///
/// You can change view model to follow remote state.
/// NOTE: RemoteState won't be included into the Bloc's state, but it's valid a BLoC has another BLoC generally.
///
/// ```dart
/// Future<State> loadData() async {
/// loading();
/// final response = await someRepository.fetch();
/// if (response.ok) {
/// succeed();
/// return State(response.data);
/// } else {
/// failed(response.error);
/// return Future.error(response.error);
/// }
/// }
///
/// Stream<State> mapStateToProps(Event event) {
/// yield loadData();
/// }
/// ```
///
/// To see how remote status is going, you can subscribe [remoteStateController] directory from the bloc instance.
/// For your convenience, I'd recommend to use [StreamBuilder] with it.
/// Example is:
///
/// ```dart
/// Widget build(BuildContext context) {
/// final theBloc = BlocProvider.of<TheBloc>(context);
/// return StreamBuilder<RemoteStatus>(
/// stream: theBloc.remoteStateController,
/// builder: (context, snapshot) {
/// final data = snapshot?.data.type;
/// if (data == null) return LoadingWidget();
/// switch (data) {
/// case RemoteStatusType.NotAsked:
/// case RemoteStatusType.Loading:
/// return LoadingWidget();
/// case RemoteStatusType.Failed:
/// return ErrorIndicate(data.reason);
/// case RemoteStatusType.Succeed:
/// return Screen();
/// }
/// }
/// );
/// }
/// ```
mixin RemoteStateHandlable {
RemoteStateController get remoteStateController;
/// Utility function in case you need state transition.
///
/// ```dart
/// withHandle(() {
/// yield RemoteStatus(RemoteStatusType.Loading);
/// final response = await fetchSomething();
/// if (response.ok) {
/// yield RemoteStatus(RemoteStatusType.Succeed);
/// } esle {
/// yield RemoteStatus(RemoteStatusType.Failed, response.error.reason);
/// }
/// await clearAll();
/// yield RemoteStatus(RemoteStatusType.NotAsked);
/// });
/// ```
void withHandle(Stream<RemoteStatus> Function() handler) {
handler().forEach((state) => remoteStateController.add(state));
}
void cleared() {
remoteStateController.add(RemoteStatus(RemoteStatusType.NotAsked));
}
void loading() {
remoteStateController.add(RemoteStatus(RemoteStatusType.Loading));
}
void succeed() {
remoteStateController.add(RemoteStatus(RemoteStatusType.Succeed));
}
void failed([String reason = '']) {
remoteStateController.add(RemoteStatus(RemoteStatusType.Failed, reason));
}
Future<void> closeController() {
return remoteStateController.close();
}
}
class RemoteStateController implements StreamSink<RemoteStatus> {
final StreamController<RemoteStatus> _controller;
RemoteStateController() : _controller = StreamController<RemoteStatus>() {
add(RemoteStatus(RemoteStatusType.NotAsked));
}
Stream<RemoteStatus> get stream => _controller.stream;
@override
void add(RemoteStatus event) {
_controller.sink.add(event);
}
@override
void addError(Object error, [StackTrace stackTrace]) {
_controller.addError(error, stackTrace);
}
@override
Future addStream(Stream<RemoteStatus> stream) {
return _controller.addStream(stream);
}
@override
Future<void> close() {
return _controller.sink.close();
}
@override
Future get done => _controller.sink.done;
}
import 'package:test/test.dart';
import 'package:sew_app/bloc_tools/remote_state.dart';
class RemoteStateHandler with RemoteStateHandlable {
@override
final RemoteStateController remoteStateController;
RemoteStateHandler() : remoteStateController = RemoteStateController();
Future<String> dummyRemoteAccess(bool shouldSuccess, [String messageIfFailed]) async {
loading();
await Future<void>.delayed(const Duration(milliseconds: 5));
if (shouldSuccess) {
succeed();
return 'test';
} else {
failed(messageIfFailed);
throw Exception('failure for test');
}
}
Future<void> dispose() async {
await closeController();
}
}
void main() {
group('RemoteStateHandler', () {
test('stream is sending corresponded value against sink input.', () async {
final state1 = RemoteStatus(RemoteStatusType.NotAsked);
final state2 = RemoteStatus(RemoteStatusType.Loading);
final state3 = RemoteStatus(RemoteStatusType.Failed, 'for test');
final handler = RemoteStateHandler();
handler.remoteStateController.add(state1);
handler.remoteStateController.add(state2);
handler.remoteStateController.add(state3);
handler.dispose();
final values = await handler.remoteStateController.stream.fold<List<RemoteStatus>>([], (x, y) => x..add(y));
expect(values, [
// [NotAsked] is first default
RemoteStatus(RemoteStatusType.NotAsked),
state1, state2, state3
]);
});
test('bloc like usecase was suceed.', () async {
final handler = RemoteStateHandler();
final result = await handler.dummyRemoteAccess(true);
expect(result, 'test');
expect(
handler.remoteStateController.stream,
emitsInOrder(<dynamic>[
RemoteStatus(RemoteStatusType.NotAsked),
RemoteStatus(RemoteStatusType.Loading),
RemoteStatus(RemoteStatusType.Succeed),
]));
});
test('bloc like usecase when it was failed.', () async {
final handler = RemoteStateHandler();
const messageRemoteStateFailure = 'failure for test';
try {
await handler.dummyRemoteAccess(false, messageRemoteStateFailure);
expect('must not called', 'but it is called');
} catch (e) {
expect(e.message, 'failure for test');
}
expect(
handler.remoteStateController.stream,
emitsInOrder(<dynamic>[
RemoteStatus(RemoteStatusType.NotAsked),
RemoteStatus(RemoteStatusType.Loading),
RemoteStatus(RemoteStatusType.Failed, messageRemoteStateFailure),
]));
});
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment