본문 바로가기
취미노트/코딩공부

[flutter] 12 효율적인 데이터 관리 3, List 응용

by 복습쟁이 2024. 8. 31.
반응형

이번 포스팅에서 해 볼 것

  • List 를 응용한 Widget의 구성
  • TextField Widget의 활용
  • Widget을 변수에 담아 활용

 

 

 

*지난번 포스팅 내용에 이어서 진행

이번 포스팅에서는 List를 활용한 사용자와 앱의 상호작용이다.

목표 횟수를 사용자가 체크하는 기능이다.

본인의 운동 기록을 입력하고 제출하면, 목표 달성 여부를 알려주는 기능의 구현이다.

 

1. 앱 화면 구성

앱 구현을 위해 아래의 세 가지 요건을 고려하여 화면을 추가적으로 만들 예정이다.

  1. 운동 횟수 기록 및 제출하는 텍스트
  2. 운동결과에 따라 목표 달성 여부를 보여주는 모서리가 라운드 처리 된 카드
  3. 화면을 꽉 채운 바닥부분

 

앱 구현을 위해서 먼저 앱 기능과 관계 없이 화면에 고정된 UI의 모양 껍데기만을 먼저 구현해보고, 추후 데이터 연결 및 기능 구현을 진행하도록 해보자.

 

 

(1) 운동 횟수 입력 및 제출 부분 화면 구성

 

1. 위에 만들었던 오늘의 운동 Card 아래에 Row 위젯을 새로 하나 만든다. 가로로 화면을 구성할 예정이니 Row를 썼다.

 

2. Row는 텍스트, 입력을 받는 텍스트필드, 제출버튼 위젯으로 구성을 했다.

 

3. Text('싯업 몇 회 진행하셨나요?') 로 텍스트 출력

 

4. 텍스트필드를 넣으려 하는데, 텍스트필드는 페어런츠가 사이즈가 정확하게 있어야 출력이 된다. 하지만 Row는 사이즈가 정확하지 않은 위젯으로, 사이즈가 있는 SizedBox를 페어런츠 위젯으로 한 번 준 뒤, 그 차일드로 TextField를 넣었다.

 

5.  Text('회') 로 텍스트 출력

 

6. OutlinedButton으로 버튼을 넣었다. 버튼은 항상 onPressed 정의가 필수적으로 필요하다. 우선 빈 함수로 정의해주자. 차일드에는 텍스트를 넣자.

 

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    Text('싯업 몇 회 진행하셨나요?',
      style: TextStyle(fontSize: 18,),
    ),
    SizedBox(
      width: 30,
      child: TextField(),
    ),
    Text('회'
    ),
    SizedBox(width: 15,
    ),
    OutlinedButton(onPressed: (){}, child: Text('제출'),
    ),
  ],
)

 

7. 정리하자면 Row는 Text-SizedBox(TextField)-Text-SizedBox-OutlinedButton으로 구성되어 위 화면처럼 출력이 되었다. Row에 maixAxisAlignment 속성을 줘서 가운데정렬을 하였고, 텍스트 사이즈를 키우고 SizedBox를 '회'와 버튼 사이에 넣어 간격을 주어 정리했다.

 

 

 

(2) 제출된 운동결과를 목표와 비교하여 출력하는 화면

Expanded(
  child: Container(
    margin: EdgeInsets.symmetric(horizontal: 5),
    padding: EdgeInsets.all(10),
    decoration: BoxDecoration(
      color: Colors.grey.shade300,
      borderRadius: BorderRadius.only(
        topLeft: Radius.circular(10),
        topRight: Radius.circular(10),
      ),
    ),
    child: Column(
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text('1. 싯업'),
            Text('00회/00회'),
            Icon(Icons.sentiment_satisfied_alt_rounded,color: Colors.green,)
        ],
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text('2. 친업'),
            Text('00회/00회'),
            Icon(Icons.sentiment_dissatisfied_rounded,color: Colors.red,)
          ],
        ),
      ],
    ),
  ),
),

 

1. 카드는 4면의 모서리가 모두 라운드처리 되어있지만, 아래쪽 모서리는 라운드 처리가 필요 없으므로, 컨테이너 위젯에서 속성을 추가함으로써 위쪽 모서리만 라운드 처리를 해보기로 한다. 박스 데코레이션에서 보더래디우스라는 값을 주면 원하는 곳에만 라운드 처리를 줄 수 있다.

 

