본문 바로가기
개발일지/GoodWishes

[굿위시 제작기] 14. Hive Box로 데이터를 저장하기

by 박기린 2024. 11. 7.

휘발되는 데이터

지금까지 개발한 과정만 놓고 보면, 굿즈를 일시적으로 Provider에 저장하더라도 앱을 껐다키면 모든 데이터가 날라갑니다.

따라서, 지금까지 기록한 내용을 스마트폰의 내부 저장소에 저장한 후 불러오는 방식이 필요합니다.

 

이렇게 데이터 지속성을 보장하기 위한 라이브러리가 Flutter에 있습니다.

바로 Hive입니다.

 

 

 

Hive 공식 링크 : https://pub.dev/packages/hive

 

hive | Dart package

Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256.

pub.dev

 

Hive의 특징
- NoSQL (Key : Value) 기반 데이터 베이스
- 디바이스 내부 저장소에 파일 형식으로 데이터를 저장하고 참조할 수 있다.
- 플러터 뿐만 아니라 다른 프레임워크에서도 사용이 가능하다.

 

 

 


플러터에서 Hive를 사용하기 위해 필요한 패키지들

지금부터 소개할 4가지 패키지를 전부 사용해야, 플러터에서 쉽게 Hive 패키지를 이용할 수 있습니다.

 

1. Hive

공식 설치 링크 : https://pub.dev/packages/hive

가장 기본이 되는 Hive 데이터베이스 패키지입니다.

 

 

2. Hive Flutter

공식 설치 링크 : https://pub.dev/packages/hive_flutter

Hive 패키지는 플러터 외의 앱에서도 사용이 가능합니다. 그러다보니 플러터 환경에 특화된 방식으로 사용하려면 Hive Flutter 패키지가 필요합니다.

 

3. Hive Generator

공식 설치 링크 : https://pub.dev/packages/hive_generator

Hive 데이터 베이스에 데이터 모델 클래스를 생성하게 돕는 패키지입니다.

Hive는 원시 타입 뿐만 아니라, 개발자가 직접 디자인한 객체 데이터 모델도 저장할 수 있습니다. 이때, Hive의 저장방식에 맞게 변형해주는 작업이 필요한데, 이 귀찮은 과정을 자동으로 수행해줍니다.

 

 

* 빌드 과정에서만 사용하는 패키지이기 때문에, 'dev_depenencies'에 넣습니다.

 

 

4. Build Runner

공식 설치 링크 : https://pub.dev/packages/build_runner

Hive Generator가 데이터 모델 클래스를 Hive에 맞게 변형한다면, Build Runner는 이 데이터 모델 클래스에 대해 자동으로 코드를 생성해줍니다. 

 

* 빌드 과정에서만 사용하는 패키지이기 때문에, 'dev_depenencies'에 넣습니다.

 

 

 

 


데이터 모델을 Hive 데이터 형식으로 변환하는 작업

이전에 생성한 model 파일들에 들어간 후,

 

 

 

 

 

@HiveType(typeId: int)

Hive 데이터 베이스에서 사용될 데이터 모델 클래스임을 명시합니다.

직접 생성한 모델의 맨 위에 적습니다. typeId는 임의의 숫자(int)로 지정하는데, 여러 모델 클래스를 식별하기 위한 값이기 때문에 겹치면 안 됩니다.

 

@HiveField(int)

데이터 모델 클래스의 필드를 Hive 데이터 베이스에서 직렬화하여 저장할 데이터라고 명시합니다.

각 필드 위에 하나씩 넣어줍니다. 임의의 숫자(int)를 입력받습니다. 역시 고유한 값이어야 합니다.

 

 

 


Build Runner로 Hive 데이터 모델 클래스 생성 (.g.dart)

dart run build_runner build

위 명령어를 터미널에 입력하면,

 

 

자동으로 '.g.dart' 파일이 생성되고, 원본 모델 파일에

 

 

part '모델이름.g.dart'

라는 코드가 추가됩니다.

 

 

 

 

.g.dart 파일을 살펴보면,

 

 

part of '모델이름.dart';

코드를 맨 위에서 확인할 수 있습니다.

 

part of : part 키워드로 분할된 코드로서 포함될 파일(master) 명시

 

 

 

