Bloc 적용하여 todo 등록 기능 구현하기

순순·2024년 11월 26일

Flutter

목록 보기
11/16

지난 프로젝트에서 상태관리 라이브러리로 getX 를 사용했었다. 그땐 규모가 그리 크지 않으니 겟엑스로 충분할 줄 알았으나, 기능이 늘어나면서 컨트롤러의 역할이 점점 비대해지는 것을 경험했다.

Getx Controller 파일 내의 코드가 너무 길어지고, 파일 하나에서 추가, 수정, 삭제 등 모든 상태를 관리하다보니 어떤 상태를 관리하는지 구분하기가 곤란했다.

물론 내가 겟엑스를 처음 써본 상황이어서 제대로 적용을 못해서 그럴 수도 있겠지만…! 결론적으로 팀내에서 다음엔 Bloc 를 써보고 싶다는 의견이 많았기 때문에 이번 사이드 프로젝트는 Bloc 를 적용해보기로 했다.

우선 가장 간단해보이는 Todo 추가 기능을 Bloc로 구현해 보았다. 투두는 로컬 DB에 저장했고 SQLite 를 썼다.


Todo 항목을 저장할 모델 정의

       class Todo {
         int idx;
         int categoryIdx;
         String userName;
         String content;
         DateTime? startStopWtDt;
         DateTime? endStopWtDt;
         DateTime? startTargetDt;
         DateTime? endTargetDt;
         DateTime? createDt;
         DateTime? updateDt;
         DateTime? deleteDt;
       
         Todo({
           required this.idx,
           required this.categoryIdx,
           required this.userName,
           required this.content,
           this.startStopWtDt,
           this.startTargetDt,
           this.endStopWtDt,
           this.endTargetDt,
           this.createDt,
           this.updateDt,
           this.deleteDt
         });

Bloc Event 정의

  • UI 에서 발생한 행동 정의
  • 어떤 Event가 발생 되었는지 Bloc 에 알려주는 역할.
  • 사용자가 UI 에 데이터 입력 → 입력받은 데이터를 Bloc Event 의 생성자를 통해 Bloc 에게 전달
abstract class TodoEvent  {}
        
class AddTodo extends TodoEvent {
	final int idx;
    final int categoryIdx;
    final String userName;
    final String content;
    DateTime? startStopWtDt;
    DateTime? endStopWtDt;
    DateTime? startTargetDt;
    DateTime? endTargetDt;
    DateTime? createDt;
    DateTime? updateDt;
    DateTime? deleteDt;
    
    AddTodo({
    	required this.content,
        required this.idx,
        required this.categoryIdx,
        required this.userName});
}

Bloc State 정의

  • Bloc 의 비즈니스 로직 처리 후 결과를 상태로 반환.
    ex) TodoLoaded 상태 일 때, 투두 목록을 화면에 표시할 수 있도록 한다.
        import 'package:equatable/equatable.dart';
        import 'package:time_todo/entity/todo_tbl.dart';
        
        abstract class TodoState extends Equatable {
        
          
          List<Object?> get props => [];
        
        }
        
        // Bloc 가 처음 시작할 때의 상태
        class TodoInitial extends TodoState {}
        
        // 새로운 Todo 항목이 추가된 후, Todo 리스트를 화면에 보여줄 때 사용.
        class TodoLoaded extends TodoState {
          final List<Todo> todos;
        
          TodoLoaded({required this.todos});
        
          
          List<Object?> get props => [todos];
        }
        
        class TodoError extends TodoState {
        
          
          String toString() {
            return "Todo 로딩 에러...";
          }
        }

