플러터 (flutter)

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

CreatoMaestro 2023. 7. 24. 21:33
반응형

이번 글에서는 저번 글에 이어서 Drift를 이용한 Todo 앱을 제작한다.

2023.07.17 - [단기 프로젝트] - Drift를 이용한 Todo 앱 제작 -(1)

 

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

이번에는 저번 글에서 만든 Todo 앱을 Drift를 이용해 만들어본다. Drift는flutter에서 sqlite를 쉽게 사용할 수 있도록 만든 라이브러리이다. 저번에 만든 앱은 저장이 되지 않기 때문에 앱을 끄면 값이

ti-project-11.tistory.com

 

1. TodoBox

TodoBox는 하나의 todo를 표현하기 위한 상자이다.

이 안에는 완료되었는지 확인 할 수 있는 체크 박스와 todo의 내용이 들어가 있다.

class TodoBox extends StatelessWidget {
  bool done;
  final String todoText;
  final void Function(bool? value) changeFunction;
  final double width;
  TodoBox({
    required this.done,
    required this.todoText,
    required this.changeFunction,
    required this.width,
    Key? key
  }):super(key: key);

위 코드는 TodoBox를 선언하는 코드이다.

4가지 파라미터가 있고 각각은 다음을 의미한다.

  • done : 현재 todo가 완료되었는지 확인하는 변수이다.
  • todoText : 할 일의 내용이다.
  • changeFunction : 체크 박스의 값이 바뀌었을 때 실행하는 함수이다.
  • width : 화면의 가로 길이다.
@override
Widget build(BuildContext context) {
  return IntrinsicHeight(
    child: Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 0.0,),
      child: Container(
        height: 40.0,
        width: width,
        decoration: BoxDecoration(
          border: Border.all(
            color: Colors.white,
            width: 1.0,
          ),
        ),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.start,
          children: [
            Checkbox(
                value: done,
                onChanged: (value){
                  changeFunction(value);
                },
            ),
            Text(
              todoText,
              style: TextStyle(
                decoration: done == true? TextDecoration.lineThrough : TextDecoration.none,
              ),
            ),
          ],
        ),
      ),
    ),
  );
}

위 코드는 todoBox를 구성하는 코드이다.

앞서 말한 것 처럼 CheckBox와 할 일 내용이 들어간다.

 

IntrinsicHeight은 자식 위젯이 차지하는 높이 만큼만 공간을 내주기 위해 사용한다.

 

Container는 바깥 선을 그려주기 위해 넣어주었다.

이 때 BoxDecoration을 사용했다.

 

Row Widget은 체크 박스와 할 일 내용을 정렬해 주기 위해 사용하였다.

Checkbox(
    value: done,
    onChanged: (value){
      changeFunction(value);
    },
),

체크박스에는 value와 onChanged 파라미터를 사용하였다.

value는 현재 체크박스가 나타내는 값을 의미한다.

 

onChanged는 값이 바뀌었을 때 실행할 함수를 의미한다.

여기서는 changeFunction을 실행하도록 넣어주었다.

changeFunction은 database에 있는 값을 업데이트 시켜준다.

 

Text(
  todoText,
  style: TextStyle(
    decoration: done == true? TextDecoration.lineThrough : TextDecoration.none,
  ),
),

Text는 값에 따라 decoration이 달라지도록 설정하였다.

만약 체크박스가 체크 되어있으면 글자 중앙에 선이 그어지도록 하여, 완료되었다는 시각적 효과를 더했다.

 

2. main.dart

필요한 요소를 모두 만들었으니 이제 이것을 합치는 일만 남았다.

import 'package:flutter/material.dart';
import 'package:todo_using_drift/component/input_todo.dart';
import 'package:todo_using_drift/database/database_class.dart';
import 'package:get_it/get_it.dart';
import 'package:todo_using_drift/component/todo_box.dart';

void main() {
  final database = TodoDatabase();

  GetIt.I.registerSingleton<TodoDatabase>(database);

  runApp(TodoDrift());
}

위 코드는 import 되는 라이브러리와 main 함수를 나타낸 코드이다.

TodoDatabase 생성자를 사용해 database를 생성하고, 이를 GetIt에 등록시켜준다.

