휘발되는 데이터
지금까지 개발한 과정만 놓고 보면, 굿즈를 일시적으로 Provider에 저장하더라도 앱을 껐다키면 모든 데이터가 날라갑니다.
따라서, 지금까지 기록한 내용을 스마트폰의 내부 저장소에 저장한 후 불러오는 방식이 필요합니다.
이렇게 데이터 지속성을 보장하기 위한 라이브러리가 Flutter에 있습니다.
바로 Hive입니다.
Hive 공식 링크 : https://pub.dev/packages/hive
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을 해줍니다.
끝.
'개발일지 > GoodWishes' 카테고리의 다른 글
[굿위시 제작기] 16. 구글 플레이스토어 출시과정 - 회원가입과 앱 콘텐츠 정보 등록 (6) | 2024.11.14 |
---|---|
[굿위시 제작기] 15. 프로필 페이지 제작하기 (0) | 2024.11.08 |
[굿위시 제작기] 13. 즐겨찾기 페이지 제작하기 (3) | 2024.11.05 |
[굿위시 제작기] 12. 굿즈 검색 페이지 제작하기 (0) | 2024.11.03 |
[굿위시 제작기] 11. 굿즈 추가 페이지 제작과정 - 굿즈와 위시 변경 버튼 (0) | 2024.10.31 |