플러터 (flutter)

Drift를 이용한 Todo 앱 제작 -(1)

CreatoMaestro 2023. 7. 17. 19:27
반응형

이번에는 저번 글에서 만든 Todo 앱을 Drift를 이용해 만들어본다.

Drift는flutter에서 sqlite를 쉽게 사용할 수 있도록 만든 라이브러리이다.

 

저번에 만든 앱은 저장이 되지 않기 때문에 앱을 끄면 값이 날라간다.

SQLite를 이용하면 작성한 Todo 데이터를 로컬에 저장할 수 있다.

Todo 앱 결과
앱 제작 결과

앱은 Todo를 생성할 수 있고 (1) ~ (3), 완료하면 체크를 할 수 있다.

체크가 되면 줄이 그어진다.

오른쪽 밑에 있는 버튼을 누르면 완료된 리스트가 삭제 된다.

앱을 끄고 다시 키면 이전에 만들어 놓은 데이터가 유지되어 있는 것을 볼 수 있다.

1. pubspec.yaml 

pubspec.yaml에 있는 dependencies와 dev_dependencies에 라이브러리를 추가해준다.

이 라이브러리는 drift를 사용하기 위함이다.

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  path_provider: 2.0.15
  path: 1.8.3
  drift: 2.10.0
  sqlite3_flutter_libs: 0.5.15
  get_it: 7.6.0


dev_dependencies:
  flutter_test:
    sdk: flutter
  drift_dev: 2.10.0
  build_runner: 2.4.6

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^2.0.0

 

2. Database 생성

데이터를 저장하기 위한 데이터베이스를 생성해준다.

이를 위해 데이터의 기본 틀을 지정해준다.

lib 파일 안에 database 폴더를 생성하고, 그 안에 data_table.dart를 생성해준다.

그리고 다음 코드를 작성해준다.

import "package:drift/drift.dart";

class Todo extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get todo => text()();
  IntColumn get done => integer()();
}

위 코드는 Todo 클래스를 생성하는 코드이다.

Table을 상속받는데, 이는 이 클래스가 SQLite의 데이터베이스에 저장됨을 의미한다.

 

각각의 Column은 다음과 같다.

1) id : 각 할일 고유의 id를 나타낸다. audoIncrement 메서드를 이용해 자동으로 늘어나도록 만들었다.

2) todo : 할 일을 저장하는 부분이다.

3) done : 할 일이 완료되었는지 저장하는 부분이다. 완료되었으면 1을 아니면 0을 저장한다.

 

이제 Todo 클래스를 이용해 데이터베이스를 생성하고, 이를 컨트롤할 메서드를 생성한다.

import 'dart:io';

import "package:drift/drift.dart" hide Column;
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'package:todo_using_drift/database/data_table.dart';

part 'database_class.g.dart';

@DriftDatabase(tables: [Todo,],)

class TodoDatabase extends _$TodoDatabase { //데이터베이스 파일 생성 및 연동
  TodoDatabase():super(_openConnection());
 }

필요한 라이브러리를 import해주고, part를 통해database_class.g.dart를 import해준다.

part는 라이브러리의 private 요소들도 사용할 수 있게 해준다.

database_class.g.dart는 직접 만드는 것이 아니라 dev_dependencies에 등록해놓은 build_runner를 이용해 생성한다.

terminal에 flutter pub run build_runner build 명령어를 생성 된다.

 

명령어가 재대로 실행이 되었다면 다음과 같이 파일이 생긴다.

명령어 실행 결과

'database_class.g.dart'파일이 자동으로 생성된 파일이다.

 

super 안에 있는 _openConnection 함수는 실제 데이터베이스 파일을 생성해주는 함수이다.

이 함수는 클래스를 다 작성한 후 따로 적어준다.

 

이제 TodoDatabase 클래스 안에 메서드를 넣어준다.

Future<int> addTodo(String? todoString) =>
  into(todo).insert(TodoCompanion(
    todo: Value(todoString!),
    done: Value(0),
  ));

위 코드는 데이터베이스에 Todo를 추가해주는 함수이다.

into()를 통해 어는 테이블에 저장할 지 지정하고, insert()를 통해 실제 데이터를 입력한다.

TodoCompanion 함수는 데이터를 저장하는 클래스이며, 생성해준 database_class.g.dart 안에 정의되어있다.

TodoCompanion 정의

Stream<List<TodoData>> watchTodo() => select(todo).watch();

다음 코드는 todo 리스트를 불러오는 함수이다.

Stream을 통해 리스트가 변할 때마다 이를 감지하고, 갱신할 수 있도록 하였다.

