스터디/Flutter+Dart

Flutter Database (SQLite) 사용하기 (1)

Dalmangyi 2019. 9. 10.

프로그램의 꽃은 데이터 저장! 그 저장과 불러오기를 쉽게 도와주는 것이 데이터베이스 인데요

이번 강좌에서는 데이터베이스의 한 종류인 SQLite를 써서 개발하는 방법을 소개해 볼까 합니다.

 

저도 Flutter에 입문한지 얼마 안되서 잘 모르지만!

Flutter에서는 Database를 어떻게 접근하고 어떻게 하면 더 편리하게 접근할 수 있는지 소개해보겠습니다

 

 


 

개발 목표

우리가 구현할 모습을 미리 GIF로 한번 보시죠

1) 플러스(+) 버튼을 누르면 강아지 이름이 리스트에 추가되고,

2) 슬라이드하게되면 이름이 삭제되고,

3) 새로고침(⎋) 버튼을 누르면 목록이 전체가 초기화 되고,

4) 앱을 종료했다 다시 실행해도 이전 데이터를 그대로 보여주는게 이 게시물의 목표 입니다 

우리의 목표! GIF

 

 

 


 

 

개발에 앞서 Package 안내

아래에 있는 패키지를 모두 pubspec.yaml에 추가해주세요

dependencies:
  flutter:
    sdk: flutter

  sqflite: ^1.1.6
  path_provider: ^1.2.0
  path: ^1.6.2

 

1) sqflite (https://pub.dev/packages/sqflite#-installing-tab-)

 

 

SQLite를 Android와 iOS에 동시에 지원해주는 패키지 입니다

 

2) path_provider (https://pub.dev/packages/path_provider#-installing-tab-)

Android와 iOS에서 필요한 경로를 알려주는 패키지 입니다

이 게시글에서는 db파일을 저장할 위치를 알아내는 용도로 사용됩니다

 

3) path (https://pub.dev/packages/path)

경로를 합칠때 사용합니다

 

 

 

 

 

 

 


 

 

 

 

 

 

 

이해가 쉬운 간단한 코드부터 진행해보겠습니다

 

1. Model 구현

먼저 강아지를 구분하는 id와 이름을 저장하는 모델을 만듭니다.

 

class Dog {
  final int id;
  final String name

  Dog({this.id, this.name});
}

dog_model.dart

 

 

 

 

 

 

2. Database 헬퍼 구현

widget코드에서 매번 database를 초기화하고 불러오고 처리하면 너무 안쓰러우니까 Helper 코드를 만들어 보겠습니다

 

 

 

데이터 베이스 초기화

final String TableName = 'Dog';

initDB() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, 'MyDogsDB.db');

    return await openDatabase(
      path,
      version: 1,
      onCreate: (db, version) async {
        await db.execute('''
          CREATE TABLE $TableName(
            id INTEGER PRIMARY KEY,
            name TEXT,
          )
        ''');
      },
      onUpgrade: (db, oldVersion, newVersion){}
    );
  }

path_provider에서 지원하는 getApplicationDocumentsDirectory() 함수를 통해서 적당한 위치를 가져오고 경로를 만듭니다.

sqflite에서 지원하는 openDatabase()함수를 이용해서 경로를 불러오고 만약 없다면 onCreate 함수가 실행됩니다. 

테이블을 위에서 알려준 모델과 같은 조건(id, name)으로 만듭니다.

migration이 필요하다면 onUpgrade 함수를 구현하면 됩니다. 이 강좌에선 필요가 없으니 패스~

 

 

 

 

Factory

class DBHelper {

  DBHelper._();
  static final DBHelper _db = DBHelper._();
  factory DBHelper() => _db;

  static Database _database;

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

    _database = await initDB();
    return _database;
  }
}

DBHelper 클래스를 불러오기 편하게 하기 위해서 factory로 가져오게 만들었습니다.

그리고 Database를 가져오는 get 키워드를 사용해서 없으면 initDB() 함수를 호출하고 있으면 반환하게 만듭니다

initDB의 내부 과정에서 Database를 가져올때, 파일 접근을 하게 되니 비동기로 Future<Database>로 반환하게 됩니다

 

 

 

CRUD 

이번에는 생성하고(c), 읽고(r), 삭제(d)하는 기능을 만들어봅시다

갱신하는 기능은 다음 목표에서 만들어봅시당

  //Create
  createData(Dog dog) async {
    final db = await database;
    var res = await db.rawInsert('INSERT INTO $TableName(name) VALUES(?)', [dog.name]);
    return res;
  }

  //Read
  getDog(int id) async {
    final db = await database;
    var res = await db.rawQuery('SELECT * FROM $TableName WHERE id = ?', [id]);
    return res.isNotEmpty ? Dog(id: res.first['id'], name: res.first['name']) : Null;
  }

  //Read All
  Future<List<Dog>> getAllDogs() async {
    final db = await database;
    var res = await db.rawQuery('SELECT * FROM $TableName');
    List<Dog> list = res.isNotEmpty ? res.map((c) => Dog(id:c['id'], name:c['name'])).toList() : [];

    return list;
  }

  //Delete
  deleteDog(int id) async {
    final db = await database;
    var res = db.rawDelete('DELETE FROM $TableName WHERE id = ?', [id]);
    return res;
  }

  //Delete All
  deleteAllDogs() async {
    final db = await database;
    db.rawDelete('DELETE FROM $TableName');
  }

database를 호출하고 raw가 포함된 insert, query, delete를 호출하였습니다

문법은 기본적인 SQLite와 같고, 물음표(?)가 들어가는 부분은 뒤에 포함된 파라매터인 배열([ ])의 원소가 치환되는 방식으로 되어 있습니다. 은근 편하더군요.

dart에서는 위에서 사용한 $TableName처럼. 굳이 물음표로 치환하지 않아도 '$변수' 를 이용하거나 '${ }' 방법으로도 대체할 수 있습니다.

 

 

그래도 혹시 궁금하신 분들이 계실꺼같아서 UPDATE도 올려드립니다

  //Update
  updateDog(Dog dog) async {
    final db = await database;
    var res = db.rawUpdate('UPDATE $TableName SET name = ? WHERE = ?', [dog.name, dog.id]);
    return res;
  }

 

 

 

 

합친 코드

import 'dart:io';

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

import 'models/dog_model.dart';

final String TableName = 'Dog';

class DBHelper {

  DBHelper._();
  static final DBHelper _db = DBHelper._();
  factory DBHelper() => _db;

  static Database _database;

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

    _database = await initDB();
    return _database;
  }

  initDB() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, 'MyDogsDB.db');

    return await openDatabase(
      path,
      version: 1,
      onCreate: (db, version) async {
        await db.execute('''
          CREATE TABLE $TableName(
            id INTEGER PRIMARY KEY,
            name TEXT,
          )
        ''');
      },
      onUpgrade: (db, oldVersion, newVersion){}
    );
  }
  
    //Create
  createData(Dog dog) async {
    final db = await database;
    var res = await db.rawInsert('INSERT INTO $TableName(name) VALUES(?)', [dog.name]);
    return res;
  }

  //Read
  getDog(int id) async {
    final db = await database;
    var res = await db.rawQuery('SELECT * FROM $TableName WHERE id = ?', [id]);
    return res.isNotEmpty ? Dog(id: res.first['id'], name: res.first['name']) : Null;
  }

  //Read All
  Future<List<Dog>> getAllDogs() async {
    final db = await database;
    var res = await db.rawQuery('SELECT * FROM $TableName');
    List<Dog> list = res.isNotEmpty ? res.map((c) => Dog(id:c['id'], name:c['name'])).toList() : [];

    return list;
  }

  //Delete
  deleteDog(int id) async {
    final db = await database;
    var res = db.rawDelete('DELETE FROM $TableName WHERE id = ?', [id]);
    return res;
  }

  //Delete All
  deleteAllDogs() async {
    final db = await database;
    db.rawDelete('DELETE FROM $TableName');
  }

}

db_helper.dart

 

 

 

 

 

 

 

3. 화면 구현

기본적인 화면 구성은 정말 간단합니다. 

리스트 뷰와 플로팅 버튼 2개!

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Dog Database'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body:  //리스트가 들어갈 위치
      ,

      floatingActionButton: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          FloatingActionButton(
            child: Icon(Icons.refresh),
            onPressed: () {
            	//모두 삭제 버튼
            },
          ),
          SizedBox(height: 8.0),
          FloatingActionButton(
            child: Icon(Icons.add),
            onPressed: () {
            	//추가 버튼
            },
          ),
        ],
      )

    );
  }
}

App -> MaterialApp -> MyHomePage -> ListView, FloatingActionButtons

자 이제 차근차근 구현해 봅시다

 

 

추가 버튼 구현

import 'dart:math';

List<Dog> dogs = [
  Dog(name: '푸들이'),
  Dog(name: '삽살이'),
  Dog(name: '말티말티'),
  Dog(name: '강돌이'),
  Dog(name: '진져'),
  Dog(name: '백구'),
];

...
onPressed: () {
  Dog dog = dogs[Random().nextInt(dogs.length)];
  DBHelper().createData(dog);
  setState(() {});
},         
...

