[flutter] sqlite로 데이터 저장하기

Shin·2022년 11월 19일
1

flutter

목록 보기
6/6

sqlite는 경량화된 데이터베이스 SW이다
서버용으로는 사용하기 어렵지만 가볍기 때문에 모바일 기기의 로컬용 데이터베이스로는 좋은 선택지가 된다
sqlite는 일반적으로 알고 있는 관계형 데이터베이스 모델을 그대로 적용할 수 있어, 로컬에서 구조가 있는 데이터를 저장할 때 우용하게 적용할 수 있다

dev_dependencies:
  flutter_test:
    sdk: flutter

  sqflite: ^2.0.0+3
class Todo {
  late int? id;
  late String title;
  late String desc;

  Todo({this.id, required this.title, required this.desc});

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

  Todo.fromMap(Map<dynamic, dynamic>? map) {
    id = map?['id'];
    title = map?['title'];
    desc = map?['desc'];
  }
}

기존 SharedPreferences에서 제작한 템플릿을 이용해 sqflite 패키지를 추가한 후 Todo 리스트를 만들기 위한 Todo 모델을 제작한다
toMap은 데이터를 외부로 보내기 위해 양식을 맞추는 기능을, fromMap은 네트워크 호출 등을 통해 받아오는 데이터를 Todo 모델로 변환하는 역할을 수행한다
데이터 양식은 json으로, 이는 플러터에서 Map<String, dynamic> 형태로 표현된다

class _ListScreenState extends State<ListScreen> {
  List<Todo> todos = [];
  bool isLoading = true;

  
  void initState() {
    // TODO: implement initState
    super.initState();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('할 일 목록 앱'),
        actions: [
          InkWell(
            onTap: () {},
            child: Container(
              padding: EdgeInsets.symmetric(horizontal: 15, vertical: 5),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [Icon(Icons.book), Text('뉴스')],
              ),
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        child: Text(
          '+',
          style: TextStyle(fontSize: 25),
        ),
        onPressed: () {},
      ),
      body: isLoading
          ? Center(
              child: CircularProgressIndicator(),
            )
          : ListView.separated(
              itemCount: todos.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(todos[index].title),
                  onTap: () {},
                  trailing: Container(
                    width: 80,
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.end,
                      children: [
                        Container(
                          padding: EdgeInsets.all(5),
                          child: InkWell(
                            onTap: () {},
                            child: Icon(Icons.edit),
                          ),
                        ),
                        Container(
                          padding: EdgeInsets.all(5),
                          child: InkWell(
                            onTap: () {},
                            child: Icon(Icons.delete),
                          ),
                        ),
                      ],
                    ),
                  ),
                );
              },
              separatorBuilder: (context, index) {
                return Divider();
              },
            ),
    );
  }
}

목록 화면을 구현하기 위해 List의 각 요소들 사이에 구분자(Divider)를 넣어주는 기능을 제공하는 ListView.separated를 활용했다
현재 isLoading 값이 true로 설정되어 있고, true일 땐 CircularProgressIndicator()가 나타난다
그렇기 때문에 initState()에서 이를 false로 설정해야 한다

import 'package:flutter_todo_backend/models/todo.dart';
import 'package:sqflite/sqflite.dart';

class TodoSqlite {
  late Database db;

  Future initDb() async {
    db = await openDatabase('my_db.db');
    await db.execute(
      'CREATE TABLE IF NOT EXISTS MyTodo '
      '(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title TEXT, desc TEXT)',
    );
  }

  Future<List<Todo>> getTodos() async {
    List<Todo> todos = [];
    List<Map> maps = await db.query('MyTodo', columns: ['id', 'title', 'desc']);
    for (var map in maps) {
      todos.add(Todo.fromMap(map));
    }
    return todos;
  }

  Future<Todo?> getTodo(int id) async {
    List<Map> map = await db.query(
      'MyTodo', columns: ['id', 'title', 'desc'],
      where: 'id = ?',
      whereArgs: [id]
    );
    if (map.isNotEmpty){
      return Todo.fromMap(map[0]);
    }
  }
  
  Future addTodo(Todo todo) async{
    int id = await db.insert('MyTodo', todo.toMap());
  }
  
  Future deleteTodo(int id) async{
    await db.delete('MyTodo', where: 'id = ?', whereArgs: [id]);
  }
  
  Future updateTodo(Todo todo) async{
    await db.update('MyTodo', todo.toMap(), where: 'id = ?', whereArgs: [todo.id]);
  }
}

sqlite와 연동을 구현한 provider이다
sqlite와 연결 및 초기 설정을 하고, 데이터를 모두 가져오거나 생성, 수정, 삭제하는 기능을 작성하였다

initDB는 sqlite 패키지를 통해 기기 내의 데이터베이스 파일과 연결을 수행한다
파일이 없는 경우 자동으로 생성되며, 데이터베이스의 이름을 'my_db.db'로 설정했다

연결한 데이터베이스는 db라는 인스턴스로 관리한다
처음으로 db.execute()로 테이블을 생성하고, id값은 자동으로 생성하게 했다

