✨
【Flutter】Todoアプリを作ってみた
はじめに
Flutterを用いてシンプルなTodoアプリを作成してみた。
完成形を以下のようなる。
使用パッケージ
hooks_riverpod: ^1.0.3
実装
Todoリストの状態を保持するクラスの作成
Todoリストの状態を保持するクラスを作成。
class Todo {
Todo(this.id, this.title, this.check, this.focusNode);
int id;
String title;
bool check;
FocusNode focusNode;
}
「check」はチェックボックスの値の管理の為、「focusNode」はテキスト入力時のフォーカスを管理する為に使用。
ViewModelの作成
todoリストの管理を行うViewModelを作成。
class TodoViewModel extends ChangeNotifier {
List<Todo> _todoList = [];
UnmodifiableListView<Todo> get todoList => UnmodifiableListView(_todoList);
void createTodo(String title) {
final id = _todoList.length + 1;
_todoList = [...todoList, Todo(id, title, false, FocusNode())];
notifyListeners();
}
void updateTodo(int id, String title) {
todoList.asMap().forEach((int index, Todo todo) {
if (todo.id == id) {
_todoList[index].title = title;
}
});
notifyListeners();
}
void updateCheck(int id, bool check) {
todoList.asMap().forEach((int index, Todo todo) {
if (todo.id == id) {
_todoList[index].check = check;
}
});
notifyListeners();
}
void deleteTodo(int id) {
_todoList = _todoList.where((todo) => todo.id != id).toList();
_todoList.asMap().forEach((int index, Todo todo) {
_todoList[index].id = index + 1;
});
notifyListeners();
}
}
Providerの作成
mainでTodoViewModelのProviderを作成
final todoProvider = ChangeNotifierProvider((ref) => TodoViewModel());
また、ProviderScopeでmainのWigetをくくる
void main() {
runApp(const ProviderScope(child: MyApp()));
}
画面の作成
最後にToodリストを表示する為の画面を作成
class TodoScreen extends HookConsumerWidget {
TodoScreen({Key? key}) : super(key: key);
final GlobalKey<AnimatedListState> _listKey = GlobalKey();
final focusNode = FocusNode();
Widget build(BuildContext context, WidgetRef ref) {
final todoModel = ref.watch(todoProvider);
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
elevation: 0,
),
drawer: Drawer(
child: ListView(
children: [
DrawerHeader(
decoration:
BoxDecoration(color: Theme.of(context).primaryColor),
child: const Text("Menu")),
ListTile(
leading: const Icon(Icons.check_box),
title: const Text("ToDo List"),
onTap: () {
Navigator.pop(context);
},
),
],
),
),
body: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
child: Column(
children: [
Container(
margin: const EdgeInsets.only(left: 20),
height: 80,
width: double.infinity,
alignment: Alignment.centerLeft,
child: Text("ToDo List",
style: Theme.of(context).textTheme.headline4),
),
Expanded(
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20))),
child: Padding(
padding: const EdgeInsets.all(10),
child: AnimatedList(
key: _listKey,
initialItemCount: todoModel.todoList.length,
itemBuilder:
(BuildContext context, int index, animation) {
return _buildItem(
ref,
index,
Theme.of(context).secondaryHeaderColor,
animation,
);
}),
)))
],
)),
floatingActionButton: FloatingActionButton(
onPressed: () {
int insertIndex = todoModel.todoList.length;
todoModel.createTodo("");
_listKey.currentState?.insertItem(insertIndex,
duration: const Duration(milliseconds: 300));
todoModel.todoList[todoModel.todoList.length - 1].focusNode
.requestFocus();
},
tooltip: 'Add Todo',
child: const Icon(Icons.add),
),
);
}
Widget _buildItem(
WidgetRef ref,
int index,
Color dismissColor,
Animation<double> animation,
) {
final todoModel = ref.watch(todoProvider);
final id = todoModel.todoList[index].id;
final title = todoModel.todoList[index].title;
final check = todoModel.todoList[index].check;
final focusNode = todoModel.todoList[index].focusNode;
return SizeTransition(
sizeFactor: animation,
child: Dismissible(
key: Key('${id.hashCode}'),
background: Container(color: dismissColor),
confirmDismiss: (direction) async {
return true;
},
onDismissed: (direction) {
_listKey.currentState?.removeItem(index,
(context, animation) => const SizedBox(width: 0, height: 0));
todoModel.deleteTodo(id);
},
child: ListTile(
leading: Checkbox(
onChanged: (e) {
todoModel.updateCheck(id, !check);
},
value: check,
),
title: TextFormField(
focusNode: focusNode,
autofocus: false,
initialValue: title,
decoration: const InputDecoration(
border: InputBorder.none,
),
onChanged: (value) {
todoModel.updateTodo(id, value);
},
onFieldSubmitted: (value) {
if (value == "") {
_listKey.currentState?.removeItem(
index,
(context, animation) =>
const SizedBox(width: 0, height: 0));
todoModel.deleteTodo(id);
}
},
)),
));
}
}
完成プロジェクト
参考
Discussion