새롭게 생성된 파일에 '모델이름Adapter'라는 클래스 모델이 생성되는데, 이 모델을 통해 Hive에 알맞게 데이터가 저장됩니다.

 

 

 

 

 


main.dart로 이동

Hive 데이터는 앱이 켜짐과 동시에 불러와져야 합니다. 그래서 main.dart로 갑니다.

 

 

 

우선, main.dart 파일의 코드 전문을 보여드린 후, 세부적으로 설명을 드리겠습니다.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:goodwishes/Layouts/build_mobile_layout.dart';
import 'package:goodwishes/Layouts/build_tablet_layout.dart';
import 'package:goodwishes/Models/category_model.dart';
import 'package:goodwishes/Models/goods_model.dart';
import 'package:goodwishes/Models/profile_model.dart';
import 'package:goodwishes/Models/wish_category_model.dart';
import 'package:goodwishes/Models/wish_model.dart';

import 'package:goodwishes/widgets/bottom_navigation.dart';

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

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Hive.initFlutter();
  Hive.registerAdapter(GoodsAdapter());
  Hive.registerAdapter(WishAdapter());
  Hive.registerAdapter(CategoryAdapter());
  Hive.registerAdapter(ProfileAdapter());

  await _openHiveBoxes();

  var categories = Hive.box<Category>('categoryListBox');
  if (categories.get('no_category') == null) {
    categories.put('no_category',
        Category(id: 'no_category', categoryName: '카테고리 없음', count: 0));
  }
  var wishCategories = Hive.box<Category>('wishCategoryListBox');
  if (wishCategories.get('no_category') == null) {
    wishCategories.put('no_category',
        Category(id: 'no_category', categoryName: '카테고리 없음', count: 0));
  }

  var profile = Hive.box<Profile>('profileBox');
  if (profile.get('profile') == null) {
    profile.put('profile', Profile([], true));
  }

  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (BuildContext context) => GoodsListProvider(),
        ),
        ChangeNotifierProvider(
          create: (BuildContext context) => CategoryListProvider(),
        ),
        ChangeNotifierProvider(
          create: (BuildContext context) => ProfiletProvider(),
        ),
        ChangeNotifierProvider(
          create: (BuildContext context) => WishListProvider(),
        ),
        ChangeNotifierProvider(
          create: (BuildContext context) => WishCategoryListProvider(),
        ),
      ],
      child: const MyApp(),
    ),
  );
}

Future<void> _openHiveBoxes() async {
  List<Future> futures = [
    Hive.openBox<Goods>('goodsListBox'),
    Hive.openBox<Wish>('wishListBox'),
    Hive.openBox<Category>('categoryListBox'),
    Hive.openBox<Profile>('profileBox'),
    Hive.openBox<Category>('wishCategoryListBox'),
  ];
  await Future.wait(futures);
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    // 안드로이드 home indicator의 뒷 배경을 검정색에서 원하는 색으로 변경
    SystemChrome.setSystemUIOverlayStyle(
      const SystemUiOverlayStyle(
        statusBarColor: Colors.transparent,
        systemNavigationBarColor: Colors.white,
      ),
    );

    var isTablet = MediaQuery.of(context).size.width > 600;

    return MaterialApp(
      debugShowCheckedModeBanner: false, // 디버그 표시를 없앤다.
      home: DefaultTabController(
        animationDuration: Duration.zero,
        length: 5,
        child: Scaffold(
          bottomNavigationBar: !isTablet ? const BottomNavigation() : null,
          body:
              isTablet ? const BuildTabletLayout() : const BuildMobileLayout(),
        ),
      ),
    );
  }
}

 

 

 

 

 


패키지 불러오기

import 'package:goodwishes/Models/category_model.dart';
import 'package:goodwishes/Models/goods_model.dart';
import 'package:goodwishes/Models/profile_model.dart';
import 'package:goodwishes/Models/wish_category_model.dart';
import 'package:goodwishes/Models/wish_model.dart';

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

사용할 데이터 모델과, Hive Flutter 패키지를 import 합니다.

 

 

 

 

 


비동기 main & Hive 초기화