2. 컨테이너에 색상을 주고, 아래 차일드로 컬럼을 주었다. 컨텐츠가 세로로 배열되기 때문이다.

 

3. 이렇게만 하면 컨테이너 아래가 딱히 변화는 없다. 컬럼 칠드런에 뭐가 없어서 공간이 0이기 때문이다. 컨테이너의 변화를 눈으로 보기 위해 칠드런에 SizedBox를 하나 잠시 주고 편집하면 눈으로 보기 편하다.

 

4. 세로로 가득 채우기 위해서 컬럼의 부모인 컨테이너를 Expanded 위젯으로 감싸주었다

 

5. 가로로 가득 채우기 위해 컨테이너의 부모인 컬럼에 crossAxisAlignment에 stretch를 주었다

 

6. 좌우 마진을 주기 위해 컨테이너에 symmmetric margin을 주었다.

 

7. 컨텐츠가 들어가는 컨테이너 내부에도 padding을 통해 마진을 주었다.

 

8. 박스 내부에 운동결과 컨텐츠를 넣고자 한다. 구성은 운동명 / 운동횟수 / 목표횟수 / 결과아이콘으로 하자.

 

9. 텍스트와 아이콘으로 넣었다.

 

10. 정렬을 위해 Row에 spaceBetween을 활용했다.

 

11. 동일한 원리로 다른 Row도 추가했다.

 

 

 

 

 

 

(3) 변수, 함수를 응용하여 운동 제출 및 결과 출력 기능 추가

화면을 위 처럼 목표한대로 구성했다. 

이제 구현을 원하는 기능은, 제출 버튼을 누를 때 마다 Row가 한 줄씩 추가되는 것이다. Row가 사용자의 액션에 의해 변하려면 변수로 처리해야 한다.

 

운동 제출 기능 추가

List<Row> resultWorkouts=[];

먼저 변수로 담기 위해서는 당연히 변수를 하나 만들어야 한다.

 

List<Row> resultWorkouts = [
  Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
      Text('1. 싯업'),
      Text('00회/00회'),
      Icon(Icons.sentiment_satisfied_alt_rounded,color: Colors.green,)
    ],
  ),
  Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
      Text('2. 친업'),
      Text('00회/00회'),
      Icon(Icons.sentiment_dissatisfied_rounded,color: Colors.red,)
    ],
  ),
];

우선, 하드코딩으로 텍스트로 박았던 Row들을 새로 만든 리스트에 잘라내서 붙여넣었다. 이렇게 하면 딱히 화면에 출력되는 것이 없다. Row들이 리스트에만 들어가있기 때문이다.

 

child: Column(
  children: resultWorkouts
),

 

기존에 비어있는 컬럼의 칠드런에는 resultWorkouts를 넣었다. 컬럼 안 칠드런 리스트를 resultWorkouts 리스트 안의 내용으로 채우라는 의미이다. 앞선 포스팅에서 봤던 기능인 [...resultWorksouts] 를 적용해줘도 된다. [...]은 10강 리스트를 더할 때 봤었던 스프레드오퍼레이터로 확장하는 의미이다.

 

OutlinedButton(
  onPressed: (){

    resultWorkouts.add(
      Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text('2. 친업'),
          Text('00회/00회'),
          Icon(Icons.sentiment_dissatisfied_rounded,color: Colors.red,)
        ],
      ),
    );
  },
  child: Text('제출'),
),

제출을 누르는 순간 Row가 추가되는 기능을 넣기 위해서는, 

onPressed 함수 안에서 작업을 해줘야 한다. 아까 리스트 안에 잘라넣었던 Row를 하나 들고 와서 onPressed 함수 안에 내용으로 채워넣었다. 이제는 버튼을 누르면, Row에 줄이 추가가 될 것이다. 

하지만, 눌러도 화면에서는 나타나지 않았다. 왜냐면 화면이 갱신되지 않기 때문이다.

 

OutlinedButton(
  onPressed: (){
    setState(() {
      resultWorkouts.add(
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text('2. 친업'),
            Text('00회/00회'),
            Icon(Icons.sentiment_dissatisfied_rounded,color: Colors.red,)
          ],
        ),
      );
    });
  },
  child: Text('제출'),
),

변경된 데이터를 새로고침하기 위해서는 setstate 명령을 통해 가능하다. resultWorks 리스트에 Row를 추가하는 구문인 resultWorks .add 이하 내용을 잘라내어 setstate 안에 넣어주면 된다.

 

