Last active
September 8, 2022 20:42
-
-
Save 3t14/5ae173d387071b859915df0037525da2 to your computer and use it in GitHub Desktop.
編集機能追加後
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// メモデータに関するモデル処理 | |
// 1件分のメモデータ | |
class Item { | |
final int id; // ユニークな識別子 | |
final String title; // メモのタイトル | |
final String memo; // メモの内容 | |
// コンストラクタ | |
const Item({ | |
this.id = -1, | |
required this.title, | |
required this.memo | |
}); | |
} | |
// 全データ | |
List<Item> items = <Item>[]; | |
// 追加 | |
addItem(Item item) { | |
items.add(item); | |
} | |
// 更新 | |
updateItem(Item item) { | |
var id = item.id; | |
var index = items.indexWhere((item) => item.id == id); | |
// 存在しない場合例外発生 | |
if (index == -1) throw Exception('Item not found: $item'); | |
// 置き換え | |
items[index] = item; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// メモデータの詳細表示、編集フォーム | |
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>? onCancel; // 編集キャンセル時のコールバック | |
final ValueChanged<Item>? onSave; // 登録・保存時のコールバック | |
final ValueChanged<Item>? onEdit; // 登録・保存時のコールバック | |
const ItemDetails({ | |
Key? key, | |
required this.isInTabletLayout, | |
required this.mode, | |
// メモデータの初期状態 | |
this.initItem = emptyItem, | |
this.onCancel, | |
this.onSave, | |
this.onEdit | |
}) : 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.headlineSmall, | |
) | |
: TextFormField( | |
initialValue: _memo, | |
style: textTheme.bodyMedium, | |
decoration: textFormFieldDecoration, | |
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.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( | |
// AppBarを含めるかどうかで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, | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// メモデータのリスト表示 | |
import 'dart:math'; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/material.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() ${items.length}, ${selectedItem?.id}, $key'); | |
return ListView.builder( | |
itemCount: items.length, | |
itemBuilder: (BuildContext context, int index) { | |
// データが空の場合は空のウィジェットを返す | |
final item = items.isNotEmpty ? items[index]: emptyItem; | |
return ListTile( // リスト表示するメモデータ1件分のウィジェット | |
leading: const Icon( | |
Icons.edit_note, | |
color: Colors.yellow, | |
size: 24.0, | |
), | |
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, | |
); | |
}, | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// エントリーポイント | |
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(), | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// モバイル、タブレット双方対応のための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), // 画面更新のために必要 | |
isInTabletLayout: !isInMobileLayout, | |
mode: _mode, | |
initItem: _selectedItem, | |
onSave: save, | |
onCancel: cancel, | |
onEdit: edit, | |
); | |
// 編集フォームの表示 | |
void edit(Item item) { | |
setState(() { | |
_mode = ItemDetailsMode.edit; | |
_selectedItem = item; | |
}); | |
modeChange(_mode); | |
} | |
// 編集のキャンセル | |
void cancel(Item item) { | |
// モバイル端末の場合は、詳細画面を閉じる | |
if (isInMobileLayout) { | |
Navigator.of(context).pop(); | |
} | |
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); | |
// アイテムを追加(item.dart内で実装) | |
addItem(newItem); | |
item = newItem; | |
} else { | |
// アイテムを更新(item.dart内で実装) | |
updateItem(item); | |
} | |
setState(() { | |
_mode = ItemDetailsMode.view; | |
_selectedItem = item; | |
}); | |
modeChange(_mode); | |
} | |
// モードの変更 | |
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; | |
}); | |
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; | |
}); | |
}, | |
selectedItem: _selectedItem, | |
), | |
), | |
), | |
Flexible( | |
flex: 3, | |
child: itemDetails, | |
), | |
], | |
); | |
} | |
@override | |
Widget build(BuildContext context) { | |
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( | |
// 1画面に収まらない場合にスクロールする | |
child: SizedBox( | |
height: MediaQuery.of(context).size.height, // 画面の高さ=スクロールの最大値 | |
child: content, // コンテンツを出力 | |
), | |
scrollDirection: Axis.vertical // 縦方向にスクロールする | |
), | |
backgroundColor: Colors.yellow, | |
floatingActionButton: FloatingActionButton( | |
child: const Icon(Icons.add), | |
onPressed: () async { | |
// 新規データを選択項目として代入 → 詳細画面上に連動して反映 | |
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