본문 바로가기
Mobile Development/Flutter

Flutter - 11. TodoList App (4) TodoList에 BLoC 패턴을 적용해보자.

by 두잇뚜 2021. 1. 27.
반응형

 

전 포스팅에서 Database를 연동하는 작업을 마쳤었다.

문제는 UI를 그려내는 Widget에서 상태와 데이터 접근을 모두 하기 때문에 역할 분리가 제대로 되지 않았다.
상태가 복잡해질수록, 데이터가 많아질수록 Widget이 가지는 역할이 너무 많아지며, 관리가 점점 어려워지고 스파게티 코드가 될 수 있다.

이러한 문제를 해결하기 위해서 Android, iOS 진영은 MVVM, MVP, RIBs 등의 아키텍처 패턴, 클린 아키텍처 패턴들을 도입한다.

클린 아키텍처 패턴은 위와 같다. 이에 대한 상세한 내용은 생략하겠다. 이를 위한 포스팅은 아니니.

아무튼, 이러한 패턴 적용의 이유는 서로의 역할을 분리하고 의존성을 줄인다.
어떤 한 모듈에 변화가 생겨도 다른 모듈에 변화가 없거나 최소화하고, 유지보수성과 확장성을 향상시킨다.

Flutter에선 이러한 방법으로 BLoC 아키텍처 패턴을 권장한다.


1. BLoC(Bussiness Logic Component)란?

Widget과 Widget이 필요한 비즈니스 로직을 분리하는 것을 말한다.
BLoC는 Flutter 때문에 생겨난 것은 아니고, 이전에 존재하던 개념이다.

https://www.toptal.com/cross-platform/code-sharing-angular-dart-flutter-bloc

BLoC 패턴은 위와 같은 구조이다. 이미지는 위 링크에서 가져왔으며, 문제 시 삭제하겠다.

동작은 아래와 같이 요약할 수 있다.

  1. View(Widget)은 BLoC에게 필요한 데이터를 요청한다. 즉, 이벤트 발생 시 BLoC에게 알린다.
  2. BLoC는 Repository에게 Data(Model)를 요청한다.
  3. Repository는 Data Layer에 접근하여 Data(Model)을 반환한다.
  4. BLoC는 Repository로부터 얻은 Data(Model)을 View에게 반환한다.

여기서 Data Layer는 현재의 TodoList앱에선 Database를 의미한다.
만약 네트워크 기반 앱이라면 서버의 API를 의미 할 것이다.

Repository를 통해 Data Layer를 한층 추상화하고 Repository가 어느 Data Source에 접근하는지는 알 필요 없게 만드는 것이다. (Data Source는 Database같은 Local인지, API 호출같은 Remote인지 등의 구분이다.)

여기서 BLoC와 Widget은 함수의 반환형 등으로 데이터를 얻는 것이 아니다.
BLoC는 데이터가 변경되었음만 알리고 Widget은 그 변경을 관찰하여 UI를 업데이트한다.
이를 위해 "Stream" 이란 것을 사용한다.


2. BLoC 패턴 적용

BLoC 패턴을 적용하기 위해 다음 단계가 필요하다.

  1. TodoRepository 클래스를 작성한다.
  2. TodoBloc 클래스를 작성한다.
  3. TodoListPage를 수정한다. (이 때, StatefulWidget에서 StatelessWidget으로 변경한다.)

코드 변경되는 부분이 많지 않고 어렵지 않다!

#1. TodoRepository 클래스 작성

// todo_repository.dart

class TodoRepository {
  final TodoDao _todoDao;

  TodoRepository(this._todoDao);

  Future<List<TodoModel>> getTodoList() => _todoDao.getTodoList();

  Future<int> createTodo(TodoModel todo) => _todoDao.createTodo(todo);
}

TodoRepository에서 하는 것은, TodoDao에 접근하여 데이터를 획득하여 반환하는 것 뿐이다.
즉, Data Layer에 접근하는 역할만 한다.

생성자로 TodoDao 객체를 받는 것은 DI를 구현하기 위함이다.

#2. TodoBloc 클래스 작성

// todo_bloc.dart

class TodoBloc {
  final TodoRepository _todoRepository;
  final StreamController<List<TodoModel>> _todoController = StreamController<List<TodoModel>>.broadcast();

  get todoListStream => _todoController.stream;

  TodoBloc(this._todoRepository) {
    getTodoList();
  }

  void getTodoList() async {
    List<TodoModel> todoList = await _todoRepository.getTodoList();
    _todoController.sink.add(todoList);
  }

  void addTodo(TodoModel todo) async {
    await _todoRepository.createTodo(todo);
    getTodoList();
  }
}

