ajwah_bloc 2.0.1
ajwah_bloc: ^2.0.1 copied to clipboard

A reactive state management library. Manage your application's states, effects, and actions easy way. Make apps more scalable with a unidirectional data-flow.

ajwah_bloc #

A reactive state management library. Manage your application's states, effects, and actions easy way. Make apps more scalable with a unidirectional data-flow. flutter demo | src

Declare your store as a global variable or enable exposeApiGlobally:true.

final store = createStore();
//or
createStore( exposeApiGlobally:true);

Now register states as much as you want and consume them where ever you want in your app.

//register [counter] state
store.registerState<int>(
    stateName: 'counter',
    initialState: 0,
    mapActionToState: (state, action, emit) {
      switch (action.type) {
        case 'inc':
          emit(state + 1);
          break;
        case 'dec':
          emit(state - 1);
          break;
        default:
      }
    },
  );

  //consuming
  store.select('counter').listen(print); // 0,1,0,1

  //dispatching actions
  store.dispatch(Action(type: 'inc'));
  store.dispatch(Action(type: 'dec'));
  store.dispatch(Action(type: 'inc'));

You can easily filter out actions using the optional filterActions:(action)=>bool param of registerState method.

For filtering out the dec action from the counter state:

//register [counter] state
store.registerState<int>(
    stateName: 'counter',
    initialState: 0,
    filterActions: (action) => action.type != 'dec',
    mapActionToState: (state, action, emit) {
      switch (action.type) {
        case 'inc':
          emit(state + 1);
          break;
        case 'dec':
          emit(state - 1);
          break;
        default:
      }
    },
  );

  //consuming
  store.select('counter').listen(print); // 0,1,2

  //dispatching actions
  store.dispatch(Action(type: 'inc'));
  store.dispatch(Action(type: 'dec'));
  store.dispatch(Action(type: 'inc'));

Now dec action is useless. Let's add an effect on dec action:

store.registerEffect(
      (action$, store) =>
          action$.whereType('dec')
          .map((event) => Action(type: 'inc')),
      effectKey: 'test');

Here we are capturing the dec action using whereType and then map the action as an inc action

//register [counter] state
store.registerState<int>(
    stateName: 'counter',
    initialState: 0,
    filterActions: (action) => action.type != 'dec',
    mapActionToState: (state, action, emit) {
      switch (action.type) {
        case 'inc':
          emit(state + 1);
          break;
        case 'dec':
          emit(state - 1);
          break;
        default:
      }
    },
  );

  //consuming
  store.select('counter').listen(print); // 0,1,2,3

  //effect on dec action - so that it works as inc
  store.registerEffect(
      (action$, store) =>
          action$.whereType('dec').map((event) => Action(type: 'inc')),
      effectKey: 'test');

  //dispatching actions
  store.dispatch(Action(type: 'inc'));
  store.dispatch(Action(type: 'dec'));
  store.dispatch(Action(type: 'inc'));

If you want to log all the action and state changed - just use the exportState() function. Call this function just after your createStore() function.

var store = createStore();
store.exportState().listen(print);

output looks like:

[Action(type: @@INIT), {counter: 0}]
[Action(type: registerState(counter)), {counter: 0}]
0
[Action(type: registerEffect(test)), {counter: 0}]
[Action(type: inc), {counter: 1}]
1
[Action(type: dec), {counter: 1}]
[Action(type: inc), {counter: 2}]
2
[Action(type: inc), {counter: 3}]
3

You can import state also:

store.importState({'counter': 100});

We have covered the basic of ajwah_bloc. Now we see:

  • how to consume states in the flutter widget.
  • how to make your app a way more declaratives.
  • how to combine multiple states and make a single stream.
  • testing ajwah_bloc

Consuming counter state through StreamBuilder widget:

StreamBuilder<int>(
    stream: store.select('counter'),
    initialData: 0,
    builder:(context, snapshot) => Text(snapshot.data.count.toString()),
),
Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
        RaisedButton(
            onPressed: () => store.dispatch(store.Action(type: 'inc')),
            child: Text('Inc'),
        ),
        RaisedButton(
            onPressed: () => sstore.dispatch(store.Action(type: 'dec')),
            child: Text('Dec'),
        ),
    ],
),

Make your app a way more declaretives simply dispatching the action, here you see an example of conditionaly rendering a widged having taps on two buttons [Show] and [Hide], and consuming those actions as you needed.

Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
        RaisedButton(
            onPressed: () => store.dispatch(store.Action(type: 'show-widget')),
            child: Text('Show'),
        ),
        RaisedButton(
            onPressed: () => sstore.dispatch(store.Action(type: 'hide-whiget')),
            child: Text('Hide'),
        ),
    ],
),
StreamBuilder<String>(
    stream: store.actions
            .whereTypes(['show-widget', 'hide-widget'])
            .map((action) => action.type),
    initialData: '',
    builder:(context, snapshot) =>
      snapshot.data == 'show-widget' ? DynamicWidget() : Container(),
),

Now we declare two states:

  • todo
  • search-category

And then consume them combining as a single stream. So that we get the latest filtered result on todo as well as search-category states.

class Todo {
  Todo({
    this.description,
    this.completed = false,
    this.id,
  });

  final String id;
  final String description;
  final bool completed;

}
class TodoAction extends Action {
  String description;
  String id;
  TodoAction({
    @required String type,
    this.id,
    this.description,
  }) : super(type: type);
}

class TodoFilterAction extends Action {
  TodoFilterAction({@required String type}) : super(type: type);
}

