Flutter Tutorial: Building An Expense Manager App – 6

This is the sixth part of Expenses Manager app Flutter tutorial series where we are building an Expenses Manager app.

Introduction

In the previous post, we completed all the functionalities of creating Category. In this part of our Flutter tutorial, we will add features for working with expenses. We will design the home page and also create a category selector using ChoiceChip widget.

Designing Dashboard Page

The home tab of our Expense Manager app will be a dashboard page. In this page, we will have:

  • Link to add new expense.
  • A Date Switcher.
  • List of expenses for a selected date.

Designing A Date Switcher

We will start by adding a date switcher for our app. Basically, we want to show current date and add buttons to change current date. The buttons should allow user to go back and forth by a day.

With the date switcher, the dashboard can start to look like this:

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

class DashboardPage extends StatelessWidget {
  String getStringDate(DateTime dt) {
    return "${dt.year}/${dt.month}/${dt.day}";
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(children: <Widget>[
        Container(
            padding: EdgeInsets.all(12.0),
            width: double.infinity,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                IconButton(
                  onPressed: () {},
                  icon: Icon(Icons.arrow_back),
                ),
                Container(
                  margin: EdgeInsets.symmetric(horizontal: 12.0),
                  child: Text(
                    getStringDate(DateTime.now()),
                    style: Theme.of(context).textTheme.title,
                  ),
                ),
                IconButton(
                  onPressed: () {},
                  icon: Icon(Icons.arrow_forward),
                )
              ],
            ))
      ]),
    );
  }
}

We have yet to implement onPressed functionality for icon buttons.

Add FloatingActionButton To Add Expenses

Next, we will add a button to add expenses. A FloatingActionButton would be the right choice for this purpose since it will be distinctly visible.

...
floatingActionButton: FloatingActionButton(
        onPressed: (){},
        child: Icon(Icons.add)
      ),
...

Add ListView To Show Expenses

Finally, we will have a list of expenses for the selected date shown. For this, we will use a ListView with sample expenses for now.

Widget _getExpenses() {
    var expense1 = ExpenseModel().rebuild((b) => b
      ..id = 1
      ..title = "Coffee"
      ..notes = "Coffee at peepalbot"
      ..amount = 129.00);

    var expense2 = ExpenseModel().rebuild((b) => b
      ..id = 2
      ..title = "Lunch"
      ..notes = "Momos at dilli bazar"
      ..amount = 150.00);

    var expense3 = ExpenseModel().rebuild((b) => b
      ..id = 3
      ..title = "Pants"
      ..notes = "Bought a pair of pants from Dbmg"
      ..amount = 2500.00);

    var ls = [expense1, expense2, expense3];

    return ListView.builder(
      itemCount: ls.length,
      itemBuilder: (context, index) {

        var expense = ls[index];
        return Container(
          decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(4.0),
              border: new Border.all(
                  width: 1.0, style: BorderStyle.solid, color: Colors.white)),
          margin: EdgeInsets.all(12.0),
          child: ListTile(
            onTap: () {},            
            trailing: IconButton(
              icon: Icon(Icons.delete),
              color: Theme.of(context).primaryColorLight,
              onPressed: () {},
            ),
            title: Text(
              expense.title + " - Rs." + expense.amount.toString(),
              style: Theme.of(context)
                  .textTheme
                  .body2
                  .copyWith(color: Theme.of(context).accentColor),
            ),
            subtitle: Text(
              expense.notes,
            ),
          ),
        );
      },
    );
  }

Our design so far should look like this:

Expense Manager Dashboard Design
Expense Manager Dashboard Design

Adding New Expense

Now let’s add functionality to the dashboard page. First we will add functionality to add new expense. Similar to what we have done for category, we will create a separate route for adding expenses.

A product business logic component will handle UI interactions as well as expenses data stream. Also the CategoryBloc shall be used to fetch list of categories as well.

Since, most of the functionalities for creating new expense is going to be similar to that of creating category, we will not repeat everything here. You can refer to the previous posts regarding:

Become A Flutter Expert With This Course

  • BLoC pattern implementation and
  • Working with StreamBuilder and data stream.

Rather, here we will build a Category selector.

Using ChoiceChip Widget In Flutter

Since, every expense will fall under a category, when creating a new expense we will have to select a category. For this selection we can make use of the ChoiceChip widget. This widget can enable to select one item from a list of chip widgets.

Get Category List By Listening To Category Stream

Use the StreamBuilder like before to listed to category list stream.

StreamBuilder(
  stream: categoryBloc.categoryListStream,
  builder: (_, AsyncSnapshot<BuiltList<CategoryModel>> snap) {

You can access individual category model using the list index.

var categoryModel = snap.data[index];

Create ChoiceChip Widget

And in your ChoiceChip widget, you can use this model to show text as well as to set selected property.

ChoiceChip(
  selectedColor: Theme.of(context).accentColor,
  selected: categoryModel.id == selectedCategoryId,
  label: Text(categoryModel.title),
  onSelected: (selected) {
	setState(() {
	  selectedCategoryId = categoryModel.id;
	});
  },
)

Complete Implementation Using ChoiceChip Widget

The complete implementation could look like this:

import 'package:built_collection/built_collection.dart';
import 'package:expense_manager/blocs/category_bloc.dart';
import 'package:expense_manager/db/services/category_service.dart';
import 'package:expense_manager/models/category_model.dart';
import 'package:flutter/material.dart';

class AddExpense extends StatefulWidget {
  @override
  _AddExpenseState createState() => _AddExpenseState();
}

class _AddExpenseState extends State<AddExpense> {
  CategoryBloc categoryBloc;

  @override
  void initState() {
    super.initState();
    categoryBloc = CategoryBloc(CategoryService());
  }

  int selectedCategoryId = 0;

  @overrideW
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Add New Expense"),
        ),
        body: Container(
          padding: EdgeInsets.all(12.0),
          child: Column(
            children: <Widget>[
              Container(
                margin: EdgeInsets.only(bottom: 12.0),
                child: Text("Pick Category", style: Theme.of(context).textTheme.title,)),
              Container(                
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(8.0),
                  color: Colors.white,
                ),
                child: StreamBuilder(
                  stream: categoryBloc.categoryListStream,
                  builder: (_, AsyncSnapshot<BuiltList<CategoryModel>> snap) {
                    if (!snap.hasData)
                      return Center(
                        child: CircularProgressIndicator(),
                      );

                    return Wrap(
                        children: List.generate(snap.data.length, (int index) {
                      var categoryModel = snap.data[index];
                      return Container(
                        margin: EdgeInsets.symmetric(horizontal: 2.0,),
                        child: ChoiceChip(
                          selectedColor: Theme.of(context).accentColor,
                          selected: categoryModel.id == selectedCategoryId,
                          label: Text(categoryModel.title),
                          onSelected: (selected) {
                            setState(() {
                              selectedCategoryId = categoryModel.id;
                            });
                          },
                        ),
                      );
                    }));
                  },
                ),
              )
            ],
          ),
        ));
  }
}

Here, we are using the setState function to rebuild the entire page on each category selection. You can also avoid the entire rebuild by putting the value of selected categoryId inside a stream and using a StreamBuilder.

ChoiceChip Category Selector
ChoiceChip Category Selector

Link To Previous Posts