코드코도

[코독하구만 2] 3주차 - Flutter : Personal Expense App 본문

Act/2021_여름_모각코_개인

[코독하구만 2] 3주차 - Flutter : Personal Expense App

고도고도 고도고도 2021. 7. 22. 22:59
728x90

지난 주에 이어 Udemy 강의에서 만들어본 개인 가계부 어플을 리뷰해보려고 한다. 지난 주에는 미완성이였는데 강의를 독학하면서 다 완성했다. 뭔가 살짝 어설프지만 어느정도 혼자서 구현할 수 있을 것 같다. 주말에 남은 50%의 강의를 다 듣고 혼자 만들어보면서 복습해보려고 한다. 아무튼 3주차 모각코 스타트!

 

우선 전체적인 UI는 아래와 같다.

 

 

소비 항목이 없는 경우 zZ의 이미지를 출력하며 상단에는 Chart를 확인할 수 있는 Switch와 새로운 소비 항목을 등록할 수 있는 Button이 AppBar의 우측과 Home의 하단에 존재한다. Switch를 On하면 오늘을 기준으로 지난 1주일 간의 소비금액이 출력된다. 글을 작성하고 있는 오늘은 목요일이므로 Thursday.

 

코드를 살펴보자. 다 지우고 main.dart에서 Switch와 FloatingButton과 관련된 Widget들만 남겨놨다.

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    final isLandscape = MediaQuery.of(context).orientation == Orientation.landscape;
    final appBar = AppBar(
      title: Text('가계부'),
      actions: <Widget>[
        Builder(
          builder: (context) => IconButton(
            icon: Icon(Icons.add),
            onPressed: () => _startAddNewTransaction(context),
          ),
        ),
      ],
    );

    return Scaffold(
      appBar: appBar,
      body: SingleChildScrollView(
        child: Column(
          // mainAxisAlignment: MainAxisAlignment.spaceAround,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          // Column이므로 main은 y축 (위에서 아래)
          children: <Widget>[
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text('Show Chart'),
                Switch(
                    value: _showChart,
                    onChanged: (val) {
                      setState(() {
                        _showChart = val;
                      });
                    }),
              ],
            ),
            _showChart
                ? Container(
                    height: (MediaQuery.of(context).size.height -
                            appBar.preferredSize.height -
                            MediaQuery.of(context).padding.top) *
                        0.3,
                    child: Chart(_recentTransactions),
                  )
                : Container(
                    height: (MediaQuery.of(context).size.height -
                            appBar.preferredSize.height -
                            MediaQuery.of(context).padding.top) *
                        0.7,
                    child:
                        TransactionList(_userTransactions, _deleteTransaction)),
          ],
        ),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      floatingActionButton: Builder(
        builder: (context) => FloatingActionButton(
          child: Icon(Icons.add),
          onPressed: () => _startAddNewTransaction(context),
        ),
      ),
    );
  }
}

 

Switch가 실행되면(onChanged) setState를 이용하여 _showChart의 값을 변경한다. 하단에 삼항연산자 코드가 있는데 이 부분을 보자. _showChart가 true인 경우 Chart를 호출하며 Chart를 보여주고 그렇지 않은 경우 TransactionLIst를 호출하며 소비 기록들을 보여준다. 그렇다면 Chart를 구성하는 ChartBar는 어떻게 구현되어 있을까?

import 'package:flutter/material.dart';

class ChartBar extends StatelessWidget {
  final String label;
  final double spendingAmount;
  final double spendingPctOfTotal;

  ChartBar(this.label, this.spendingAmount, this.spendingPctOfTotal);

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (ctx, constraints) {
      return Column(
        children: <Widget>[
          Container(
            height: constraints.maxHeight * 0.15,
            child: FittedBox(
              child: Text('${spendingAmount.toStringAsFixed(0)}원'),
            ),
          ),
          SizedBox(
            height: constraints.maxHeight * 0.05,
          ),
          Container(
            height: constraints.maxHeight * 0.6,
            width: 10,
            child: Stack(
              children: <Widget>[
                Container(
                  decoration: BoxDecoration(
                    border: Border.all(color: Colors.grey, width: 1.0),
                    color: Color.fromRGBO(220, 220, 220, 1),
                    borderRadius: BorderRadius.circular(10),
                  ),
                ),
                FractionallySizedBox(
                  heightFactor: spendingPctOfTotal,
                  child: Container(
                    decoration: BoxDecoration(
                      color: Theme.of(context).primaryColor,
                      borderRadius: BorderRadius.circular(10),
                    ),
                  ),
                ),
              ],
            ),
          ),
          SizedBox(
            height: constraints.maxHeight * 0.05,
          ),
          Container(
            height: constraints.maxHeight * 0.15,
            child: FittedBox(
              child: Text(label),
            ),
          ),
        ],
      );
    });
  }
}

ChartBar의 경우 LayoutBuilder로 구현한다. LayoutBuilder는 반응형 레이아웃이라고 생각하면 된다. 부모의 레이아웃 크기에 따라 자식들의 레이아웃을 정한다. builer의 인자로 ctx, constraints가 넘어가는 것을 볼 수 있는데 constraints가 부모의 크기라고 생각하면 된다. 이후 Column들을 살펴보면 constriants.maxHeight과 같이 부모의 크기를 참조하는 것을 볼 수 있다. 이런식으로 구현하면 다양한 사이즈의 핸드폰에서 같은 모습의 UI를 구현할 수 있게 된다. 좀 더 자세한 설명을 위해 공식 문서를 들고와봤다.

 

