Flutter Database - NoSQL Hive 사용하기

2023. 4. 21. 20:32DEV/Flutter

반응형

 

Flutter의 Local NoSQL Database 중 Hive를 사용해보려고 한다. 쉽고, 간단하고, 빠르게~

Hive란?

Hive는 순수 Dart로 만들어진 Key-Value 형식의 빠른 데이터 베이스이다.

💡 쿼리, 멀티-아이솔레이트 지원 또는 객체 간 연결이 필요한 경우 Isar Database를 확인할 것.

Features

  • 🚀 Cross platform: mobile, desktop, browser
  • ⚡ Great performance (see benchmark)
  • ❤️ Simple, powerful, & intuitive API
  • 🔒 Strong encryption built in
  • 🎈 NO native dependencies
  • 🔋 Batteries included

Work Progress

📎 Hive Doc

작업 과정을 간단하게 요약하면 

  1. Add to project : Install hive_flutter package
  2. Create Hive Object class : Model Class를 생성한다 → @HiveType , @HiveField , part 'filename.g.dart'
  3. Generate adapter : build_runner로 Build해서 .g.dart에 Model Class의 TypeAdapter를 생성한다.
  4. Initialize Flutter : 가장 최상단에서 Hive를 초기화한다. main.dart → awit Hive.initFlutter();
  5. Register adapter : 생성한 Adapter를 등록한다.
  6. Open Box : 박스를 열어 사용할 수 있게 한다.
  7. CRUD database : 객체(데이터)를 사용한다.

 

Installation

Hive를 사용하기 위해서는 다음 라이브러리를 설치해야 한다.

  • dependencies
    • hive : Hive 실제 Database
    • hive_flutter : Flutter에서 Hive 사용을 할 수 있도록 해준다.
  • dev_dependencies
    • hive_generator : Database와 Flutter를 연결해주는 어댑터를 자동적으로 생성해준다.
    • build_runner : TypeAdapter를 생성할 때 사용한다.
dependencies:
  hive:
  hive_flutter:

dev_dependecies:
  hive_generator:
  build_runner:

 

1. Create Hive Object class

  • 클래스에 대한 TypeAdapter를 생성하려면 해당 클래스에 @HiveType 어노테이션을 추가하고 typeId(0에서 223 사이의 고유한 값)를 지정한다.
  • 저장되어야 하는 모든 필드에 @HiveField 어노테이션을 추가한다.
  • HiveObject를 상속받아서 Model을 생성하면 해당 객체의 정보를 변경하는 것만으로도 수정/삭제 등이 가능하다.
  • HiveList Type으로 다른 객체와의 관계를 설정할 수 있다. RDB의 Join으로 이해하면 된다.
import 'package:hive/hive.dart';

part 'person.g.dart';

@HiveType(typeId: 1)
class Person extends HiveObject {
  @HiveField(0)
  String name;

  @HiveField(1)
  int age;

  @HiveField(2)
  HiveList<Person>? friends;  // Nullable 설정 : 생성할 때 친구가 없으므로

  Person({
    required this.name,
    required this.age,
    this.friends,  // Nullable 설정
  });

  @override
  String toString() => '{name:$name, age:$age, frends:$friends}';

  String getFriends() { // 친구 데이터를 String으로 UI에 표시하기 위해 사용
    var names = '';
    if (friends != null) {
      for (var friend in friends!) {
        names = '$names${names == '' ? '' : ', '}${friend.name}';
      }
    }
    return names;
  }
}

2. Generate Adapter

📎 generate-adapter

Front-end에서 Database를 사용하면 데이터베이스 형식이 아닌 파일 형식으로 저장된다. 이를 데이터로 변환해주기 위한 Adapter를 만들어서 사용한다.

3. TypeAdapter 생성

Adapter를 만들기 위해 터미널을 열고 Project Root 경로에서 build를 실행한다.

% flutter pub run build_runner build

생성된 Adapter를 확인하면 이렇다.

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'person.dart';

// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************

class PersonAdapter extends TypeAdapter<Person> {
  @override
  final int typeId = 1;