이름 입력창을 넣게 되면 복잡해지니까, 미리 이름을 만들어놓았습니다 (푸들이, 삽살이, 말티말티, 강돌이, 진져, 백구~)

버튼을 누를때마다 랜덤하게 가져와서, 데이터베이스에 기록할 수 있게 createData() 함수를 실행 합니다

그리고 statefulWidget이 갱신할 수 있게 setState()함수를 호출 해 줍니다

 

 

모두 삭제 버튼 구현

...
onPressed: () {
  DBHelper().deleteAllDogs();
  setState(() {});
},
...

모든 삭제는 간단히 DBHelper의 Delete함수를 호출하고

statefulWidget 화면 갱신용 setState()함수를 호출 합니다.

 

 

 

리스트 구현

FutureBuilder(
  future: DBHelper().getAllDogs(),
  builder: (BuildContext context, AsyncSnapshot<List<Dog>> snapshot) {

    if(snapshot.hasData) {
    
      return ListView.builder(
        itemCount: snapshot.data.length,
        itemBuilder: (BuildContext context, int index) {
        
          Dog item = snapshot.data[index];
          
          return Dismissible(
            key: UniqueKey(),
            onDismissed: (direction) {
              DBHelper().deleteDog(item.id);
              setState(() {});
            },
            child: Center(child: Text(item.name)),
          );
        },
      );
    }
    else
    {
      return Center(child: CircularProgressIndicator(),);
    }
  },
),

파일 입출력을 하는 Database로 인해 DBHelper().getAllDogs() 함수는 Future로 반환됩니다. 

그렇기 때문에 FutureBuilder를 사용하여 받기로 합니다 

 

데이터를 받을땐 FutureBuilder의 builder 함수로 받아집니다. 

한 순간 받아온 데이터를 snapshot이라 합니다.

snapshot안에 데이터가 있는지 체크하고, 없을때는 로딩(CircularProgressIndicator)을 보여주고

있을때는 당연히 데이터를 리스트형태로 보여줍니다 

 

리스트 형태로 보여줄때는 ListView.builder를 이용하여 Element를 하나씩 그리게 됩니다 

리스트의 각 Element는 Dismissible로 구현합니다

 

Dismissible은 왼쪽 혹은 오른쪽으로 드래그 하였을때 사라지는 기능을 지원하는데,

사라질때 onDismissed함수가 호출됩니다.

이때 실제로 DB에서 사라질 수 있게 DBHelper().deleteDog 함수를 호출해 줍니다.

그 뒤에 화면이 갱신될 수 있게 setState()도 호출해주면 기능 구현도 끝이 납니다.

 

 

 

 

화면 구현 소스

import 'package:flutter/material.dart';

import 'sqlite/db_helper.dart';
import 'sqlite/models/dog_model.dart';
import 'dart:math';


List<Dog> dogs = [
  Dog(name: '푸들이'),
  Dog(name: '삽살이'),
  Dog(name: '말티말티'),
  Dog(name: '강돌이'),
  Dog(name: '진져'),
  Dog(name: '백구'),
];

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Dog Database'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: FutureBuilder(
        future: DBHelper().getAllDogs(),
        builder: (BuildContext context, AsyncSnapshot<List<Dog>> snapshot) {

          if(snapshot.hasData) {
            return ListView.builder(
              itemCount: snapshot.data.length,
              itemBuilder: (BuildContext context, int index) {
                Dog item = snapshot.data[index];
                return Dismissible(
                  key: UniqueKey(),
                  onDismissed: (direction) {
                    DBHelper().deleteDog(item.id);
                    setState(() {});
                  },
                  child: Center(child: Text(item.name)),
                );
              },
            );
          }
          else
          {
            return Center(child: CircularProgressIndicator(),);
          }
        },
      ),

      floatingActionButton: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          FloatingActionButton(
            child: Icon(Icons.refresh),
            onPressed: () {
              DBHelper().deleteAllDogs();
              setState(() {});
            },
          ),
          SizedBox(height: 8.0),
          FloatingActionButton(
            child: Icon(Icons.add),
            onPressed: () {
              Dog dog = dogs[Random().nextInt(dogs.length)];
              DBHelper().createData(dog);
              setState(() {});
            },
          ),
        ],
      )

    );
  }
}

main.dart

 

 

 

 

끝!

giphy

데이터 베이스 구현이 어려울꺼 같았지만 flutter에서는 정말 쉬웠습니다!

심지어 코드를 다 합쳐도 200줄이 안되는 코드니.....

 

다음 게시글에서는 좀 더 어떻게 하면 DB를 편하게 쓸 수 있는지에 대해서 

알아보겠습니다!

 

 

 

 

 

 

댓글