Bloc 정의

  • 여기서 비즈니스 로직 작성
  • 혹은 Repository 메서드 호출
  • TodoBloc 클래스를 만들어서, Event 가 발생했을 때 State 를 어떻게 바꿀지 정의 한다.
  • emit 메서드를 통해 BlocState 에 정의한 상태로 트리거
    ex) TodoLoaded 상태, TodoLoadError 상태, TodoInitial 상태
        import 'package:flutter_bloc/flutter_bloc.dart';
        import 'package:time_todo/bloc/todo/todo_event.dart';
        import 'package:time_todo/bloc/todo/todo_state.dart';
        import 'package:time_todo/domain/repository/todo_repository.dart';
        import 'package:time_todo/entity/todo_tbl.dart';
        
        class TodoBloc extends Bloc<TodoEvent, TodoState> {
        // TodoBloc 생성 시, 초기 상태를 지정 해준다.
          TodoBloc() : super(TodoInitial()) {
            // 투두 추가
            on<AddTodo>((event, emit) async {
              final newTodo = Todo(
                  content: event.content,
                  idx: event.idx,
                  categoryIdx: event.categoryIdx,
                  userName: event.userName
              );
        
              // DB에 저장
              await TodoRepository.insertTodo(newTodo);
        
              // DB에 저장된 투두 목록 가져오기
              final todoList = await TodoRepository.getAllTodo();
              // 새로운 리스트로 상태 emit
              emit(TodoLoaded(todos: todoList));
            });
        
            // 투두 목록 조회
            on<GetTodo>((event, emit) async {
              try {
                final allTodos = await TodoRepository.getAllTodo();
                emit(TodoLoaded(todos: allTodos));
              } catch (e) {
                emit(TodoError());
              }
            });
          }
        }

  • on<T>(handler) : Bloc 에서 제공하는 메서드. 특정 이벤트가 발생했을 때, 해당 이벤트를 어떻게 처리할지 정의하는 것. Todo_Event.dart 에서 정의해놓은 ‘AddTodo’ 라는 이벤트가 발생했을 때만 작동하는 코드블록을 설정하는 것.
  • event : 현재 발생한 event. (여기서는 AddTodo)
  • emit : Bloc 의 상태를 업데이트하는 함수. 이벤트 발생 후 새로운 상태를 방출하여 UI 상태를 업데이트 하도록 함. emit 을 호출하여 상태를 새값으로 바꾸면, BlocBuilder 가 이를 감지하여 화면을 새로고침 함.
  • Bloc은 상태가 새 값으로 완전히 바뀌었다고 판단할 때만 UI를 업데이트 한다.
    • 그러므로 새로운 상태를 emit 할 때, Bloc은 상태가 이전과 다른 객체인지 여부를 확인한다.
    • 그런데 List 같은 컬렉션은 참조형 데이터다..
    • 따라서 만약 리스트에 새로운 값을 추가해도, 같은 참조를 가지고 있는 객체들은 이를 하나의 동일한 리스트로 인식하기 때문에, Bloc은 새로운 상태가 아니라고 인식할 가능성이 있다.
    • 그래서 Bloc 사용 시, 객체를 비교하기 위해서는 Equtable 을 사용해야 한다.
    • Dart 에서는 기본적으로 객체를 비교할 때, 객체의 내용이 같다고 해도, 각각 다른 메모리 주소를 가지고 있으면 다르다고 판단한다. equtable 을 사용하면, 객체의 ‘내용’을 비교하여 동일한 상태로 간주할 수 있게 도와준다.

Bloc 사용 흐름

UI 이벤트 발생 → 이벤트 처리 → 비즈니스 로직 처리 → 상태 업데이트 → UI 갱신


class AddTodo extends TodoEvent {
  final int idx;
  final int categoryIdx;
  final String userName;
  final String content;
  DateTime? startStopWtDt;
  DateTime? endStopWtDt;
  DateTime? startTargetDt;
  DateTime? endTargetDt;
  DateTime? createDt;
  DateTime? updateDt;
  DateTime? deleteDt;

  AddTodo({
    required this.content,
    required this.idx,
    required this.categoryIdx,
    required this.userName});
}

