Skip to content

Instantly share code, notes, and snippets.

@3t14
Last active August 24, 2022 20:06
Show Gist options
  • Save 3t14/99951d954ff50bfa217ec6eea9fb5633 to your computer and use it in GitHub Desktop.
Save 3t14/99951d954ff50bfa217ec6eea9fb5633 to your computer and use it in GitHub Desktop.
Master-Detail型簡易メモ帳アプリ(SQLite対応版)
// メモデータに関するモデル処理
import 'package:flutter/foundation.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
// 1件分のメモデータ
class Item {
final int id;
final String title;
final String memo;
// idを時刻データにする
const Item({this.id = -1, required this.title, required this.memo});
//Map型への変換
Map<String, Object?> toMap() {
return {
'id': id,
'title': title,
'memo': memo,
};
}
}
// 全データ
List<Item> items = <Item>[
// const Item(
// id: 0,
// title: 'Item 1',
// memo: 'This is the first item.',
// ),
// const Item(
// id: 1,
// title: 'Item 2',
// memo: 'This is the second item.',
// ),
// const Item(
// id: 2,
// title: 'Item 3',
// memo: 'This is the third item.',
// ),
];
Database? _database;
// データベースを開く
Future<Database> getDb() async {
// 未代入のときは取得する
if (_database == null) {
var databasesPath = await getDatabasesPath();
var path = join(databasesPath, 'memos.db');
if (kDebugMode) print("path = $path");
_database = await openDatabase(
path, // データベースファイルのパス
onCreate: (db, version) {
// データベースファイルがない場合は作成する
// テーブルmp作成
db.execute(
'CREATE TABLE memos ('
'id INTEGER PRIMARY KEY, '
'title TEXT, '
'memo TEXT'
')',
);
},
// データベースファイルのバージョン
version: 1,
);
}
return _database!;
}
bool get isDBReady => _database != null;
// 最初のデータベースの読み込み
initItems(initItemsCallback) async {
if (isDBReady) return;
await getItems();
initItemsCallback();
}
// 全データの取得
Future<List<Item>> getItems() async {
print('getItems');
final db = await getDb();
final List<Map<String, dynamic>> maps = await db.query('memos');
items = List.generate(maps.length, (i) {
return Item(
id: maps[i]['id'],
title: maps[i]['title'],
memo: maps[i]['memo'],
);
});
return items;
}
// メモの追加
Future<void> addItem(Item item) async {
items.add(item);
final db = await getDb();
await db.insert('memos', item.toMap());
}
// メモの更新
Future<void> updateItem(Item item) async {
var id = item.id;
var index = items.indexWhere((item) => item.id == id);
// 存在しない場合例外発生
if (index == -1) throw Exception('Item not found: $item');
// 置き換え
items[index] = item;
// データベース側の更新は非同期で行う
final db = await getDb();
await db.update(
'memos',
item.toMap(),
where: "id = ?",
whereArgs: [item.id],
);
}
// メモの削除
Future<void> removeItem(Item item) async {
// idが一致するindexを調べ削除する
var id = item.id;
var index = items.indexWhere((item) => item.id == id);
// 存在しない場合例外発生
if (index == -1) throw Exception('Item not found: $item');
// 削除
items.removeAt(index);
// データベース側の更新は非同期で行う
final db = await getDb();
await db.delete(
'memos',
where: "id = ?",
whereArgs: [item.id],
);
}
// メモデータの詳細表示、編集フォーム
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'item.dart';
// ウィジェットのモード
enum ItemDetailsMode {
view, // 表示モード
add, // 追加モード
edit, // 編集モード
}
const emptyItem = Item(
title: '',
memo: '',
);
class ItemDetails extends StatefulWidget {
final bool isInTabletLayout;
final ItemDetailsMode mode; // 表示モード or 編集モード
final Item? initItem; // 生成時の初期メモデータ
final ValueChanged<Item>? onEdit; // 編集モードのコールバック
final ValueChanged<Item>? onCancel; // 編集キャンセル時のコールバック
final ValueChanged<Item>? onSave; // 登録・保存時のコールバック
final ValueChanged<Item>? onDelete; // 削除時のコールバック
const ItemDetails({
Key? key,
required this.isInTabletLayout,
required this.mode,
// メモデータの初期状態
this.initItem = emptyItem,
this.onEdit,
this.onCancel,
this.onSave,
this.onDelete,
}) : super(key: key);
@override
_ItemDetailsState createState() => _ItemDetailsState();
}
class _ItemDetailsState extends State<ItemDetails> {
var _title = "";
var _memo = "";
var _id = -1;
@override
void initState() {
super.initState();
initData();
}
Item _item = emptyItem;
// データの初期化
void initData() {
var initItem = widget.initItem;
initItem ??= emptyItem; // initItemがnullの場合はemptyItemを使用する
_title = initItem.title;
_memo =initItem.memo;
_id = initItem.id;
regenerateItem();
}
// データを再生成する
void regenerateItem() {
_item = Item(
id: _id,
title: _title,
memo: _memo,
);
}
@override
Widget build(BuildContext context) {
if (kDebugMode) print("build ${_id}, ${_title}, ${_memo}");
final textTheme = Theme.of(context).textTheme;
const textFormFieldDecoration = InputDecoration(
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(),
);
var children = items.isNotEmpty || widget.mode == ItemDetailsMode.add
? [
Text(
"タイトル",
style: textTheme.caption,
),
widget.mode == ItemDetailsMode.view
? SelectableText(
_title,
style: textTheme.headlineSmall,
)
: TextFormField(
initialValue: _title,
style: textTheme.bodyMedium,
decoration: textFormFieldDecoration,
onChanged: (value) {
setState(() => _title = value);
regenerateItem();
},
),
const SizedBox(height: 10),
Text(
"メモ",
style: textTheme.caption,
),
widget.mode == ItemDetailsMode.view
? SelectableText(
_memo,
style: textTheme.subtitle1,
)
: TextFormField(
initialValue: _memo,
style: textTheme.bodyMedium,
decoration: textFormFieldDecoration,
keyboardType: TextInputType.multiline,
maxLines: null,
onChanged: (value) {
setState(() => _memo = value);
regenerateItem();
},
),
const SizedBox(height: 48),
]
: [
Text(
"+ボタンをクリックしてメモを追加してください。",
style: textTheme.caption,
)
];
var cancelButton = ElevatedButton(
child: const Text('キャンセル'),
onPressed: () {
if (widget.onCancel != null) widget.onCancel!(_item);
},
);
if (widget.mode == ItemDetailsMode.view && items.isNotEmpty) {
children.add(
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
child: const Text(
'削除',
),
onPressed: () {
if (widget.onDelete != null) widget.onDelete!(_item);
},
style: ElevatedButton.styleFrom(
primary: Colors.red,
),
),
ElevatedButton(
child: const Text('編集'),
onPressed: () {
if (widget.onEdit != null) widget.onEdit!(_item);
},
),
],
),
);
} else if (widget.mode == ItemDetailsMode.edit) {
children.add(Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
cancelButton,
ElevatedButton(
child: const Text('保存'),
onPressed: () {
print("save memo: ${_item.memo}");
if (widget.onSave != null) widget.onSave!(_item);
},
),
],
));
} else if (widget.mode == ItemDetailsMode.add) {
children.add(Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
cancelButton,
ElevatedButton(
child: const Text('新規登録'),
onPressed: () {
if (widget.onSave != null) widget.onSave!(_item);
},
),
],
));
}
final body = SafeArea(
child: Padding(
padding: widget.isInTabletLayout
? const EdgeInsets.symmetric(horizontal: 72, vertical: 48)
: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
),
);
return widget.isInTabletLayout
? body
: Scaffold(
appBar: AppBar(
title: const Text("メモ詳細"),
),
backgroundColor: Colors.yellow,
body: body,
);
}
}
// メモデータのリスト表示
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hello_flutter/item_details.dart';
import 'item.dart';
class ItemListing extends StatelessWidget {
const ItemListing({
Key? key,
required this.itemSelectedCallback,
this.selectedItem,
}): super(key: key);
final ValueChanged<Item> itemSelectedCallback;
final Item? selectedItem;
@override
Widget build(BuildContext context) {
if (kDebugMode) print('ItemListing.build() ${selectedItem?.id}, $key');
return ListView.builder(
itemCount: items.length,
itemBuilder: (BuildContext context, int index) {
final item = items.isNotEmpty ? items[index]: emptyItem;
return ListTile(
leading: const Icon(
Icons.edit_note,
color: Colors.yellow,
size: 24.0,
semanticLabel: 'Text to announce in accessibility modes',
),
title: Text(item.title),
subtitle: Text(item.memo.substring(0, min(20, item.memo.length))),
selected: item.id == selectedItem?.id,
onTap: () {
itemSelectedCallback(item);
},
tileColor: Colors.lightBlueAccent,
selectedTileColor: Colors.amberAccent,
);
},
);
}
}
// エントリーポイント
import 'package:flutter/material.dart';
import 'master_detail_container.dart';
void main() {
runApp(const MemoApp());
}
class MemoApp extends StatelessWidget {
const MemoApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '簡易メモアプリ',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MasterDetailContainer(),
);
}
}
// モバイル、タブレット双方対応のためのMaster-Detailコンテナー
import 'package:flutter/foundation.dart';
import 'item.dart';
import 'item_details.dart';
import 'item_listing.dart';
import 'package:flutter/material.dart';
class MasterDetailContainer extends StatefulWidget {
const MasterDetailContainer({Key? key}) : super(key: key);
@override
_ItemMasterDetailContainerState createState() =>
_ItemMasterDetailContainerState();
}
class _ItemMasterDetailContainerState extends State<MasterDetailContainer> {
static const int kTabletBreakpoint = 600;
Item? _selectedItem = items.isNotEmpty ? items[0] : null;
ItemDetailsMode _mode = ItemDetailsMode.view;
// モバイル端末か否かを判定する
bool get isInMobileLayout =>
MediaQuery.of(context).size.shortestSide < kTabletBreakpoint;
// 詳細画面ウィジェットの生成
Widget get itemDetails => ItemDetails(
key: ValueKey(_selectedItem!.id), // idごとにItemDetailsを更新する
isInTabletLayout: !isInMobileLayout,
mode: _mode,
initItem: _selectedItem,
onEdit: edit,
onCancel: cancel,
onSave: save,
onDelete: delete,
);
// 編集フォームの表示
void edit(Item item) {
setState(() {
_mode = ItemDetailsMode.edit;
_selectedItem = item;
});
modeChange(_mode);
}
// 編集をキャンセル
void cancel(Item item) {
if (_mode == ItemDetailsMode.add && isInMobileLayout) {
Navigator.of(context).pop();
}
if (_mode == ItemDetailsMode.edit) {
modeChange(_mode);
}
setState(() {
_mode = ItemDetailsMode.view;
});
}
void save(Item item) {
if (kDebugMode) print("save ${item.title}");
if (_mode == ItemDetailsMode.add) {
// idは重複しないようにタイムスタンプを利用
var newItem = Item(
id: DateTime.now().microsecondsSinceEpoch,
title: item.title,
memo: item.memo);
addItem(newItem);
item = newItem;
} else if (_selectedItem != null) {
updateItem(item);
}
setState(() {
_mode = ItemDetailsMode.view;
_selectedItem = item;
});
modeChange(_mode);
}
// 削除
void delete(Item item) {
try {
removeItem(item);
} catch (e) {
// 対象のレコードが配列に存在しない
if (kDebugMode) print(e);
}
if (items.isNotEmpty) {
setState(() {
_selectedItem = items[0];
});
} else {
setState(() {
_selectedItem = emptyItem;
});
}
// モバイル画面の場合は一覧表示画面に遷移する
if (isInMobileLayout) {
Navigator.of(context).pop();
}
}
// モードの変更
modeChange(ItemDetailsMode mode) async {
if (kDebugMode) print('updateMode: $_mode to $mode');
setState(() {
_mode = mode;
});
if (isInMobileLayout) {
await Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation1, animation2) => itemDetails,
transitionDuration: const Duration(seconds: 0),
),
);
}
}
Widget _buildMobileLayout() {
return ItemListing(itemSelectedCallback: (item) async {
setState(() {
_selectedItem = item;
_mode = ItemDetailsMode.view;
});
await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => itemDetails,
),
);
});
}
Widget _buildTabletLayout() {
return Row(
children: <Widget>[
Flexible(
flex: 1,
child: Material(
elevation: 4.0,
child: ItemListing(
itemSelectedCallback: (item) {
print("itemSelectedCallback: ${item.title}");
setState(() {
_selectedItem = item;
_mode = ItemDetailsMode.view;
});
},
selectedItem: _selectedItem,
),
),
),
Flexible(
flex: 3,
child: itemDetails,
),
],
);
}
// DBから読み込みとその反映
initItemsCallback() {
setState((){
_selectedItem = items.isNotEmpty ? items[0] : emptyItem;
});
if (kDebugMode) print("_selectedItem: ${_selectedItem!.id}");
}
@override
Widget build(BuildContext context) {
// 初めてのアクセスの場合、DBからデータを非同期で読み込む
if (!isDBReady) initItems(initItemsCallback);
Widget content;
if (isInMobileLayout) {
content = _buildMobileLayout();
} else {
content = _buildTabletLayout();
}
var newItem = const Item(
title: '',
memo: '',
);
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: const Text('簡易メモ帳'),
),
body: SingleChildScrollView(
child:SizedBox(
height: MediaQuery.of(context).size.height,
child:content,
),
scrollDirection: Axis.vertical
),
backgroundColor: Colors.yellow,
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
setState(() {
_selectedItem = newItem;
_mode = ItemDetailsMode.add;
});
if (isInMobileLayout) {
Navigator.of(context).push(
MaterialPageRoute(builder: (BuildContext context) => itemDetails),
);
}
},
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment