스터디/Flutter+Dart

Flutter Database (SQLite) 사용하기 (2) - 좀 더 편하게! (BLoC, Json)

Dalmangyi 2019. 9. 11.

지난번 게시글에서 SQLite를 Flutter에서 사용하는 방법을 알아봤습니다 

( 지난 게시글 : https://dalgonakit.tistory.com/116 )

 

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

프로그램의 꽃은 데이터 저장! 그 저장과 불러오기를 쉽게 도와주는 것이 데이터베이스 인데요 이번 강좌에서는 데이터베이스의 한 종류인 SQLite를 써서 개발하는 방법을 소개해 볼까 합니다. 저도 Flutter에 입..

dalgonakit.tistory.com

 

이번에는 지난 번 게시글에 이어서

어떻게 하면 좀 더 Database를 편하게 접근 하고

어떻게 하면 좀 더 Flutter 답게 쓸 수 있는지 알아 보겠습니다

 

giphy


 

 

sqflite 살펴보기 

https://pub.dev/packages/sqflite

 

sqflite | Flutter Package

Flutter plugin for SQLite, a self-contained, high-reliability, embedded, SQL database engine.

pub.dev

 

ReadMe를 좀 읽어보니 rawInsert가 아닌 insert가 있습니다!

뿐만 아니고 query, update, delete도 있네요 

이미지 입니다만? 코드 아닙니다만? ...

 

 

함수를 보니 쿼리를 일일히 스트링으로 작성하지 않아고 되고, 테이블 이름과 Map 형태의 데이터만 있으면 되나봅니다

그럼 우리도 DogModel을 Map으로 세팅하고 Map으로 추출할 수 있게 클래스를 바꿔 봅시다

 

Dart 파일을 보고, Solution 탭을 눌러서 변경된 Dart파일을 봅시다 

dog_model.dart

 

Dog.fromJson() 생성자를 이용해서 주어진 Map<String, dynamic>을 파라매터 하나씩 매핑 시킵니다 

Dog 인스턴스에서  toJson()함수를 호출 하면 'id' 키에는 id 값을. 'name'키에는 name 값을 대입후 반환 합니다.

 

 

 

 

 

 

CRUD를 변경해 봅시다

map을 사용할 수 있는 raw 함수가 아닌 sqflite 함수를 사용해 보겠습니다

 

Solution 버튼을 눌러보세요

여기도 그냥 코드가 있고 Solution코드도 있습니다.

db_helper.dart의 CRUD부분 코드

 

 

코드 수가 줄어든 것도 아니고, 엄청~~~나게 편해진건 아니지만

문자열을 직접 쓰지 않으니 개발할때 상당히 깔끔하게 개발할 수 있습니다 

 

 

 

 

 

이쯤에서 전체 코드 투척

https://gist.github.com/Dalmangyi/a2938f22b26cef84d80e4a5c60230e1f

 

dart_sqflite_json_exam

dart_sqflite_json_exam. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

 

 

 

 


 

 

여기에서 끝나면 뭔가 허전하니까

조금 다른 방법으로 메인을 바꿔봅시다!!

 

 

 

 

 

 

 

 

BLoC 패턴

이번에 적용할 방법은 BLoC 패턴을 사용해 볼까 합니다. 

다른 개발을 하시다 오시면, 아니 무슨 처음 보는 패턴이야? 하시겠지만

flutter를 개발하다보면 정말 많이 쓰는 개발 패턴이기 때문에 잘 알아둬야 하는 패턴입니다.

 

 

간단히 이야기 하자면,

n개의 입구와 m개의 출구가 있는 큐를 컨트롤하는 패턴이라고 생각하시면 됩니다

입구는 sink로 불리우고, 출구는 stream이라고 부릅니다.

입구로는 데이터를 넣고, 출구로는 데이터를 받아서 Widget을 갱신할때 많이 쓰는 패턴 입니다.

입구는 여러개일 수 있지만, 반응하는 출구는 1개로 인식되며, 출구를 여러개로 바꿀때는 broadcast를 이용합니다

이런 입구와 출구를 관리해주고 쉽게 접근할 수 있게 해주는 클래스를 StreamController라고 합니다.

 

BLoC 패턴에 대한 다른 접근 방법에 대해서는 '수지아빠' 님의 게시글을 참고하시면 됩니다

https://javaexpert.tistory.com/970

 

Relative Programming - BLoC 패턴

Relative Programming - BLoC 패턴 아래 내용은 https://www.didierboelens.com/2018/08/reactive-programming---streams---bloc/ 을 공부하고 요약해놓은 글입니다. BLoC Pattern 은 구글 개발자 Paolo Soares 와..

javaexpert.tistory.com

 

 

 

 

그래서 BLoC패턴은 어디다 사용하나요?

이번 Database 게시글에선 BLoC 패턴으로 데이터와 뷰의 갱신을 담당해주는 부분에 사용됩니다

 

아래는 이전 게시글에 있던 개발 목표입니다.

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

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

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

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

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

이 개발 목표를 토대로 우리가 만들어 줘야 하는 부분을 데이터와 갱신으로 나눠서 상세하게 표현해 봅시다

 

 

 

 

 

 

 

 

0. 우선

BLoC 패턴이 있는 클래스를 만들어 보겠습니다

// lib/bloc/dog_bloc.dart

import 'package:flutter_sqlite/sqlite/models/dog_model.dart';

class DogBloc {

  final _dogsController = StreamController<List<Dog>>.broadcast();
  get dogs => _dogsController.stream;

  dispose() {
    _dogsController.close();
  }
}

dog_bloc.dart

 

이전 게시물에서 강아지 목록을 리스트에 전달해서 그리고 있었기 때문에 

BLoC 패턴도 List<Dog> 형태로 작성하였습니다. 

그리고 여러곳에서 받을 수 있도록 broadcast()로 StreamController를 만들었습니다.

(*이 강좌에서는 broadcast가 필요없지요..)

get 키워드를 이용해서 dogController의 stream(출구)을 가져옵니다.

 

 

 

BLoC을 메인 화면에 적용

기존 게시물에서 알려드린 구조는 FutureBuilder에 ListView.builder를 연결하는 방식이였는데 

async로 인해 반환되는 Future형을 받아주는 FutureBuilder보단 StreamController에 맞는 StreamBuilder로 변경하면 됩니다

Builder내부에 있던 future도 stream으로 변경되어서, bloc.dogs(stream)를 할당해 주면 됩니다. 

class _MyHomePageState extends State<MyHomePage> {

  final DogBloc bloc = DogBloc();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: StreamBuilder(
        stream: bloc.dogs,
        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(),
                  child: Center(child: Text(item.name)),
                );
              },
            );
          }
          else
          {
            return Center(child: CircularProgressIndicator(),);
          }
        },
      ),
      ...
    );
  }
}

 

 

 

 

 

 

 

