플러터 (flutter)

Flutter를 이용한 Todo 앱 제작하기

CreatoMaestro 2023. 7. 9. 18:43
반응형

이번 글에서는 Todo app을 제작해본다.

 

이 앱의 기능은 다음과 같다.

Todo 앱 제작 결과
앱 동작 과정

(1) 앱이 처음 실행되면 뜨는 창이다.

(2) 텍스트 필드를 터치하면 필드 안에 글을 쓸 수 있다.

(3) 텍스트 필드 밑에 있는 '+' 버튼을 누르면 할 일이 추가된다.

(4) 체크 박스를 터치하면 완료로 뜬다.

(5) 오른쪽 아래에 있는 refresh 버튼을 누르면 완료된 일이 사라진다.

 

이 앱의 제작에는 3개의 dart 파일이 사용되었다.

lib 파일 안에 다음과 같이 폴더와 파일을 추가해준다.

폴더 및 파일 추가

1. textBox.dart

textBox.dart에서는 맨 위의 제목과 할 일을 적는 TextField가 포함되어 있다.

그 밑에는 '+' 버튼을 생성하여 할 일을 추가할 수 있도록 했다.

 

'textBox.dart'의 위젯트리는 다음과 같다.

Todo 앱 위젯 트리
textBox 위젯 트리

StatefulWidget 클래스를 상속 받는 TodoInput이 맨 위에 있고, 그 밑에 정렬을 위한 Column 위젯이 위치해있다.

그 밑으로 4개의 위젯이 있다.

IconButton, TextField, Text를 제외한 나머지는 정렬을 위한 위젯이다.

 

import 'package:flutter/material.dart';

class TodoInput extends StatefulWidget {
  final Function(String value,double size) addTodo;
  double size;
  TodoInput({
    required this.addTodo,
    required this.size,
    Key? key
}):super(key: key);

  @override
  State<TodoInput> createState() => _TodoInputState();
}

위 코드는 TodoInput을 선언하기 위한 코드이다.

StatefulWidget을 상속 받으며, Function 타입의 변수와 size 변수를 갖는다.

생성자를 통해 이 두 변수를 초기화해준다.

 

addTodo는 버튼이 눌렸을 때, 할 일을 추가해주는 함수이다.

size는 기긱의 width 값을 받아온다.

 

class _TodoInputState extends State<TodoInput> {
  late TextEditingController _controller;
  final FocusNode _focus = FocusNode();

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void sendText(String value) {
      widget.addTodo(value,widget.size);
  }

TodoInput의 State 함수인 _TodoInputState 클래스이다.

변수로는 텍스트 필드의 controller인 _controller 변수는 initState함수를 실행할 때 초기화해준다.

_focus는 텍스트 필드에 포커스르 맞출 지 결정하기 위해 사용하는 변수이다.

 

sendText는 '+'버튼이 눌렸을 때 실행할 함수이다.

변수로 받아온 addTodo 함수를 실행해준다.

 

2.TodoBox.dart

TodoBox에서는 할 일을 나타내는 상자를 생성해낸다.

체크 박스와 텍스트로 이루어져 있으며 체크 박스를 통해 할 일이 끝났는지 확인한다.

 

다음은 위젯 트리이다.

TodoBox 위젯트리
TodoBox 위젯 트리

import 'package:flutter/material.dart';

class TodoBox extends StatefulWidget {
  bool isDone = false;
  double size;
  final String todoText;

  TodoBox({
    required this.size,
    required this.todoText,
    Key? key}):super(key: key);

  @override
  State<TodoBox> createState() => _TodoBoxState();
}

다음 코드는 TodoBox를 선언하는 코드이다.

isDone 변수는 이 할일이 끝났는지 확인한다.

체크 박스가 체크 되면 true가 된다. 

아니면 false 상태이다.

 

size 변수는 디바이스의 width를 나타낸다.

todoText는 텍스트 필드에서 받아온 문자열을 저장한다.

 

size와 todoText는 생성자에서 초기화한다.

 

class _TodoBoxState extends State<TodoBox> {