select()함수를 통해 불러올 테이블을 선택하고, watch를 통해 streaming 한다.

void changeBool(int id, bool inputDone) async {
  Future<List<TodoData>> temp = (select(todo)..where((tbl) => tbl.id.equals(id))).get();
  List<TodoData> list = await temp;
  await update(todo).replace(TodoData(
    id: id,
    todo: list[0].todo,
    done: inputDone == false ? 0 : 1,
  ));
}

다음 코드는 체크 박스를 통해 상태가 변화했을 때, 그 변화를 저장하는 함수이다.

파라미터롤 id와 inputDone을 받는다.

id는 todo의 고유 아이디이고, inputDone은 체크박스에서 들어온 데이터이다.

 

위에 있는 2줄의 코드는 todo 데이터에서 id가 일치하는 데이터를 뽑아내는 과정이다.

select()를 통해 테이블을 선택하고, where 메서드를 이용해 id가 일치하는 것만 뽑아낸다.

이는 tbl.id.equals를 통해 이루어진다.

여기소 tbl은 테이블을 의미한다.

 

이후 replace 메서드를 통해 완료 데이터(done)을 바꿔준다.

 

Future<int> removeDoneTodo() =>
  (delete(todo)..where((tbl) => tbl.done.equals(1))).go();

마지막으로 데이터를 삭제하는 코드이다.

delete안에 테이블을 적어주고 where을 통해 완료된 일정을 삭제시킨다.

done 값이 1이면 완료된 것이기 때문에 done.equals(1)인 일정만 걸러준다.

go()메서드를 써야 삭제가 진행된다.

 

@override
int get schemaVersion => 1;

schemaVersion은 테이블의 개수를 의미한다.

여기서는 todo 하나만 썼기 때문에 1을 써준다.

 

LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'db123.splite'));
    return NativeDatabase(file);
  });
}

LazyDatabase는 실제 데이터베이스가 열리진 않았지만 나중에 열릴 것임을 의미하는 데이터 타입이다.

return 값으로 NativeDatabase를 주는데 NativeDatabase는 실제 데이터베이스를 반환해준다.

 

getApplicationDocumentsDirectory()는 path_provider 라이브러리에 있는 함수로, 어플리케이션이 파일을 저장할 수 있는 위치를 반환해준다.

await을 통해 이 값이 다 올 때 까지 기다리고 path 라이브러리에 있는 join 메서드를 이용해 그 위치에 데이터베이스 파일을 저장히준다.

이후 NativeDatabase를 이용해 이 파일을 반환해준다.

 

3. Todo 입력

지금부터는 TextFormField와 Form 위젯을 이용하여 todo 입력을 받는 위젯을 만들어본다.

 

Form 위젯은 타당성 조사와 현재 값 저장 등 다양한 기능을 지원하며, 여러 개의 입력을 한 번에 제어할 수 있는 장점이 있다.

TextFormField는 Form 위젯 안에 들어가는 위젯으로 텍스트 입력을 받는 위젯이다.

 

이제 코드를 작성해보자

import 'package:flutter/material.dart';

class TodoInput extends StatefulWidget {
  final double width; //화면의 width
  final void Function(String?) saveFunction;
  TodoInput({
    required this.width,
    required this.saveFunction,
    Key? key}):super(key: key);
  
  @override
  State<TodoInput> createState() => _TodoInputState();
}

위 코드는 TodoInput 클래스를 정의하는 위젯이다.

파라미터로 width와 saveFunction을 받는다.

width는 화면의 가로 길이를 의미하고, saveFunction은 저장 할 때 실행할 함수를 의미한다.

 

class _TodoInputState extends State<TodoInput> {
  final _formKey = GlobalKey<FormState>();
  final _controller = TextEditingController();

  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }

위 코드는 TodoInput의 상태 클래스이다.

_formKey는 Form 위젯의 고유한 key이다.

_controller는 TextFormField를 컨트롤하는 컨트롤러를 의미한다.

 

dispose를 override 했는데, 이는 위젯이 없어질 때 마다 컨트롤러도 dispose해줘야 하기 때문이다.

@override
Widget build(BuildContext context) {
  return Form(
    key: _formKey,
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Container(
          width: widget.width,
          decoration: BoxDecoration(
            border: Border.all(
              color: Colors.white,
              width: 1.0,
            ),
          ),
          child: TextFormField( //todo 입력자리
            controller: _controller,
            validator: (value) { //입력이 들어갔는지 확인
              if(value == null || value.isEmpty){
                return "Please enter the text!";
              }
              return null;
            },
            onSaved: (value) {
              widget.saveFunction(value);
            }, //데이터베이스에 저장
          ),
        ),
        Container( //저장 버튼
          width: widget.width,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              IconButton(
                  onPressed: onSave,
                  icon: Icon(Icons.add),
              ),
            ],
          ),
        ),
      ],
    ),
  );
}

