Front-End/Flutter_Project_02_Expense Tracker

4. Udemy Flutter 강의를 통한 Project - Expense Tracker

sd4beatles 2024. 12. 10. 23:41

1. TextEditingController Class

이전의 방식으로 text filed의 값을 조정하는 방법은 사실 초기변수를 선언해야하고, 그리고 user가 interface를 통해서 값을 집어넣으면,이를 수정하는 함수를 선언도 했어야 했다. 그러나, 이러한 방식은 사실 상당히 귀찮다는 것을 알수가 있다. 이러한 코드라인을 간결하게 해주는 flutter에서 제공되어지는 TexEdigingController class를 대안으로 고려해볼 수 있다.

"If you modify the text or selection properties, the text field will be notified and will update itself appropriately."

이의 공식문서 한 문장이 이 class를 잘 대변해주고 있는데, 입력된 text를 수정해주는 본연의 역활을 충실히 하는 class이다.

단,이렇게 간단하고 자동으로 업뎃이 돠는 TextEditing도 한 가지 주의사항이 있는데,이는 아래와 같다.

"Remember to dispose of the TextEditingController when it is no longer needed. This will ensure we discard any resources used by the object."

TextEditingController가 더 이상 필요가 없을 땐, 이를 없애야 한다는 점이다. 이를 위해선 dispose라는 내장함수를 사용하는데, 이도 역시 State, StatefulWidget에서만 사용할 수 있다는 점을 인지하도록 하자.

class _NewExpenseState extends State<NewExpense> {

  //textEdigintController 객체 생성
  final _titleController=TextEditingController();

