본문 바로가기

Flutter/10 app Todo with GetX

[Flutter] App Todo(with Getx) - 1단계 화면, 모델링 및 리스트 관리

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

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

오늘은 화면, 모델링 및 리스트 관리 기능을 먼저만들어 보겠습니다.

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

소스코드 위치 - Release 02_model&todolist · mike-bskim/todo_getx · GitHub

 

Release 02_model&todolist · mike-bskim/todo_getx

 

github.com

 

 

이전 provider 로 구현한 블로그는 아래와 같다.

2022.05.13 - [Flutter/10 app Todo with provider] - [Flutter] App Todo(with Provider) - 1단계 화면구성

2022.05.18 - [Flutter/10 app Todo with provider] - [Flutter] App Todo(with Provider) - 2단계 모델링 및 리스트 관리

 

 

화면구성은 아래와 같다.

 

 

 

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

./main.dart

./bindings/todo_binding.dart

./controller/todo_list_controller.dart

./model/todo_model.dart

./screens/todos_screen.dart

 

 

./main.dart

 

import 'package:flutter/material.dart';
import 'package:get/get.dart';

import 'bindings/todo_binding.dart';
import 'screens/todos_screen.dart';


void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // GetMaterialApp 으로 변경, from MaterialApp
    return GetMaterialApp(
      // 화면이 하나라서 초기 바인딩으로 dependency injection 함
      initialBinding: TodoBinding(),
      title: 'TODOS',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const TodosScreen(),
    );
  }
}

 

./bindings/todo_binding.dart

 

import 'package:get/get.dart';

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

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

 

./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'),
    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));
  }
}

 

./model/todo_model.dart

 

import 'package:uuid/uuid.dart';

Uuid uuid = const Uuid();

class Todo {
  final String id;
  final String desc;
  final bool completed;

  // id를 입력하지 않으면 기본값으로 설정됨
  Todo({
    String? id,
    required this.desc,
    this.completed = false,
  }) : id = id ?? uuid.v4();

  @override
  String toString() {
    return 'Todo(id: $id, desc: $desc, completed: $completed)';
  }
}

 

./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 StatefulWidget {
  const TodosScreen({Key? key}) : super(key: key);

  @override
  State<TodosScreen> createState() => _TodosScreenState();
}

class _TodosScreenState extends State<TodosScreen> {
  @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 {
  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}');
      },
    );
  }
}

class SearchAndFilterTodo extends StatefulWidget {
  const SearchAndFilterTodo({Key? key}) : super(key: key);

  @override
  State<SearchAndFilterTodo> createState() => _SearchAndFilterTodoState();
}

class _SearchAndFilterTodoState extends State<SearchAndFilterTodo> {
  String clickedType = 'all';

  @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, 'all'),
            filterButton(context, 'active'),
            filterButton(context, 'completed'),
          ],
        ),
      ],
    );
  }

  Widget filterButton(BuildContext context, String filter) {
    //, Filter filter
    return TextButton(
      onPressed: () {
        // context.read<TodoFilter>().changeFilter(filter);
        clickedType = filter;
        debugPrint('Clicked button $clickedType');
        setState(() {});
      },
      child: Text(
        filter == 'all'
            ? 'All'
            : filter == 'active'
                ? 'Active'
                : 'Completed',
        style: TextStyle(
          fontSize: 18.0,
          color: textColor(context, filter),
        ),
      ),
    );
  }

  Color textColor(BuildContext context, String filter) {
    var currentFilter = clickedType;
    return currentFilter == filter ? Colors.blue : Colors.grey;
  }
}

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) {

    return Obx(() {
      final currentTodos = TodoList.to.todos;

      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 StatefulWidget {
  final Todo todo;

  const TodoItem({Key? key, required this.todo}) : super(key: key);

  @override
  _TodoItemState createState() => _TodoItemState();
}

class _TodoItemState extends State<TodoItem> {
  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Checkbox(
        value: widget.todo.completed,
        onChanged: (bool? checked) {
          debugPrint('clicked toggle button~~');
        },
      ),
      title: Text(widget.todo.desc),
    );
  }
}

 

 

 

 

 

[참고자료] 헤비프랜

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