https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html

 

LayoutBuilder class - widgets library - Dart API

Builds a widget tree that can depend on the parent widget's size. Similar to the Builder widget except that the framework calls the builder function at layout time and provides the parent widget's constraints. This is useful when the parent constrains the

api.flutter.dev

 

중간에 Stack이 처음 등장하는데 Widget들을 겹치게 만들 수 있다. 이를 어디에 사용했냐면 소비 항목이 추가될 때 각각의 ChartBar에 지정한 색으로 ChartBar가 추가되는데 이 부분이 두 개의 Widget이 겹쳐진 부분이다. 말로 설명하자면 어려우니 사진을 보자.

 

이런 식으로 Grey의 ChartBar와 Blue의 ChartBar가 겹치게 되는데 이를 위해 Stack를 사용했다.

 

다음은 new_transaction.dart. 파일명 그대로 새로운 트랜잭션이 추가될 때의 Widget이다. 지난 주와 거의 유사하지만 추가된 것이 있다면 날짜 선택과 선택한 날짜를 보여준다는 점이다. 이 부분은 DatePicket를 사용했다.

 

 

코드가 좀 길긴한데 천천히 살펴보자.

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

class NewTransaction extends StatefulWidget {
  final Function addTx;

  NewTransaction(this.addTx);

  @override
  State<NewTransaction> createState() => _NewTransactionState();
}

class _NewTransactionState extends State<NewTransaction> {
  final _titleController = TextEditingController();
  final _amountController = TextEditingController();
  DateTime _selectedDate = DateTime.now();

  void _submitData() {
    final enteredTitle = _titleController.text;
    final enteredAmount = double.parse(_amountController.text);

    if (enteredTitle.isEmpty || enteredAmount <= 0) {
      return;
    }
    widget.addTx(
      enteredTitle,
      enteredAmount,
      _selectedDate,
    );

    Navigator.of(context).pop();
  }

  void _presentDatePicket() {
    showDatePicker(
      context: context,
      initialDate: DateTime.now(),
      firstDate: DateTime(2000),
      lastDate: DateTime.now(),
    ).then(
      (pickedDate) {
        if (pickedDate == null) {
          return;
        }
        setState(
          () {
            _selectedDate = pickedDate;
          },
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Card(
        elevation: 3,
        child: Container(
          padding: EdgeInsets.only(top : 10, left : 10, right : 10, bottom : MediaQuery.of(context).viewInsets.bottom + 10),
          child: Container(
            padding: EdgeInsets.only(
              top: 10,
              left: 10,
              right: 10,
              bottom: MediaQuery.of(context).viewInsets.bottom + 10,
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.end,
              children: <Widget>[
                TextField(
                  decoration: InputDecoration(labelText: '품목명'),
                  // 1번 방법을 사용하면 아래 1줄처럼 코드를 작성
                  // onChanged: (val) => titleInput = val,
                  controller: _titleController,
                  onSubmitted: (_) => _submitData,
                ),
                TextField(
                  decoration: InputDecoration(labelText: '가격'),
                  // 1번 방법을 사용하면 아래 1줄처럼 코드를 작성
                  // onChanged: (val) => amountInput = val,
                  controller: _amountController,
                  onSubmitted: (_) => _submitData,
                ),
                Row(
                  children: <Widget>[
                    Expanded(
                      child: Text(
                        _selectedDate == null
                            ? "Picked Date"
                            : '선택된 날짜 : ${DateFormat.yMd().format(_selectedDate)}',
                      ),
                    ),
                    FlatButton(
                      textColor: Theme.of(context).primaryColor,
                      child: Text("날짜 선택"),
                      onPressed: _presentDatePicket,
                    ),
                  ],
                ),
                RaisedButton(
                  child: Text("항목 추가"),
                  color: Colors.blueAccent,
                  textColor: Colors.white,
                  onPressed: _submitData,
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

_presentDataPicket이 날짜 선택 버튼을 눌렀을 때 이를 처리하는 부분이다. 우선 해당 버튼을 클릭했을 때 DatePicket가 실행되면서 달력이 띄어지는데 이 때 초기 날짜를 고를 수 있다. 이 부분이 바로 initialDate가 된다. FristDate는 선택 가능한 최소(?) 날짜, lastDate는 선택 가능한 최대(?) 날짜이다. .then을 이용하여 선택된 날짜가 없는 경우 오늘 날짜를 그대로 return하고 선택된 경우 해당 날짜를 return한다. 이렇게 날짜를 선택하고 하단의 항목 추가 버튼을 클릭하면 _submitData가 실행되면서 transactionList에 새로운 소비 항목이 추가된다.

 

뭔가 급하게 마무리한 느낌이 있지만 실제로 나 혼자서 다시 한 번 짜보면서 다시 한번 정리해보려고 한다. 확실히 Flutter를 처음 접했을 때보다는 많이 늘은 것 같지만 아직 갈 길이 먼 것 같다.

 

728x90
0 Comments
댓글쓰기 폼
Prev 1 2 3 4 5 6 7 Next