Streaming data from local ObjectBox (or Hive) database in a Clean Architecture Style

809 views Asked by At

Maybe someone else has the same issues as I had, so here is how I implemented it, finally. I am showing this for ObjectBox, but some of the methods work for Hive or other databases as well.

My issues was about setting-up the ObjectBox stream and especially about transforming the ObjectBox data model to my domain entities within the stream.

1

There are 1 answers

3
w461 On

The data streams from the ObjectBox in the data layer, which also contains the repository implementation, through the contracts in the domain layer (I skip use cases here) to the presentation layer, where I am managing the state with flutter-bloc.

The todo domain entity (only showing the id to shorten things):

class Todo extends Equatable{
  Todo(this.id);
  final String id;

  @override
  List<Object?> get props => [id,];
}

The data model for ObjectBox including the transformation methods to/from the domain entity

@Entity()
class TodoObjectBox {
  @Id()
  int id;
  final String todoId;

  TodoObjectBox ({
    this.id = 0,
    required this.todoId});


  factory TodoObjectBox.toObjectbox(Todo todo, int obId) => TodoObjectBox(
    id: obId,
    todoId: todo.id,
  );

  User fromObjectbox() => Todo(
    id: todoId,
  );
}

The method toObjectBox requires the internal, integer Objectbox id, which I supplement to the method when storing a todo.

Now, in the local datasource implementation, I have the following class to stream the todo's.

@override     
Stream<List<Todo>> streamTodos() {
  Stream<List<TodoObjectBox>> todoObStream = objectbox.Todo.query()
      .watch(triggerImmediately: true).map((query) => query.find());


  StreamTransformer<List<TodoObjectBox>, List<Todo>> streamTransformer =
    StreamTransformer<List<TodoObjectBox>, List<Todo>>.fromHandlers(
      handleData: (List<TodoObjectBox> data, EventSink sink) {
        var todos = data!.map((todoOb) {
          final Todo todo = todoOb.fromObjectbox();
          return todo;
        }).toList();
        sink.add(todos);
      },
      handleError: (Object error, StackTrace stacktrace, EventSink sink) {
        sink.addError('Something went wrong: $error');
      },
      handleDone: (EventSink sink) => sink.close(),
  );
  return streamTransformer.bind(todoObStream);
}

So the stream is generated and then transformed from the data model TodoObjectBox to the domain entity Todo.

In the presentation layer, I instantiate a TodoListBloc with the repository and the event TodoListRequested to start the stream. The bloc then handles this event with the following routine:

Future<void> _onListRequested(
    TodoListRequested event,
    Emitter<TodoListState> emit,
    ) async {
  emit(state.copyWith(status: () => DataTransStatus.loading));

  await emit.forEach<dynamic>(
    _todoRepository.streamTodos(),
    onData: (todos) {
      return state.copyWith(
        status: () => DataTransStatus.success,
        todosList: () => List.from(
            todos.map((t) => TodosListViewmodel.fromDomain(t);
            )),
      );
    },
    onError: (Object error, StackTrace stack) {
      print('Bloc stream error $error $stack');
      return state.copyWith(
        status: () => DataTransStatus.failure,
      );
    },
  );
}

I hope this helps someone also trying to stream from Objectbox or another database to the presentation layer using different data models/entities.

Some context to this answer can be found in ResoCoder's TDD tutorial and BlocLibrary's Todo tutorial.