GetIt은 프로그램 전반에 걸쳐 등록된 타입의 메서드나 파라미터를 사용할 수 있도록 만들어준다,.

 

다음 runApp을 이용해 TodoDrift를 실행해준다.

 

class TodoDrift extends StatefulWidget {
  TodoDrift({Key? key}) : super(key: key);

  @override
  State<TodoDrift> createState() => _TodoDriftState();
}

class _TodoDriftState extends State<TodoDrift> {
  int countList = 0;

  void saveTheData(String? value) { //Todo 추가
    GetIt.I<TodoDatabase>().addTodo(value);
  }

위 코드는 TodoDrift 클래스와 그 상태 클래스를 나타낸다.

 

saveTheData는 할 일을 받아 이를 데이터베이스에 저장해주는 함수이다.

override
Widget build(BuildContext context) {
  return MaterialApp(
    theme: ThemeData.dark(),
    home: Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: ()async{
          await GetIt.I<TodoDatabase>().removeDoneTodo();
        },
        child: const Icon(Icons.delete),
      ),

위 함수는 build 함수이다.

MaterialApp과 Scaffold 위젯이 상위에 있고 그 아래 floatingActionButton이 있다.

floatingActionButton은 항상 화면의 오른쪽 아래에 위치하며, 체크된 할 일을 삭제시켜준다.

 

body: SafeArea(
  child: Padding(
    padding: const EdgeInsets.symmetric(horizontal: 16.0),  // add padding if necessary
    child: Column(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        Text(
          "Todo List",
          style: TextStyle(
            fontWeight: FontWeight.w700,
            fontSize: 30.0,
          ),
        ),
        SizedBox(
          height: 10.0,
        ),
        TodoInput(
          width: MediaQuery.of(context).size.width,
          saveFunction: saveTheData,
        ),
        SizedBox(
          height: 10.0,
        ),
        Expanded(
          child: StreamBuilder(
            stream: GetIt.I<TodoDatabase>().watchTodo(),
            builder: (context, snapshot) {
              if(snapshot.data == null) {
                return Container();
              } else {
                return ListView.builder(
                  itemCount: snapshot.data!.length,
                  itemBuilder: (context, index) {
                    final todo = snapshot.data![index];
                    return TodoBox(
                      done: todo.done == 1? true : false,
                      todoText: todo.todo,
                      changeFunction: (bool? done) {
                        GetIt.I<TodoDatabase>().changeBool(todo.id, done!);
                      },
                      width: MediaQuery.of(context).size.width,
                    );
                  },
                );
              }
            },
          ),
        ),
      ],
    ),
  ),
),

위 코드는 화면을 나타내는 코드이다.

 

위에 있는 SafeArea, Padding, Column은 정렬을 위한 함수이다.

SafeArea는 화면이 잘리지 않도록 하고, Padding은 padding을 넣어주는 위젯이다.

Column은 새로로 위젯들을 정렬시켜 준다.

 

또한 SizedBox는 위젯 간의 간격을 넣어주기 위해 사용하였다.

 

Column의 맨 위에 있는 Text는 제목이다.

그 아래 TodoInput은 저번 글에서 만든 클래스이다.

여기에 할 일을 입력하고 이를 저장해줄 수 있다.

 

그 아래 Expanded 위젯이 있는데 이는 남은 화면을 자식 위젯이 모두 차지할 수 있도록 해주는 위젯이다.

그 아래에는 StreamBuilder가 있는데, 이는 database를 관찰하고 여기에 변경사항이 있으면 이를 감지하여 builer 파라미터에서 반환되는 위젯을 다시 그려준다.

 

child: StreamBuilder(
  stream: GetIt.I<TodoDatabase>().watchTodo(),
  builder: (context, snapshot) {
    if(snapshot.data == null) {
      return Container();
    } else {
      return ListView.builder(
        itemCount: snapshot.data!.length,
        itemBuilder: (context, index) {
          final todo = snapshot.data![index];
          return TodoBox(
            done: todo.done == 1? true : false,
            todoText: todo.todo,
            changeFunction: (bool? done) {
              GetIt.I<TodoDatabase>().changeBool(todo.id, done!);
            },
            width: MediaQuery.of(context).size.width,
          );
        },
      );
    }
  },
),

stream은 계속 관찰할 변수를 지정해준다.

이곳에 넣어주는 것은 변수를 반환하는 함수를 지정해준다.

Stream과 StreamBuilder에 대해선 다음에 좀 더 설명하도록 하겠다.

 

그 밑에 builder 가 있는데 이 안에는 BuildContext와 snapshot을 변수로 갖는 함수를 넣어준다.

이 값들을 이용해 build한다.

 

여기서는 ListView.builder를 이용했다.

ListView.builder는 ListView와 비슷한 함수이다.

여기서는 Stream 결과 값에 따라 계속해서 변해야 하기 때문에 이에 대응할 수 있는 ListView.builder를 사용했다.

 

itemCount는 요소의 개수를 의미하고,

itemBuilder는 위젯을 반환하는 파라미터이다.

여기서는 위에서 선언해준 TodoBox 위젯을 반환하도록 만들었다.

 

이로써 모든 코드를 짜 보았다.

 

아래는 전체 코드이다.

 

1) todo_box.dart

