본문 바로가기

앱 개발/Flutter

Flutter 앱 개발 (6) : To-Do List 기능 구현하기(할 일 수정 및 Style 수정)

이전 포스팅에서는 할 일 추가, 할 일 불러오기, 할 일 삭제 기능을 구현하였다.

Flutter 앱 개발 (5) : To-Do List 기능 구현하기(할 일 추가, 할 일 삭제) (tistory.com)

 

Flutter 앱 개발 (5) : To-Do List 기능 구현하기(할 일 추가, 할 일 삭제)

To Do List 소개 간단하게 To-Do List의 기능을 소개하면 다음과 같다. 1. 할 일 추가 - 할 일 객체를 생성한 후, 할 일 내용을 저장한다. 할 일 객체에는 할 일 내용을 저장하는 work 변수와 할 일을 완료

j-d-e.tistory.com

1. 할 일 추가

- 할 일 객체를 생성한 후, 할 일 내용을 저장한다. 할 일 객체에는 할 일 내용을 저장하는 work 변수와 할 일을 완료했는지 확인할 수 있는 isComplete 변수가 들어있다.

2. 할 일 삭제

- 생성된 할 일 객체를 삭제한다.

3. 할 일 수정

- 생성한 할 일 객체의 할 일 내용을 수정한다.

4. 할 일 완료

- 할 일 목록에 나타난 할 일 요소를 터치하면 해당하는 객체의 isComplete 변수를 수정한다.

5. 진척도 표시

- 완료한 할 일들의 비율을 계산하여 진척도를 표시한다.

6. 할 일 불러오기

- 생성한 할 일들을 화면에 출력한다.

 

이번 포스팅에서는 할 일 완료, 할 일 수정, 진척도 표시 기능을 구현해 보자.

 

1. 할 일 완료

간단하게 끝낼 수 있는 할 일 완료 기능부터 구현해 보자.

우리의 To-Do List는 할 일 내용을 터치하면 할 일 완료 기능을 수행한다. 그러므로, 할 일 내용이 나타나는 TextButton의 onPressed 옵션에 할 일 완료 기능을 수행하는 함수를 넣어주면 된다.

다음과 같이 코드를 수정해 보자.

 

for (var i = 0; i < tasks.length; i++)
              Row(
                children: [
                  Flexible(
                    child: TextButton(
                      style: TextButton.styleFrom(
                        shape: const RoundedRectangleBorder(
                          borderRadius: BorderRadius.all(Radius.zero),
                        ),
                      ),
                      onPressed: () {
                        setState(() {
                          tasks[i].isComplete = !tasks[i].isComplete;
                        });
                      },
                      child: Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: Row(
                          children: [
                            tasks[i].isComplete
                                ? const Icon(Icons.check_box_rounded)
                                : const Icon(
                                    Icons.check_box_outline_blank_rounded),
                            Text(tasks[i].work)
                          ],
                        ),
                      ),
                    ),
                  ),
                  TextButton(
                    onPressed: () {},
                    child: const Text("수정"),
                  ),
                  TextButton(
                    onPressed: () {
                      setState(() {
                        tasks.remove(tasks[i]);
                      });
                    },
                    child: const Text("삭제"),
                  ),
                ],
              )

 

task 내부의 멤버변수인 isComplete의 bool값을 반전시키는 기능을 추가하였다.

isComplete의 값을 참조하여 화면에 보이는 할 일 목록의 check icon을 수정하는 기능을 추가하였다.

 

2. 할 일 수정

우리가 구현할 기능 중 가장 복잡한 기능이라고 생각한다. 할 일 수정 기능을 추가해 보자.

 

1. 할 일 요소에 있는 수정 버튼을 누르면 해당하는 work 내용이 TextField에 나타나고,

2. TextField 내용을 수정한 후 add버튼을 누르면 수정을 눌렀던 할 일 요소의 내용이 바뀌는 방식

으로 수정 기능을 구현할 것이다.

 

수정 기능을 추가하기 전에, 사전 작업을 진행해 보자.

 

_MyHomePageState 클래스 내부에 다음과 같은 변수를 추가하자.

 

  bool isModifying = false;
  int modifyingIndex = 0;

isModifying 변수는 현재 내가 수정 중인지 확인해 주는 역할을 할 것이다.

modifyingIndex 변수는 현재 내가 수정 중인 task의 index를 저장하는 역할을 할 것이다.

 

변수 추가를 완료했다면, 이제 수정 기능을 구현해 보자.

 

1.

우선, 할 일 요소에 있는 수정 버튼을 누르면 해당하는 work 내용이 TextField에 나타나는 것부터 구현해 보자.

수정 버튼의 코드를 다음과 같이 수정해 보자.

 

TextButton(
                    onPressed: isModifying
                        ? null
                        : () {
                            setState(() {
                              isModifying = true;
                              _textController.text = tasks[i].work;
                              modifyingIndex = i;
                            });
                          },
                    child: const Text("수정"),
                  ),

 

수정 버튼을 누르면, 위에서 선언한 변수인 isModifying 값을 확인한다.

 

만약 수정중이라면(isModifying == true), onPressed 옵션의 값을 null로 만들어준다.

그렇지 않으면(isModifying == false), isModifying의 값을 true로 수정하고, 현재 버튼을 누른 task의 work 값을 _textController.text를 통해 TextField에 올려준다.

 

이후, modifyingIndex 변수에 현재 task의 index를 저장하여 값을 수정하고 저장할 곳을 기억해 둔다.

 

버튼을 누른 뒤 변경 내용을 화면에 보여주기 위해서 setState를 사용하였다.

 

2.

이제, TextField 내용을 수정한 후 add버튼을 누르면 수정을 눌렀던 할 일 요소의 내용이 바뀌는 기능을 추가해 보자.

add 버튼의 내용을 다음과 같이 수정해 보자.

 

ElevatedButton(
                    onPressed: () {
                      if (_textController.text == '') {
                        return;
                      } else {
                        isModifying
                            ? setState(
                                () {
                                  tasks[modifyingIndex].work =
                                      _textController.text;
                                  tasks[modifyingIndex].isComplete = false;
                                  _textController.clear();
                                  modifyingIndex = 0;
                                  isModifying = false;
                                },
                              )
                            : setState(
                                () {
                                  var task = Task(_textController.text);
                                  tasks.add(task);
                                  _textController.clear();
                                },
                              );
                      }
                    },
                    child: isModifying ? const Text("수정") : const Text("추가"),
                  ),

 

현재, 수정중이라면 저장된 modifyingIndex를 이용해 수정 중인 task의 work 내용을 TextField의 내용으로 교체해 준다.

그 후, isComplete를 false로 초기화하고, TextField를 비워준다.

이후, modifyingIndex와 isModifying 변수를 초기화해 준다.

 

버튼을 누른 뒤 변경 내용을 화면에 보여주기 위해서 setState를 사용하였다.

 

버튼에 나타나는 내용은 isModifying의 값에 따라 수정 또는 추가로 변하게 해 주었다.

 

3. 진척도 표시

LinearPercentIndicator에서 percent를 나타내기 위해서, percent를 계산하는 함수를 만들어 보자.

우선, LinearPercentIndicator에서 percent를 업데이트해 줄 변수를 선언해 주자.

_MyHomePageState 클래스 내부에 다음과 같이 변수를 추가하자.

 

double percent = 0.0;

 

이 변수를 통해 우리는 LinearPercentIndicator를 최신화할 것이다.

 

그렇다면, percent를 최신화해 주는 함수를 작성해 보자.

 

getToday() 함수 아래에 다음과 같은 함수를 추가해 주자.

 

  void updatePercent() {
    if (tasks.isEmpty) {
      percent = 0.0;
    } else {
      var completeTaskCnt = 0;
      for (var i = 0; i < tasks.length; i++) {
        if (tasks[i].isComplete) {
          completeTaskCnt += 1;
        }
      }
      percent = completeTaskCnt / tasks.length;
    }
  }

 

tasks에서 완료된 task의 개수를 세서, 전체 개수로 나누면 percent 계산이 완료된다. 

task들을 삭제하다가 0이 되었을 때 NaN 오류를 막아주기 위해 if else문을 사용하였다.

 

이제, LinearPercentIndicator에서 percent 변수를 사용하고, task가 업데이트될 때마다 updatePercent 함수를 호출해 주면 된다. updatePercent 함수를 호출하는 경우는 수정, 추가, 삭제 기능을 수행한 후 마지막으로 호출해 주면 된다.

코드가 추가된 부분은 맨 아래 전체 코드를 통해서 확인하도록 하자.

 

4. 간단한 style 수정

 

현재 우리가 만든 To-Do List의 글자 크기가 너무 작다. 

flutter의 theme를 이용하여 Text style을 관리해 보자.

 

build 부분의 theme 옵션을 다음과 같이 수정해 보자.

 

Widget build(BuildContext context) {
    return MaterialApp(
      title: 'To-Do List',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
        textTheme: const TextTheme(
          bodyMedium: TextStyle(fontSize: 24, fontWeight: FontWeight.w400),
          labelMedium: TextStyle(fontSize: 16, fontWeight: FontWeight.w400),
        ),
      ),
      home: const MyHomePage(title: 'To-Do List'),
    );
  }

bodyMedium과 labelMedium이라는 TextTheme을 추가하였다. 이를 통해 Row에 있는 TextButton의 Text 스타일을 지정해 주자.

 

Row(
                  children: [
                    Flexible(
                      child: TextButton(
                        style: TextButton.styleFrom(
                          shape: const RoundedRectangleBorder(
                            borderRadius: BorderRadius.all(Radius.zero),
                          ),
                        ),
                        onPressed: () {
                          setState(() {
                            tasks[i].isComplete = !tasks[i].isComplete;
                            updatePercent();
                          });
                        },
                        child: Padding(
                          padding: const EdgeInsets.all(8.0),
                          child: Row(
                            children: [
                              tasks[i].isComplete
                                  ? const Icon(Icons.check_box_rounded)
                                  : const Icon(
                                      Icons.check_box_outline_blank_rounded),
                              Text(
                                tasks[i].work,
                                style: Theme.of(context).textTheme.labelMedium,
                              )
                            ],
                          ),
                        ),
                      ),
                    ),
                    TextButton(
                      onPressed: isModifying
                          ? null
                          : () {
                              setState(() {
                                isModifying = true;
                                _textController.text = tasks[i].work;
                                modifyingIndex = i;
                              });
                            },
                      child: const Text("수정"),
                    ),
                    TextButton(
                      onPressed: () {
                        setState(() {
                          tasks.remove(tasks[i]);
                          updatePercent();
                        });
                      },
                      child: const Text("삭제"),
                    ),
                  ],
                )

