(Flutter-기초 강의) 15. SQLite를 이용하여 데이터베이스(database) 관리하기

Flutter에서의 Database

데이터 관리는 역동적이고 효율적이며 개인화된 사용자 경험을 위한 기초입니다.

Flutter는 널리 사용되는 SQLite와 매끄럽게 연동되어 개발자들이 데이터를 쉽고 효과적으로 관리할 수 있게 해줍니다.

이 글에서는 Flutter에서 SQLite를 사용하여 데이터베이스를 만들고 관리하는 방법을 자세히 설명하겠습니다.

(데이터베이스에 대해서는 Database란 무엇인가? 글을 참조해 주세요.)

데이터베이스는 앱에서 데이터를 저장, 검색 및 관리하는 데 필수적인 부분입니다.

Flutter에서는 다양한 데이터베이스 들을 사용할 수 있지만, 이 글에서는 SQLite를 사용하여 소개해드리겠습니다.

sqlite database in flutter

SQLite란 무엇인가?

SQLite는 서버가 필요 없는, 즉 서버리스 데이터베이스로, 트랜잭션 SQL 데이터베이스 엔진입니다.

가볍고, 설치 및 설정이 필요 없으며, 신뢰성이 높습니다.

SQLite는 앱에 내장되어 사용자의 디바이스에서 직접 실행되므로 별도의 데이터베이스 서버를 설정하고 관리할 필요가 없습니다.

공식 홈페이지: https://www.sqlite.org/index.html

wiki: https://en.wikipedia.org/wiki/SQLite

Flutter에서 SQLite 사용하기

Flutter에서 SQLite를 사용하려면, 먼저 sqflite 패키지를 프로젝트에 추가해야 합니다.

command line에 다음 명령어를 이용해 추가합니다.

$ flutter pub add sqflite

데이터베이스 사용하기

1. 모델 정의하기

데이터베이스와 연결하여 사용할 모델을 정의합니다.

(SQLite는 bool 타입이 없으므로 int 타입으로 대체 하여 사용합니다.)

데이터 모델을 Map 형태로 반환하는 toMap() method를 정의하여 사용합니다.

class Todo {
  String? title;
  String? content;
  int? active;
  int? id;

  Todo({this.title, this.content, this.active, this.id});

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'title': title,
      'content': content,
      'active': active,
    };
  }
}

2. Helper class를 이용한 데이터베이스 관리

반복되는 코드를 줄이기 위해 Helper class를 정의하여 데이터베이스 Open 동작을 관리하도록 합니다.

