플러터(Flutter)의 상태와 영속성
이 글은 시리즈 글입니다.
또 다시 쓸만한 라이브러리를 찾아서
지난 글에서 캘린더 메모앱의 UI를 거의 다 만들었지만 실제로 동작하는 기능은 하나도 없었습니다.
이제 날짜를 선택하고, 그 날의 메모를 적어넣는 기능을 만들어 보려 합니다.
State
로 이런 내용들을 관리했다간 앱을 껐다가 켤 때마다 메모들이 몽땅 날아갈 것이기 떄문에 영속성을 보장하는 저장소를 이용할 생각입니다.
Cloud Firestore 같은 걸 써볼 수도 있겠지만, 뭐 나중에 쓰게 되더라도 앱 안에선 영속화된 로컬 저장소를 쓰고,
나중에 백업이 필요하면 저장소를 덤프해서 클라우드에 저장하면 되지 않나 싶습니다.
합리화일수도 있겠지만 네트워크가 연결된 상황이 아니더라도 계속 서비스를 이용할 수 있다는 장점도 하나 챙겨가겠네요.
Hive vs sqflite
sqflite 와 hive 사이에서 고민을 좀 했습니다.
가장 두드러지는 차이점은 hive
는 NoSQL 이고, sqflite
는 RDB라는 점입니다.
결국 선택한 것은 hive
인데, hive
가 더 좋아서라기보단 sqflite
는 RDB다 보니 테이블 스키마도 확장성을 신경써서 설계해야 하고,
변경될 때마다 migration에 계속 신경을 써주어야 하는 게 부담스러웠기 때문입니다.
라이브러리 의존성 추가하기
다음과 같이 hive
의존성을 추가하고 flutter pub get
으로 의존성들을 내려 받았습니다.
1dependencies:2 flutter:3 sdk: flutter4
5 # hive database.6 hive: ^2.0.37 hive_flutter: ^1.0.08 9...10
11dev_dependencies:12 flutter_test:13 sdk: flutter14
15 # hive database generators.16 build_runner: ^1.12.217 hive_generator: ^1.0.1
그리고 나서 main()
함수를 다음과 같이 수정해 줍니다.
1Future main() async {2 WidgetsFlutterBinding.ensureInitialized();3 await Hive.initFlutter();4 initializeDateFormatting().then((_) => runApp(const MyApp()));5}
Memo Model 도입하기
문서를 참고해 아주 간단한 Memo
라는 모델을 만들었습니다.
메모를 특정할 수 있는 ID는 날짜가 될 것 같아서 createKey
라는 static 함수도 같이 추가했습니다.
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 = targetDate20 ..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_runner
가 memo.dart
파일과 같은 위치에 part
로 선언한 memo.g.dart
가 생성 해주고, 그 안에 MemoAdapter
를 선언합니다.
선언된 adapter는 Hive
에 다음과 같이 등록해주어야 합니다.
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
라는 클래스를 선언해줍니다.
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
는 변경된 메모를 저장하고 캘린더에서 날짜 선택이 이루어질 때마다 화면에 보여지도록 했습니다.
1class _HomePageState extends State<HomePage> {2 Memo _currentMemo = Boxes.getMemo(DateTime.now());3
4 @override5 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 @override28 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}
실행 결과 입니다.