  Widget checkBox() {
    return Checkbox(
        value: widget.isDone,
        onChanged: (bool? value) {
          setState(() {
            widget.isDone = value!;
          });
        }
    );
  }

다음은 TodoBox클래스의 상태클래스인 _TodoBoxState 클래스이다.

 

checkBox 함수는 Checkbox위젯을 반환한다.

이 체크 박스는 체크가 되면 true를 isDone에 저장하고, 체크가 해제되면, isDone에 false르 전환한다.

value에 넣은 값이 false면 체크가 해제된 상태가 되고, true면 체크가 된 상태가 된다.

 

@override
Widget build(BuildContext context) {
  return IntrinsicHeight(
    child: Column(
      children: [
        const SizedBox(
          height: 5.0,
        ),
        Container(
          decoration: BoxDecoration(
            border: Border.all(color: Colors.white),
          ),
          width: widget.size,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              checkBox(),
              Text(
                widget.todoText,
                style: TextStyle(
                  decoration: widget.isDone == true ? TextDecoration.lineThrough : TextDecoration.none,
                  fontSize: 20.0,
                ),
              ),
            ],
          ),
        ),
        const SizedBox(
          height: 5.0,
        ),
      ],
    ),
  );
}

다음은 실제 위젯을 반환하는 build 함수이다.

Column, SizedBox는 배치를 위한 위젯이고, 동작에는 영향을 주지 않는다.

 

IntrinsicHeight 위젯은 자식 위젯이 가지는 높이 만큼만 높이를 차지하도록 해주는 위젯이다.

 

Container 안에 decoriation 파라미터를 이용해 border를 생성하고, 그 색을 흰색으로 바꿔준다.

자식 위젯으로는 Row 위젯을 가지며, 이 안에 체크박스와 텍스트가 있다.

Checkbox는 위에서 선언해준 checkBox 함수를 이용해 만들어주었다.

 

Text 위젯의 TextStyle 함수에서 ?:구문을 이용해 isDone이 true이면 줄을 긋게 만들었다.

 

3.main.dart

import 'package:flutter/material.dart';
import 'package:todo_project/component/textbox.dart';
import 'package:todo_project/component/TodoBox.dart';

void main() {
  runApp(MaterialApp(
    theme: ThemeData.dark(),
    home: MainScreen(),
  ));
}

main.dart의 main함수이다.

테마는 어두운 테마를 적용해 주었고, MainScreen에서 반환하는 위젯을 넣어준다.

 

class MainScreen extends StatefulWidget {
  List<TodoBox> todolist = [];

  MainScreen({Key? key}) : super(key: key);

  @override
  State<MainScreen> createState() => _MainScreenState();
}

위 코드는 MainScreen 클래스이다.

앞서 만들어준 위젯들을 하나로 묶어주는 역할을 한다.

 

생성해준 TodoBox는 todolist 변수 안에 리스트로 저장해놓는다.

 