setState()     // 함수호출

(){}                // setState의 argument 값으로서 함수

 

우리는 argument로 넘어가는 함수의 바디 부분인 (){}에 Row추가 구문을 작성한 것이다.

 

그러면 이제 제출 버튼을 누를 때 마다 Row가 신나게 추가가 된다.

이제는 위 운동 순서에 맞춰서 누를 때마다 그 순서에 맞는 운동이 들어가도록 기능을 넣으면 된다.

 

 

 

int workoutIndex=0;

버튼이 눌려지는 횟수를 인식하기 위한 변수를 하나 만들어야 한다. 리스트 변수들이 있던 곳에 int 변수도 하나 추가하자. 초기값은 0으로 줬다.

 

이 인덱스가 버튼이 눌려질 때 마다 1씩 증가되도록 하기 위해, 위 코드를 넣었다.

 

운동명을 변수를 통한 작업을 했다. workoutName을 workoutIndex의 숫자대로 뜨도록 한 것이다. 처음 누를 때는 Index 숫자는 0이 되고, 0에 해당하는 workoutName은 싯업이다.

 

같은 원리로, 운동명 앞의 숫자와 목표운동횟수도 바꿔주었다. 제출을 누르면 오른쪽 결과와 같이 출력이 된다. 여기까지 확인했으니 처음 List<Row> resultWorkouts 에 넣었던 Row 리스트는 삭제하여 1번째 줄 박혀있는 기본값을 없애주자.

 

 

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    Text('${workoutName[workoutIndex]} 몇 회 진행하셨나요?',
      style: TextStyle(fontSize: 18,),
    ),

'싯업 몇 회 진행하셨나요?' 도 운동명을 같은 원리로 변경하게 만들 수 있다. 그러나 이 때, 주의할 것이 있다. 리스트 안에 있는 운동의 숫자를 넘어서면 위 사진의 오른쪽과 같이 RangeError 에러가 발생한다.

 

이를 수정하는 방법은 어떤 수를 쓰던 간에 workoutIndex가 workoutName의 length(현재기준으로는 0~4까지 5개임)를 넘지 않게 해주면 된다. workoutIndex가 증가되는 부분이 OutlineButton에 있는 workoutIndex ++부분이었다. 여기를 수정해보자.

 

if(workoutIndex>=workoutName.length-1){
  workoutIndex=0;
}else{
  workoutIndex++;
}

if문으로 workoutIndex가 workoutName.length보다 크거나 같아지면, 0으로 돌리고 그렇지 않으면 1을 추가해 주는 조건문이다. 이 때, workoutIndex는 0부터 시작하기 때문에 workoutName의 길이는 -1을 해주어야 한다.

 

유저가 입력한 Text값의 컨트롤

 

TextEditingController workoutController=TextEditingController();

사용자가 입력한 값을 읽어오려면 컨트롤러 변수를 써야한다. 위 구문을 변수들이 모여있는 곳에 넣어주자. 텍스트에디팅컨트롤러 라는 타입워크아웃컨트롤러라는 변수명을 주고, 이 변수에는 텍스트에디팅컨트롤러를 생성해서 저장한다. 이 변수가 이제 컨트롤러 객체를 갖고 있게 된다.

 

SizedBox(
  width: 30,
  child: TextField(
    controller: workoutController,
    keyboardType: TextInputType.number,
  ),
),

방금 만든 변수를 기존에 만들어 놓았던 텍스트필드에 주면 된다. 컨트롤러 속성에 주면, 텍스트필드가 워크아웃컨트롤러라는 변수를 제어할 수 있게 된다. 이 때, 숫자만 입력할 수 있도록 키보드타입을 숫자로 제한해주자.

 

 

int userInputCount=int.parse(workoutController.text);

TextField에서 유저가 입력한 값은 제출 버튼이 클릭 되었을 때 읽어와야 하므로, onPressed 아래에 구문을 입력한다. 이 때, 유저가 입력한 값의 타입은 String이므로 이를 강제로 int로 바꿔주는 int.parse를 사용해준다.

결과물에 뜨는 값도 기존에 입력해 놓은 하드코딩 00회에서 userInputCount로 바꿔준다.

 

입력을 한 뒤, 새로 입력을 할 때 과거에 입력했던 값이 지워지도록 구문을 하나 추가해줬다. 이제 남은 것은 입력 값과 목표값을 비교하여 그 결과에 따라 아이콘이 변하도록 하는 기능이다.

 

 

