Skip to content

Instantly share code, notes, and snippets.

@3t14
Last active September 8, 2022 20:42
Show Gist options
  • Save 3t14/5ae173d387071b859915df0037525da2 to your computer and use it in GitHub Desktop.
Save 3t14/5ae173d387071b859915df0037525da2 to your computer and use it in GitHub Desktop.
編集機能追加後
// メモデータに関するモデル処理
// 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;
}
// メモデータの詳細表示、編集フォーム
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,
);
}
}
// メモデータのリスト表示
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,
);
},
);
}
}
// エントリーポイント
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), // 画面更新のために必要
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