Last active
August 20, 2022 11:51
-
-
Save 3t14/82f4e70cb9ee18cd6f6f8a66317ef684 to your computer and use it in GitHub Desktop.
Master-Detail型簡易メモ帳アプリ(データは配列に格納)
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:meta/meta.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}); | |
} | |
// 全データ | |
final 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.', | |
// ), | |
]; | |
// 更新 | |
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; | |
} | |
// idが一致するindexを調べ削除する | |
removeItem(Item item) { | |
var id = item.id; | |
var index = items.indexWhere((item) => item.id == id); | |
// 存在しない場合例外発生 | |
if (index == -1) throw Exception('Item not found: $item'); | |
// 削除 | |
items.removeAt(index); | |
} |
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>? 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(); | |
var initItem = widget.initItem; | |
initItem ??= emptyItem; // initItemがnullの場合はemptyItemを使用する | |
_title = initItem.title; | |
_memo =initItem.memo; | |
_id = initItem.id; | |
} | |
Item get _item => Item(id: _id, title: _title, memo: _memo); | |
@override | |
Widget build(BuildContext context) { | |
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), | |
), | |
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), | |
), | |
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: () { | |
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, | |
); | |
} | |
} |
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 '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) { | |
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), | |
selected: item == selectedItem, | |
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()); | |
} | |
const _primaryColor = Color(0xFF6200EE); | |
class MemoApp extends StatelessWidget { | |
const MemoApp({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
import 'dart:math'; | |
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: UniqueKey(), | |
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); | |
items.add(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) { | |
setState(() { | |
_selectedItem = item; | |
_mode = ItemDetailsMode.view; | |
}); | |
}, | |
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( | |
appBar: AppBar( | |
title: const Text('簡易メモ帳'), | |
), | |
body: content, | |
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