OutlinedButton(
  onPressed: (){

    int userInputCount=int.parse(workoutController.text);
    Icon icon;
      if(userInputCount<workoutGoal[workoutIndex]){
        icon=Icon(Icons.sentiment_dissatisfied_rounded,color: Colors.red,);
      }else{
        icon=Icon(Icons.sentiment_satisfied_rounded,color: Colors.green,);
      }

현재 아이콘은 고정되어 있는데, 유저인풋카운트에 따라 변화할 수 있도록 유저인풋카운트 아래줄에 입력하자. 아이콘을 저장할 수 있는 변수는 Icon 타입이다. icon이라는 변수를 만들자.

if를 활용하여, userInputCount가 현재 하고 있는 운동의 목표 횟수보다 작을 때 빨간 아이콘을 출력하도록 조건을 주고 그렇지 않으면 파란 아이콘을 출력하도록 구문을 입력했다. 이 상태에서는 Icon이라는 위젯에 아이콘이 저장만 된 상태이다.

 

setState(() {
  resultWorkouts.add(
    Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text('${workoutIndex+1}. ${workoutName[workoutIndex]}'),
        Text('${userInputCount}회/${workoutGoal[workoutIndex]}회'),
        icon
      ],
    ),
  );

기존에 Icon 위젯으로 하드코딩이 되어있던 구문을 삭제하고, icon 변수만 입력해주면 조건에 따라 icon이 변화하여 출력된다.

 

 

 

 

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: WorkoutTrackerPage(),
    );
  }
}

class WorkoutTrackerPage extends StatefulWidget {
  @override
  State<WorkoutTrackerPage> createState() => _WorkoutTrackerPageState();
}

class _WorkoutTrackerPageState extends State<WorkoutTrackerPage> {
  List<String> workoutName = ['싯업','친업','스쿼트','푸시업','버피'];
  List<int> workoutGoal = [50,15,100,45,30];
  List<Row> workoutsToday = [];
  int workoutIndex = 0;
  List<Row> resultWorkouts = [];
  TextEditingController workoutController = TextEditingController();

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    for (var i=0;i<workoutName.length;i++) {
      workoutsToday.add(
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text('${i+1}.${workoutName[i]}'),
            Text('${workoutGoal[i]}회'),
          ],
        ),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Workout Tracker Page'),
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Card(
            color: Colors.grey.shade300,
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                children: [
                  Text(
                    '오늘의 운동',
                    style: TextStyle(fontSize: 19),
                  ),
                  ...workoutsToday
                ],
              ),
            ),
          ),

          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('${workoutName[workoutIndex]} 몇 회 진행하셨나요?',
                style: TextStyle(fontSize: 18,),
              ),
              SizedBox(
                width: 30,
                child: TextField(
                  controller: workoutController,
                  keyboardType: TextInputType.number,
                ),
              ),
              Text('회'
              ),
              SizedBox(width: 15,
              ),
              OutlinedButton(
                onPressed: (){

                  int userInputCount=int.parse(workoutController.text);
                  Icon icon;
                    if(userInputCount<workoutGoal[workoutIndex]){
                      icon=Icon(Icons.sentiment_dissatisfied_rounded,color: Colors.red,);
                    }else{
                      icon=Icon(Icons.sentiment_satisfied_rounded,color: Colors.green,);
                    }

                  setState(() {
                    resultWorkouts.add(
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          Text('${workoutIndex+1}. ${workoutName[workoutIndex]}'),
                          Text('${userInputCount}회/${workoutGoal[workoutIndex]}회'),
                          icon
                        ],
                      ),
                    );
                  });
                  if(workoutIndex>=workoutName.length-1){
                    workoutIndex=0;
                  }else{
                    workoutIndex++;
                  }
                  workoutController.clear();
                },
                child: Text('제출'),
              ),
            ],
          ),

          Expanded(
            child: Container(
              margin: EdgeInsets.symmetric(horizontal: 5),
              padding: EdgeInsets.all(10),
              decoration: BoxDecoration(
                color: Colors.grey.shade300,
                borderRadius: BorderRadius.only(
                  topLeft: Radius.circular(10),
                  topRight: Radius.circular(10),
                ),
              ),
              child: Column(
                children: resultWorkouts
              ),
            ),
          ),
        ],
      ),
    );
  }
}

 

*다음 포스팅 내용에 이어서 진행

728x90
반응형

댓글