Skip to content

Instantly share code, notes, and snippets.

@3t14
Last active August 20, 2022 11:51
Show Gist options
  • Save 3t14/82f4e70cb9ee18cd6f6f8a66317ef684 to your computer and use it in GitHub Desktop.
Save 3t14/82f4e70cb9ee18cd6f6f8a66317ef684 to your computer and use it in GitHub Desktop.
Master-Detail型簡易メモ帳アプリ(データは配列に格納)
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);
}
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,
);
}
}
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,
);
},
);
}
}
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(),
);
}
}
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