→ Bloc 비즈니스 로직 실행

    // 투두 추가
    on<AddTodo>((event, emit) async {
      final newTodo = Todo(
          content: event.content,
          idx: event.idx,
          categoryIdx: event.categoryIdx,
          userName: event.userName
      );

      // DB에 저장
      await TodoRepository.insertTodo(newTodo);

      // DB에 저장된 투두 목록 가져오기
      final todoList = await TodoRepository.getAllTodo();
   
    });

→ Bloc State 업데이트

// 상태 변경: TodoLoaded 상태로 emit
emit(TodoLoaded(todos: todoList));

Equtable

터미널로 추가

flutter pub add equatable
  • Equatable 을 상속받은 클래스는 props 라는 리스트를 구현해야 한다. props 에 객체의 비교 기준이 되는 값을 넣는다.
  • TodoLoaded 클래스에서는 todos 가 비교 기준이 되기때문에 todos 를 넣어준다.
  • 이제 Bloc 는 상태가 바뀌었는지 여부를 todos 내용을 기준으로 판단한다. (todos 리스트가 같은 내용이라면 상태가 변하지 않았다고 판단)
  • 이렇게 하면 불필요한 UI 업데이트가 발생하지 않는다.
  • 안드로이드의 diffUtil 과 비슷한..
  • 코드
    import 'package:equatable/equatable.dart';
    
    class TodoState extends Equatable {
      
      List<Object?> get props => [];
    }
    
    class TodoLoaded extends TodoState {
      final List<Todo> todos;
    
      TodoLoaded(this.todos);
    
      
      List<Object?> get props => [todos]; // todos 리스트를 기준으로 비교
    }

TodoRepository

import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import '../../entity/todo_tbl.dart';

class TodoRepository {
  static Database? _database;

  // 데이터베이스에 접근할 때 사용하는 getter
  static Future<Database?> get database async {
    try {
      if (_database != null) {
        return _database;
      } else {
        return _database = await initDatabase();
      }
    } catch (e) {
      print('get database 중 오류 발생: $e');
      return null;
    }
  }

  // DB 초기화&테이블 생성
  // 파일이 존재하지 않으면, 새로운 데이터베이스 파일을 생성
  static Future<Database?> initDatabase() async {
    try {
      return await openDatabase(
          join(
              await getDatabasesPath(), 'todo.db'
          ),
          onCreate: (Database db, int version) {
            print("Todo db 생성");
            return db.execute(
              '''CREATE TABLE todo(
               idx INTEGER PRIMARY KEY AUTOINCREMENT,
               category_idx INTEGER,
               user_name TEXT,
               content TEXT,
               start_stop_wt_dt TEXT,
               end_stop_wt_dt TEXT,
               start_target_dt TEXT,
               end_target_dt TEXT,
               create_dt TEXT,
               update_dt TEXT,
               delete_dt TEXT
              )'''
            );
          },
          version: 1);
    } catch (e) {
      print('_initDatabase 중 오류 발생: $e');
      return null;
    }
  }

  static Future<void> insertTodo(Todo todo) async {
    final Database? db = await database;

    if(db != null) {
     try {
       await db.insert(
           'todo',
           todo.toMap(),
           conflictAlgorithm: ConflictAlgorithm.replace
       );
       print("todo.toMap // ${todo.toMap()}");  // toMap() 결과 확인
     } catch (e) {
       print("insertTodo 중 에러 발생 $e");
     }
    }
  }

  static Future<void> deleteTodo(int idx) async {
    final Database? db = await database;
    await db?.delete(
        'todo',
      where: 'idx = ?',
      whereArgs: [idx]
    );
  }

  static Future<List<Todo>> getAllTodo() async {
    final Database? db = await database;

    if(db != null) {
      try {
        final List<Map<String, dynamic>> maps = await db.query('todo');
        return List.generate(maps.length, (i) {
          return Todo.fromMap(maps[i]);
        });
      } catch (e) {
        print("getAllTodos 중 에러 발생 $e");
      }
    } return [];
  }
}

결과물

고쳐야 할 점이 많이 보이지만 우선 텍스트 저장 및 불러오기 성공!

profile
플러터와 안드로이드를 공부합니다

0개의 댓글