본문 바로가기

Flutter/10 app Todo with GetX

[Flutter] App Todo(with Getx) - 2단계 추가, 필터링 및 리스트

상태관리에서 배운 Getx를 이용하여 todo 어플을 만들어 보려고 합니다.

이전 버전은 provider 로 상태관리하는 todo 어플을 만들었다.

오늘은 새로운 todo 추가, 필터(all/active/completed) 에 따른 todo 리스트 보여주기 기능을 만들어 보겠습니다.

개발환경 : 윈도우11, 안드로이드 스튜디오, flutter 2.10.3

소스코드 위치 - Release 03_filter&filteredList2 · mike-bskim/todo_getx · GitHub

 

Release 03_filter&filteredList2 · mike-bskim/todo_getx

 

github.com

 

 

화면구현은 아래와 같습니다. 새로 추가된 리스트("추가된 리스트")가 리스트 하단에 있습니다.

 

 

 

오늘의 프로젝트 주요파일은 아래와 같다.

./bindings/todo_binding.dart

./controller/todo_list_controller.dart

./model/todo_model.dart

./screens/todos_screen.dart

 

 

./bindings/todo_binding.dart - controller 2개 추가

 

import 'package:get/get.dart';

import '../controller/todo_list_controller.dart';

class TodoBinding extends Bindings {
  @override
  void dependencies() {
    // dependency injection
    Get.put<TodoList>(TodoList());
    Get.put<TodosFilter>(TodosFilter());
    Get.put<FilteredTodos>(FilteredTodos());
  }
}

 

./controller/todo_list_controller.dart

 

import 'package:get/get.dart';

import '../model/todo_model.dart';

class TodoList 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 TodoList 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));
  }

// 토글 함수 추가.
  void toggleTodo(String id) {
    todos.assignAll(todos.map((todo) {
      return todo.id == id
          ? Todo(
              id: id,
              desc: todo.desc,
              completed: !todo.completed,
            )
          : todo;
    }).toList());
  }
}

class TodosFilter extends GetxController {
  Rx<Filter> todosFilter = Filter.all.obs;

  static TodosFilter get to => Get.find();
}

class FilteredTodos extends GetxController {
// 감시해야할 값 추가
  final todos = TodoList.to.todos;
  final filter = TodosFilter.to.todosFilter;

  RxList<Todo> filteredTodos = <Todo>[].obs;

  static FilteredTodos get to => Get.find();

  @override
  void onInit() {
  // 최초 화면에 표시할 정보 추가
    filteredTodos.assignAll(todos);

// 감시해야할 값들은 리스트 형태로 전달, 값들중 하나라도 변경되면 everAll 자동으로 호출됨
// 자동으로 리스트정보가 갱신되고 화면에 다시 rendering 됨
    everAll([todos, 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;
      }

      filteredTodos.assignAll(tempTodos);
    });

    super.onInit();
  }
}

 

./model/todo_model.dart - enum 추가

 

enum Filter {
  all,
  active,
  completed,
}

 

./screens/todos_screen.dart - todo 기능 추가, 필터 기능 추가, 필터링된 리스트 기능 추가

 

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),
        ),
        Text(
          '0 items left',
          style: TextStyle(
            fontSize: 20.0,
            color: Colors.redAccent,
          ),
        ),
      ],
    );
  }
}

class CreateTodo extends StatefulWidget { //StatefulWidget//StatelessWidget
  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) {
          TodoList.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');
          },
        ),
        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) {

    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) {
    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 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~~');
          TodoList.to.toggleTodo(todo.id);
        },
      ),
      title: Text(todo.desc),
    );
  }
}

 

 

 

 

 

[참고자료] 헤비프랜

- https://www.youtube.com/watch?v=HZJsKlN-kmc&list=PLGJ958IePUyDQwYbPcz-5W9o4p1__20V0&index=5