[Flutter] Hive를 이용한 데이터 저장

NoFlue·2022년 6월 16일
0
post-thumbnail

메모앱을 만들면서 Local에 데이터를 저장하기 위해 SQLite를 사용해왔다. 하지만 많은 데이터를 저장할 것도 아니고 무엇보다 저장되는 데이터 타입의 수가 적기 때문에 불편한 감이 없지 않아 있다.

대체할 만한 Local DB를 찾던 중 Hive가 편하다는 정보를 찾게 되었다.
그렇기에 오늘은 Hive의 사용법에 대해 알아보고 간단한 예시인 Todo List를 만들어 보자!

써본 느낌으로는 사용법이 직관적이고 성능도 좋아 Hive만 쓸 거 같다 ㅎㅎ


Package 설치

우선 Hive를 사용하기 위해 pubspec.yaml 에 패키지들을 추가하자.

dependencies:
  hive: ^[version]
  hive_flutter: ^[version]

dev_dependencies:
  hive_generator: ^[version]
  build_runner: ^[version]

dev_dependencies 에 추가한 패키지들은 밑에서 알아 볼 TypeAdapter를 자동으로 생성해주는 패키지들이다. 자세한 건 밑에서 다시 알아보자.

데이터를 담는 Box

Hive는 Box에 모든 데이터를 저장하며, SQL의 Table처럼 구조화되지 않았기에 어떤 데이터든 넣을 수 있다.

Flutter에서 Hive를 사용하기 위해선 사용할 디렉토리를 초기화하고 Box를 열어주어야 한다.

import 'package:hive_flutter/hive_flutter.dart';

void main() async{
  await Hive.initFlutter(); // Directory Initialize
  await Hive.openBox('box_name'); //Open Box
}

Box를 열었으면 그 다음부터는 Hive.box('box_name') 메소드를 통해 사용할 수 있다.

var box = Hive.box('box_name');

Hive를 사용할 준비는 다 끝났다. 이제 DB에 어떻게 읽고 쓰는지에 대해 알아보자!


Hive에 데이터 저장하기

Hive의 장점은 사용법이 매우 직관적이라는 것이다. 데이터 저장하는 법도 매우 쉽다.
Key-Value Database 이기 때문에 Key와 Value를 지정해주면 된다.

var exBox = Hive.box('ex_box');

exBox.add('random_key_value');

exBox.put('key', 'value');
exBox.put('name', 'Kim');
exBox.put('fruits', ['Apple', 'Banana', 'Grape']);

exBox.putAll({
  'key_1': 'value_1',
  'key_2': 12,
  'key_3': 'value_3'
  });
  
exBox.putAt(3, 'index_value');

Hive에 저장할 수 있는 데이터 타입들은
Primitive Type List Map DateTime Unit8List 들이 있다.

어... 그럼 User와 같은 참조형 데이터는 Hive에 못 저장하나요?

당연히 가능하다. 위에서 언급한 TypeAdapter 를 만들어 주면 된다.
물론 TypeAdapter도 직접 작성할 필요 없이 hive_generator 를 통해 자동으로 생성할 수 있다!



Hive를 통해 Todo List를 예시로 만들자고 했으니 Todo 클래스를 설계해보자

import 'package:hive/hive.dart';
import 'package:uuid/uuid.dart';

part 'todo.g.dart';

var uuid = Uuid();

(typeId: 2)
class Todo{
  (0)
  final String id;
  (1)
  final String todo;

  User({
    String? id,
    required this.todo,
  }) : id = id ?? uuid.v4();
}

HiveType(typeId: 2)HiveField(0) 은 build_runner로 TypeAdapter를 만들 때 Hive가 각 필드들을 식별하기 위해 선언해주어야 한다.
typeId와 HiveField에 들어가는 숫자는 0~255 사이로 입력해주면 된다.

TypeAdapter를 생성하기 위해선 위 코드처럼 part '*.g.dart';(*는 class 이름) 를 선언해준 뒤 아래 명령어를 터미널에서 실행하면 된다.

flutter pub run build_runner build

part 구문은 해당 클래스가 build_runner 로 생성된 파일의 private 멤버들에 접근할 수 있도록 해준다는데 이 내용은 나중에 다시 공부를 해봐야겠다.

 

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'todo.dart';

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

class TodoAdapter extends TypeAdapter<Todo> {
  
