作成するアプリ
検索バーに入力されたキーワードで、メモのタイトル検索を行うアプリです。
検索の他に、メモの登録・編集・削除を行うことが出来ます。
実装
- Flutter: 2.0.6
- Dart: 2.12.3
パッケージ構成
├── lib
│ ├── main.dart
│ ├── model
│ │ ├── db
│ │ │ └── app_database.dart
│ │ ├── entity
│ │ │ └── memo.dart
│ │ └── repository
│ │ └── memo_repository.dart
│ └── ui
│ ├── memo_detail (メモ登録・編集・削除画面)
│ │ ├── memo_detail.dart
│ │ └── memo_detail_view_model.dart
│ └── memo_list (メモ一覧画面)
│ ├── memo_list.dart
│ └── memo_list_view_model.dart
├── pubspec.yaml
まず、pubspec.yaml
にライブラリを追加します。
pubspec.yaml
dependencies:
flutter:
sdk: flutter
# 追加: memo.dartの「DateFormat」で使用する
flutter_localizations:
sdk: flutter
# 以下を追加
sqflite: ^2.0.0+3
# app_database.dartの「join」で使用する
path: 1.8.0
provider: ^5.0.0
# Memoのid生成で使用する
uuid: ^3.0.4
model
modelパッケージのapp_database.dart
、memo.dart
、memo_repository.dart
を作成します。
app_database.dart
import 'package:path/path.dart';
import 'package:search_bar_sample_app/model/entity/memo.dart';
import 'package:sqflite/sqflite.dart';
class AppDatabase {
final String _tableName = 'Memo';
final String _columnId = 'id';
final String _columnTitle = 'title';
final String _columnContent = 'content';
final String _columnCreatedAt = 'created_at';
Database _database;
Future<Database> get database async {
if (_database != null) return _database;
_database = await _initDB();
return _database;
}
Future<Database> _initDB() async {
String path = join(await getDatabasesPath(), 'memo.db');
return await openDatabase(
path,
version: 1,
onCreate: _createTable,
);
}
Future<void> _createTable(Database db, int version) async {
String sql = '''
CREATE TABLE $_tableName(
$_columnId TEXT PRIMARY KEY,
$_columnTitle TEXT,
$_columnContent TEXT,
$_columnCreatedAt TEXT
)
''';
return await db.execute(sql);
}
Future<List<Memo>> loadAllMemo() async {
final db = await database;
var maps = await db.query(
_tableName,
orderBy: '$_columnCreatedAt DESC',
);
if (maps.isEmpty) return [];
return maps.map((map) => fromMap(map)).toList();
}
Future<List<Memo>> search(String keyword) async {
final db = await database;
var maps = await db.query(
_tableName,
orderBy: '$_columnCreatedAt DESC',
where: '$_columnTitle LIKE ?',
whereArgs: ['%$keyword%'],
);
if (maps.isEmpty) return [];
return maps.map((map) => fromMap(map)).toList();
}
Future insert(Memo memo) async {
final db = await database;
return await db.insert(_tableName, toMap(memo));
}
Future update(Memo memo) async {
final db = await database;
return await db.update(
_tableName,
toMap(memo),
where: '$_columnId = ?',
whereArgs: [memo.id],
);
}
Future delete(Memo memo) async {
final db = await database;
return await db.delete(
_tableName,
where: '$_columnId = ?',
whereArgs: [memo.id],
);
}
Map<String, dynamic> toMap(Memo memo) {
return {
_columnId: memo.id,
_columnTitle: memo.title,
_columnContent: memo.content,
_columnCreatedAt: memo.createdAt.toUtc().toIso8601String()
};
}
Memo fromMap(Map<String, dynamic> json) {
return Memo(
id: json[_columnId],
title: json[_columnTitle],
content: json[_columnContent],
createdAt: DateTime.parse(json[_columnCreatedAt]).toLocal(),
);
}
}
memo.dart
import 'package:intl/intl.dart';
class Memo {
String id;
String title;
String content;
DateTime createdAt;
String getContent() {
String cont = content.replaceAll('\n', ' ');
if (cont.length <= 10) return cont;
return '${cont.substring(0, 10)}...';
}
String getCreatedAt() {
try {
// 曜日を表示したいときは「'yyyy/MM/dd(E) HH:mm:ss'」
var fomatter = DateFormat('yyyy/MM/dd HH:mm:ss', 'ja_JP');
return fomatter.format(createdAt);
} catch (e) {
print(e);
return '';
}
}
Memo({
this.id,
this.title,
this.content,
this.createdAt,
});
}
memo_repository.dart
import 'package:search_bar_sample_app/model/db/app_database.dart';
import 'package:search_bar_sample_app/model/entity/memo.dart';
class MemoRepository {
final AppDatabase _appDatabase;
MemoRepository(this._appDatabase);
Future<List<Memo>> loadAllMemo() => _appDatabase.loadAllMemo();
Future<List<Memo>> search(String keyword) => _appDatabase.search(keyword);
Future insert(Memo memo) => _appDatabase.insert(memo);
Future update(Memo memo) => _appDatabase.update(memo);
Future delete(Memo memo) => _appDatabase.delete(memo);
}
ui
次は「メモ一覧画面」「メモ登録・編集・削除画面」を作成します。
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:search_bar_sample_app/ui/memo_list/memo_list.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Search Bar App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: <String, WidgetBuilder>{
'/': (_) => MemoList(),
},
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('ja'),
],
);
}
}
メモ一覧画面
memo_list.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:search_bar_sample_app/ui/memo_detail/memo_detail.dart';
import 'package:search_bar_sample_app/ui/memo_list/memo_list_view_model.dart';
import 'package:search_bar_sample_app/model/db/app_database.dart';
import 'package:search_bar_sample_app/model/entity/memo.dart';
import 'package:search_bar_sample_app/model/repository/memo_repository.dart';
class MemoList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final vm = MemoListViewModel(MemoRepository(AppDatabase()));
final page = _MemoListPage();
return ChangeNotifierProvider(
create: (_) => vm,
child: Scaffold(
// AppBarにTextFieldを配置することで、検索バーになる
appBar: AppBar(
title: TextField(
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
prefixIcon: Icon(Icons.search, color: Colors.white),
hintText: 'タイトルを検索',
hintStyle: const TextStyle(color: Colors.white),
),
onChanged: (value) => vm.search(value),
),
),
backgroundColor: Color(0xffF2F2F2),
body: page,
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
// メモ登録画面に遷移する
onPressed: () => page.goToMemoDetailScreen(context, null),
),
),
);
}
}
class _MemoListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final vm = Provider.of<MemoListViewModel>(context);
if (vm.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (vm.memos.isEmpty) {
return const Center(child: const Text('メモが登録されていません'));
}
return ListView.builder(
itemCount: vm.memos.length,
itemBuilder: (BuildContext context, int index) {
var memo = vm.memos[index];
return _buildMemoListTile(context, memo);
},
);
}
Widget _buildMemoListTile(BuildContext context, Memo memo) {
return Card(
child: ListTile(
title: Text(
memo.title,
style: TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(memo.getContent()),
trailing: Text(memo.getCreatedAt()),
// メモ編集・削除画面に遷移する
onTap: () => goToMemoDetailScreen(context, memo),
),
);
}
void goToMemoDetailScreen(BuildContext context, Memo memo) {
var route = MaterialPageRoute(
settings: RouteSettings(name: '/ui.memo_detail'),
builder: (BuildContext context) => MemoDetail(memo),
);
Navigator.push(context, route);
}
}
memo_list_view_model.dart
import 'package:flutter/material.dart';
import 'package:search_bar_sample_app/model/entity/memo.dart';
import 'package:search_bar_sample_app/model/repository/memo_repository.dart';
class MemoListViewModel extends ChangeNotifier {
final MemoRepository _repository;
MemoListViewModel(this._repository) {
loadAllMemo();
}
List<Memo> _memos = [];
List<Memo> get memos => _memos;
bool _isLoading = false;
bool get isLoading => _isLoading;
void loadAllMemo() async {
_startLoading();
_memos = await _repository.loadAllMemo();
_finishLoading();
}
void search(String keyword) async {
_startLoading();
_memos = await _repository.search(keyword);
_finishLoading();
}
void _startLoading() {
_isLoading = true;
notifyListeners();
}
void _finishLoading() {
_isLoading = false;
notifyListeners();
}
}
メモ登録・編集・削除画面
memo_detail.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:search_bar_sample_app/model/db/app_database.dart';
import 'package:search_bar_sample_app/model/entity/memo.dart';
import 'package:search_bar_sample_app/model/repository/memo_repository.dart';
import 'package:search_bar_sample_app/ui/memo_detail/memo_detail_view_model.dart';
class MemoDetail extends StatelessWidget {
final Memo _memo;
MemoDetail(this._memo);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MemoDetailViewModel(_memo, MemoRepository(AppDatabase())),
child: _MemoDetailPage(),
);
}
}
class _MemoDetailPage extends StatelessWidget {
final GlobalKey<FormState> _globalKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
final vm = Provider.of<MemoDetailViewModel>(context);
return Scaffold(
appBar: AppBar(
title: Text(vm.isNew ? '登録' : '編集'),
actions: [
IconButton(
icon: Icon(Icons.save),
onPressed: () => _showSaveOrUpdateDialog(context),
),
IconButton(
icon: Icon(Icons.delete),
onPressed: vm.isNew ? null : () => _showDeleteDialog(context),
),
],
),
body: SafeArea(
child: Form(
key: _globalKey,
child: ListView(
padding: EdgeInsets.all(15),
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'タイトル'),
initialValue: vm.isNew ? '' : vm.memo.title,
validator: (value) => (value.isEmpty) ? 'タイトルを入力して下さい' : null,
onChanged: (value) => vm.setTitle(value),
),
Padding(
padding: EdgeInsets.only(top: 20),
child: TextFormField(
decoration: InputDecoration(labelText: 'メモ'),
keyboardType: TextInputType.multiline,
maxLines: null,
initialValue: vm.isNew ? '' : vm.memo.content,
onChanged: (value) => vm.setContent(value),
),
),
Padding(
padding: EdgeInsets.only(top: 20),
child: Align(
alignment: Alignment.centerRight,
child: Text('${vm.contentCounts} 文字'),
),
),
],
),
),
),
);
}
void _showSaveOrUpdateDialog(BuildContext context) {
if (!_globalKey.currentState.validate()) return;
var vm = Provider.of<MemoDetailViewModel>(context, listen: false);
bool isNew = vm.isNew;
String saveOrUpdateText = (isNew ? '保存' : '更新');
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text('メモを$saveOrUpdateTextしますか?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('キャンセル'),
),
TextButton(
onPressed: () =>
isNew ? _save(context, vm) : _update(context, vm),
child: Text(saveOrUpdateText),
),
],
);
},
);
}
void _showDeleteDialog(BuildContext context) {
var vm = Provider.of<MemoDetailViewModel>(context, listen: false);
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: const Text('メモを削除しますか?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('キャンセル'),
),
TextButton(
onPressed: () => _delete(context, vm),
child: const Text('削除'),
),
],
);
},
);
}
void _save(BuildContext context, MemoDetailViewModel vm) async {
_showIndicator(context);
await vm.save();
_goToMemoListScreen(context);
}
void _update(BuildContext context, MemoDetailViewModel vm) async {
_showIndicator(context);
await vm.update();
_goToMemoListScreen(context);
}
void _delete(BuildContext context, MemoDetailViewModel vm) async {
_showIndicator(context);
await vm.delete();
_goToMemoListScreen(context);
}
void _showIndicator(BuildContext context) {
showGeneralDialog(
context: context,
barrierDismissible: false,
transitionDuration: Duration(milliseconds: 300),
barrierColor: Colors.black.withOpacity(0.5),
pageBuilder: (
BuildContext context,
Animation animation,
Animation secondaryAnimation,
) {
return Center(child: CircularProgressIndicator());
},
);
}
void _goToMemoListScreen(BuildContext context) {
Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false);
}
}
memo_detail_view_model.dart
import 'package:flutter/material.dart';
import 'package:search_bar_sample_app/model/entity/memo.dart';
import 'package:search_bar_sample_app/model/repository/memo_repository.dart';
import 'package:uuid/uuid.dart';
class MemoDetailViewModel extends ChangeNotifier {
final MemoRepository _repository;
MemoDetailViewModel(memo, this._repository) {
_memo = memo ?? initMemo();
_isNew = (memo == null);
_contentCounts = _memo.content.length;
notifyListeners();
}
Memo _memo;
Memo get memo => _memo;
bool _isNew;
bool get isNew => _isNew;
int _contentCounts = 0;
int get contentCounts => _contentCounts;
Memo initMemo() {
return Memo(
id: Uuid().v4(),
title: '',
content: '',
createdAt: null
);
}
void setTitle(String title) {
_memo.title = title;
notifyListeners();
}
void setContent(String content) {
_memo.content = content;
_contentCounts = content.length;
notifyListeners();
}
Future save() async {
_memo.createdAt = DateTime.now();
return await _repository.insert(_memo);
}
Future update() async {
_memo.createdAt = DateTime.now();
return await _repository.update(_memo);
}
Future delete() async => _repository.delete(_memo);
}