관리 메뉴

cococo-coding

[flutter/투두리스트 분석] home.dart 파일 구조 및 코드 분석 본문

[Flutter] Todolist/프로젝트 분석글

[flutter/투두리스트 분석] home.dart 파일 구조 및 코드 분석

_dani 2024. 2. 16. 14:16

프로젝트 구조

home.dart파일은 lib디렉터리 > screens 디렉터리에 있다. 

투두리스트에서 가장 긴 파일이라 코드 분석 글도 길어질 것 같다. 


전체 코드

import 'package:flutter/material.dart';
import 'package:flutter_todolist_app/model/todo.dart';
import 'package:flutter_todolist_app/constants/colors.dart';
import 'package:flutter_todolist_app/widgets/todo_item.dart';

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

  @override
  State<Home> createState() => _HomeState();
}
   class _HomeState extends State<Home>{
  final todosList = ToDo.todoList();
  List<ToDo> _foundToDo =[];
  final _todoController=TextEditingController();

  @override
  void initState(){
    _foundToDo = todosList;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: toBGColor,
      appBar: _buildAppBar(),
      body: Stack(
        children: [
          Container(
            padding: EdgeInsets.symmetric(
                horizontal:20,
                vertical: 15
            ),
            child: Column(
              children: [
                searchBox(),
                Expanded(
                  child: ListView(
                    children: [
                      Container(
                        margin: EdgeInsets.only(top: 50, bottom: 20),
                        child: Text('All ToDos', style: TextStyle(
                          fontSize: 30,
                          fontWeight: FontWeight.w500,
                        ),
                        ),
                      ),

                      for ( ToDo todoo in _foundToDo.reversed)
                        ToDoItem(
                            todo: todoo,
                            onToDoChanged: _handleToDoChange,
                            onDeleteItem: _deleteToDoItem,
                        ),

                    ],
                  ),
                )
              ],
            ),
          ),
          Align(
            alignment: Alignment.bottomCenter,
            child: Row(
              children: [
                Expanded(child: Container(
                  margin: EdgeInsets.only(
                    bottom: 20,
                    right:20,
                    left: 20,
                  ),
                  padding:EdgeInsets.symmetric(
                     horizontal: 20,
                     vertical: 5,
                  ),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    boxShadow: const [BoxShadow(
                      color: Colors.grey,
                      offset: Offset(0.0, 0.0),
                      blurRadius: 10.0,
                      spreadRadius: 0.0,
                    ),],
                    borderRadius: BorderRadius.circular(10),
                  ),
                  child: TextField(
                    controller: _todoController,
                    decoration: InputDecoration(
                      hintText: 'Add a new todo item',
                      border: InputBorder.none
                    ),
                  ),
                ),
                ),
                Container(
                  margin: EdgeInsets.only(
                      bottom:20,
                      right: 20,
                  ),
                  child: ElevatedButton(
                    child: Text('+', style: TextStyle(fontSize: 40,),),
                    onPressed: () {
                      _addToDoItem(_todoController.text);
                    },
                    style: ElevatedButton.styleFrom(
                      primary: toBlue,
                      minimumSize: Size(60, 60),
                      elevation: 10,
                    ),
                  ),
                ),
              ],
            )
          )
        ],
      ),
    );
  }

  void _handleToDoChange(ToDo todo){
    setState(() {
      todo.isDone = !todo.isDone;
    });
  }

  void _deleteToDoItem(String id){
    setState(() {
      todosList.removeWhere((item) => item.id == id);
    });
  }

  void _addToDoItem(String toDo){
    setState(() {
      todosList.add(ToDo(
          id: DateTime.now().millisecondsSinceEpoch.toString(),
          todoText: toDo
      ));
    });
    _todoController.clear();
  }

  void _runFilter(String enteredKeyword) {
    List<ToDo> results = [];
    if (enteredKeyword.isEmpty) {
      results = todosList;
    } else {
      results = todosList.
      where((item) =>
          item.todoText!
              .toLowerCase()
              .contains(enteredKeyword.toLowerCase()))
          .toList();
    }

    setState(() {
      _foundToDo = results;
    });
  }

  Widget searchBox() {
    return Container(
      padding: EdgeInsets.symmetric(horizontal: 15),
      decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(20)
      ),
      child: TextField(
        onChanged: (value) => _runFilter(value),
        decoration: InputDecoration(
          contentPadding: EdgeInsets.all(0),
          prefixIcon: Icon(
            Icons.search,
            color: toBlack,
            size: 20,
          ),
          prefixIconConstraints: BoxConstraints(
            maxHeight: 20,
            maxWidth: 25,
          ),
          border: InputBorder.none,
          hintText: 'Search',
          hintStyle: TextStyle(color: toGrey),
        ),
      ),
    );
  }

  AppBar _buildAppBar(){
    return AppBar(
      backgroundColor: toBGColor,
      elevation: 0,
      title: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Icon(
              Icons.menu,
              color: toBlack,
              size:30,
            ),
            Container(
              height: 40,
              width: 40,
              child: ClipRRect(
                borderRadius: BorderRadius.circular(20),
                child:Image.asset('assets/images/avatar.jpg'),
              ),
            ),
          ]),
    );
  }
}