import 'package:flutter/material.dart';

class TodoBox extends StatelessWidget {
  bool done;
  final String todoText;
  final void Function(bool? value) changeFunction;
  final double width;
  TodoBox({
    required this.done,
    required this.todoText,
    required this.changeFunction,
    required this.width,
    Key? key
  }):super(key: key);

  @override
  Widget build(BuildContext context) {
    return IntrinsicHeight(
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 0.0,),
        child: Container(
          height: 40.0,
          width: width,
          decoration: BoxDecoration(
            border: Border.all(
              color: Colors.white,
              width: 1.0,
            ),
          ),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              Checkbox(
                  value: done,
                  onChanged: (value){
                    changeFunction(value);
                  },
              ),
              Text(
                todoText,
                style: TextStyle(
                  decoration: done == true? TextDecoration.lineThrough : TextDecoration.none,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

 

2) main.dart

import 'package:flutter/material.dart';
import 'package:todo_using_drift/component/input_todo.dart';
import 'package:todo_using_drift/database/database_class.dart';
import 'package:get_it/get_it.dart';
import 'package:todo_using_drift/component/todo_box.dart';

void main() {
  final database = TodoDatabase();

  GetIt.I.registerSingleton<TodoDatabase>(database);

  runApp(TodoDrift());
}

class TodoDrift extends StatefulWidget {
  TodoDrift({Key? key}) : super(key: key);

  @override
  State<TodoDrift> createState() => _TodoDriftState();
}

class _TodoDriftState extends State<TodoDrift> {
  int countList = 0;

  void saveTheData(String? value) { //Todo 추가
    GetIt.I<TodoDatabase>().addTodo(value);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark(),
      home: Scaffold(
        floatingActionButton: FloatingActionButton(
          onPressed: ()async{
            await GetIt.I<TodoDatabase>().removeDoneTodo();
          },
          child: const Icon(Icons.delete),
        ),
        body: SafeArea(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16.0),  // add padding if necessary
            child: Column(
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                Text(
                  "Todo List",
                  style: TextStyle(
                    fontWeight: FontWeight.w700,
                    fontSize: 30.0,
                  ),
                ),
                SizedBox(
                  height: 10.0,
                ),
                TodoInput(
                  width: MediaQuery.of(context).size.width,
                  saveFunction: saveTheData,
                ),
                SizedBox(
                  height: 10.0,
                ),
                Expanded(
                  child: StreamBuilder(
                    stream: GetIt.I<TodoDatabase>().watchTodo(),
                    builder: (context, snapshot) {
                      if(snapshot.data == null) {
                        return Container();
                      } else {
                        return ListView.builder(
                          itemCount: snapshot.data!.length,
                          itemBuilder: (context, index) {
                            final todo = snapshot.data![index];
                            return TodoBox(
                              done: todo.done == 1? true : false,
                              todoText: todo.todo,
                              changeFunction: (bool? done) {
                                GetIt.I<TodoDatabase>().changeBool(todo.id, done!);
                              },
                              width: MediaQuery.of(context).size.width,
                            );
                          },
                        );
                      }
                    },
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
반응형