Skip to content
피스타치오는 맛있어
Instagram

플러터(Flutter)의 상태와 영속성

개발, , flutter1 min read

이 글은 시리즈 글입니다.

  1. 플러터(Flutter)로 앱개발 시작하기
  2. 플러터(Flutter)로 캘린더 기반 메모앱 만들기
  3. 플러터(Flutter)의 상태와 영속성
  4. 플러터(Flutter) Splash 화면 만들기

또 다시 쓸만한 라이브러리를 찾아서

지난 글에서 캘린더 메모앱의 UI를 거의 다 만들었지만 실제로 동작하는 기능은 하나도 없었습니다. 이제 날짜를 선택하고, 그 날의 메모를 적어넣는 기능을 만들어 보려 합니다. State 로 이런 내용들을 관리했다간 앱을 껐다가 켤 때마다 메모들이 몽땅 날아갈 것이기 떄문에 영속성을 보장하는 저장소를 이용할 생각입니다. Cloud Firestore 같은 걸 써볼 수도 있겠지만, 뭐 나중에 쓰게 되더라도 앱 안에선 영속화된 로컬 저장소를 쓰고, 나중에 백업이 필요하면 저장소를 덤프해서 클라우드에 저장하면 되지 않나 싶습니다. 합리화일수도 있겠지만 네트워크가 연결된 상황이 아니더라도 계속 서비스를 이용할 수 있다는 장점도 하나 챙겨가겠네요.

Hive vs sqflite

sqflitehive 사이에서 고민을 좀 했습니다. 가장 두드러지는 차이점은 hive는 NoSQL 이고, sqflite 는 RDB라는 점입니다. 결국 선택한 것은 hive 인데, hive가 더 좋아서라기보단 sqflite는 RDB다 보니 테이블 스키마도 확장성을 신경써서 설계해야 하고, 변경될 때마다 migration에 계속 신경을 써주어야 하는 게 부담스러웠기 때문입니다.

라이브러리 의존성 추가하기

다음과 같이 hive 의존성을 추가하고 flutter pub get 으로 의존성들을 내려 받았습니다.

pubspec.yaml
1dependencies:
2 flutter:
3 sdk: flutter
4
5 # hive database.
6 hive: ^2.0.3
7 hive_flutter: ^1.0.0
8
9...
10
11dev_dependencies:
12 flutter_test:
13 sdk: flutter
14
15 # hive database generators.
16 build_runner: ^1.12.2
17 hive_generator: ^1.0.1

그리고 나서 main()함수를 다음과 같이 수정해 줍니다.

main.dart
1Future main() async {
2 WidgetsFlutterBinding.ensureInitialized();
3 await Hive.initFlutter();
4 initializeDateFormatting().then((_) => runApp(const MyApp()));
5}

Memo Model 도입하기

문서를 참고해 아주 간단한 Memo라는 모델을 만들었습니다. 메모를 특정할 수 있는 ID는 날짜가 될 것 같아서 createKey라는 static 함수도 같이 추가했습니다.

memo.dart
1@HiveType(typeId: 0)
2class Memo extends HiveObject {
3 @HiveField(0)
4 late String title;
5
6 @HiveField(1)
7 late String content;
8
9 @HiveField(2)
10 late DateTime date;
11
12 @HiveField(3)
13 late DateTime createdAt;
14
15 static Memo createNew(DateTime targetDate) {
16 return Memo()
17 ..title = DateFormat('yyyy-MM-dd').format(targetDate)
18 ..content = ''
19 ..date = targetDate
20 ..createdAt = DateTime.now();
21 }
22
23 static String createKey(DateTime targetDate) {
24 return DateFormat('yyyy-MM-dd').format(targetDate);
25 }
26}

모델을 선언한 뒤에는 프로젝트 루트 경로로 가서 flutter packages pub run build_runner build 를 실행시켜 줍니다. 그러면 우리가 앞서 설치한 build_runnermemo.dart파일과 같은 위치에 part 로 선언한 memo.g.dart가 생성 해주고, 그 안에 MemoAdapter를 선언합니다. 선언된 adapter는 Hive에 다음과 같이 등록해주어야 합니다.