그 후 데이터 가져오기, 생성, 삭제, 수정을 구현해준다

class _ListScreenState extends State<ListScreen> {
  List<Todo> todos = [];
  TodoSqlite todoSqlite = TodoSqlite();
  bool isLoading = true;

  Future initDb() async{
    await todoSqlite.initDb().then((value) async{
      todos = await todoSqlite.getTodos();
    });
  }
  
  
  void initState() {
    // TODO: implement initState
    super.initState();
    Timer(Duration(seconds: 2), (){
      initDb().then((_) async{
        setState(() {
          isLoading = false;
        });
      });
    });
  }
  
// (생략)...
  
}

TodoSqlite의 initDb()를 수행하고, 이후에 getTodos()를 실행한다
데이터베이스 연결 및 초기화가 되지 않으면 getTodos()에서 오류가 발생하므로 반드시 실행해줘야 한다

floatingActionButton: FloatingActionButton(
  child: Text(
    '+',
    style: TextStyle(fontSize: 25),
  ),
  onPressed: () {
    showDialog(
        context: context,
        builder: (BuildContext context) {
          String title = '';
          String desc = '';
          return AlertDialog(
            title: Text('할 일 추가하기'),
            content: Container(
              height: 200,
              child: Column(
                children: [
                  TextField(
                    onChanged: (value) {
                      title = value;
                    },
                    decoration: InputDecoration(labelText: '제목'),
                  ),
                  TextField(
                    onChanged: (value) {
                      desc = value;
                    },
                    decoration: InputDecoration(labelText: '설명'),
                  ),
                ],
              ),
            ),
            actions: [
              TextButton(
                child: Text('추가'),
                onPressed: () async {
                  await todoSqlite.addTodo(
                    Todo(title: title, desc: desc),
                  );
                  List<Todo> newTodos = await todoSqlite.getTodos();
                  setState(() {
                    todos = newTodos;
                  });
                  Navigator.of(context).pop();
                },
              ),
              TextButton(
                child: Text('취소'),
                onPressed: () {
                  Navigator.of(context).pop();
                },
              ),
            ],
          );
        });
  },
),

floatingActionButton에 Todo를 추가하는 기능을 작성한다
showDialog를 통해 다이얼로그를 출력하고 제목과 설명을 받고 '추가'버튼을 누르면 todoSqlite의 addTodos()를 통해 추가된다
그 후 todos를 갱신한 후 다이얼로그를 종료한다

onTap: () {
  showDialog(
      context: context,
      builder: (BuildContext context) {
        return SimpleDialog(
          title: Text('할 일'),
          children: [
            Container(
              padding: EdgeInsets.all(10),
              child: Text('제목 : ' + todos[index].title),
            ),
            Container(
              padding: EdgeInsets.all(10),
              child: Text('설명 : ' + todos[index].desc),
            ),
          ],
        );
      });
},

showDialog를 통해 상세보기 기능을 구현하고

InkWell(
  onTap: () {
    showDialog(
        context: context,
        builder: (BuildContext context) {
          String title = todos[index].title;
          String desc = todos[index].desc;
          return AlertDialog(
            title: Text('할 일 수정하기'),
            content: Container(
              height: 200,
              child: Column(
                children: [
                  TextField(
                    onChanged: (value) {
                      title = value;
                    },
                    decoration: InputDecoration(
                        hintText: todos[index].title),
                  ),
                  TextField(
                    onChanged: (value) {
                      desc = value;
                    },
                    decoration: InputDecoration(
                        hintText: todos[index].desc),
                  ),
                ],
              ),
            ),
            actions: [
              TextButton(
                child: Text('수정'),
                onPressed: () async {
                  Todo newTodo = Todo(
                    id: todos[index].id,
                    title: title,
                    desc: desc,
                  );
                  await todoSqlite
                      .updateTodo(newTodo);
                  List<Todo> newTodos =
                      await todoSqlite.getTodos();
                  setState(() {
                    todos = newTodos;
                  });
                  Navigator.of(context).pop();
                },
              ),
              TextButton(
                child: Text('취소'),
                onPressed: () {
                  Navigator.of(context).pop();
                },
              ),
            ],
          );
        });
  },
  child: Icon(Icons.edit),
),
InkWell(
  onTap: () async {
    await todoSqlite.deleteTodo(todos[index].id ?? 0);
    List<Todo> newTodos = await todoSqlite.getTodos();
    setState(() {
      todos = newTodos;
    });
    Navigator.of(context).pop();
  },
  child: Icon(Icons.delete),
),

수정과 삭제 기능도 구현해준다

이렇게 sqlite를 통한 데이터 저장을 구현했다
다음에는 firebase를 이용한 원격 저장소를 이용하는 방법을 구현하도록 하자


출처 : 쉽고 빠른 플러터 앱 개발

1개의 댓글

comment-user-thumbnail
2023년 1월 29일

안녕하세요. 코드를 참고하여 제가 코드를 작성했는데 late Database = db; 에서 db가 초기화가 안되었다고 오류가 뜨던데 제가 놓친게 있을까요?

답글 달기