BLoC 클래스를 만들었으면 이제 행동에 따른 화면 출력을 구현해 보도록 하겠습니다

 

1. 플러스 버튼

플러스 버튼을 눌러서 새로운 강아지 정보를 데이터 베이스에 넣고, 

새로운 데이터가 있는 데이터베이스를 호출해서 이를 리스트로 반영해야합니다 

 

 

아까 BLoC 패턴에서는 입구와 출구가 있다고 했죠? 

입구로는 데이터를 넣어주고, 출구로는 데이터가 빠져 나가는데 

 

 

입구에는 어떤 데이터를 넣어줘야 할까요?

플러스 버튼은 강아지를 새로 추가한단 의미 입니다.

그래서 플러스 버튼을 누르면, 새로운 강아지 정보를 데이터베이스에 기록하고,

기록한 강아지 정보를 입구에 넣어주면 됩니다!

 

 

 

코드로 한번 볼까요?

addDog(Dog dog) async {
  await DBHelper().createData(dog);
  _dogsController.sink.add([dog]); //출구가 리스트를 받아 들어기 때문에 리스트 형태로 넣어줍니다.
}

강아지 추가 함수를 만들고!

강아지를 DB에 넣어 줍니다.

그리고 입구를 관리하는 StreamController(_dogController)의 입구(sink)에 강아지 배열[]을 넣어 줍니다 

 

 

 

 

위와 같이 코드를 작성하면 출구에는 어떤 데이터가 나올까요?

당연히 넣어준 dog 데이터만 출구로 나오게 됩니다.

넣어준 dog 데이터만 나오다 보니 화면에는 하나의 데이터만 출력되게 되고, 

리스트를 만들려던 우리의 의도와는 많이 멀어지게 됩니다...

 

플러스를 눌러도 내 뜻 데로 되지 않는 너어.. 

 

 

 

 

그래서 아래코드와 같이 리스트형태로 그려줄 수 있게 sink에 여러개의 데이터를 넣어줍니다

addDog(Dog dog) async {
  await DBHelper().createData(dog);
  _dogsController.sink.add(await DBHelper().getAllDogs());
}

 

addDog 함수에는 데이터를 1개만 넣었지만, DB에 저장하고, 

DB에 저장된 모든 강아지 정보를 sink에 넣어줍니다.

 

1개를 넣고 모든걸 가져간다 gif

 

이제서야 의도한 대로 플러스를 눌러서 강아지 데이터베이스에 쌓이는걸 볼 수 있습니다

 

 

 

 

2. Dismissible을 이용한 스와이프 삭제

스와이프로 삭제하는 방법은 선택된 강아지 정보를 DB에서 삭제하고,

1) 선택된 강아지 화면만 갱신하는 방법

또는

2) 화면을 초기화 하고, DB에 저장된 모든 강아지 정보를 불러와서 화면에 반영하는 방법이 있습니다

 

 

첫 번째 방법은 기존 게시물에서 Dismisible을 이용해서 구현을 했었고

두번째 방법도 물론 기존 게시물에서 구현했지만 BLoC패턴에 맞게 변경해보겠습니다