Future<void> main() async {

Hive 데이터를 비동기로 불러오기 위해, main 함수를 Future 비동기 함수로 변환합니다.

 

 

 

  await Hive.initFlutter();

Hive 데이터 베이스 연동을 위해, 초기화 작업을 합니다.

 

 

 

 

 


Hive 데이터 베이스에 생성한 클래스 모델 등록

  Hive.registerAdapter(GoodsAdapter());
  Hive.registerAdapter(WishAdapter());
  Hive.registerAdapter(CategoryAdapter());
  Hive.registerAdapter(ProfileAdapter());

아까 g.dart파일에서 '모델이름Adapter' class가 생성된 것을 확인했었습니다.

Hive.registerAdapter() 함수에 모두 넘겨줍니다.

이 과정이 있어야, 저장된 Goods 인스턴스들을 다시 플러터 앱의 Goods 모델에 그대로 적용해서 불러올 수 있습니다.

 

원시타입으로 저장할 경우 위의 과정은 필요 없습니다.

 

 

 

 


Hive Box 열기

Hive는 Box 단위로 저장을 합니다. 그리고 이 Box를 열람해서 데이터를 가져와야 합니다.

 

Future<void> _openHiveBoxes() async {
  List<Future> futures = [
    Hive.openBox<Goods>('goodsListBox'),
    Hive.openBox<Wish>('wishListBox'),
    Hive.openBox<Category>('categoryListBox'),
    Hive.openBox<Profile>('profileBox'),
    Hive.openBox<Category>('wishCategoryListBox'),
  ];
  await Future.wait(futures);
}

이 앱은 모델이 여러 개이고, 열어야 할 박스도 여러 개여서, 위와 같은 함수를 따로 만들었습니다.

 

 

Hive.openBox<모델이름 - 저장할 값의 타입>('박스 이름')

위 형태를 통해, '저장할 모델'과 '저장되고 불러올 박스의 이름'을 생성/지정할 수 있습니다.

 

 

 

 

 


앱을 처음 켰을 때를 대비하기

  var categories = Hive.box<Category>('categoryListBox');
  if (categories.get('no_category') == null) {
    categories.put('no_category',
        Category(id: 'no_category', categoryName: '카테고리 없음', count: 0));
  }
  var wishCategories = Hive.box<Category>('wishCategoryListBox');
  if (wishCategories.get('no_category') == null) {
    wishCategories.put('no_category',
        Category(id: 'no_category', categoryName: '카테고리 없음', count: 0));
  }

  var profile = Hive.box<Profile>('profileBox');
  if (profile.get('profile') == null) {
    profile.put('profile', Profile([], true));
  }

앱을 처음 킨 상태라, 카테고리와 프로필이 생성되지 않았을 경우, 초기값을 자동으로 넣어주는 코드입니다.

 

 

 

 

 


Provider에서 생성한 Box를 참조하기

본 앱은 전역 변수 관리 패키지인 Provider를 사용하고 있습니다.

이를 HiveBox와 연결하는 방법에 대해 소개하겠습니다.

 

 

설명을 위해 Category 모델을 가져왔습니다.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';

part 'category_model.g.dart';

@HiveType(typeId: 1)
class Category {
  @HiveField(0)
  String id;

  @HiveField(1)
  String categoryName;

  @HiveField(2)
  int count;

  Category({
    required this.id,
    required this.categoryName,
    required this.count,
  });
}

class CategoryListProvider with ChangeNotifier {
  late Box<Category> _categoryListBox;

  CategoryListProvider() {
    _initBox();
  }

  Future<void> _initBox() async {
    _categoryListBox = Hive.box<Category>('categoryListBox');
    notifyListeners();
  }

  Iterable<Category> get categoryList => _categoryListBox.values;

  void addCategory(Category element) {
    _categoryListBox.put(element.id, element);
    notifyListeners(); // 값 변경 후 상태 변경 알림
  }

  removeCategory(String id) {
    for (var categoryItem in _categoryListBox.values) {
      if (categoryItem.id == id) {
        if (categoryItem.count <= 0) {
          _categoryListBox.delete(id);
          notifyListeners();
          return true;
        } else {
          notifyListeners();
          return false;
        }
      }
    }
  }

  void upCountCategory(String categoryName) {
    for (var categoryItem in _categoryListBox.values) {
      if (categoryItem.categoryName == categoryName) {
        categoryItem.count = categoryItem.count + 1;
        _categoryListBox.put(categoryItem.id, categoryItem); 
        return;
      }
    }
    notifyListeners();
  }

  void downCountCategory(String categoryName) {
    for (var categoryItem in _categoryListBox.values) {
      if (categoryItem.categoryName == categoryName) {
        categoryItem.count = categoryItem.count - 1;
        _categoryListBox.put(categoryItem.id, categoryItem); 
        return; 
      }
    }
    notifyListeners();
  }

  // 카테고리 변경할 때 카운트 이동
  void rewriteCount(String beforeCate, String afterCate) {
    for (var categoryItem in _categoryListBox.values) {
      if (categoryItem.categoryName == beforeCate) {
        categoryItem.count = categoryItem.count - 1;
        _categoryListBox.put(categoryItem.id, categoryItem); 
        return;
      }

      if (categoryItem.categoryName == afterCate) {
        categoryItem.count = categoryItem.count + 1;
        _categoryListBox.put(categoryItem.id, categoryItem); 
        return;
      }
    }
    notifyListeners();
  }
}

 

 

 

 

_categoryListBox = Hive.box<Category>('categoryListBox');
// _categoryListBox 변수에 카테고리 정보가 담긴 Hive Box를 init

Provider를 통해 CategoryListProvider가 초기화될 때, _initBox라는 함수를 통해서 categoryList가 담긴 Hive Box를 불러옵니다.

 

 

 

_categoryListBox는 Box라는 데이터로 저장되어 있습니다.

 

 

  Iterable<Category> get categoryList => _categoryListBox.values;

외부 위젯 중에 categoryList 전체를 원할 경우를 대비해서, categoryList 변수를 생성합니다.

get과 Box.values를 이용해서 내부 값에 접근하면 Iterable<Category> 타입의 데이터를 확인할 수 있습니다. 이걸 categoryList에 저장합니다.

 

 

 

  void addCategory(Category element) {
    _categoryListBox.put(element.id, element);
    notifyListeners(); // 값 변경 후 상태 변경 알림
  }

Box.put(key, value)

Hive 박스에 <key : value> 형태로 저장합니다.

notifyListeners()를 통해 Provider가 변경된 Hive 박스 데이터를 확인하고, 앱 전역으로 다시 state를 뿌립니다.

 

 

 

 

  removeCategory(String id) {
    for (var categoryItem in _categoryListBox.values) {
      if (categoryItem.id == id) {
        if (categoryItem.count <= 0) {
          _categoryListBox.delete(id);
          notifyListeners();
          return true;
        } else {
          notifyListeners();
          return false;
        }
      }
    }
  }

Box.delete(key)

박스에 지정된 키의 값을 삭제합니다.

만약, 해당되는 키의 값이 없다면 아무 작업도 수행하지 않습니다.

 

 

 

 

 

 void upCountCategory(String categoryName) {
    for (var categoryItem in _categoryListBox.values) {
      if (categoryItem.categoryName == categoryName) {
        categoryItem.count = categoryItem.count + 1;
        _categoryListBox.put(categoryItem.id, categoryItem);
        
        return; // break를 안 걸면, 반복중에 리스트 요소가 사라진 탓에, 인덱스 탐색에 오류가 발생한다. (꼬임이 발생)
      }
    }
    notifyListeners();
  }

Box.put(key, value)

put은 데이터를 추가하는 기능 뿐만 아니라, 수정하는 기능도 갖췄습니다.

만약 이미 해당 키의 값이 존재할 경우, 기존 데이터를 덮어씌워줍니다.

 

만약 반복문 중에 Box내 데이터를 수정한 후 break/return을 안 걸면, 오류가 발생합니다.

반복문 중에 put이나 delete를 하면, 갑자기 element가 사라졌다가 나타난 탓에 반복문의 인덱스 탐색과정에서 꼬임이 발생합니다.

게다가 성능상으로도 낭비가 있으니 습관적으로 break/return을 해줍니다.

 

 

 

끝.

 

 

 

반응형