  final int typeId = 2;

  
  Todo read(BinaryReader reader) {
    final numOfFields = reader.readByte();
    final fields = <int, dynamic>{
      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
    };
    return Todo(
      id: fields[0] as String?,
      todo: fields[1] as String,
    );
  }

  
  void write(BinaryWriter writer, Todo obj) {
    writer
      ..writeByte(2)
      ..writeByte(0)
      ..write(obj.id)
      ..writeByte(1)
      ..write(obj.todo);
  }

  
  int get hashCode => typeId.hashCode;

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

명령어를 실행하면 이런 코드가 생기게 된다. 그럼 이제 Hive에 해당 TypeAdapter를 등록만 하면 Todo를 저장할 수 있다!

void main() async{
  await Hive.initFlutter();
  Hive.registerAdapter(TodoAdapter());
  // 위에서 만든 TodoAdapter를 등록한다.
  
  await Hive.openBox<Todo>('todos');
  // 제네릭을 선언하여 Todo만 Box에 담을 수 있게 할 수도 있다.
}
Box<Todo>? box = Hive.open('todos');
//

final _todo = Todo(todo: 'Hive 공부 하기');
box!.put(_todo.id, _todo);
// key 값은 uuid로 생성한 todo의 id로 지정해주었다.

이제 Hive의 데이터를 읽어보자

쓰는 것과 마찬가지로 읽는 것도 매우 쉽다.

var box = Hive.box('ex_box');

// 위에서 작성한 ex_box 박스에 들어있는 key-value 이다.
print(box.get('key')); // value
print(box.get('fruits')); // [Apple, Banana, Grape]

print(box.getAt(3)); // index_value

모든 값을 불러오고 싶다면 box.values를 호출해 Iterable 타입으로 가져올 수 있다.


그럼 데이터 삭제하는 법도?

그렇다 매우 단순하다!

var box = Hive.box('ex_box');

box.delete('key');
box.deleteAt(3);

이제 Hive를 사용할 준비는 끝났다. 본격적으로 Todo List를 만들어 보자!
Todo 클래스는 위에서 선언했으니 로직과 UI만 만들면 된다.

로직과 UI를 분리하기 위해 Provider를 사용해 만들어 보도록 하자

import 'package:hive_flutter/hive_flutter.dart';
import 'package:state_notifier/state_notifier.dart';

import '../model/todo.dart';

Box<Todo>? todoList = Hive.box('todos');

class TodoListState{
  final List<Todo> todos;

  TodoListState({
    required this.todos,
  });

  factory TodoListState.initial(){
    return TodoListState(todos: todoList!.values.toList());
  }

  TodoListState copyWith({
    List<Todo>? todos,
  }) {
    return TodoListState(
      todos: todos ?? this.todos,
    );
  }
}

class TodoList extends StateNotifier<TodoListState>{
  TodoList(): super(TodoListState.initial());

  void addTodo(Todo todo){
    final _todos = [...state.todos, todo];

    state.copyWith(todos: _todos);
    todoList!.put(todo.id, todo);
  }
  
  void deleteTodo(Todo todo){
    final _todos = state.todos.where((t) => t.id != todo.id).toList();
    
    state.copyWith(todos: _todos);
    todoList!.delete(todo.id);
  }
}

Todo 목록들을 가지는 TodoListState를 선언하고 상태 관리를 하기 위해 StateNotifier를 이용한 TodoList를 만들었다.

Todo를 추가하고 삭제하는 로직을 작성했으니 UI를 만들어보자

stateNotifierProvider<TodoList, TodoListState>(
  create: (_) => TodoList(),
  child: MaterialApp(
  	...
  ),
);

Provider 위젯을 먼저 선언해주어 모든 하위 위젯에서 데이터를 소비할 수 있다.

class AddTodo extends StatefulWidget {
  const AddTodo({Key? key}) : super(key: key);

  
  State<AddTodo> createState() => _AddTodoState();
}

class _AddTodoState extends State<AddTodo> {
  final _todoController = TextEditingController();

  
  void dispose() {
    _todoController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: TextField(
            controller: _todoController,
            decoration: const InputDecoration(
              border: InputBorder.none,
              hintText: '할 일을 작성해주세요!',
            ),
          ),
        ),
        IconButton(
          onPressed: () {
            if(_todoController.text != null && _todoController.text.isNotEmpty){
              final _todo = Todo(name: _todoController.text);
              _todoController.clear();

              context.read<TodoList>().addTodo(_todo);
            }
          },
          icon: Icon(Icons.add),
        ),
      ],
    );
  }
}

Todo를 작성하고 추가하는 UI다.
할 일을 입력하고 IconButton을 누르면 addTodo 메서드가 실행되어 Todo가 추가된다.

class TodoListView extends StatelessWidget {
  TodoListView({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    final _todoList = context.watch<TodoListState>().todos;

    return ListView.separated(
      itemCount: _todoList.length,
      separatorBuilder: (BuildContext context, int index) {
        return const Divider(height: 10);
      },
      itemBuilder: (BuildContext context, int index) {
        return Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              _todoList[index].todo,
              style: const TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
              ),
              overflow: TextOverflow.ellipsis,
            ),
            IconButton(
              onPressed: () {
                final _todo = _todoList[index];

                context.read<TodoList>().deleteTodo(_todo);
              },
              icon: Icon(Icons.delete),
            )
          ],
        );
      },
    );
  }
}

ListView를 이용해 Todo 목록들을 나타내었다. 목록 안에 있는 IconButton을 누르면 deleteTodo 메서드가 실행되어 Todo가 삭제된다.

작성한 두 위젯을 Column 위젯 안에 넣으면 Hive를 이용한 간단한 Todo List 완성!


여담으로 Hive 에서 SQLite, SharedPreferences와 퍼포먼스를 비교한 자료를 보면

Hive가 다른 Local DB에 비해 압도적인 성능을 가진다는 걸 알 수 있다.

이 글에서 다룬 메소드 외에도 따로 필요한 기능이 있다면 Hive Docs를 참고하면 된다.

profile
앱 개발에 호기심이 많은 대학생 개발자 :3

0개의 댓글