import 'dart:async';
import 'package:flutter_sqlite/sqlite/models/dog_model.dart';
import 'package:flutter_sqlite/sqlite/db_helper.dart';

class DogBloc {

  final _dogsController = StreamController<List<Dog>>.broadcast();
  get dogs => _dogsController.stream;

  deleteDog(int id) async {
    await DBHelper().deleteDog(id);
    _dogsController.sink.add(await DBHelper().getAllDogs());
  }

}

dog_bloc.dart파일에 deleteDog 함수를 추가하고, 

DB에서는 id를 기준으로 강아지 정보를 삭제한 다음

DB에 저장된 모든 강아지 정보를 가져와서 sink로 전달합니다

 

 

main.dart에서는 Dismisible 뷰를 스와이프 하면 deleteDog함수가 호출되게 변경합니다

Dog item = snapshot.data[index];

return Dismissible(
  key: UniqueKey(),
  onDismissed: (direction) {
    bloc.deleteDog(item.id);
  },
  child: Center(child: Text(item.name)),
);

deleteDog 함수가 Dismissible에 의해서 실행되면

삭제 후, sink로 전달된 모든 강아지 정보를 StreamBuilder 가 반응하여 화면을 새로 그려주게 됩니다

추가후, 스와이프 삭제 gif

 

 

 

 

3. 전체 삭제

새로고침 버튼을 눌러서 전체 삭제를 구현해 봅시다. 

  deleteAll() async {
    await DBHelper().deleteAllDogs();
    _dogsController.sink.add(await DBHelper().getAllDogs());
  }

이전 다른 기능들 처럼 DB에서 삭제후, 

모든 데이터를 DB로 부터 호출해서 sink로 넘겨줍니다. 

 

모든 데이터가 삭제 됬다면 모든 데이터를 불러올 필요없이 빈 배열을 넘겨줘도 되지만, 

혹시라도 DB 처리 과정에서 오류가 발생되지 않을까하는 염려때문에 sink에는 getAllDogs()를 호출해서 데이터를 넘겨주었습니다

 

FloatingActionButton(
  child: Icon(Icons.refresh),
  onPressed: () {
    bloc.deleteAll();
  },
),

main.dart에서는 bloc.deleteAll 함수를 호출하면 간단히 해결됩니다.

 

 

새로고침 버튼을 누른 gif

 

 

 

4. 앱을 종료후 실행에도 데이터 불러오기

지금 코드를 적용후에 앱을 껏다 켜보면, 계속해서 프로그래스바가 돌고 있게 됩니다.

우리가 원하는건 앱을 켰을때 기존에 저장된 데이터가 나와야 되는데 말입니다.

처음 실행시 돌리고 돌리고 돌리는 gif

class DogBloc {

  DogBloc() {
    getDogs();
  }

  final _dogsController = StreamController<List<Dog>>.broadcast();
  get dogs => _dogsController.stream;

  dispose() {
    _dogsController.close();
  }

  getDogs() async {
    _dogsController.sink.add(await DBHelper().getAllDogs());
  }
}

생성자에서 데이터를 호출하면 해결됩니다.

생성자에는 async함수를 적용할 수 없어서 별도로 다른 함수(getDogs함수)를 만들어 사용하였습니다

 

데이터를 추가하고 앱 다시 실행하기 gif

 

 

 

 

BLoC 전체 코드 

import 'dart:async';
import 'package:flutter_sqlite/sqlite/models/dog_model.dart';
import 'package:flutter_sqlite/sqlite/db_helper.dart';

class DogBloc {

  DogBloc() {
    getDogs();
  }

  final _dogsController = StreamController<List<Dog>>.broadcast();
  get dogs => _dogsController.stream;

  dispose() {
    _dogsController.close();
  }

  getDogs() async {
    _dogsController.sink.add(await DBHelper().getAllDogs());
  }

  addDog(Dog dog) async {
    await DBHelper().createData(dog);
    getDogs();
  }

  deleteDog(int id) async {
    await DBHelper().deleteDog(id);
    getDogs();
  }

  deleteAll() async {
    await DBHelper().deleteAllDogs();
    getDogs();
  }
}

dog_bloc.dart

 

모든 BLoC함수를 합치고,

전체 화면을 갱신하는 공통적인 부분은 getDogs() 함수로 변경해주었습니다

 

 

 

 

 

 

마치며

글은 많았지만 실제로 해보면 얼마 안되기도 하고 이해가 어려운 부분은 아니라 

다들 금방 따라오셨으리라 생각됩니다

 

Json으로 인해 코드가 추가되고 BLoC패턴 추가로 인해서 코드는 더 늘어났지만,

String 쿼리를 쓸 필요가 없어지고, setState() 같은 상태 함수를 다룰 필요가 없어지게 되었습니다.

그로 인해 좀 더 명확하게 상황을 컨트롤 할 수 있게 되었네요

 

이해안가는 부분이 계시다면 언제든 댓글 달아주세요

감사합니다~ 

 

 

 

 

 

 

 

댓글