120줄까지는 Widget build에 관한 코드이고

그 이후부터는 사용자정의함수나 위젯, appBar같은 정의코드이다. 

 

하나씩 분석해보자.


 

import 'package:flutter/material.dart';
import 'package:flutter_todolist_app/model/todo.dart';
import 'package:flutter_todolist_app/constants/colors.dart';
import 'package:flutter_todolist_app/widgets/todo_item.dart';

 

material 디자인을 이용하기 위해 material.dart 패키지 인포트

그리고 직접 만든 세가지 다트파일들(todo.dart, colors.dart, todo_item.dart)도 인포트해서 쓰도록 한다.

//변경가능한 Home클래스
class Home extends StatefulWidget {
  //{Key? key} : key타입의 매개변수가 있거나 null일수 있음.
  //아래코드는 Home 생성자의 인자인 key가 상속해준 클래스의 key를 value로 갖는다는 뜻
  Home({Key? key}) : super(key: key);

  //상속받은 클래스의 메소드를 재정의한다
  //statefulWidget은 createState() 메소드에서 생성한 객체를 반환해야 함
  //statefulWidget 안이 아니라 State 클래스에 build 메소드가 있음
  //statelessWidget의 경우는 build메서드에서 생성한 객체를 바로 반환하지만
  //statefulWidget은 createState() 메서드에서 생성한 객체를 반환함
  //_HomeState()에서 build가 진행된다는 뜻임.
  @override
  State<Home> createState() => _HomeState();
}
  
  //State<Home>을 상속한 클래스 _HomeState를 만든다.
  class _HomeState extends State<Home>{
  //final: 불변 상수로 취급 (const와 비슷함)
  //빈 리스트 <ToDo>를 생성
  //TextEditingController(): 편집가능한 텍스트필드 컨트롤러
  final todosList = ToDo.todoList();
  List<ToDo> _foundToDo =[];
  final _todoController=TextEditingController();

  //initState 메소드
  //객체가 트리에 삽입될 때 호출되며, 각 State 개체에 대해 이 메서드를 한번만 호출함
  //todosList를 _foundToDo 리스트에 넣음
  //super.initState(): 필수로 있어야하며 material super constructor for init state
  @override
  void initState(){
    _foundToDo = todosList;
    super.initState();
  }

Home

statefulWidget은 변경가능한 위젯이며, 이를 상속한 Home 클래스를 생성한다.

이때 statefulWidget은 statelessWidget과 다르게 createState()메서드에서 생성한 객체를 반환해야하는데

_HomeState()에서 build가 진행된다.

 

_HomeState

그리고 이 Home 클래스를 상속받은 클래스 _HomeState를 생성한다.

_HomeState클래스는 todosList, _foundToDo, _todoController라는 멤버변수와 함수를 갖는다.

각각이 무엇을 의미하는지 아직 정확하지는 않으므로 추후에 추가하겠다.

 

void initState()

각 State개체가 호출될 때 딱 한번 호출되는 메서드로, 초기화를 수행한다. 

 

 @override
  //BuildContext: 위젯트리에서 현재 위젯위치를 알 수 있는 locator
  //즉 현재빌드에 대한 정보를 저장하는 객체이다. 
  //모든 위젯은 build method를 가지는데, 그 build method의 타입은 widget이며, 인자로 BuildContext를 가짐
  Widget build(BuildContext context) {
    //Scaffold를 리턴한다.
    //이때 백그라운드색은 미리 지정한 색으로 지정한다. 
    return Scaffold(
        backgroundColor: toBGColor,
      //appBar: 상단의 앱바를 나타낸다. 코드 제일 하단에 _buildAppBar()를 설명해두었다.
      appBar: _buildAppBar(),
      //body: 화면중앙에 나타날 부분이다.
      //Stack: 여러 위젯들이 중첩가능한 위젯, 제일 처음오는 child가 제일 밑에 깔린다.
      body: Stack(
        children: [
          Container(
            padding: EdgeInsets.symmetric(
                horizontal:20,
                vertical: 15
            ),
            //child로 Column을 선언
            //Column: children을 수직으로 나열하는 위젯
            child: Column(
              //children에는 serachBox(), Expanded(), for문이 있음
              children: [
                searchBox(),
                //Expanded: row, column, flex 의 자식들이 이용할수있는 공간을 채울수있도록 만드는 위젯(각 주축에 따라서)
                Expanded(
                  child: ListView(
                    children: [
                      Container(
                        margin: EdgeInsets.only(top: 50, bottom: 20),
                        child: Text('All ToDos', style: TextStyle(
                          fontSize: 30,
                          fontWeight: FontWeight.w500,
                        ),
                        ),
                      ),

                      for ( ToDo todoo in _foundToDo.reversed)
                        ToDoItem(
                            todo: todoo,
                            onToDoChanged: _handleToDoChange,
                            onDeleteItem: _deleteToDoItem,
                        ),

                    ],
                  ),
                )
              ],
            ),
          ),
          //Align: 부모위젯 내의 자식위젯을 원하는 위치에 정렬하는 위젯
          //새로운 투두를 추가하는 하단바 정의
          Align(
            alignment: Alignment.bottomCenter,
            child: Row(
              children: [
                Expanded(child: Container(
                  margin: EdgeInsets.only(
                    bottom: 20,
                    right:20,
                    left: 20,
                  ),
                  padding:EdgeInsets.symmetric(
                     horizontal: 20,
                     vertical: 5,
                  ),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    boxShadow: const [BoxShadow(
                      color: Colors.grey,
                      offset: Offset(0.0, 0.0),
                      blurRadius: 10.0,
                      spreadRadius: 0.0,
                    ),],
                    borderRadius: BorderRadius.circular(10),
                  ),
                  child: TextField(
                    controller: _todoController,
                    decoration: InputDecoration(
                      hintText: 'Add a new todo item',
                      border: InputBorder.none
                    ),
                  ),
                ),
                ),
                //+버튼 정의
                Container(
                  margin: EdgeInsets.only(
                      bottom:20,
                      right: 20,
                  ),
                  child: ElevatedButton(
                    child: Text('+', style: TextStyle(fontSize: 40,),),
                    onPressed: () {
                      _addToDoItem(_todoController.text);
                    },
                    style: ElevatedButton.styleFrom(
                      primary: toBlue,
                      minimumSize: Size(60, 60),
                      elevation: 10,
                    ),
                  ),
                ),
              ],
            )
          )
        ],
      ),
    );
  }

 

이 구조가 여러 위젯이 있어서 복잡한데 간단히 나타내면 다음과 같다. 

Scaffold 
ㄴAppBar
ㄴBody
    ㄴStack
    ㄴAlign

 

Scaffold

기본 material 디자인 시각적 레이아웃 구조를 구현하는 클래스

 

AppBar

material 디자인의 appbar이며, 다른 위젯(tabBar나 FlexibleSpaceBar)이나 toolBar로 구성된다.

AppBar는 보통 Scaffold.appBar 속성으로 쓰이며, 화면 상단에 고정된 높이의 위젯으로 위치한다.

여기서는 코드 제일 하단에 _buildAppBar() 를 따로 정의하고, build(BuildContext context)에서는 이를 가져다썼다.

 

Body

scaffold의 주요 콘텐츠이다. appBar의 아래, scaffold 하단 사이에 위치하는 공간이다.

 

Stack 

상자의 가장자리를 기준으로 자식위치를 지정하는 위젯이다.

여러 하위 항목(위젯)들을 겹칠 때 쓰면 된다. 이때 첫 번째 자식이 맨 아래에 오도록 순서대로 자식을 그린다. 

 

Align

간단히 말하면 정렬 위젯을 만드는 클래스이다. 

부모위젯 내의 자식위젯을 원하는 구역에 위치시킬 수 있다.  

 

이외의 부수적인 설정들은 따로 코멘트를 달지는 않았다.


 

//사용자정의함수들
//todo.isDone의 상태를 바꾸는 _handleToDoChange
void _handleToDoChange(ToDo todo){
    setState(() {
      todo.isDone = !todo.isDone;
    });
  }
  
  //투두리스트를 삭제하는 _deleteToDoItem
  //item의 id를 삭제함으로 구현
  void _deleteToDoItem(String id){
    setState(() {
      todosList.removeWhere((item) => item.id == id);
    });
  }
  
  //새로운 일정을 추가하는 _addToDoItem
  //id는 고유해야하므로 DateTime.now()로 생성하고
  //todoText는 todo를 받는다.
  //_todoController도 위의 Home클래스에서 정의한 멤버함수로 textEditingController를 받는다.
  
  void _addToDoItem(String toDo){
    setState(() {
      todosList.add(ToDo(
          id: DateTime.now().millisecondsSinceEpoch.toString(),
          todoText: toDo
      ));
    });
    _todoController.clear();
  }

  //서치한 결과를 보여주는 _runFilter
  //enteredKeyword가 empty이면 todosList를 그대로 result로 주고
  //아니라면 enteredKeyword를 소문자로 바꾸어서 보여준다.
  void _runFilter(String enteredKeyword) {
    List<ToDo> results = [];
    if (enteredKeyword.isEmpty) {
      results = todosList;
    } else {
      results = todosList.
      where((item) =>
          item.todoText!
              .toLowerCase()
              .contains(enteredKeyword.toLowerCase()))
          .toList();
    }

    //빈리스트 _foundToDo에 results를 넣는다
    setState(() {
      _foundToDo = results;
    });
  }

사용자정의함수들이다.

기본적으로 setState()라는 함수가 있는데, results를 받아 _foundToDo에 넣는다.

_foundToDo는 우리가 맨 처음에 클래스 _HomeState를 정의하면서 멤버함수로 정의했었던 빈 리스트이다. 

 

여기가 실제적인 기능이 들어간 코드라서

이 부분을 집중공부해야겠다.

 

우선은 서치해서 기본적으로 어떤 기능을 하는지만 써놨다.

 

//searchBox위젯 설명
Widget searchBox() {
    //컨테이너를 리턴한다.
    return Container(
      //컨터이너의 속성 선언
      padding: EdgeInsets.symmetric(horizontal: 15),
      decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(20)
      ),
      //컨테이너 child 속성으로 TextField 선언
      //onChanged 속성은 내용이 바뀔 때마다 시행되는 함수로
      //value속성을 받아 _runFilter(value)로 넘겨준다.    
      child: TextField(
        onChanged: (value) => _runFilter(value),
        //데코속성들
        decoration: InputDecoration(
          contentPadding: EdgeInsets.all(0),
          //아이콘
          prefixIcon: Icon(
            Icons.search,
            color: toBlack,
            size: 20,
          ),
          //접두사아이콘에 대한 제약조건
          prefixIconConstraints: BoxConstraints(
            maxHeight: 20,
            maxWidth: 25,
          ),
          border: InputBorder.none,
          hintText: 'Search',
          hintStyle: TextStyle(color: toGrey),
        ),
      ),
    );
  }

화면에 나오는 searchBox를 설명한 코드이다.

 

우선 searchBox()위젯은 컨테이너를 리턴한다.

padding과 decoration을 설정해주고

child속성으로 TextField를 받는다.

 

이때 우리는 서치를 할 때마다 서치내용이 나와야하는데 이를 TextField와 onChanged로 구현한다.

onChanged: (value) => _runFilter(value)는

onChanged(value){

_runFilter(value);

}

라고 생각하면 된다. 

 

나머지는 아이콘과 데코속성이 대부분이라 생략한다. 

AppBar _buildAppBar(){
    return AppBar(
      backgroundColor: toBGColor,
      elevation: 0,
      title: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Icon(
              Icons.menu,
              color: toBlack,
              size:30,
            ),
            Container(
              height: 40,
              width: 40,
              child: ClipRRect(
                borderRadius: BorderRadius.circular(20),
                child:Image.asset('assets/images/avatar.jpg'),
              ),
            ),
          ]),
    );

위의 scaffold 위젯 안에 AppBar에 _buildAppBar로 지정하고 끝냈었다.

여기에서 _buildAppBar에 대한 설명을 할 것이다. 

 

AppBar constructor(
Key?key
Widget? leading
....
)

: AppBar 생성자이며, 일반적으로 Scaffoold.appBar 속성에 쓴다. 

 

아래는 여기에서 쓰인 AppBar를 분석한 내용이다.

return AppBar(
      backgroundColor: toBGColor,
      elevation: 0,

AppBar 생성자이며

배경색은 사전에 지정한 toBGColor로 지정한다.

elevation 앱바의 그림자정도이다. elevation이 커질수록 그림자가 많아진다. 

(공식API문서는 설명이 이해안가는 경우가 많으므로 구글링하는 것을 추천한다.)

title: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Icon(
              Icons.menu,
              color: toBlack,
              size:30,
            ),
            Container(
              height: 40,
              width: 40,
              child: ClipRRect(
                borderRadius: BorderRadius.circular(20),
                child:Image.asset('assets/images/avatar.jpg'),
              ),
            ),
          ]),

title을 Row로 나열한다. 즉 가로로 위젯들을 둘 것이다.

mainAxisAlignment 는 레이아웃을 mainAxis에 따라 배치하는 방법이며 

mainAxisAlignment.spaceBetween은 아이들 사이에 여유공간을 주는 배치방식이다.

 

children 에는 Icon과 Container를 넣어준다.

Icon은 메뉴아이콘을 넣고, 블랙컬러를, 사이즈는 30으로 설정한다.

컨테이너 위젯을 생성하고 너비 높이를 모두 40으로 설정한 뒤에, child로 ClipRRect를 넣는다.

 

ClipRRect

모서리를 둥글게 설정하는 위젯

 

child속성으로 Image.asset의 미리 넣어둔 스누피사진을 이용했다.