abstract class TodoActionTypes {
  static const all = 'todo-all';
  static const active = 'todo-active';
  static const completed = 'todo-completed';
  static const add = 'todo-add';
  static const update = 'todo-update';
  static const toggle = 'todo-toggle';
  static const remove = 'todo-remove';
}
//register [todo] state
store.registerState<List<Todo>>(
    stateName: 'todo',
    initialState: [
        Todo(id: 'todo-0', description: 'hi'),
        Todo(id: 'todo-1', description: 'hello'),
        Todo(id: 'todo-2', description: 'learn reactive programming'),
      ],
    mapActionToState: (state, action, emit) {
        if (action is TodoAction) {
            switch (action.type) {
            case TodoActionTypes.add:
                emit([
                ...state,
                Todo(description: action.description)
                ]);
                break;
            case TodoActionTypes.update:
                emit([
                for (var item in state)
                    if (item.id == action.id)
                    Todo(
                        id: item.id,
                        completed: item.completed,
                        description: action.description)
                    else
                    item,
                ]);
                break;
            case TodoActionTypes.toggle:
                emit([
                for (var item in state)
                    if (item.id == action.id)
                    Todo(
                        id: item.id,
                        completed: !item.completed,
                        description: item.description)
                    else
                    item,
                ]);
                break;
            case TodoActionTypes.remove:
                emit(state
                        .where((item) => item.id != action.id)
                        .toList());
                break;
            }
        }
    },
  );

//register [search-category] state
store.registerState<String>(
    stateName: 'search-category',
    initialState: TodoActionTypes.all,
    mapActionToState: (state, action, emit) {
        if (action is TodoFilterAction) {
          emit(action.type);
        }
    },
  );

Combining two states

  • do not forget - import 'package:rxdart/rxdart.dart';
Stream<List<Todo>> getFilteredTodos() => CombineLatestStream([
      select('search-category'),
      select('todo'),
    ], (values) {
      final todos = values[1] as List<Todo>;
      switch (values[0]) {
        case TodoActionTypes.active:
          return todos.where((todo) => !todo.completed).toList();
        case TodoActionTypes.completed:
          return todos.where((todo) => todo.completed).toList();
        default:
          return todos;
      }
    });

Testing: We need to add the testing dependency ajwah_bloc_test then here you go:

todo_test.dart

import 'package:ajwah_bloc/ajwah_bloc.dart';
import 'package:ajwah_bloc_test/ajwah_bloc_test.dart';

import 'package:flutter_test/flutter_test.dart';
import 'package:example/main.dart';

void main() {
  AjwahStore store;

  setUp(() {
    store = createStore(exposeApiGlobally: true);
    registerTodoStates();
  });

  tearDown(() {
    store.dispose();
  });

  ajwahTest<List<Todo>>(
    'Render the default todos',
    build: () => getFilteredTodos(),
    expect: [isA<List<Todo>>()],
    verify: (models) {
      expect(3, models[0].length);
    },
  );

  ajwahTest<List<Todo>>(
    'Editing the todo on done',
    build: () => getFilteredTodos(),
    act: () =>
        store.dispatch(TodoAction(type: TodoActionTypes.toggle, id: 'todo-0')),
    skip: 1,
    expect: [isA<List<Todo>>()],
    verify: (models) {
      expect(true, models[0][0].completed);
    },
  );

  ajwahTest<List<Todo>>(
    'Add new todo',
    build: () => getFilteredTodos(),
    act: () => store.dispatch(
        TodoAction(type: TodoActionTypes.add, description: 'new todo')),
    skip: 1,
    expect: [isA<List<Todo>>()],
    verify: (models) {
      expect('new todo', models[0][3].description);
    },
  );

  ajwahTest<List<Todo>>(
    'removing the first todo',
    build: () => getFilteredTodos(),
    act: () =>
        store.dispatch(TodoAction(type: TodoActionTypes.remove, id: 'todo-0')),
    skip: 1,
    expect: [isA<List<Todo>>()],
    verify: (models) {
      expect('todo-1,todo-2', models[0].map((e) => e.id).join(','));
    },
  );

  ajwahTest<List<Todo>>(
    'show only uncomplete todos',
    build: () => getFilteredTodos(),
    act: () {
      store.dispatch(TodoAction(type: TodoActionTypes.toggle, id: 'todo-0'));
      store.dispatch(TodoFilterAction(type: TodoActionTypes.active));
    },
    skip: 2,
    expect: [isA<List<Todo>>()],
    verify: (models) {
      expect(2, models[0].length);
    },
  );
}

Api #

dispatch(Action action)
Stream<T> select<T>(String stateName)
Stream<T> selectMany<T>(T callback(Map<String, dynamic> state))
void registerState<S>(
      {@required String stateName,
      @required S initialState,
      @required MapActionToStateCallback<S> mapActionToState})
void unregisterState({@required String stateName})
void registerEffect(EffectCallback callback, {@required String effectKey})
void unregisterEffect({@required String effectKey})
BehaviorSubject<Action> get dispatcher
Actions get actions
T getState<T>({@required String stateName})
Stream<List<dynamic>> exportState()
void importState(Map<String, dynamic> state)
void dispose()
1
likes
110
pub points
0%
popularity

A reactive state management library. Manage your application's states, effects, and actions easy way. Make apps more scalable with a unidirectional data-flow.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-2-Clause (LICENSE)

Dependencies

meta, rxdart

More

Packages that depend on ajwah_bloc