🦋
【Flutter】Riverpod / MVVM / SQLite で「メモ検索アプリ」をつくる
最近 Riverpod
を触り始めたので、以前作成した メモ検索アプリ を Riverpod
に置き換えてみました。
GitHub : yass97/flutter_memo_app
スクリーンショット
メモ一覧 | メモ登録・編集 | メモ検索 |
---|---|---|
環境
Flutter 3.13.9 • channel stable • https://github.com/flutter/flutter.git
Framework • revision d211f42860 (2 weeks ago) • 2023-10-25 13:42:25 -0700
Engine • revision 0545f8705d
Tools • Dart 3.1.5 • DevTools 2.25.0
pubspec.yaml
dependencies:
...
flutter_riverpod: ^2.4.5
riverpod_annotation: ^2.3.0
hooks_riverpod: ^2.4.5
flutter_hooks: ^0.20.3
dev_dependencies:
...
build_runner: ^2.4.6
riverpod_generator: ^2.3.5
実装(例: メモ一覧)
app_database.dart
import 'package:path/path.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sqflite/sqflite.dart';
part 'app_database.g.dart';
AppDatabase appDatabase(AppDatabaseRef ref) => AppDatabase();
class AppDatabase {
Database? _db;
Future<Database> get db async {
if (_db != null) return _db!;
_db = await _init();
return _db!;
}
final queries = [
[
'CREATE TABLE Memos(id TEXT PRIMARY KEY, title TEXT, description TEXT, created_at TEXT, updated_at TEXT)'
]
];
Future<Database> _init() async {
final path = join(await getDatabasesPath(), 'memo.db');
return await openDatabase(
path,
version: queries.length,
onCreate: (db, version) async {
for (final query in queries.first) {
await db.execute(query);
}
},
onUpgrade: (Database db, int oldVersion, int newVersion) async {
for (var i = oldVersion; i < newVersion; i++) {
for (final query in queries[i]) {
await db.execute(query);
}
}
},
);
}
}
memo_dao.dart
import 'package:flutter_memo_app/data/local/app_database.dart';
import 'package:flutter_memo_app/data/entity/memo.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'memo_dao.g.dart';
MemoDao memoDao(MemoDaoRef ref) => MemoDao(ref.read(appDatabaseProvider));
class MemoDao {
const MemoDao(this._database);
final AppDatabase _database;
Future<List<Memo>> loadAll() async {
final db = await _database.db;
final data = await db.query('Memos', orderBy: 'updated_at DESC');
return data.isEmpty ? [] : data.map((e) => Memo.fromJson(e)).toList();
}
Future<List<Memo>> search(String keyword) async {
final db = await _database.db;
final data = await db.query(
'Memos',
where: 'title LIKE ?',
whereArgs: ['%$keyword%'],
orderBy: 'updated_at DESC',
);
return data.isEmpty ? [] : data.map((e) => Memo.fromJson(e)).toList();
}
...
}
memo_repository.dart
import 'package:flutter_memo_app/data/entity/memo.dart';
import 'package:flutter_memo_app/data/local/memo_dao.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'memo_repository.g.dart';
MemoRepository memoRepository(MemoRepositoryRef ref) {
return MemoRepository(ref.read(memoDaoProvider));
}
class MemoRepository {
const MemoRepository(this._dao);
final MemoDao _dao;
Future<List<Memo>> loadAll() async {
return _dao.loadAll();
}
Future<List<Memo>> search(String keyword) async {
return _dao.search(keyword);
}
...
}
memo_list_viewmodel.dart
import 'package:flutter_memo_app/data/entity/memo.dart';
import 'package:flutter_memo_app/data/repository/memo_repository.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'memo_list_viewmodel.g.dart';
class MemoListViewModel extends _$MemoListViewModel {
List<Memo> build() => [];
Future<void> loadAll() async {
state = await ref.read(memoRepositoryProvider).loadAll();
}
Future<void> search(String keyword) async {
state = await ref.read(memoRepositoryProvider).search(keyword);
}
void add(Memo memo) => state = <Memo>[memo, ...state];
void update(Memo memo) {
state = state.map((m) => (m.id == memo.id) ? memo : m).toList();
}
void delete(String id) {
state = state.where((m) => m.id != id).toList();
}
}
memo_list_screen.dart
class MemoListScreen extends HookConsumerWidget {
const MemoListScreen({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final memos = ref.watch(memoListViewModelProvider);
final isLoading = useState<bool>(true);
useEffect(
() {
final notifier = ref.read(memoListViewModelProvider.notifier);
notifier.loadAll().then((_) => isLoading.value = false);
return null;
},
const [],
);
return Scaffold(
appBar: AppBar(
title: TextField(
style: const TextStyle(
color: Colors.white,
),
cursorColor: Colors.white,
decoration: const InputDecoration(
prefixIcon: Icon(
Icons.search,
color: Colors.white,
),
hintText: 'Title',
hintStyle: TextStyle(color: Colors.white),
border: InputBorder.none,
),
onChanged: (keyword) async {
isLoading.value = true;
await ref.read(memoListViewModelProvider.notifier).search(keyword);
isLoading.value = false;
},
),
),
body: isLoading.value
? const Center(child: CircularProgressIndicator())
: memos.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.sticky_note_2_outlined, size: 42),
SizedBox(height: 10),
Text('No memo...')
],
),
)
: ListView.separated(
separatorBuilder: (context, index) {
return const Divider(height: 1);
},
itemCount: memos.length,
itemBuilder: (context, index) {
final memo = memos[index];
return InkWell(
onTap: () {
Navigator.of(context).push(MemoEditScreen.route(memo));
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 10),
Text(
memo.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
memo.description,
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: Text(
memo.updatedAt.format(),
style: const TextStyle(fontSize: 12),
),
),
const SizedBox(height: 10),
],
),
),
);
},
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () async {
final route = MemoRegistrationScreen.route();
Navigator.of(context).push(route);
},
),
);
}
}
Discussion