Front-End/Flutter_Project_02_Expense Tracker

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

sd4beatles 2024. 12. 17. 01:55

1. Dismissible

웹을 사용할때, 손가락으로 swiping해서 제거하는 경우를 많이 보았을 것이다. 이러한 기능을 우리 어플에게도 구현을 해보도록 하자. 이럴 때,쓰는 것이 바로 Dismissble이라는 클래스인데, 이는 아래와 같다.

"Dragging or flinging this widget in the DismissDirection causes the child to slide out of view."

Dismissible({required Key key, required Widget child,...)

1) Dismissible : Widget child (required)

현재 우리의 어플 같은 경우엔, 지우고 싶은 item은 ExpenseItem이며, 해당되는 index의 expense를 집어넣으면 된다.

2) Dismissible: key (required)
근데, Dismissible은 key를 요구하는데, 이 key는 유일한 widget과 그에 연관된 data를 인식할 수 있게 해주는 key가 필요하다. 즉, 우리가 이 key를 만들어야 한다는 건데, 이는 ValueKey를 통해서 생성가능하다.

3) onDismissed: optional

Dismissible이 활성화 됐을 때, 우리가 추가적으로 집어넣고 싶은 code를 작성한다. 이때 주의할 점은 있는데, 이는 선택된 expense를 목록에서 제거하는 것이다. 이를 위해서, expense.dart에 Widget Class에 함수를 다음과 같이 추가한다.

//expense.dart
void _addExpense(Expense expense){
  setState((){
  _registeredExpenses.add(expense);
  });
}

void _removeExpense(Expense expense){
  setState((){
 _registeredExpenses.remove(expense);
  });
}

이 후에, ExpenseList Widget class에 와래와 같이 constructor에 추가적으로 onRemoveExpense function을 추가한다. 물론, onRemoveExpense function의 매개변수는 당연히 우리가 위에서 정의내렸던 _removeExpense이다.

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


  final List<Expense>expenses;
  final void Function(Expense expense) onRemoveExpense;

이렇게 member function을 지정한 것을, Dismissible의 onDismissed optiond에 매개변수로 집어넣으려고 하니 문제가 생긴다. 이는 onDismissed function은 DismissDirection 즉 매개변수를 집어넣어야 하는데, 우리가 정의내린 onRemoveExpesne는 이러한 매개변수를 가지고 있지 않기 때문이다. 이 오류를 우회하기 위해선, 아래와 같이

"무익명 함수를 통해서 우회하는 방법을 선택할 수 있다. "

onDismissed Function


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


  final List<Expense>expenses;
  final void Function(Expense expense) onRemoveExpense;



  @override
  Widget build(BuildContext context){

    return ListView.builder(itemCount:expenses.length ,
    itemBuilder: (ctx,index)=>Dismissible(
      key: ValueKey(expenses[index]), 
      child:ExpenseItem(expenses[index]),
      onDismissed:(direction){
        onRemoveExpense(expenses[index]);
        },
      ),
    );
  }
}

마지막으로, onRemoveExpense의 매개변수를 반영해줄 코드를 추가하도록 하자.



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),

//생략 

@override
Widget build(BuildContext context){
  return Scaffold(
    appBar:AppBar(
      title:Text("Flutter Expense Tracker"),
      actions:[
        IconButton(onPressed:_openAddExpenseOverlay, 
        icon:const Icon(Icons.add),
        ),
      ]
    ),
    body:Column(
      children:  [
        Text("The chart"),
        Expanded(child:ExpensesList(expenses:_registeredExpenses,onRemoveExpense:_removeExpense,)),
      ],
    ),


  );
}

}

2. Default Display

우리는 아래 첨부된 이미지처럼, expense_list에 아무것도 없을때, 다음과 같은 화면을 보여주고 싶어 한다.

main theme if no expense in the list

expense.dart 이동하여, build widget에 mainContent를 생성한다.그리고, 만약, expense_list에 하나도 없다면, "Get read to add expense"를 보여주고, 그렇지 않으면, ExpenseList widget class를 실행한다. 이는 아래의 코드를 참조할것.



@override
Widget build(BuildContext context){
  Widget mainContent=const Center(
    child:Text('Get ready to add expense!'),
  );

  if(_registeredExpenses.isNotEmpty){
    mainContent=ExpensesList(
      expenses:_registeredExpenses,
      onRemoveExpense:_removeExpense,);
  }



  return Scaffold(
    appBar:AppBar(
      title:Text("Flutter Expense Tracker"),
      actions:[
        IconButton(onPressed:_openAddExpenseOverlay, 
        icon:const Icon(Icons.add),
        ),
      ]
    ),
    body:Column(
      children:  [
        Text("The chart"),
        Expanded(
          child:mainContent),
      ],
    ),


  );
}

}

3. showing 'snackbars'

snackbar는 사용자가 특정 행위를 할 때, '안내 메시지' 역할을 한다고 보면 된다. 예를 들어, 리스트에 있는 아이템을 지울 때, 메세지가 삭제되었다는 것을 알리는 용도로 쓰이면 좋다. 또한 삭제된 item을 다시 복구하는 역할로도 snackbar는 훌륭한 역할을 수행한다.

"It can be useful to briefly inform your users when certain actions take place. For example, when a user swipes away a message in a list, you might want to inform them that the message has been deleted. You might even want to give them an option to undo the action."

이러한 snackbars는 우리가 expense를 목록에서 지웠을 때, 안내 메시지나 삭제된 expense를 복구하는 역할로 써보도록 하자.

step 1) snackbars의 메시지를 보여주는 ScaffoldMessener의 of method를 불러오자.

step 2) ShosSnackBar에 SnackBar를 추가하고, 아래의 option등을 코드로 작성하자.

  • duration

  • content

  • action: SnackBarAction

    이때, registredExpesnses.insert 문은 index와 value를 필요로 함. expenseIndex 변수를 만들고, 'undo'를 클릭할 때, 복구시키도록 하자.


void _removeExpense(Expense expense){
  //track down the index of expense
  final expenseIndex=_registeredExpenses.indexOf(expense);

  setState((){
 _registeredExpenses.remove(expense);
  });

  ScaffoldMessenger.of(context).clearSnackBars();
  ScaffoldMessenger.of(context)
  .showSnackBar(
    SnackBar(
      duration:Duration(seconds: 3),
      content:const Text('Expense Deleted.'),
      action:SnackBarAction(
        label: 'Undo', 
        onPressed: (){
          setState((){
            _registeredExpenses.insert(expenseIndex,expense);

          });

        }
        ),)
  );
}