지난 프로젝트에서 상태관리 라이브러리로 getX 를 사용했었다. 그땐 규모가 그리 크지 않으니 겟엑스로 충분할 줄 알았으나, 기능이 늘어나면서 컨트롤러의 역할이 점점 비대해지는 것을 경험했다.
Getx Controller 파일 내의 코드가 너무 길어지고, 파일 하나에서 추가, 수정, 삭제 등 모든 상태를 관리하다보니 어떤 상태를 관리하는지 구분하기가 곤란했다.
물론 내가 겟엑스를 처음 써본 상황이어서 제대로 적용을 못해서 그럴 수도 있겠지만…! 결론적으로 팀내에서 다음엔 Bloc 를 써보고 싶다는 의견이 많았기 때문에 이번 사이드 프로젝트는 Bloc 를 적용해보기로 했다.
우선 가장 간단해보이는 Todo 추가 기능을 Bloc로 구현해 보았다. 투두는 로컬 DB에 저장했고 SQLite 를 썼다.
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
});
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});
}
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 로딩 에러...";
}
}
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’ 라는 이벤트가 발생했을 때만 작동하는 코드블록을 설정하는 것.Equtable 을 사용해야 한다.
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));
터미널로 추가
flutter pub add equatable
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 리스트를 기준으로 비교
}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 [];
}
}
고쳐야 할 점이 많이 보이지만 우선 텍스트 저장 및 불러오기 성공!