main.dart
1Future main() async {
2 WidgetsFlutterBinding.ensureInitialized();
3 await Hive.initFlutter();
4
5 Hive.registerAdapter(MemoAdapter());
6 await Hive.openBox<Memo>('memos');
7
8 initializeDateFormatting().then((_) => runApp(const MyApp()));
9}

Hive 로 읽고 쓰기

우선 Hive.box는 코드를 따라가보니 이미 인스턴스가 생성되어 있다면 재활용하도록 되어 있었기 때문에 간편하게 Singleton으로 만들면 좋겠다는 생각이 들었습니다. 아래처럼 Boxes라는 클래스를 선언해줍니다.

boxes.dart
1class Boxes {
2 static Box<Memo> memos() => Hive.box<Memo>('memos');
3
4 static void closeMemos() => Hive.box<Memo>('memos').close();
5
6 static Memo getMemo(DateTime targetDate) =>
7 memos().get(Memo.createKey(targetDate),
8 defaultValue: Memo.createNew(targetDate))!;
9
10 static void addMemo(Memo memo) =>
11 Boxes.memos().put(Memo.createKey(memo.date), memo);
12}

PostPage가 파라미터로 메모 객체와 콜백을 받도록 해 메모가 업데이트되면 부모 위젯인 HomePage가 알 수 있도록 했고, HomePage는 변경된 메모를 저장하고 캘린더에서 날짜 선택이 이루어질 때마다 화면에 보여지도록 했습니다.

home_page.dart
1class _HomePageState extends State<HomePage> {
2 Memo _currentMemo = Boxes.getMemo(DateTime.now());
3
4 @override
5 void dispose() {
6 // 문서에서 하라는 대로 더 이상 memo를 안쓸 것 같으면 close 해줍니다.
7 Boxes.closeMemos();
8 super.dispose();
9 }
10
11 void onDateTargeted(DateTime targetDate) {
12 setState(() {
13 // 날짜가 선택되면 Box에서 새로 가져옵니다.
14 _currentMemo = Boxes.getMemo(targetDate);
15 });
16 }
17
18 void onSaved(Memo memo) {
19 // PostPage 에서 Save를 눌렀다면 Box에 저장합니다.
20 Boxes.addMemo(memo);
21 setState(() {
22 // 현재 보여지는 화면에서 변경된 메모를 바로 보여주기 위해 setState에서 업데이트 해줍니다.
23 _currentMemo = memo;
24 });
25 }
26
27 @override
28 Widget build(BuildContext context) {
29 return Scaffold(
30 appBar: AppBar(
31 backgroundColor: Colors.transparent,
32 shadowColor: Colors.transparent,
33 ),
34 body: Center(
35 child: Column(
36 mainAxisAlignment: MainAxisAlignment.start,
37 children: <Widget>[
38 Calendar(
39 onDateSelected: onDateTargeted,
40 ),
41 Expanded(
42 child: SingleChildScrollView(
43 scrollDirection: Axis.vertical,
44 child: Container(
45 margin: const EdgeInsets.all(20.0),
46 child: Row(
47 children: [
48 Text(
49 _currentMemo.content,
50 style: const TextStyle(height: 1, fontSize: 16),
51 ),
52 ],
53 )),
54 ),
55 )
56 ],
57 ),
58 ),
59 floatingActionButton: FloatingActionButton(
60 onPressed: () {
61 Navigator.push(
62 context,
63 MaterialPageRoute(
64 builder: (context) =>
65 PostPage(memo: _currentMemo, onSaved: onSaved)));
66 },
67 backgroundColor: Colors.blueGrey,
68 child: const Icon(Icons.mode_edit),
69 ),
70 );
71 }
72}

실행 결과 입니다.

© 2023 by 피스타치오는 맛있어. All rights reserved.