Created
January 24, 2020 03:26
-
-
Save hachibeeDI/a1d97fcec6ba4eb2d6a84e2f58a90a73 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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