또한, 할 일 목록이 길어지면 레이아웃이 깨지는 현상이 발생한다. 이를 막기 위해서 body 부분을 SingleChildScrollView Widget으로 감싸서 할 일 요소가 많아지거나 키보드가 올라왔을 때 스크롤을 할 수 있도록 만들어 주었다.

 

최종적으로 완성된 코드는 다음과 같다.

 
import 'package:flutter/material.dart';
import 'package:flutter_soss_class/task.dart';
import 'package:intl/intl.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'To-Do List',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
        textTheme: const TextTheme(
          bodyMedium: TextStyle(fontSize: 24, fontWeight: FontWeight.w400),
          labelMedium: TextStyle(fontSize: 16, fontWeight: FontWeight.w400),
        ),
      ),
      home: const MyHomePage(title: 'To-Do List'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _textController = TextEditingController();
  List<Task> tasks = [];

  bool isModifying = false;
  int modifyingIndex = 0;
  double percent = 0.0;

  String getToday() {
    DateTime now = DateTime.now();
    String strToday;
    DateFormat formatter = DateFormat('yyyy-MM-dd');
    strToday = formatter.format(now);
    return strToday;
  }

  void updatePercent() {
    if (tasks.isEmpty) {
      percent = 0.0;
    } else {
      var completeTaskCnt = 0;
      for (var i = 0; i < tasks.length; i++) {
        if (tasks[i].isComplete) {
          completeTaskCnt += 1;
        }
      }
      percent = completeTaskCnt / tasks.length;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(
          widget.title,
        ),
      ),
      body: SingleChildScrollView(
        child: Center(
          child: Column(
            children: [
              Text(getToday()),
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: Row(
                  children: [
                    Flexible(
                      flex: 1,
                      child: TextField(
                        controller: _textController,
                      ),
                    ),
                    ElevatedButton(
                      onPressed: () {
                        if (_textController.text == '') {
                          return;
                        } else {
                          isModifying
                              ? setState(
                                  () {
                                    tasks[modifyingIndex].work =
                                        _textController.text;
                                    tasks[modifyingIndex].isComplete = false;
                                    _textController.clear();
                                    modifyingIndex = 0;
                                    isModifying = false;
                                  },
                                )
                              : setState(
                                  () {
                                    var task = Task(_textController.text);
                                    tasks.add(task);
                                    _textController.clear();
                                  },
                                );
                          updatePercent();
                        }
                      },
                      child: isModifying ? const Text("수정") : const Text("추가"),
                    ),
                  ],
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(vertical: 20.0),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    LinearPercentIndicator(
                      width: MediaQuery.of(context).size.width - 50,
                      lineHeight: 14.0,
                      percent: percent,
                    )
                  ],
                ),
              ),
              for (var i = 0; i < tasks.length; i++)
                Row(
                  children: [
                    Flexible(
                      child: TextButton(
                        style: TextButton.styleFrom(
                          shape: const RoundedRectangleBorder(
                            borderRadius: BorderRadius.all(Radius.zero),
                          ),
                        ),
                        onPressed: () {
                          setState(() {
                            tasks[i].isComplete = !tasks[i].isComplete;
                            updatePercent();
                          });
                        },
                        child: Padding(
                          padding: const EdgeInsets.all(8.0),
                          child: Row(
                            children: [
                              tasks[i].isComplete
                                  ? const Icon(Icons.check_box_rounded)
                                  : const Icon(
                                      Icons.check_box_outline_blank_rounded),
                              Text(
                                tasks[i].work,
                                style: Theme.of(context).textTheme.labelMedium,
                              )
                            ],
                          ),
                        ),
                      ),
                    ),
                    TextButton(
                      onPressed: isModifying
                          ? null
                          : () {
                              setState(() {
                                isModifying = true;
                                _textController.text = tasks[i].work;
                                modifyingIndex = i;
                              });
                            },
                      child: const Text("수정"),
                    ),
                    TextButton(
                      onPressed: () {
                        setState(() {
                          tasks.remove(tasks[i]);
                          updatePercent();
                        });
                      },
                      child: const Text("삭제"),
                    ),
                  ],
                )
            ],
          ),
        ),
      ),
    );
  }
}

 

이렇게 간단하게 To-Do List 구현을 마치도록 하겠다.

 

간단하게 만들어 본 To-Do List라서 코드가 깔끔하진 않다. 발견하지 못한 오류도 있을 수 있다.

앞으로 Flutter와 친해지면서 점점 개선해 보도록 하자!