(이를 통해 코드 중복을 줄이고, 유지보수에 이롭게 합니다.

SqliteHelper class에 getDatabasePath()를 통해 database 파일 경로를 얻어오고,

openDatabase() 통해 데이터베이스 파일을 열거나 생성합니다.

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

class SqliteHelper {
  static Future<Database> getDatabase() async {
    final dbPath = await getDatabasesPath();
    return openDatabase(
      join(dbPath, 'todo_database.db'),
      onCreate: (db, version) {
        return db.execute(
          'CREATE TABLE todos(id INTEGER PRIMARY KEY, title TEXT, content TEXT, active INTEGER)',
        );
      },
      version: 1,
    );
  }
}

3. Service class를 활용한 CROUD 수행

View에서 Database를 사용할 CROUD 서비스를 정의합니다.

2. 에서 만든 SqliteHelper Class를 이용하여 Database에 접근하고 sqflite 패키지의 insert, query, update, delete를 이용해 다음을 정의하였습니다.

  • Create: insertTodo(),
  • Read: getTodos()
  • Update: updateTodo()
  • Delete: deleteTodo()
class TodoDatabase {
  static Future<void> insertTodo(Todo todo) async {
    final db = await SqliteHelper.getDatabase();
    await db.insert('todos', todo.toMap(),
        conflictAlgorithm: ConflictAlgorithm.replace);
  }

  static Future<List<Todo>> getTodos() async {
    final db = await SqliteHelper.getDatabase();
    final List<Map<String, dynamic>> maps = await db.query('todos');
    return List.generate(maps.length, (i) {
      return Todo(
        id: maps[i]['id'],
        title: maps[i]['title'],
        content: maps[i]['content'],
        active: maps[i]['active'],
      );
    });
  }

  static Future<void> updateTodo(Todo todo) async {
    final db = await SqliteHelper.getDatabase();
    await db.update(
      'todos',
      todo.toMap(),
      where: 'id = ?',
      whereArgs: [todo.id],
    );
  }

  static Future<void> deleteTodo(int id) async {
    final db = await SqliteHelper.getDatabase();
    await db.delete(
      'todos',
      where: 'id = ?',
      whereArgs: [id],
    );
  }
}

4. View

3. 에서 추가한 TodoDatabase 서비스 class를 사용하여

ListView를 통해 목록을 표시하고,

          : ListView.builder(
              itemCount: _todos.length,
              itemBuilder: (context, index) => Card(
                child: ListTile(
                  title: Text(_todos[index].title ?? 'No Title'),
                  subtitle: Row(
                    children: [
                      Text(_todos[index].content ?? 'No Content'),
                      Checkbox(
                        value: _todos[index].active == 1,
                        onChanged: (bool? newValue) {
                          setState(() {
                            _todos[index].active = newValue! ? 1 : 0;
                            TodoDatabase.updateTodo(_todos[index]);
                          });
                        },
                      ),
                    ],
                  ),

추가, 수정, 삭제 등을 구현합니다.

  void _addNewTodo() async {
    final titleController = TextEditingController();
    final contentController = TextEditingController();
    bool active = false;

    showDialog(
      context: context,
      builder: (BuildContext ctx) {
        return StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
          return AlertDialog(
            title: const Text('Add New Todo'),
            content: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                TextField(
                  controller: titleController,
                  decoration: const InputDecoration(labelText: 'Title'),
                ),
                TextField(
                  controller: contentController,
                  decoration: const InputDecoration(labelText: 'Contents'),
                ),
                Row(
                  children: [
                    const Text('Active'),
                    Checkbox(
                      value: active,
                      onChanged: (bool? newValue) {
                        setState(() {
                          active = newValue!;
                        });
                      },
                    ),
                  ],
                ),
              ],
            ),
            actions: <Widget>[
              TextButton(
                child: const Text('Cancel'),
                onPressed: () {
                  Navigator.of(ctx).pop();
                },
              ),
              TextButton(
                child: const Text('Add'),
                onPressed: () async {
                  final String title = titleController.text;
                  final String content = contentController.text;

                  if (title.isNotEmpty && content.isNotEmpty) {
                    await TodoDatabase.insertTodo(Todo(
                      title: title,
                      content: content,
                      active: active ? 1 : 0,
                    ));
                    _refreshTodos();
                    Navigator.of(ctx).pop();
                  }
                },
              ),
            ],
          );
        });
      },
    );
  }

  void _editTodo(Todo todo) async {
    final titleController = TextEditingController(text: todo.title);
    final contentController = TextEditingController(text: todo.content);
    bool active = todo.active == 1;

    showDialog(
        context: context,
        builder: (BuildContext ctx) {
          return StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              return AlertDialog(
                title: const Text('Edit Todo'),
                content: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: <Widget>[
                    TextField(
                      controller: titleController,
                      decoration: const InputDecoration(labelText: 'Title'),
                    ),
                    TextField(
                      controller: contentController,
                      decoration: const InputDecoration(labelText: 'Contents'),
                    ),
                    Row(
                      children: [
                        const Text('Active'),
                        Checkbox(
                          value: active,
                          onChanged: (bool? newValue) {
                            setState(() {
                              active = newValue!;
                            });
                          },
                        ),
                      ],
                    ),
                  ],
                ),
                actions: <Widget>[
                  TextButton(
                    child: const Text('Cancel'),
                    onPressed: () {
                      Navigator.of(ctx).pop();
                    },
                  ),
                  TextButton(
                    child: const Text('Save'),
                    onPressed: () async {
                      final String updatedTitle = titleController.text;
                      final String updatedContent = contentController.text;

                      if (updatedTitle.isNotEmpty &&
                          updatedContent.isNotEmpty) {
                        await TodoDatabase.updateTodo(Todo(
                          id: todo.id,
                          title: updatedTitle,
                          content: updatedContent,
                          active: active ? 1 : 0,
                        ));
                        _refreshTodos();
                        Navigator.of(ctx).pop();
                      }
                    },
                  ),
                ],
              );
            },
          );
        });
  }

  Future _refreshTodos() async {
    setState(() => _isLoading = true);
    _todos = await TodoDatabase.getTodos();
    setState(() => _isLoading = false);
  }

결과

참고 링크

Leave a Comment