Front-End/Flutter_Project_02_Expense Tracker

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

sd4beatles 2024. 12. 1. 22:01

1. Defining Expenses/Expense Widget Class

✍️ Flutter Note StatefulWidget, StatelessWidget에 관련된 내용은 "면접질문->Flutter->생명주기 편"을 볼것.

- 우리는 두개의 class를 만들예정이다. 이때, 첫번째 widget은 StatefulWidget으로 상시간의 변경을 UI에 반영해 줄 수 있는 widget이다. 다른 하나는, original widget과 소통할 수 있는 state class로 __ExpensesState라고 명시한다. 또한 StatefulWidget은 State객체를 통해서 상태의 변화를 나타내는데,

- createState()만들 때 나왔던, 을 로 바꾸며 , Expenses의 constructor에는 parent class의 key를 전달해 준다.

- 현재 step에선 부가적인 기능이 들어가기 전이라 Scaffold(앱의 뼈대를 위한 구성요소를 제공하는 Widget)에 임시 Text Widget을 집어넣는다.

import 'package:flutter/material.dart';

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

  @override
  State<Expenses>createState(){
    return _ExpensesState();
  }
}




class _ExpensesState extends State<Expenses>{

@override
Widget build(BuildContext context){
  return Scaffold(
    body:Column(
      children: const [
        Text("The car"),
        Text("The here"),
      ],
    ),


  );}
}

이에 정의한 StatefulWidget을 main.dart의 homde으로 임시적으로 불러들이자.

import 'package:flutter/material.dart';
import 'package:expense_tracker/expenses.dart';


void main(){
  runApp(MaterialApp(
    home:Expenses(),
    )
  );
}

2.1 List Initializer

✍️ Flutter Note

각 데이터가 지니는 특징을 구형할 수 있는 widget class를 만들어야 하는데,이를 Model이라는 폴더를 생성하고 그
아래에 expense.dart라는 파일을 집어넣도록 하자.

아래는 Expense Widget Class의 구성을 담은 내용을 간략하게 적어보았다.

  • title (String)
  • amount(double)
  • date (DateTime)
  • id (uuid)

이 때, backend에서도 많이 보았던 고유식별번호인 UUID를 사용하는데, 이를 위해선 flutter에서 module package를 설치해야 한다.

flutter pub add uuid

 

 

더불어서, flutter에는 'initializer list'라는 keyword가 존재하게 된다. 이는, 현재 construcotr에서 받아들이는 외부 parameter외에 이미 class에서 member 변수로서 초기화시킬 때 사용하는 하나의 방법이라 생각하면 된다. 말이 어려우나, 간단히 "constructor에서 오지 않는 매개변수를 내부적으로 class 맴버 변수를 초기화 시키는 하나의 방법이다" 라고 생각하면 됨.

import 'package:uuid/uuid.dart';



// make uuid unresulable 
const  uuid= Uuid();


class Expense{

  //constructor;retrieve arguments
  Expense({
    required this.title,
    required this.amount,
    required this.date,

  }): id=uuid.v4();

  final String id; 
  final String title;
  final double amount;
  final DateTime date;

}

2.2 Enumerator

User에게 category를 선택할 수 있도록, 하나의 category를 만들 필요가 있다. 이 때 사용하는 자료구조가 바로 enum이라는 것이다.

enum <Name> {....}

아래의 코드는 Category에 end-user가 선택할 수 있는 목록들을 집어넣으면 되는데, 이때 _주의할 점은 Caetory에 있는 목록들은 쌍따움표나 따움표를 사용하지 않는다는 점_이다.

import 'package:uuid/uuid.dart';



// make uuid unresulable 
const  uuid= Uuid();

enum Category {food,travel,leisure,work}

class Expense{

  //constructor;retrieve arguments
  Expense({
    required this.title,
    required this.amount,
    required this.date,
    required this.category,
  }): id=uuid.v4();

  final String id; 
  final String title;
  final double amount;
  final DateTime date;
  final Category category;
}

3. Add List of Expense to Expenses class

Expenses class에서 memeber변수 하나를 추가하도록 해보자. Expenses Class의 맴버변수의 _registeredExpenses 를 하나 추가하고, 이는 Expense를 원소를 갖는 list로 정하도록 하자. 물론, list안에 있는 Expense의 매개변수는 하드코딩으로 하자.

import 'package:flutter/material.dart';
import 'package:expense_tracker/models/expense.dart';


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

  @override
  State<Expenses>createState(){
    return _ExpensesState();
  }
}




class _ExpensesState extends State<Expenses>{
final List<Expense>_registeredExpenses=[
  Expense(
    title:"Flutter Course",
    amount:12.22,
    date:DateTime.now(),
    category:Category.work),

  Expense(
    title:"50-60 Nights",
    amount:50,
    date:DateTime.now(),
    category:Category.leisure),
]

@override
Widget build(BuildContext context){
  return Scaffold(
    body:Column(
      children: const [
        Text("The car"),
        Text("The here"),
      ],
    ),


  );
}

}

4. Efiiciently Rendering Long Lists with ListView

