상태관리에서 배운 Getx를 이용하여 todo 어플을 만들어 보려고 합니다.
이전 버전은 provider 로 상태관리하는 todo 어플을 만들었다.
오늘은 검색기능, 할일 개수 표시 기능을 만들어 보겠습니다.
개발환경 : 윈도우11, 안드로이드 스튜디오, flutter 2.10.3
소스코드 위치 - Release 04_search&activeCount · mike-bskim/todo_getx · GitHub
화면구현은 아래와 같습니다.
오늘의 프로젝트 주요파일은 아래와 같다.
./bindings/todo_binding.dart
./controller/todo_list_controller.dart
./screens/todos_screen.dart
./bindings/todo_binding.dart - TodoList 클래스명 변경(변경후 TodosList) 및 controller 2개 추가
class TodoBinding extends Bindings {
@override
void dependencies() {
// dependency injection
Get.put<TodosList>(TodosList());
Get.put<TodosFilter>(TodosFilter());
Get.put<TodosSearch>(TodosSearch());
// 상기 3개의 controller 는 ActiveCount/FilteredTodos 에 영향을 주기때문에
// ActiveCount/FilteredTodos 보다 위에/먼저 선언되어야 한다
Get.put<ActiveCount>(ActiveCount());
Get.put<FilteredTodos>(FilteredTodos());
}
}
./controller/todo_list_controller.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../model/todo_model.dart';
class TodosFilter extends GetxController {
Rx<Filter> todosFilter = Filter.all.obs;
static TodosFilter get to => Get.find();
}
// 검색관련 controller 추가
class TodosSearch extends GetxController {
RxString searchWord = ''.obs;
static TodosSearch get to => Get.find();
}
// 클래스명 변경함 TodoList => TodosList
class TodosList extends GetxController {
// 샘플 데이터 생성
RxList<Todo> todos = <Todo>[
Todo(id: '1', desc: 'Clean the room', completed: true),
Todo(id: '2', desc: 'Do homework'),
Todo(id: '3', desc: 'Wash the dish'),
].obs;
static TodosList get to => Get.find();
void addTodo({required String todoDesc}) {
int? newNum;
if (todos.isEmpty) {
newNum = 1;
} else {
// 마지막 id 에서 1 증가
newNum = int.parse(todos.last.id) + 1;
}
todos.add(Todo(id: newNum.toString(), desc: todoDesc));
}
// 추가, 특정 todo 리스트 삭제
void deleteTodo({required String id}) {
todos.assignAll(todos.where((t) => t.id != id).toList());
}
void toggleTodo({required String id}) {
todos.assignAll(todos.map((todo) {
return todo.id == id
? Todo(
id: id,
desc: todo.desc,
completed: !todo.completed,
)
: todo;
}).toList());
}
// 추가, 특정 todo 리스트 편집
void editTodo({required String id, required String desc}) {
todos.assignAll(todos.map((todo) {
return todo.id == id
? Todo(
id: id,
desc: desc,
completed: todo.completed,
)
: todo;
}).toList());
}
}
// 추가, 할일 개수 계산
class ActiveCount extends GetxController {
final todos = TodosList.to.todos;
RxInt activeCount = 0.obs;
static ActiveCount get to => Get.find();
@override
void onInit() {
activeCount.value = todos.where((todo) => !todo.completed).toList().length;
// todos 의 추가/변경/삭제시 worker 자동호출해서 activeCount 자동계산
ever(todos, (_) {
activeCount.value =
todos.where((todo) => !todo.completed).toList().length;
debugPrint('active count: ${activeCount.value}');
});
super.onInit();
}
}
class FilteredTodos extends GetxController {
final todos = TodosList.to.todos;
final filter = TodosFilter.to.todosFilter;
// 검색기능 추가
final search = TodosSearch.to.searchWord;
RxList<Todo> filteredTodos = <Todo>[].obs;
static FilteredTodos get to => Get.find();
@override
void onInit() {
// 초기에 화면에 표시할 리스트를 filteredTodos 에 할당.
filteredTodos.assignAll(todos);
everAll([todos, search, filter], (_) {
List<Todo> tempTodos;
switch (filter.value) {
case Filter.active:
tempTodos = todos.where((todo) => !todo.completed).toList();
break;
case Filter.completed:
tempTodos = todos.where((todo) => todo.completed).toList();
break;
case Filter.all:
default:
tempTodos = todos.toList();
break;
}
// 검색기능 추가
if (search.value.isNotEmpty) {
tempTodos =
tempTodos.where((t) => t.desc.toLowerCase().contains(search.value)).toList();
}
filteredTodos.assignAll(tempTodos);
});
super.onInit();
}
}
./screens/todos_screen.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:todo_test/controller/todo_list_controller.dart';
import '../model/todo_model.dart';
class TodosScreen extends StatelessWidget {
const TodosScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: SingleChildScrollView(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 20.0, vertical: 40.0),
child: Column(
children: const [
TodoHeader(),
CreateTodo(),
SizedBox(height: 20.0),
SearchAndFilterTodo(),
ShowTodos(),
],
),
),
),
),
);
}
}
class TodoHeader extends StatelessWidget {
const TodoHeader({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'TODO',
style: TextStyle(fontSize: 40.0),
),
// Obx 로 ActiveCount 자동 감시 및 rendering
Obx(() {
return Text(
'${ActiveCount.to.activeCount} items left',
style: const TextStyle(
fontSize: 20.0,
color: Colors.redAccent,
),
);
}),
],
);
}
}
// StatelessWidget 로 변경가능,
// 싱글 페이지 이므로 newTodoController.dispose(); 생략가능함
class CreateTodo extends StatefulWidget {
const CreateTodo({Key? key}) : super(key: key);
@override
_CreateTodoState createState() => _CreateTodoState();
}
class _CreateTodoState extends State<CreateTodo> {
final newTodoController = TextEditingController();
@override
void dispose() {
newTodoController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextFormField(
controller: newTodoController,
decoration: const InputDecoration(labelText: 'What to do?'),
onFieldSubmitted: (String? todoDesc) {
debugPrint('CreateTodo Clicked: ${newTodoController.text}');
if (todoDesc != null && todoDesc.trim().isNotEmpty) {
TodosList.to.addTodo(todoDesc: todoDesc);
newTodoController.clear();
}
},
);
}
}
class SearchAndFilterTodo extends StatelessWidget {
const SearchAndFilterTodo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
TextFormField(
decoration: const InputDecoration(
labelText: 'Search todos',
border: InputBorder.none,
filled: true,
prefixIcon: Icon(Icons.search),
),
// 검색어 기능 추가
onChanged: (String? newSearchTerm) {
debugPrint('Search todos: $newSearchTerm');
if (newSearchTerm != null) {
TodosSearch.to.searchWord.value = newSearchTerm;
}
},
),
const SizedBox(height: 10.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
filterButton(context, Filter.all),
filterButton(context, Filter.active),
filterButton(context, Filter.completed),
],
),
],
);
}
Widget filterButton(BuildContext context, Filter filter) {
//, Filter filter
return TextButton(
onPressed: () {
// 일반적으로는 함수처리해야 함. 직접 접근하지 말것
TodosFilter.to.todosFilter.value = filter;
debugPrint('Clicked button $filter');
},
child: Obx(
() => Text(
filter == Filter.all
? 'All'
: filter == Filter.active
? 'Active'
: 'Completed',
style: TextStyle(
fontSize: 18.0,
color: textColor(context, filter),
fontWeight: textFontWeight(context, filter),
),
),
),
);
}
Color textColor(BuildContext context, Filter filter) {
//Filter filter
final currentFilter = TodosFilter.to.todosFilter;
return currentFilter.value == filter ? Colors.blue : Colors.grey;
}
FontWeight textFontWeight(BuildContext context, Filter filter) {
var currentFilter = TodosFilter.to.todosFilter;
return currentFilter.value == filter ? FontWeight.bold : FontWeight.normal;
}
}
class ShowTodos extends StatelessWidget {
const ShowTodos({Key? key}) : super(key: key);
Widget showBackground(int direction) {
return Container(
margin: const EdgeInsets.all(4.0),
padding: const EdgeInsets.symmetric(horizontal: 10.0),
color: Colors.red,
alignment: direction == 0 ? Alignment.centerLeft : Alignment.centerRight,
child: const Icon(
Icons.delete,
size: 30.0,
color: Colors.white,
),
);
}
@override
Widget build(BuildContext context) {
// final todos = context.watch<FilteredTodos>().state.filteredTodos;
final currentTodos = FilteredTodos.to.filteredTodos;
return Obx(() {
return ListView.separated(
primary: false,
shrinkWrap: true,
itemCount: currentTodos.length,
separatorBuilder: (BuildContext context, int index) {
return const Divider(color: Colors.grey);
},
itemBuilder: (BuildContext context, int index) {
return TodoItem(todo: currentTodos[index]);
},
);
});
}
}
class TodoItem extends StatelessWidget {
final Todo todo;
const TodoItem({Key? key, required this.todo}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
leading: Checkbox(
value: todo.completed,
onChanged: (bool? checked) {
debugPrint('clicked toggle button~~');
// toggleTodo 의 인자 전달 구조 변경
TodosList.to.toggleTodo(id: todo.id);
},
),
title: Text(todo.desc),
);
}
}
[참고자료] 헤비프랜
- https://www.youtube.com/watch?v=HZJsKlN-kmc&list=PLGJ958IePUyDQwYbPcz-5W9o4p1__20V0&index=5
'Flutter > 10 app Todo with GetX' 카테고리의 다른 글
[Flutter] App Todo(with Getx) - 5단계 todo 편집 refactoring (0) | 2022.06.16 |
---|---|
[Flutter] App Todo(with Getx) - 4단계 todo 편집, 삭제 (0) | 2022.06.16 |
[Flutter] App Todo(with Getx) - 2단계 추가, 필터링 및 리스트 (0) | 2022.06.14 |
[Flutter] App Todo(with Getx) - 1단계 화면, 모델링 및 리스트 관리 (0) | 2022.06.14 |