  @override
  void dispose(){
    //더이상 필요없을 때, 이를 제거한다. 
    _titleController.dispose();
    super.dispose();
  }




  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(18),
      child: Column(
        children:  [
          TextField(
            //onChanged는 함수를 매개변수로 받음
            controller:_titleController,
            maxLength: 50,
            decoration: InputDecoration(
              label: Text('Title'),
            ),
          ),
          Row(children: [
            ElevatedButton(onPressed: (){
              //버튼을 누른 후 작동할 함수 설정
              print(_titleController.text);
            }, 
            //button의 label 사용
            child: const Text('Save Expense'),)
          ],)

2. Add More Inputs

아래와 같이 추가적인 amount section을 집어넣는 코드는 아래와 같다.

import 'package:flutter/material.dart';

class NewExpense extends StatefulWidget {
  const NewExpense({super.key});

  @override
  State<NewExpense> createState() {
    return _NewExpenseState();
  }
}

class _NewExpenseState extends State<NewExpense> {
  final _titleController = TextEditingController();
  final _amountController = TextEditingController();

  @override
  void dispose() {
    _titleController.dispose();
    _amountController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
         //children01) Title TextField
         //flutter widget for getting text input from a user
          TextField(
            controller: _titleController,
            maxLength: 50,
            decoration: const InputDecoration(
              label: Text('Title'),
            ),
          ),
          TextField(
            controller: _amountController,
            keyboardType: TextInputType.number,
            decoration: const InputDecoration(
              prefixText: '\$ ',
              label: Text('Amount'),
            ),
          ),
          Row(
            children: [
              TextButton(
                onPressed: () {},
                child: const Text('Cancel'),
              ),
              ElevatedButton(
                onPressed: () {
                  print(_titleController.text);
                  print(_amountController.text);
                },
                child: const Text('Save Expense'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

3. Closing Moudle Manually

user가 추가적인 expense를 담을 순 있지만, 중간에 맘이 바껴서 cancel을 누르고 모든 것을 취소할 수도 있다. 이때, cancel button을 누르면, button module이 자동으로 사라지는 기능을 이 섹션에서 구현하고자 한다. 이를 위해선 Navigator.pop() 을 고려해볼 필요가 있다.

✍️ Flutter Note

1) Navigator.push()  : 새로운 route로 이동하고자 할 땐, push를 사용
2)  Navigator.pop(): 현재 route에서 벗어나서, 이전의 route으로 이동할 때 사용. 이를 실행하기 위해선, onPressed() Callback함수에서 작성할 것 
3) Navigator.pushReplacement: 현재 화면을 새 화면으로 교체합니다. 새로운 화면을 푸시(push)하지만, 현재 화면은 스택에서 제거됩니다.
4) Navigator.pushNamed: 이름을 가진 경로(route)로 화면을 이동할 때 사용합니다. 이를 통해 복잡한 화면 구조에서도 직관적으로 화면 이동을 처리할 수 있습니다.
5) Navigator.popUntil: 특정 화면까지 팝(pop)하여 그 화면을 표시합니다. 특정 화면까지 화면을 돌아가고 싶을 때 유용합니다.
class _NewExpenseState extends State<NewExpense> {
  final _titleController = TextEditingController();
  final _amountController = TextEditingController();

  @override
  void dispose() {
    _titleController.dispose();
    _amountController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          //1) Title Section (생략)

          //2) Amount Section (생략)

         //3) Cancel and Save Section
          Row(
            children: [
              TextButton(
                onPressed: () {
                  //Navigator.pop 실행
                  Navigator.pop(context);

                },
                child: const Text('Cancel'),
              ),
              ElevatedButton(
                onPressed: () {
                  print(_titleController.text);
                  print(_amountController.text);
                },
                child: const Text('Save Expense'),
              ),
            ],

4. Showing Date Picker ()

데이트를 일일이 입력하는 방법도 있지만, 그것보다는 달력에서 달과 일을 선택하는 것이 오히려 더 user에게 친근하게 받아들일거 같다. 그렇다면, 이러한 방법을 어떻게 코드로 실행할 수 있는지 알아보도록 하자.

윗 그림처럼 Column2 - Row2 부분에서 다시 Row를 이용해서, 하나는 TextField를 위한 곳으로 다른 하나는 Calnder Field를 만드는 방법을 생각해보도록 하자. 이때, 주의할 점이 있는데 , 이는 아래와 같다.

✍️ Flutter Note <Row 기본적으로 공간의 양을 제한하지 않는다.> "TextField와 Calender Field(Row Field)안에다가 Expanded를 Wrapping해주지 않으면, Row는 기본적으로 차지할 수 있는 공간의 양을 제한하지 않기 때문에, 앞에 있는 TextField는 공간을 최대한 많이 쓰려고 시도한다. 즉, Calnder Field에게 여유공간을 줄 수 없다는 것을 인지하고, Flutter는 rendering시 이를 오류를 주게된다. 그러므로 SizedBox(여유공간)을 제외하고는 모두 Expanded로 Wrapping해야 한다. "

윗 flutter 사항을 잘 인지하고, 아래와 같이 코드를 작성하도록 하자. 이때 주의할 점은, Amount/Calnder Section안에 있는 Row안에 TextField와 또 다른 Row Section은 Expanded로 감싸줘서, 오류를 피해야 한다.

 @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          //1) Title Section
          TextField(
            controller: _titleController,
            maxLength: 50,
            decoration: const InputDecoration(
              label: Text('Title'),
            ),
          ),
          //2) Amount/Calender Section
          Row(
            children: [
          //amount
          Expanded(child: TextField(
            controller: _amountController,
            keyboardType: TextInputType.number,
            decoration: const InputDecoration(
              prefixText: '\$ ',
              label: Text('Amount'),
            ),
          )
        ),

          //insert space 
          const SizedBox(width:16),


          //calender section
          Expanded(child:Row(
            mainAxisAlignment: MainAxisAlignment.end,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
            const Text('Selected Date'),
            IconButton(
              onPressed: (){},
              icon: const Icon(
                Icons.calendar_month,
              ),
              ),
           ],
          ),
        ),
         ]),

마지막으로, calender를 활용할 수 있도록, calender icon 선택시 활성화 할 수 있는 함수를 만들도록 하자.
아래의 예시는, inital date은 오늘날짜이고 start날짜는 1년적으로 작성했다.


void _presentDatePicker(){
    final now=DateTime.now();
    final firstDate=DateTime(now.year-1,now.month,now.day);
    showDatePicker(context: context,initialDate:now, firstDate: firstDate, lastDate: now);
  }

5. Full Code

import 'package:flutter/material.dart';

class NewExpense extends StatefulWidget {
  const NewExpense({super.key});

  @override
  State<NewExpense> createState() {
    return _NewExpenseState();
  }
}

class _NewExpenseState extends State<NewExpense> {
  final _titleController = TextEditingController();
  final _amountController = TextEditingController();

  void _presentDatePicker(){
    final now=DateTime.now();
    final firstDate=DateTime(now.year-1,now.month,now.day);
    showDatePicker(context: context,initialDate:now, firstDate: firstDate, lastDate: now);
  }

  @override
  void dispose() {
    _titleController.dispose();
    _amountController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          //1) Title Section
          TextField(
            controller: _titleController,
            maxLength: 50,
            decoration: const InputDecoration(
              label: Text('Title'),
            ),
          ),
          //2) Amount/Calender Section
          Row(
            children: [
          //amount
          Expanded(child: TextField(
            controller: _amountController,
            keyboardType: TextInputType.number,
            decoration: const InputDecoration(
              prefixText: '\$ ',
              label: Text('Amount'),
            ),
          )
        ),

          //insert space 
          const SizedBox(width:16),


          //calender section
          Expanded(child:Row(
            mainAxisAlignment: MainAxisAlignment.end,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
            const Text('Selected Date'),
            IconButton(
              onPressed: _presentDatePicker,
              icon: const Icon(
                Icons.calendar_month,
              ),
              ),
           ],
          ),
        ),
         ]),


          //3) Cancel and Save Section  
          Row(
            children: [
              TextButton(
                onPressed: () {
                  Navigator.pop(context);

                },
                child: const Text('Cancel'),
              ),
              ElevatedButton(
                onPressed: () {
                  print(_titleController.text);
                  print(_amountController.text);
                },
                child: const Text('Save Expense'),
              ),
            ],

          //4) 



          ),
        ],
      ),
    );
  }
}