  @override
  Person read(BinaryReader reader) {
    final numOfFields = reader.readByte();
    final fields = <int, dynamic>{
      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
    };
    return Person(
      name: fields[0] as String,
      age: fields[1] as int,
      friends: (fields[2] as List).cast<String>(),
    );
  }

  @override
  void write(BinaryWriter writer, Person obj) {
    writer
      ..writeByte(3)
      ..writeByte(0)
      ..write(obj.name)
      ..writeByte(1)
      ..write(obj.age)
      ..writeByte(2)
      ..write(obj.friends);
  }

  @override
  int get hashCode => typeId.hashCode;

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is PersonAdapter &&
          runtimeType == other.runtimeType &&
          typeId == other.typeId;
}

만약 build 오류가 발생한다면 아래의 명령어를 실행한다.

flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs

Adapter가 잘 생성됐다면 이제 사용할 준비가 됐다.

 

4. Initialize Hive & Adapter 등록

main() 함수에서 Hive를 초기화하고, 생성한 TypeAdapter를 등록한다.

import 'package:hive_flutter/hive_flutter.dart';

void main() async {
  await Hive.initFlutter(); // * Hive 초기화
  Hive.registerAdapter(TaskAdapter()); // * Adapter 등록

  runApp(
    const MyApp(),
  );
}

 

5. Open Box & Create box file

 

  • 모든 데이터는 Box에 저장된다.
  • Box를 SQL Database의 Table로 이해하면 된다.
  • Box는 구조를 갖고 있지 않아서 어떤 것이든 담을 수 있다.

 

6. HiveRepository로 관리하기

Hive 비즈니스 로직을 별도의 파일로 만들어 관리를 용이하게 한다. 

Singleton 방식과 static 방식 중 선택해서 사용한다. main() 함수에서 openBox() 함수를 호출할 때 코드가 달라진다.

Singleton 방식

const String TASK_BOX = 'TASK_BOX';

class HiveRepository {
  // * Singleton 설정
  static final HiveRepository _singleton = HiveHelper._internal();
  factory HiveRepository() {
    return _singleton;
  }
  HiveRepository._internal();

  // * Box 생성 : 모든 데이터는 Box에 저장된다.
  Box<Task>? tasksBox;

  // * Box를 열어주는 함수 생성
  // -> Box에 데이터를 담을 준비를 한다.
  Future openBox() async {

    // * Open Box : Box가 여러 개인 경우 여기에 계속 추가화면 됨
    tasksBox = await Hive.openBox(TASK_BOX);
  }
}

Future<void> main() async {
  // * Hive 설정
  await Hive.initFlutter(); // Initialize Hive
  Hive.registerAdapter(PersonAdapter()); // Register Adapter
  await HiveRepository().openBox(); // Open Box

  runApp(const App());
}

static 방식

import 'package:hive/hive.dart';
import 'package:madang/todo/task.dart';

const String TASK_BOX = 'TASK_BOX';

class HiveRepository {

  // * Box 생성 : 모든 데이터는 Box에 저장된다.
  Box<Task>? tasksBox;

  // * Box를 열어주는 함수 생성
  // -> Box에 데이터를 담을 준비를 한다.
  static Future openBox() async {
    // * Open Box : Box가 여러 개인 경우 여기에 계속 추가화면 됨
    tasksBox = await Hive.openBox(TASK_BOX);
  }
}

Future<void> main() async {
  // * Hive 설정
  await Hive.initFlutter(); // Initialize Hive
  Hive.registerAdapter(PersonAdapter()); // Register Adapter
  await HiveRepository.openBox(); // Open Box

  runApp(const App());
}

 

Data CRUD

Create

  • box.add(value) : id를 자동으로 생성하고 데이터 입력
  • box.put(key, value) : key(id)를 지정해서 데이터 입력
// * Key값을 직접 지정하면서 저장하는 경우 -> Key를 직접 지정하면 그 Key로 읽어올 수 있다.
box.put('key', 'value');
box.putAll([Person1, Person2, Person3, ...]);

// * HiveObject 상속 시 단순 저장 가능
box.add(Person);