다음은 build 함수이다.

코드가 꽤 긴데 위젯트리를 이용해 간단하게 살펴보자.

위젯트리
위젯트리

Columnm, Row, Container 위젯은 위치 정렬을 위해 넣은 위젯이다.

실질적으로 동작은 Form, TextFormField, IconButton이 담당한다.

 

From 위젯은 앞서 말했듯이 위젯 안의 모든 입력을 관리한다.

여기서는 TextFormField의 값을 관리한다.

From은 다른 Form 위젯과 구별하기 위해 key 값을 부여해주어야 한다.

 

TextFormField에서 중요하게 봐야 하는 파라미터는 validator와 onSaved이다.

 

validator는 입력된 값의 타당성을 조사한다.

타당하지 않으면 String 값을 반환할 수 있다.

key의 currentState.validate() 함수을 통해 실행된다.

 

onSaved는 값을 저장하는 함수이다.

key의 currentState.save() 함수를 통해 실행된다.

 

IconButton은 눌릴 시 위에 있는 validator와 onSaved를 실행한다.

그 코드는 다음과 같다.

 

void onSave() { //저장 버튼 눌릴 시 실행할 코드. 데이터베이스 저장용
  if(_formKey.currentState!.validate()) {
    _formKey.currentState!.save();
    _controller.clear();
  }
}

위 코드에서는 우선 입력된 값의 타당성을 조사하고, 타당할 경우 값을 저장한다.

이후 텍스트 필드 컨트롤러를 이용해 값을 초기화시켜준다.

 

많은 내용이 나와 한 번 끊고 다음 글에서 이어서 만들어보도록 한다.

아래는 전체 코드이다.

 

-data_table.dart

import "package:drift/drift.dart";

class Todo extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get todo => text()();
  IntColumn get done => integer()();
}

-database_class.dart

import 'dart:io';

import "package:drift/drift.dart" hide Column;
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'package:todo_using_drift/database/data_table.dart';

part 'database_class.g.dart';

@DriftDatabase(tables: [Todo,],)

class TodoDatabase extends _$TodoDatabase { //데이터베이스 파일 생성 및 연동
  TodoDatabase():super(_openConnection());

  Future<int> addTodo(String? todoString) =>
    into(todo).insert(TodoCompanion(
      todo: Value(todoString!),
      done: Value(0),
    ));

  Stream<List<TodoData>> watchTodo() => select(todo).watch();

  void changeBool(int id, bool inputDone) async {
    Future<List<TodoData>> temp = (select(todo)..where((tbl) => tbl.id.equals(id))).get();
    List<TodoData> list = await temp;
    await update(todo).replace(TodoData(
      id: id,
      todo: list[0].todo,
      done: inputDone == false ? 0 : 1,
    ));
  }


  Future<int> removeDoneTodo() =>
    (delete(todo)..where((tbl) => tbl.done.equals(1))).go();

  @override
  int get schemaVersion => 1;

}

LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'db123.splite'));
    return NativeDatabase(file);
  });
}

-input_todo.dart

import 'package:flutter/material.dart';

class TodoInput extends StatefulWidget {
  final double width; //화면의 width
  final void Function(String?) saveFunction;
  TodoInput({
    required this.width,
    required this.saveFunction,
    Key? key}):super(key: key);
  
  @override
  State<TodoInput> createState() => _TodoInputState();
}

class _TodoInputState extends State<TodoInput> {
  final _formKey = GlobalKey<FormState>();
  final _controller = TextEditingController();

  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Container(
            width: widget.width,
            decoration: BoxDecoration(
              border: Border.all(
                color: Colors.white,
                width: 1.0,
              ),
            ),
            child: TextFormField( //todo 입력자리
              controller: _controller,
              validator: (value) { //입력이 들어갔는지 확인
                if(value == null || value.isEmpty){
                  return "Please enter the text!";
                }
                return null;
              },
              onSaved: (value) {
                widget.saveFunction(value);
              }, //데이터베이스에 저장
            ),
          ),
          Container( //저장 버튼
            width: widget.width,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                IconButton(
                    onPressed: onSave,
                    icon: Icon(Icons.add),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  void onSave() { //저장 버튼 눌릴 시 실행할 코드. 데이터베이스 저장용
    if(_formKey.currentState!.validate()) {
      _formKey.currentState!.save();
      _controller.clear();
    }
  }
}
반응형