TodoBloc는 TodoRepository 객체에 접근하여 TodoList를 획득하거나 추가하는 역할을 한다.

  • StreamController를 통해 데이터의 Stream을 만든다. 그리고 이 것을 통해 관찰자(Widget)에게 변경을 알린다.
    • _todoController.stream을 외부에서 관찰하게 된다.
  • addTodo는 UI(Widget)에서 호출한다. 호출 시 TodoRepository 객체에 접근하여 데이터를 추가하고 TodoList를 갱신한다.
  • getTodoList는 외부에서도 호출 가능하며, 내부에서도 TodoList의 갱신이 필요 한 경우 호출한다.

#3. TodoListPage 수정

// todo_list.dart

class TodoListPage extends StatelessWidget {
  ...
  final TodoBloc _todoBloc = TodoBloc(
      TodoRepository(
          TodoDao()
      )
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // _createFloatingActionButton에 BuildContext를 파라미터로 넘기게 수정되었다.
      floatingActionButton: _createFloatingActionButton(context),
      ...
      body: _createTodoListStreamBuilder(),
    );
  }

  Widget _createFloatingActionButton(BuildContext context) {
    return FloatingActionButton(
      child: Icon(Icons.add, color: Colors.white),
      onPressed: () => {
        _openAddTodoDialog(context)
      },
    );
  }

  Widget _createTodoListStreamBuilder() {
    return StreamBuilder(
        stream: _todoBloc.todoListStream,
        builder: (BuildContext context, AsyncSnapshot<List<TodoModel>> snapshot) {
          if (snapshot.hasData) {
            if (snapshot.data.length > 0) {
              return _createTodoList(snapshot.data);
            } else {
              return Container();
            }
          } else {
            return Container();
          }
        }
    );
  }

  Widget _createTodoList(List<TodoModel> todoList) {
    return ListView.separated(
      itemCount: todoList.length,
      itemBuilder: (BuildContext context, int index) {
        return _createTodoCard(todoList[index]);
      },
      separatorBuilder: (BuildContext context, int index) {
        return Divider(
          thickness: 8.0,
          height: 8.0,
          color: Colors.transparent,
        );
      },
    );
  }

  void _openAddTodoDialog(BuildContext context) {
    showDialog(
        context: context,
        builder: (BuildContext context) {
          return AlertDialog(
          );
        }
    );
  }

  void _addNewTodo(String title) async {
    TodoModel newTodo = TodoModel(null, title, DateTime.now(), TodoState.todo);
    _todoBloc.addTodo(newTodo);
  }
}


변경 된 부분은 다음과 같다.

  • StatefulWidget이었던 TodoListPage가 StatelessWidget이 되었다.
    • BuildContext 참조 때문에 _createFloatingActionButton(), _openAddTodoDialog가 수정되었다.
  • _createTodoListStreamBuilder 함수가 추가되었다.
    • StreamBuilder라는 Widget을 사용하고 TodoBloc에 있는 Stream을 구독하여 데이터 변경을 관찰한다.
    • 데이터가 변경되었을 때 갱신되며, 갱신된 데이터의 상태에 따라 Widget을 반환하는 Builder를 구현한다.
  • _createTodoList에 List<TodoModel> todoList 파라미터가 추가되었다.
    • StreamBuilder에서 변경 된 데이터를 넘겨주어 기존과 같은 TodoList를 생성하게 되었다.

3. StatelessWidget인데 상태에 따라 Widget이 변경된다고?!

현재까지 Widget의 상태를 변경하고 Widget을 갱신하려면 StatefulWidget을 사용해야 했었다.

하지만 Stream과 StreamBuilder를 이용하면 StatelessWidget도 상태를 관찰하고 변경되면 Widget을 변경할 수 있다.
동적으로 변하는 Widget을 위해 항상 StatefulWidget을 사용 할 필요는 없다는 것이다.

그럼 StatefulWidget이 필요가 없느냐? 그것은 아니다. 상황에 따라 다르다. 필요없다면 구글이 지우고 StreamBuilder만 사용하게 만들었을 것이다.


Stream과 StreamBuilder를 사용하고 데이터 접근을 분리함으로써 각자의 역할이 분리되었다.

  • Widget은 본인이 알아야 할 데이터의 변경만을 관찰하고 Widget을 그리는 역할만 한다.
  • BLoC 객체는 Widget의 상태와 이벤트 발생에 따라 데이터를 변경할 것을 TodoRepository에 요청한다.
  • TodoRepository는 DataSource에 접근하여 Data를 획득할 수 있게 해준다. 여기선 Database다.

의존성을 보면 Widget은 BLoC를, BLoC는 Repository를, Repository는 Data를 참조한다.

서로 단방향으로 의존하고 강하게 묶이지 않기 때문에 어느 한 쪽을 수정해도 다른 부분에 영향이 아예 없을수도, 최소화할 수 있게 되었다.

 

 

 

 

댓글0