class _MainScreenState extends State<MainScreen> {
  void addTodo(String value, double size) {
    print(value);
    setState(() {
      widget.todolist.add(TodoBox(size: size, todoText: value));
    });
  }

위 코드는 MainScreen의 상태 클래스와 그 안에 있는 addTodo method이다.

 

'+' 버튼을 통해 새로운 텍스트가 들어오면 todolist 안에 위젯 형태로 넣어준다. (.add 메서드 이용)

위에 있는 print는 테스트 용이므로 없어도 무관하다.

 

@override
Widget build(BuildContext context) {
  double deviceWidth = MediaQuery.of(context).size.width;
  return Scaffold(
    floatingActionButton: FloatingActionButton(
      onPressed: () {
        setState(() {
          widget.todolist = widget.todolist
              .where((element) => element.isDone != true)
              .toList();
        });
      },
      child: const Icon(Icons.refresh),
    ),

위 코드는 build 함수이다.

floatingActionButton은 완료된 할 일을 삭제하는 여갛ㄹ을 한다.

onPressed 함수를 통해 눌렀을 때 실행할 코드를 정의해준다.

setState 함수를 통해 위젯을 다시 build 해주고, 그 전에 where 메서드를 통해 isDone 변수가 true인 요소를 정리해준다.

where 메서드는 뒤에 있는 조건 식이 참인 요소만 뽑아낸다.

 

body: SafeArea(
    child: Column(
  children: [
    TodoInput(
      addTodo: addTodo,
      size: deviceWidth,
    ),
    ...widget.todolist,
  ],
)),

다음은 지금까지 만들어준 위젯을 하나로 묶어주는 코드이다.

Column 위젯을 이용해 새로로 정렬하였다.

 

TodoInput은 textbox.dart에서 정의해주었고, todolist에 들어가 있는 TodoBox는 TodoBox.dart에서 정의해주었다.

 

SafeArea는 위젯이 잘려 나오지 않도록 막아주는 역할을 한다.

 

아래는 전체 코드이다.

 

1) textbox.dart

import 'package:flutter/material.dart';

class TodoInput extends StatefulWidget {
  final Function(String value,double size) addTodo;
  double size;
  TodoInput({
    required this.addTodo,
    required this.size,
    Key? key
}):super(key: key);

  @override
  State<TodoInput> createState() => _TodoInputState();
}

class _TodoInputState extends State<TodoInput> {
  late TextEditingController _controller;
  final FocusNode _focus = FocusNode();

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void sendText(String value) {
      widget.addTodo(value,widget.size);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
                "Todo App",
              style: TextStyle(
                fontWeight: FontWeight.w700,
                fontSize: 30.0,
              ),
            ),
          ],
        ),
        SizedBox(
          height: 16.0,
        ),
        SizedBox(
          width: widget.size,
          child: TextField(
            controller: _controller, //컨트롤러는 위에서 선언한 컨트롤러
            decoration: InputDecoration(
              border: OutlineInputBorder(), //border 생성
              labelText: "Write the TODO list", // 힌트 텍스트
            ),
            onSubmitted: sendText,
            maxLines: 3,
            focusNode: _focus,
          ),
        ),
        SizedBox(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              IconButton(onPressed: () {
                if(_controller.text != ''){
                  sendText(_controller.text);
                }
                _controller.clear();
                _focus.unfocus();
              }, icon: Icon(Icons.add),),
            ],
          ),
        ),
      ],
    );
  }
}

 

2) TodoBox.dart

import 'package:flutter/material.dart';

class TodoBox extends StatefulWidget {
  bool isDone = false;
  double size;
  final String todoText;

  TodoBox({
    required this.size,
    required this.todoText,
    Key? key}):super(key: key);

  @override
  State<TodoBox> createState() => _TodoBoxState();
}

class _TodoBoxState extends State<TodoBox> {

  Widget checkBox() {
    return Checkbox(
        value: widget.isDone,
        onChanged: (bool? value) {
          setState(() {
            widget.isDone = value!;
          });
        }
    );
  }

  @override
  Widget build(BuildContext context) {
    return IntrinsicHeight(
      child: Column(
        children: [
          const SizedBox(
            height: 5.0,
          ),
          Container(
            decoration: BoxDecoration(
              border: Border.all(color: Colors.white),
            ),
            width: widget.size,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                checkBox(),
                Text(
                  widget.todoText,
                  style: TextStyle(
                    decoration: widget.isDone == true ? TextDecoration.lineThrough : TextDecoration.none,
                    fontSize: 20.0,
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(
            height: 5.0,
          ),
        ],
      ),
    );
  }

}

 

3) main.dart

import 'package:flutter/material.dart';
import 'package:todo_project/component/textbox.dart';
import 'package:todo_project/component/TodoBox.dart';

void main() {
  runApp(MaterialApp(
    theme: ThemeData.dark(),
    home: MainScreen(),
  ));
}

class MainScreen extends StatefulWidget {
  List<TodoBox> todolist = [];

  MainScreen({Key? key}) : super(key: key);

  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  void addTodo(String value, double size) {
    print(value);
    setState(() {
      widget.todolist.add(TodoBox(size: size, todoText: value));
    });
  }

  @override
  Widget build(BuildContext context) {
    double deviceWidth = MediaQuery.of(context).size.width;
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            widget.todolist = widget.todolist
                .where((element) => element.isDone != true)
                .toList();
          });
        },
        child: const Icon(Icons.refresh),
      ),
      body: SafeArea(
          child: Column(
        children: [
          TodoInput(
            addTodo: addTodo,
            size: deviceWidth,
          ),
          ...widget.todolist,
        ],
      )),
    );
  }
}

 

반응형