Read

  • get(key) : key값으로 데이터를 가져온다.
  • getAt(index) : index(위지)로 데이터를 가져온다.
// * 특정 데이터 하나만 읽어오는 경우
box.get(0); // 0 : index
box.get('key');

// * 데이터 전체를 리스트로 읽어오는 경우
box.values.toList();

Filter & Sort

  • List 함수로 처리한다.
// Filter
var filteredUsers = userBox.values.where((user) => user.name.startsWith('s')).toList();
var filtered = box.values
  .where((object) => object['country'] == 'GB')
  .toList();

// Sort
var items = box.values.toList();
items.sort((a, b) => a.name.compareTo(b.name)); //ASC
items.sort((b, a) => a.name.compareTo(b.name)); //DESC

Update

// * Model에서 HiveObject를 상속받은 경우
Person person = box.get('key');  // 데이터 가져오기
person.age = 0;  // 데이터 값 변경
age.save();  // 변경된 데이터 저장

Delete

person.delete(); // Hive Object로 삭제하기

personsBox.clear();  // 데이터 전체 삭제하기

 

Update Model class

데이터 모델을 수정해야하는 경우 아래의 규칙을 준수하면 기존 코드에 문제 없이 adapter를 업데이트할 수 있다.

  • 기존 field의 field number를 변경하면 안된다.
  • 새로운 field를 추가하더라도 old apdater가 작성한 객체(데이터)를 new adapter가 읽을 수 있다.
    • new code로 생성된 객체를 old code가 읽을 수 있다.
    • 새로 추가된 field는 무시하고 parsing 하기 때문이다.
  • field number가 동일하다면 field 이름을 변경할 수 있고 공개에서 비공개로 전환 가능하며 그 반대도 가능하다.
  • field number가 업데이트된 class에서 다시 사용되지 않는 다면 field를 제거할 수 있다.
  • field type 변경은 지원하지 않는다. 새로 만들어야 한다.
  • null safety가 활성화한 후 null를 허용하지 않는 field에서는 defaultValue를 제공해야 한다.

 

ValueListenableBuilder

  • Stream처럼 데이터의 변경을 감지해서 자동으로 Re-Build해준다.
  • valueListenable : 연결할 데이터에 listenable 설정
  • child : 데이터에 따라 변화되지 않는 위젯 입력 -> 그 위젯은 다시 렌더링하지 않는다.
  • builder : 데이터를 사용하여 페이지 구성 - 기본 생성자 (BuildContext context, dynamic value, Widget? child)
    • value : Hive Box 지정 eg. Box<Model Type> box
    • child : child로 받은 위젯 전달용
ValueListenableBuilder(
	valueListenable:
    	HiveRepository.personBox.listenable(), // Listening 설정
	builder:
		(BuildContext context, Box<Person> box, Widget? child) {
	// Box의 데이터를 모두 가져온다.
    List<Person> people = box.values.toList();
    
    // 데이터 사용해서 Widget 생성 //
}

 

Example

간단한 테스트 예제를 보자.

Person이라는 Model Class의 Box를 만들었다. 기본 기능 사용을 위해 모든 기능은 간소화해서 억지스러운 부분도 있지만 단순 테스트를 위한 거니 그러려니 하자.

  • Create : TextField에 입력한 name, age로 Person 데이터를 Box에 추가한다.
  • Read : 입력한 데이터를 아래에 ListView에 출력한다.
  • Update
    1. 리스트에서 수정 아이콘(2번째 아이콘)을 클릭하면 해당 객체의 값이 TextField에 추가해서 수정할 수 있게 한다.
    2. 값을 수정한 다음 Update 버튼을 누르면 수정 완료.
  • 리스트의 삭제 아이콘(3번째 아이콘)을 클릭하면 해당 데이터가 삭제된다.
  • Delete 버튼을 클릭하면 전체 데이터가 삭제된다.
  • 리스트의 추가 아이콘(1번째 아이콘)을 클릭하면 1번 데이터와 친구로 설정된다. → HiveList 사용

 

 

테스트 끝!

반응형