Flutter Advance Application - Offline Expense Calculator

Last updated Apr 20, 2021

In this flutter advance application tutorial we are going to create a Expense Calculator with flutter local storage module SQFlite database.This complete expense calculator application consist of below features

  • List of Expenses
  • Add new expense by a Add Expense Form widget
  • Edit or delete existing expenses
  • Swipe to delete items

In this flutter advance application we will using the below widgets

  • Listview with advance use
  • Form programming
  • SQLite Database
  • State Management with Scoped Model

 

 

Let's get started our expense calculator application

Step 1: Create flutter application

Step 2: Add required dependencies in pubspec.yaml file

dependencies:
  flutter:
    sdk: flutter
  sqflite:
  path_provider:
  scoped_model:
  intl: any

 

Step 3: Create an Expense model class and add below code

This Expense class contains below properties

property: id − Unique id to represent an expense entry in SQLite database.

property: amount − Amount spent.

property: date − Date when the amount is spent.

property: category − Category represents type example Food, Travel, etc.,

formattedDate − Used to format the date property

 

import 'package:intl/intl.dart';

class Expense {
  final int id;
  final double amount;
  final DateTime date;
  final String category;
  String get formattedDate {
    var formatter = new DateFormat('yyyy-MM-dd');
    return formatter.format(this.date);
  }
  static final columns = ['id', 'amount', 'date', 'category'];

  Expense(this.id, this.amount, this.date, this.category);
  factory Expense.fromMap(Map<String, dynamic> data) {
    return Expense(
        data['id'],
        data['amount'],
        DateTime.parse(data['date']), data['category']
    );
  }
  Map<String, dynamic> toMap() => {
    "id" : id,
    "amount" : amount,
    "date" : date.toString(),
    "category" : category,
  };
}

 

Create a Database file and add below code

This Database class will have a functionality to create Local SQLite Database and create required database tables and having the functionality to CRUD operations to handle all expenses data.

 

import 'dart:async';
import 'dart:io';
import 'package:flutter_expense_calculator/expense.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';

class SQLiteDbProvider {
  SQLiteDbProvider._();
  static final SQLiteDbProvider db = SQLiteDbProvider._();

  static Database _database; Future<Database> get database async {
    if (_database != null)
      return _database;
    _database = await initDB();
    return _database;
  }
  initDB() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, "ExpenseDB2.db");
    return await openDatabase(
        path, version: 1, onOpen:(db){}, onCreate: (Database db, int version) async {
      await db.execute(
          "CREATE TABLE Expense ( ""id INTEGER PRIMARY KEY," "amount REAL," "date TEXT," "category TEXT"" ) ");
      await db.execute(
          "INSERT INTO Expense ('id', 'amount', 'date', 'category')  values (?, ?, ?, ?)",[1, 1000, '2019-04-01 10:00:00', "Food"]
      );

    }
    );
  }
  Future<List<Expense>> getAllExpenses() async {
    final db = await database;
    List<Map>
    results = await db.query(
        "Expense", columns: Expense.columns, orderBy: "date DESC"
    );
    List<Expense> expenses = new List();
    results.forEach((result) {
      Expense expense = Expense.fromMap(result);
      expenses.add(expense);
    });
    return expenses;
  }
  Future<Expense> getExpenseById(int id) async {
    final db = await database;
    var result = await db.query("Expense", where: "id = ", whereArgs: [id]);
    return result.isNotEmpty ? Expense.fromMap(result.first) : Null;
  }
  Future<double> getTotalExpense() async {
    final db = await database;
    List<Map> list = await db.rawQuery(
        "Select SUM(amount) as amount from expense"
    );
    return list.isNotEmpty ? list[0]["amount"] : Null;
  }
  Future<Expense> insert(Expense expense) async {
    final db = await database;
    var maxIdResult = await db.rawQuery(
        "SELECT MAX(id)+1 as last_inserted_id FROM Expense"
    );
    var id = maxIdResult.first["last_inserted_id"];
    var result = await db.rawInsert(
        "INSERT Into Expense (id, amount, date, category)"
            " VALUES (?, ?, ?, ?)", [
      id, expense.amount, expense.date.toString(), expense.category
    ]
    );
    return Expense(id, expense.amount, expense.date, expense.category);
  }
  update(Expense product) async {
    final db = await database;
    var result = await db.update(
        "Expense", product.toMap(), where: "id = ?", whereArgs: [product.id]
    );
    return result;
  }
  delete(int id) async {
    final db = await database;
    db.delete("Expense", where: "id = ?", whereArgs: [id]);
  }
}

 