화면에 user가 사용했던 expense의 정보를 모아서, 이릃 화면에 보여주는 class widget을 만들어보도록 하자. 화면에 저장된 정보를 보여주는 feature로는 "ListView"를 생각할 수 있다. 그러나, 이 ListView에는 치명적인 단점이 존재하는데, List에 속한 원소들의 수가 적은 경우에는 쉽게 사용할 수 있지만, 이게 원소의 숫자가 길어지면 사용하기가 무진장 힘들어진다. 그러므로, 우리는 이를 대체할 수 있는, ListView.builder를 사용하도록 하자.

 

 

 

이들을 간략하게 구별하는 방법은 아래를 참고하도록 하자!

  1. 일반적인 ListView를 명시적으로 호출하고 children 전달하는 방법 - 적은 데이터에 사용시 용이함
  2. ListView.builder builder를 사용하여 동적으로 호출 - 많은 양의 데이터 리스트에 용이함 index사용가능

✍️ListView.builder 아래의 supporting document를 참고하자면,

ListView.builder는 - input: Function을 받으며, 매개변수로는 BuildContext 와 int를 받는다. - output: Widget를 return 해야함.

lib 폴더에 expesen_list.dart파일을 만들고  ExpensesList class를 정의한다. build Widget에서 ListView.Builder를 반환하도록 하자. 이때,  Text Widget은 선택된 index를 받아서 expense을 선정하며, 그에 해당되는 expense의 title를 화면에 보여준다. 

 

import 'package:flutter/material.dart';
import 'package:expense_tracker/models/expense.dart';

class ExpensesList extends StatelessWidget{
  //initialize constructor
  const ExpensesList({super.key,required this.expenses,});


  final List<Expense>expenses;



  @override
  Widget build(BuildContext context){

    return ListView.builder(itemCount:expenses.length ,itemBuilder: (ctx,index)=>Text(expenses[index].title),
    );
  }
}

5. Efiiciently Rendering Long Lists with ListView

위에 정의내린 ExpensesList를 이제 Expense 화면에 집어 넣으려고 하자. 이때, 우리가 할 수 있는 방법은 아래와 같다. Expense class의 children list속에 ExpenseList를 집어넣는 것이다. 아래처럼 말이다.

#expense.dart
import 'package:flutter/material.dart';
import 'package:expense_tracker/models/expense.dart';
import 'package:expense_tracker/expenses_list.dart';

class Expenses extends StatefulWidget{
# 생략
class _ExpensesState extends State<Expenses>{
final List<Expense>_registeredExpenses=[
  Expense(
    title:"Flutter Course",
    amount:12.22,
    date:DateTime.now(),
    category:Category.work),

  Expense(
    title:"50-60 Nights",
    amount:50,
    date:DateTime.now(),
    category:Category.leisure),
];

@override
Widget build(BuildContext context){
  return Scaffold(
    body:Column(
      children:  [
         Text("The chart"),
        #List안에 list를 집어넣지만 작동이 안됨. 
        ExpensesList(expenses:_registeredExpenses)
      ],
    ),


  );
}

}

그러나, 우리의 기대와는 다르게 화면에는 우리가 ListView에서 지정한 원소들이 나오지를 않는다. 이게 왜 그런것일까? ChatGpt를 통해서 알아본 결과는 아래와 같다. 

 

"The Column widget tries to take as much vertical space as possible. However, when you put a ListView (or any scrollable widget) inside a Column, the ListView might not know how much space it can occupy because Column doesn't provide an intrinsic height. As a result, it doesn't render correctly, or you might not see it at all."

 

Solution: Use Expanded or Flexible

"To fix this issue, you can wrap the ListView widget with an Expanded widget. This will give the ListView the remaining available space inside the Column, allowing it to scroll properly."

 

즉,  Column안에 위치한 ListView가 자신이 차지할 공간이 얼마인지를 알 수가 없어서, 적절하게 렌더링이 안되는 모순이 생긴것이다. 다시말해, Column이 자신의 고유높이(Intrinsic Height: 위젯이 자신만의 크기를 자동으로 결정할 수 있는 특성)결정하지 않으며, 그로 인해 내부에 스크롤 가능한 Widget(우리 경우에는 ListView)를 넣을 때, ListView가 얼마나 많은 공간을 사용할 수 없는 상황이 발생하게 된다는 점이다. 이때, 이를 사전에 방지하기 위해서 Expanded나 Flexible를 사용한다. 

 

 

Column 위젯 자체는 자식 위젯들의 크기를 기반으로 자신의 높이를 결정하지 않으며, 그로 인해 내부에 스크롤 가능한 위젯(예: ListView)을 넣을 때, ListView가 얼마나 많은 공간을 사용할 수 있는지 알 수 없는 상황이 발생할 수 있습니다.

 

 

 

 

class _ExpensesState extends State<Expenses>{
final List<Expense>_registeredExpenses=[
  Expense(
    title:"Flutter Course",
    amount:12.22,
    date:DateTime.now(),
    category:Category.work),

  Expense(
    title:"50-60 Nights",
    amount:50,
    date:DateTime.now(),
    category:Category.leisure),
];

@override
Widget build(BuildContext context){
  return Scaffold(
    body:Column(
      children:  [
         Text("The chart"),
        Expanded(child:ExpensesList(expenses:_registeredExpenses)),
      ],
    ),


  );
}

}

 

Expanded 사용 후