Last active
October 1, 2019 17:48
-
-
Save PlugFox/e3710bbb18d5b201a656811e5ef46ad8 to your computer and use it in GitHub Desktop.
Playground for BLoC state managment by @PlugFox https://dartpad.dartlang.org/e3710bbb18d5b201a656811e5ef46ad8
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
| <div id="app"> |
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
| The MIT License | |
| Copyright (c) 2019 @PlugFox | |
| Permission is hereby granted, free of charge, to any person obtaining a copy | |
| of this software and associated documentation files (the "Software"), to deal | |
| in the Software without restriction, including without limitation the rights | |
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| copies of the Software, and to permit persons to whom the Software is | |
| furnished to do so, subject to the following conditions: | |
| The above copyright notice and this permission notice shall be included in | |
| all copies or substantial portions of the Software. | |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| THE SOFTWARE. |
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
| /* | |
| * Playground for BLoC state managment by @PlugFox | |
| * https://gist.github.com/PlugFox/e3710bbb18d5b201a656811e5ef46ad8 | |
| * https://dartpad.dartlang.org/e3710bbb18d5b201a656811e5ef46ad8 | |
| * | |
| * BLoC design pattern: | |
| * pub.dev/packages/bloc | |
| * felangel.github.io/bloc | |
| * github.com/felangel/bloc/tree/master/examples | |
| * | |
| * +----------------+ | |
| * | Presentation | | |
| * | Layer | | |
| * | (UI) | | |
| * +--+----------+--+ | |
| * | ^ | |
| * Event | | States | |
| * .dispatch(<Event>) | | .state.listen(...) | |
| * v | | |
| * +---+----------+---+ | |
| * | transformer | | |
| * | BLoC | | |
| * | mapEventToState()| | |
| * +---+----------+---+ | |
| * | ^ | |
| * Async requests | | Async response | |
| * Repository.fetch() | | await -> yield <State> | |
| * v | | |
| * +--+----------+--+ | |
| * | Business Logic | | |
| * | Layer | | |
| * | (Backend) | | |
| * +----------------+ | |
| * | |
| */ | |
| import "dart:core"; | |
| import "dart:async"; | |
| import "dart:html"; | |
| import "dart:math"; | |
| /* | |
| * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| * Simplified BLoC package | |
| * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| */ | |
| /// Takes a [Stream] of [Event]s as input | |
| /// and transforms them into a [Stream] of [State]s as output. | |
| abstract class Bloc<Event, State> { | |
| final StreamController<Event> _eventSubject = StreamController<Event>(); | |
| final StreamController<State> _stateSubject = new StreamController<State>.broadcast(); | |
| /// Returns the [State] before any [Event]s have been `dispatched`. | |
| State get initialState; | |
| /// Returns [State] stream of the [Bloc] | |
| Stream<State> get state => this._stateSubject.stream; | |
| /// Returns the current [State] of the [Bloc]. | |
| State get currentState => _currentState; | |
| State _currentState; | |
| Bloc() { | |
| this._currentState = initialState; | |
| _bindStateSubject(); | |
| } | |
| /// Takes an [Event] and triggers `mapEventToState`. | |
| /// `Dispatch` may be called from the presentation layer or from within the [Bloc]. | |
| /// `Dispatch` notifies the [Bloc] of a new [Event]. | |
| /// If `dispose` has already been called, any subsequent calls to `dispatch` will | |
| /// be delegated to the `onError` method which can be overriden at the [Bloc] | |
| /// as well as the [BlocDelegate] level. | |
| void dispatch(Event event) { | |
| try { | |
| onEvent(event); | |
| this._eventSubject.sink.add(event); | |
| } catch (error) { | |
| _handleError(error); | |
| } | |
| } | |
| /// Must be implemented when a class extends [Bloc]. | |
| /// Takes the incoming `event` as the argument. | |
| /// `mapEventToState` is called whenever an [Event] is `dispatched` by the presentation layer. | |
| /// `mapEventToState` must convert that [Event] into a new [State] | |
| /// and return the new [State] in the form of a [Stream] which is consumed by the presentation layer. | |
| Stream<State> mapEventToState(Event event); | |
| /// Called whenever an [Exception] is thrown within `mapEventToState`. | |
| /// By default all exceptions will be ignored and [Bloc] functionality will be unaffected. | |
| /// The stacktrace argument may be `null` if the state stream received an error without a [StackTrace]. | |
| /// A great spot to handle exceptions at the individual [Bloc] level. | |
| void onError(Object error, StackTrace stacktrace) => null; | |
| /// Called whenever an [Event] is dispatched to the [Bloc]. | |
| /// A great spot to add logging/analytics at the individual [Bloc] level. | |
| void onEvent(Event event) => null; | |
| /// Closes the [Event] and [State] [Stream]s. | |
| /// This method should be called when a [Bloc] is no longer needed. | |
| /// Once `dispose` is called, events that are `dispatched` will not be | |
| /// processed and will result in an error being passed to `onError`. | |
| /// In addition, if `dispose` is called while [Event]s are still being processed, | |
| void dispose() { | |
| this._eventSubject.close(); | |
| this._stateSubject.close(); | |
| } | |
| Stream<State> transform( | |
| Stream<Event> events, | |
| Stream<State> next(Event event), | |
| ) { | |
| return events.asyncExpand(next); | |
| } | |
| void _bindStateSubject() { | |
| Event currentEvent; | |
| transform(_eventSubject.stream, (Event event) { | |
| currentEvent = event; | |
| return mapEventToState(currentEvent).handleError(_handleError); | |
| }).forEach((State nextState) { | |
| this._currentState = nextState; | |
| this._stateSubject.add(nextState); | |
| }, | |
| ); | |
| } | |
| void _handleError(Object error, [StackTrace stacktrace]) { | |
| onError(error, stacktrace); | |
| } | |
| } | |
| /* | |
| * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| * MyBloc singleton example | |
| * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| */ | |
| /// EVENTS | |
| abstract class MyBlocEvent { | |
| String get status; | |
| @override | |
| String toString() => this.status; | |
| } | |
| class SplitStringEvent extends MyBlocEvent { | |
| String get status => 'Split string to char list'; | |
| final String payload; | |
| SplitStringEvent(this.payload); | |
| } | |
| /// STATES | |
| abstract class MyBlocState { | |
| String get status; | |
| @override | |
| String toString() => this.status; | |
| } | |
| class IdlingState extends MyBlocState { | |
| final String status; | |
| IdlingState([this.status = 'Idle']); | |
| } | |
| class ProcessingState extends MyBlocState { | |
| final String status; | |
| ProcessingState([this.status = 'Processing in progress']); | |
| } | |
| class SplitStringState extends MyBlocState { | |
| String get status => 'Gotcha char: $char'; | |
| int index; | |
| String char; | |
| SplitStringState({int index, String char}) | |
| : assert(char is String && index is int && char.length == 1 && index >= 0) | |
| , this.index = index | |
| , this.char = char; | |
| } | |
| class ErrorState extends MyBlocState { | |
| String get status => this.error; | |
| final String error; | |
| ErrorState([this.error = 'Error']); | |
| } | |
| /// MyBLoC BLoC | |
| class MyBLoC extends Bloc<MyBlocEvent, MyBlocState> { | |
| @override | |
| MyBlocState get initialState => IdlingState(); | |
| // Ability to listen to a reduced stream with one kind of states | |
| Stream<bool> get isIdlingState => this.state | |
| .where((MyBlocState state) => state is IdlingState || state is ProcessingState) | |
| .map<bool>((MyBlocState state) => state is IdlingState); | |
| Stream<SplitStringState> get splitStringStream => this.state | |
| .where((MyBlocState state) => state is SplitStringState) | |
| .cast<SplitStringState>(); | |
| // Event => States router | |
| Stream<MyBlocState> mapEventToState(MyBlocEvent event) async* { | |
| yield ProcessingState(); | |
| if (event is SplitStringEvent) { | |
| yield* _splitStringEvent(event.payload); | |
| } | |
| yield IdlingState(); | |
| } | |
| Stream<MyBlocState> _splitStringEvent(String payload) async* { | |
| try { | |
| Future<dynamic> sleep() => Future.delayed(Duration(seconds: 1)); | |
| for (int _i=0;_i<payload?.length ?? 0;_i++) { | |
| await sleep(); | |
| yield SplitStringState(index: _i, char: payload[_i]); | |
| } | |
| } catch(e) { | |
| yield ErrorState(); | |
| } | |
| } | |
| void dispose() { | |
| super.dispose(); | |
| } | |
| // SINGLETON + | |
| // A single instance and the ability to call from anywhere | |
| static final MyBLoC _internalBloc = MyBLoC._internal(); | |
| factory MyBLoC() => _internalBloc; | |
| MyBLoC._internal() {} | |
| // SINGLETON - | |
| } | |
| /* | |
| * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| * Presentation layer | |
| * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| */ | |
| void runApp() => | |
| AppInstance() | |
| ..init() | |
| ..build(); | |
| class AppInstance { | |
| final MyBLoC _myBloc; | |
| final Widget _sourceField; | |
| final Widget _outputList; | |
| AppInstance() | |
| : this._myBloc = MyBLoC() | |
| , this._sourceField = TextField(key: 'source-field') | |
| , this._outputList = ListView(key: 'output-list'); | |
| void onDispatch(_) => this._myBloc.dispatch(SplitStringEvent(this._sourceField.text ?? '')); | |
| // Init BLoC listners | |
| void init() => | |
| this._myBloc | |
| ..isIdlingState.forEach(_idleSwitcher) | |
| ..splitStringStream.forEach(_addChar); | |
| // Build UI | |
| void build() => | |
| App( | |
| home: Scaffold( | |
| children: <Widget>[ | |
| Header(title: 'BLoC sample by @PlugFox'), | |
| Divider(), | |
| Form( | |
| key: 'source-form', | |
| children: <Widget>[ | |
| Label('String to char list: '), | |
| this._sourceField, | |
| RaisedButton(key: 'dispatch-button', text: 'Dispatch', onClick: onDispatch), | |
| ], | |
| ), | |
| Divider(), | |
| this._outputList, | |
| ], | |
| ), | |
| ); | |
| void _idleSwitcher(bool isIdle) { | |
| final Map<String,String> _att = document.querySelector('form#source-form>fieldset')?.attributes; | |
| if (_att is! Map<String,String>) { | |
| return; | |
| } else if (isIdle) { | |
| _att.remove('disabled'); | |
| } else { | |
| this._outputList.clear(); | |
| _att['disabled'] = 'true'; | |
| } | |
| } | |
| void _addChar(SplitStringState state) { | |
| this._outputList.add( | |
| Row( | |
| children: <Widget>[ | |
| Text('Char #${state.index.toString()}: \'${state.char}\''), | |
| ], | |
| ), | |
| ); | |
| } | |
| } | |
| class Widget { | |
| final Element _element; | |
| String get key => this._element.id; | |
| String get text => this._element is InputElement ? (this._element as InputElement).value : this._element.innerText; | |
| Widget(String tag, {String key, String text = ''}) | |
| : this._element = Element.tag(tag??'div')..id=key??_genKey()..innerText=text; | |
| Widget.input(String type, {String key, String text = '', Function onClick}) | |
| : this._element = InputElement(type: type??'text')..id=key??_genKey()..value=text { | |
| if (onClick != null) this._element.onClick.forEach(onClick); | |
| } | |
| Widget.fromId(String key) | |
| : this._element = document.getElementById(key); | |
| Element build() => this._element; | |
| void add(Widget child) => this._element.children.add(child.build()); | |
| void addAll(List<Widget> children) => this._element.children.addAll(children.map((v) => v.build())); | |
| void clear() => this._element.children.clear(); | |
| static String _genKey() => 'rnd_${Random().nextInt(16777215).toRadixString(16)}'; | |
| @override | |
| String toString() => '<Widget #$key>'; | |
| @override | |
| int get hashCode => this.key.hashCode; | |
| @override | |
| operator ==(Object obj) => obj is Widget && obj.key == this.key; | |
| } | |
| class App extends Widget { | |
| App({String id = 'app', Widget home}) : super.fromId(id) { | |
| this.add(home); | |
| } | |
| } | |
| class Scaffold extends Widget { | |
| Scaffold({List<Widget> children}) : super('div') { | |
| this.addAll(children); | |
| } | |
| } | |
| class Form extends Widget { | |
| Form({String key, List<Widget> children}) : super('form', key: key) { | |
| add(Widget('fieldset')..addAll(children)); | |
| } | |
| } | |
| class TextField extends Widget { | |
| TextField({String key, String text, Function onClick}) : super.input('text', key: key, text: text); | |
| } | |
| class RaisedButton extends Widget { | |
| RaisedButton({String key, String text, Function onClick}) : super.input('button', key: key, text: text, onClick: onClick); | |
| } | |
| class Label extends Widget { | |
| Label(String text, {String key}) : super('span', key: key, text: text); | |
| } | |
| class Text extends Widget { | |
| Text(String text, {String key}) : super('p', key: key, text: text); | |
| } | |
| class Header extends Widget { | |
| Header({String key, String title = 'Title'}) : super('h3', key: key, text: title); | |
| } | |
| class ListView extends Widget { | |
| ListView({String key, List<Row> rows}) : super('ul', key: key) { | |
| rows?.forEach((Row row) => this.addRow(row)); | |
| } | |
| addRow(Row row) => this.add(row); | |
| } | |
| class Row extends Widget { | |
| Row({String key, List<Widget> children}) : super('li', key: key) { | |
| this.addAll(children); | |
| } | |
| } | |
| class Divider extends Widget { | |
| Divider() : super('div') { | |
| this.addAll(<Widget>[ | |
| Widget('br'), | |
| Widget('hr'), | |
| Widget('br'), | |
| ]); | |
| } | |
| } | |
| /* | |
| * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| * Main point of entry | |
| * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| */ | |
| void main() => runApp(); |
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
| * { | |
| margin: 0; | |
| padding: 0; | |
| font-size: 16px; | |
| font-weight: bold; | |
| letter-spacing: -.5px; | |
| overflow: hidden; | |
| } | |
| #app { | |
| max-width: 600px; | |
| overflow: hidden; | |
| text-align: center; | |
| padding: 12px 0; | |
| overflow: hidden; | |
| } | |
| fieldset { | |
| padding: 12px 6px; | |
| } | |
| ul { | |
| max-height: 200px; | |
| overflow-y: auto; | |
| overflow-x: hidden; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment