이전 포스팅에서는 할 일 추가, 할 일 불러오기, 할 일 삭제 기능을 구현하였다.
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 클래스 내부에 다음과 같이 변수를 추가하자.
이 변수를 통해 우리는 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와 친해지면서 점점 개선해 보도록 하자!