본문 바로가기
Development/Flutter

Flutter - 10. TodoList App (3) Database를 연동해보자 (SQLite, sqflite)

by du.it.ddu 2021. 1. 26.
반응형

화면 자체는 이전 포스팅와 다르지 않다.

현재까지의 작업은 앱을 종료하면 Todo Item들이 모두 사라진다. 런타임에만 저장하기 때문이다.
이번 포스팅은 SQLite 데이터베이스에 Todo Item을 저장하고 불러올 수 있도록 할 것이다.
작업 할 목록은 다음과 같다.

  1. sqflite, path_provider 패키지 추가
  2. SQLite 데이터베이스 클래스 작성
  3. 데이터베이스와 상호작용을 DAO 클래스 작성
  4. 데이터베이스와 상호작용할 수 있도록 TodoModel 클래스 추가 구현
  5. TodoListPage 수정

많아보이지만 별거 없다. 바로 고고씽!


1. sqflite, path_provider 패키지 추가

# pubspec.yaml
name: ...
description: ...

...

dependencies:
  ...
  
  sqflite: ^1.1.0
  path_provider: ^0.5.0+1

...

pubspec.yaml파일로 이동하여 dependencies에 sqflite와 path_provider를 추가한다.

sqflite는 SQLite를 Flutter 에서 연동할 수 있도록 도와주는 패키지이다.

path_provider는 이름 그대로 앱에서 파일 시스템의 경로에 접근할 수 있게 해준다. SQLite 데이터베이스는 내용을 파일에 저장한다. 이를 위해 파일 시스템의 경로에 접근이 필요하다.

잊지않고 패키지를 갱신해주자! (Pub get)


2. SQLite 데이터베이스 클래스 작성

// database.dart
import 'dart:async';
import 'dart:io';

import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';

final todoTable = 'todo_table';

class DatabaseProvider {
  static final DatabaseProvider provider = DatabaseProvider();

  Database _database;

  Future<Database> get database async {
    if (_database != null) return _database;
    _database = await createDatabase();
    return _database;
  }

  createDatabase() async {
    Directory docsDir = await getApplicationDocumentsDirectory();
    String path = join(docsDir.path, "todo.db");

    var database = await openDatabase(path, version: 1, onCreate: initDB, onUpgrade: onUpgrade);
    return database;
  }

  void onUpgrade(Database database, int oldVersion, int newVersion) {
    if (newVersion > oldVersion) {
      // TODO :: Migration
    }
  }

  void initDB(Database database, int version) async {
    await database.execute("CREATE TABLE $todoTable ("
        "id INTEGER PRIMARY KEY, "
        "title TEXT, "
        "state INTEGER, "
        "created_time INTEGER"
        ")");
  }
}

위 코드를 그대로 사용해도 좋다.
database.dart 혹은 원하는 이름의 파일을 생성하고 위 코드를 작성한다.

여기서 처음 등장하는 "async", "await", "Future" 가 보일 것이다.
이것은 Flutter에서 비동기 작업을 하기 위한 키워드이다.

자바스크립트 혹은 어떤 프로그래밍에서 비동기 작업을 해 보았다면 쉽게 이해할 수 있다.

  • 함수에 붙은 async -> 이 함수가 비동기적으로 동작함을 표시한다.
  • 함수 내부 코드에 await -> 비동기로 동작하는 함수가 결과를 반환할 때 까지 기다린다.
  • Future<T> -> 비동기로 동작하는 함수의 반환형이다.

비동기 작업이 필요한 이유는, I/O 처리나 네트워크 데이터 처리 등은 모바일 어플리케이션에서 별도의 쓰레드에서 작업하도록 하기 때문이다.
메인쓰레드 혹은 UI쓰레드라고 불리우는 쓰레드에서 오랜 시간 작업을 하지 못하도록 하는 이유이다.


3. 데이터베이스와 상호작용을 위한 DAO 클래스 작성

// todo_dao.dart
import 'dart:async';
import 'package:flutter_todolist/database/database.dart';
import 'package:flutter_todolist/models/todo_model.dart';

class TodoDao {
  final dbProvider = DatabaseProvider.provider;

  Future<int> createTodo(TodoModel todo) async {
    final db = await dbProvider.database;
    final result = db.insert(todoTable, todo.toDatabaseJson());
    return result;
  }

  Future<List<TodoModel>> getTodoList() async {
    final db = await dbProvider.database;

    List<Map<String, dynamic>> result = await db.query(todoTable);
    List<TodoModel> todoList = result.isNotEmpty ? result.map((item) => TodoModel.fromDatabaseJson(item)).toList() : [];

    return todoList;
  }
}

todo_dao.dart 혹은 다른 이름으로 파일을 만들고 위와 같은 코드를 작성한다.

DAO란, "Data Access Object"로, 데이터를 접근하는 코드와 이를 사용하는 비즈니스 로직 코드를 분리하기 위해 사용한다.

외부에선 TodoDao 객체를 통해 TodoModel을 획득할 수 있고, TodoDao 객체는 Database 객체와 상호작용한다.