Create an Scoped Model class to manage the all expense user data with in the application scope. This class will extends Model class of the Scoped Model dependency.

import 'dart:collection';
import 'package:flutter_expense_calculator/database.dart';
import 'package:flutter_expense_calculator/expense.dart';
import 'package:scoped_model/scoped_model.dart';

class ExpenseListModel extends Model {
  ExpenseListModel() {
    this.load();
  }
  final List<Expense> _items = [];
  UnmodifiableListView<Expense> get items =>
      UnmodifiableListView(_items);

  /*Future get totalExpense {
      return SQLiteDbProvider.db.getTotalExpense();
   }*/

  double get totalExpense {
    double amount = 0.0;
    for(var i = 0; i < _items.length; i++) {
      amount = amount + _items[i].amount;
    }
    return amount;
  }
  void load() {
    Future<List<Expense>> list = SQLiteDbProvider.db.getAllExpenses();
    list.then( (dbItems) {
      for(var i = 0; i < dbItems.length; i++) {
        _items.add(dbItems[i]);
      }
      notifyListeners();
    });
  }
  Expense byId(int id) {
    for(var i = 0; i < _items.length; i++) {
      if(_items[i].id == id) {
        return _items[i];
      }
    }
    return null;
  }
  void add(Expense item) {
    SQLiteDbProvider.db.insert(item).then((val) {
      _items.add(val);
      notifyListeners();
    });
  }
  void update(Expense item) {
    bool found = false;
    for(var i = 0; i < _items.length; i++) {
      if(_items[i].id == item.id) {
        _items[i] = item;
        found = true;
        SQLiteDbProvider.db.update(item);
        break;
      }
    }
    if(found) notifyListeners();
  }
  void delete(Expense item) {
    bool found = false;
    for(var i = 0; i < _items.length; i++) {
      if(_items[i].id == item.id) {
        found = true;
        SQLiteDbProvider.db.delete(item.id);
        _items.removeAt(i); break;
      }
    }
    if(found) notifyListeners();
  }
}

 

Create add expense class which will be used to add your expenses.

import 'package:flutter/material.dart';
import 'package:flutter_expense_calculator/expense.dart';

import 'expense_list_model.dart';


class FormPage extends StatefulWidget {
  FormPage({Key key, this.id, this.expenses}) : super(key: key);
  final int id;
  final ExpenseListModel expenses;

  @override _FormPageState createState() => _FormPageState(id: id, expenses: expenses);
}
class _FormPageState extends State<FormPage> {
  _FormPageState({Key key, this.id, this.expenses});

  final int id;
  final ExpenseListModel expenses;
  final scaffoldKey = GlobalKey<ScaffoldState>();
  final formKey = GlobalKey<FormState>();

  double _amount;
  DateTime _date;
  String _category;