여기서 주의할 점은 두 가지다.

  • TodoModel을 데이터베이스에 저장하기 위해서 Map(Json) 형태로 변경해야 한다.
    • 다음 단계에서 TodoModel에 toDatabaseJson 함수를 구현한다.
  • 데이터베이스에서 읽은 Map(Json) 데이터를 TodoModel로 변경해야 한다.
    • 다음 단계에서 TodoModel에 fromDatabaseJson 함수를 구현한다.

4. 데이터베이스와 상호작용할 수 있도록 TodoModel 클래스 추가 구현

class TodoModel {
  int _id;
  String _title;
  TodoState _state;
  final DateTime _createdTime;

  TodoModel(this._id, this._title, this._createdTime, this._state);
  
  ...

  factory TodoModel.fromDatabaseJson(Map<String, dynamic> data) => TodoModel(
      data['id'],
      data['title'],
      DateTime.fromMillisecondsSinceEpoch(data['created_time'] as int),
      getTodoStateByValue(data['state'] as int)
  );

  Map<String, dynamic> toDatabaseJson() => {
    'title': this._title,
    'created_time': this._createdTime.millisecondsSinceEpoch,
    'state': getTodoStateValue(_state)
  };
}

enum TodoState {
  todo, inProgress, done
}

int getTodoStateValue(TodoState state) {
  switch(state) {
    case TodoState.todo:
      return 0;
    case TodoState.inProgress:
      return 1;
    case TodoState.done:
      return 2;
    default:
      return 0;
  }
}

TodoState getTodoStateByValue(int stateValue) {
  switch(stateValue) {
    case 0:
      return TodoState.todo;
    case 1:
      return TodoState.inProgress;
    case 2:
      return TodoState.done;
    default:
      return TodoState.todo;
  }
}
  • 데이터베이스에서 얻을 수 있는 데이터 형태인 Map<String, dynamic>을 파라미터로 받아 TodoModel을 생성할 수 있는 생성자인 fromDatabaseJson를 factory 생성자로 구현하였다.
  • TodoModel을 Map<String, dynamic> 형태로 변경하는 toDatabaseJson() 함수를 구현하였다.
  • SQLite에 DataTime을 저장할 수 없기 때문에 int로 바꾸고 획득 시 int를 다시 DataTime으로 변경한다.
  • SQLite에 TodoState를 저장할 수 없기 때문에 int로 바꾸고 획득 시 int를 다시 TodoState로 변경한다.
    • int getTodoStateValue, TodoState getTodoStateByValue 를 참고

5. TodoListPage 수정

class _TodoListPageState extends State<TodoListPage> {
  ...

  final TodoDao _todoDao = TodoDao();
  List<TodoModel> _todoList = [];

  @override
  void initState() {
    super.initState();
    _loadTodoList();
  }
  ...

  void _addNewTodo(String title) async {
    TodoModel newTodo = TodoModel(null, title, DateTime.now(), TodoState.todo);
    await _todoDao.createTodo(newTodo);
    _loadTodoList();
  }

  void _loadTodoList() async {
    List<TodoModel> newList = await _todoDao.getTodoList();

    setState(() {
      this._todoList = newList;
    });
  }
}

다른 Widget 관련 부분은 모두 동일하다.

  • TodoDao 객체를 생성한다.
  • _addNewTodo는 TodoModel을 생성하고 TodoDao객체에 모델을 전달하여 데이터베이스에 저장한다.
    • async, await이 사용되었다.
  • 데이터베이스 저장 후 _loadTodoList 함수를 호출하고 TodoDao 객체로부터 TodoList를 획득한다.
    • async, await이 사용되었다.
  • _loadTodoList 함수 내부에서 TodoList를 획득하고 나면(await 후) setState를 통해 상태를 갱신한다.
  • initState 함수에서도 _loadTodoList를 호출하여 앱 실행 시 최초에 TodoList를 얻도록 한다.

현재까지의 코드는 아래 Github에 저장되어 있다.
Github : 
github.com/DuItDDu/Flutter-Codelabs/tree/master/Flutter-TodoList/flutter_todolist

큰 어려움 없이 데이터베이스를 연동할 수 있었다. (짝짝짝)

하지만, 문제가 있다.

UI를 표현하는 Widget에서 너무 많은 일을 하고 있다.
Widget도 그려야 하며 TodoList 상태관리를 해야하고 데이터베이스 접근 또한 하고 있다.

현재는 상태와 데이터베이스에 접근이 적지만 상태가 많아질수록, 데이터가 많아지고 복잡해질수록 코드 또한 점점 더 복잡해진다.

이런 문제점을 해결하기 위해 Android, iOS에선 다양한 아키텍처 패턴을 활용한다. (MVP, MVVM, RIBs 등등)
Flutter 또한 이런 부분을 해결하기 위해 "BLoC" 라는 패턴을 권장하고 있다.

다음 포스팅에선 현재 상태를 "BLoC" 패턴이 무엇인지 알아보고 리팩토링하는 작업을 진행 할 것이다.

반응형