  void _submit() {
    final form = formKey.currentState;
    if (form.validate()) {
      form.save();
      if (this.id == 0) expenses.add(Expense(0, _amount, _date, _category));
      else expenses.update(Expense(this.id, _amount, _date, _category));
      Navigator.pop(context);
    }
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: scaffoldKey, appBar: AppBar(
      title: Text('Enter expense details'),
    ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: formKey, child: Column(
          children: [
            TextFormField(
              style: TextStyle(fontSize: 22),
              decoration: const InputDecoration(
                  icon: const Icon(Icons.monetization_on),
                  labelText: 'Amount',
                  labelStyle: TextStyle(fontSize: 18)
              ),
              validator: (val) {
                Pattern pattern = r'^[1-9]\d*(\.\d+)?$';
                RegExp regex = new RegExp(pattern);
                if (!regex.hasMatch(val))
                  return 'Enter a valid number'; else return null;
              },
              initialValue: id == 0
                  ? '' : expenses.byId(id).amount.toString(),
              onSaved: (val) => _amount = double.parse(val),
            ),
            TextFormField(
              style: TextStyle(fontSize: 22),
              decoration: const InputDecoration(
                icon: const Icon(Icons.calendar_today),
                hintText: 'Enter date',
                labelText: 'Date (yyyy-mm-dd)',
                labelStyle: TextStyle(fontSize: 18),
              ),
              validator: (val) {
                Pattern pattern = r'^((?:19|20)\d\d)[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$';
                RegExp regex = new RegExp(pattern);
                if (!regex.hasMatch(val))
                return 'Enter a valid date';
                else return null;
              },
              onSaved: (val) => _date = DateTime.parse(val),
              initialValue: id == 0
                  ? '' : expenses.byId(id).formattedDate,
              keyboardType: TextInputType.datetime,
            ),
            TextFormField(
              style: TextStyle(fontSize: 22),
              decoration: const InputDecoration(
                  icon: const Icon(Icons.category),
                  labelText: 'Category',
                  labelStyle: TextStyle(fontSize: 18)
              ),
              onSaved: (val) => _category = val,
              initialValue: id == 0 ? ''
                  : expenses.byId(id).category.toString(),
            ),
            RaisedButton(
              onPressed: _submit,
              child: new Text('Submit'),
            ),
          ],
        ),
        ),
      ),
    );
  }
}

 

Update your main dart file to show list of all expenses

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'Expense.dart';
import 'add_expense.dart';
import 'expense_list_model.dart';

void main() {
  final expenses = ExpenseListModel();
  runApp(
      ScopedModel<ExpenseListModel>(
        model: expenses, child: MyApp(),
      )
  );
}
class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Expense',
      theme: ThemeData(
        primarySwatch: Colors.pink,
      ),
      home: MyHomePage(title: 'Expense calculator'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(this.title),
        ),
        body: ScopedModelDescendant<ExpenseListModel>(
          builder: (context, child, expenses) {
            return ListView.separated(
              itemCount: expenses.items == null ? 1
                  : expenses.items.length + 1,
              itemBuilder: (context, index) {
                if (index == 0) {
                  return ListTile(
                      title: Text("Total expenses: "
                          + expenses.totalExpense.toString(),
                        style: TextStyle(fontSize: 24,
                            fontWeight: FontWeight.bold),)
                  );
                } else {
                  index = index - 1;
                  return Dismissible(
                      key: Key(expenses.items[index].id.toString()),
                      onDismissed: (direction) {
                        expenses.delete(expenses.items[index]);
                        Scaffold.of(context).showSnackBar(
                            SnackBar(
                                content: Text(
                                    "Item with id, "
                                        + expenses.items[index].id.toString() +
                                        " is dismissed"
                                )
                            )
                        );
                      },
                      child: ListTile( onTap: () {
                        Navigator.push(
                            context, MaterialPageRoute(
                            builder: (context) => FormPage(
                              id: expenses.items[index].id,
                              expenses: expenses,
                            )
                        )
                        );
                      },
                          leading: Icon(Icons.monetization_on),
                          trailing: Icon(Icons.keyboard_arrow_right),
                          title: Text(expenses.items[index].category + ": " +
                              expenses.items[index].amount.toString() +
                              " \nspent on " + expenses.items[index].formattedDate,
                            style: TextStyle(fontSize: 18, fontStyle: FontStyle.normal),))
                  );
                }
              },
              separatorBuilder: (context, index) {
                return Divider();
              },
            );
          },
        ),
        floatingActionButton: ScopedModelDescendant<ExpenseListModel>(
            builder: (context, child, expenses) {
              return FloatingActionButton( onPressed: () {
                Navigator.push(
                    context, MaterialPageRoute(
                    builder: (context) => ScopedModelDescendant<ExpenseListModel>(
                        builder: (context, child, expenses) {
                          return FormPage( id: 0, expenses: expenses, );
                        }
                    )
                )
                );
               
              },
                tooltip: 'Increment', child: Icon(Icons.add), );
            }
        )
    );
  }
}

 

Step 4: Run application

Flutter Advance application expense calculator

 

Flutter Expanse calculator SQlite

 

Tags: Flutter SQlite Storage, Expense Calculator, Local Storage, Flutter Advance Tutorial

 

Article Contributed By :
https://www.rrtutors.com/site_assets/profile/assets/img/avataaars.svg

1546 Views