Successfully migrate the application to SQLIte!
This commit is contained in:
		
							
								
								
									
										108
									
								
								lib/db.dart
									
									
									
									
									
								
							
							
						
						
									
										108
									
								
								lib/db.dart
									
									
									
									
									
								
							@@ -1,45 +1,101 @@
 | 
			
		||||
// https://docs.flutter.dev/cookbook/persistence/sqlite
 | 
			
		||||
 | 
			
		||||
// SQLite
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
import 'package:flutter/widgets.dart';
 | 
			
		||||
import 'package:flutter_expense_tracker/models/frequency.dart';
 | 
			
		||||
import 'package:path/path.dart';
 | 
			
		||||
import 'package:path_provider/path_provider.dart';
 | 
			
		||||
import 'package:sqflite/sqflite.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
// Local
 | 
			
		||||
import '/models/frequency.dart';
 | 
			
		||||
import '/models/expense.dart';
 | 
			
		||||
 | 
			
		||||
void loadDB() async {
 | 
			
		||||
  // Avoid errors caused by flutter upgrade.
 | 
			
		||||
  WidgetsFlutterBinding.ensureInitialized();
 | 
			
		||||
// Leaned on this example:
 | 
			
		||||
//   https://learnflutterwithme.com/sqlite
 | 
			
		||||
class DatabaseHelper {
 | 
			
		||||
  DatabaseHelper._privateConstructor();
 | 
			
		||||
  static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
 | 
			
		||||
 | 
			
		||||
  final String frequencies =
 | 
			
		||||
      "'${Frequency.values.map((freq) => freq.title).join("','")}'";
 | 
			
		||||
  print(frequencies);
 | 
			
		||||
  static Database? _db;
 | 
			
		||||
  Future<Database> get db async => _db ??= await _initDatabase();
 | 
			
		||||
 | 
			
		||||
  // Open the database and store the reference.
 | 
			
		||||
  final database = openDatabase(
 | 
			
		||||
    // Set the path to the database. Note: Using the `join` function from the
 | 
			
		||||
    // `path` package is best practice to ensure the path is correctly
 | 
			
		||||
    // constructed for each platform.
 | 
			
		||||
    join(await getDatabasesPath(), 'expense_tracker.db'),
 | 
			
		||||
  Future<Database> _initDatabase() async {
 | 
			
		||||
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
 | 
			
		||||
    String path = join(documentsDirectory.path, "com_hyperling_expense.db");
 | 
			
		||||
    return await openDatabase(
 | 
			
		||||
      path,
 | 
			
		||||
      version: 1,
 | 
			
		||||
      onCreate: _onCreate,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    onCreate: (db, version) {
 | 
			
		||||
      // Run the CREATE TABLE statement on the database.
 | 
			
		||||
      return db.execute(
 | 
			
		||||
        """
 | 
			
		||||
  Future _onCreate(Database db, int version) async {
 | 
			
		||||
    await db.execute("""
 | 
			
		||||
        CREATE TABLE expense
 | 
			
		||||
          ( id INTEGER PRIMARY KEY
 | 
			
		||||
          , name TEXT
 | 
			
		||||
          , cost DOUBLE
 | 
			
		||||
          , frequency TEXT CHECK(frequency IN ($frequencies) )
 | 
			
		||||
          , name TEXT NOT NULL UNIQUE
 | 
			
		||||
          , cost DOUBLE NOT NULL
 | 
			
		||||
          , frequency TEXT NOT NULL
 | 
			
		||||
          , description TEXT
 | 
			
		||||
        )""",
 | 
			
		||||
        )
 | 
			
		||||
        """);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Expense Section
 | 
			
		||||
  ///
 | 
			
		||||
  Future<List<Expense>> getExpenses() async {
 | 
			
		||||
    Database db = await instance.db;
 | 
			
		||||
    var expenses = await db.query("expense", orderBy: "name");
 | 
			
		||||
    List<Expense> expenseList = expenses.isNotEmpty
 | 
			
		||||
        ? expenses.map((c) => Expense.fromMap(c)).toList()
 | 
			
		||||
        : [];
 | 
			
		||||
    return expenseList;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<int> addExpense(Expense expense) async {
 | 
			
		||||
    Database db = await instance.db;
 | 
			
		||||
    return await db.insert(
 | 
			
		||||
      "expense",
 | 
			
		||||
      expense.toMap(),
 | 
			
		||||
    );
 | 
			
		||||
    },
 | 
			
		||||
    // Set the version. This executes the onCreate function and provides a
 | 
			
		||||
    // path to perform database upgrades and downgrades.
 | 
			
		||||
    version: 1,
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<int> removeExpense(int id) async {
 | 
			
		||||
    Database db = await instance.db;
 | 
			
		||||
    return await db.delete(
 | 
			
		||||
      "expense",
 | 
			
		||||
      where: "id = ?",
 | 
			
		||||
      whereArgs: [id],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<int> updateExpense(Expense expense) async {
 | 
			
		||||
    Database db = await instance.db;
 | 
			
		||||
    return await db.update(
 | 
			
		||||
      "expense",
 | 
			
		||||
      expense.toMap(),
 | 
			
		||||
      where: "id = ?",
 | 
			
		||||
      whereArgs: [expense.id],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> checkExpenseNameExists(String name) async {
 | 
			
		||||
    Database db = await instance.db;
 | 
			
		||||
    var expenses = await db.query("expense",
 | 
			
		||||
        where: "name = ?", whereArgs: [name],);
 | 
			
		||||
    return expenses.isNotEmpty;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ///
 | 
			
		||||
 | 
			
		||||
  /// Income Section
 | 
			
		||||
  ///
 | 
			
		||||
 | 
			
		||||
  ///
 | 
			
		||||
  /// Liquid Asset Section
 | 
			
		||||
 | 
			
		||||
  ///
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										143
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										143
									
								
								lib/main.dart
									
									
									
									
									
								
							@@ -2,16 +2,36 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
// Local
 | 
			
		||||
import '/pages/expense.dart';
 | 
			
		||||
import '/pages/income.dart';
 | 
			
		||||
import '/pages/asset.dart';
 | 
			
		||||
import '/pages/report.dart';
 | 
			
		||||
import '/pages/settings.dart';
 | 
			
		||||
import '/pages/help.dart';
 | 
			
		||||
import '/db.dart';
 | 
			
		||||
import '/pages/home.dart';
 | 
			
		||||
 | 
			
		||||
// SQLite
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
 | 
			
		||||
import 'package:path/path.dart';
 | 
			
		||||
import 'package:path_provider/path_provider.dart';
 | 
			
		||||
 | 
			
		||||
const bool testing = true;
 | 
			
		||||
 | 
			
		||||
void main() {
 | 
			
		||||
  loadDB();
 | 
			
		||||
  // I see no good explanations of why to use this other package yet, but
 | 
			
		||||
  // trying this to see if it fixes the DB factory errors.
 | 
			
		||||
  // "Unhandled Exception: Bad state: databaseFactory not initialized databaseFactory is only initialized when using sqflite. When using `sqflite_common_ffi`You must call `databaseFactory = databaseFactoryFfi;` before using global openDatabase API
 | 
			
		||||
  //   https://stackoverflow.com/questions/76158800/databasefactory-not-initialized-when-using-sqflite-in-flutter
 | 
			
		||||
  if (Platform.isWindows || Platform.isLinux) {
 | 
			
		||||
    // Initialize FFI
 | 
			
		||||
    sqfliteFfiInit();
 | 
			
		||||
    databaseFactory = databaseFactoryFfi;
 | 
			
		||||
  }
 | 
			
		||||
  WidgetsFlutterBinding.ensureInitialized();
 | 
			
		||||
 | 
			
		||||
  if (testing) {
 | 
			
		||||
    () async {
 | 
			
		||||
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
 | 
			
		||||
    String path = join(documentsDirectory.path, 'com_hyperling_expense.db');
 | 
			
		||||
      await deleteDatabase(path);
 | 
			
		||||
    }();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  runApp(const MainApp());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -36,110 +56,3 @@ class MainApp extends StatelessWidget {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class HomePage extends StatefulWidget {
 | 
			
		||||
  const HomePage({
 | 
			
		||||
    super.key,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<HomePage> createState() => _HomePageState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _HomePageState extends State<HomePage> {
 | 
			
		||||
  var pageSelected = 0;
 | 
			
		||||
 | 
			
		||||
  refresh() {
 | 
			
		||||
    setState(() {});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    Widget page;
 | 
			
		||||
    Widget? dialog;
 | 
			
		||||
    switch (pageSelected) {
 | 
			
		||||
      case 0:
 | 
			
		||||
        page = ExpensePage();
 | 
			
		||||
        dialog = ExpenseInputDialog(
 | 
			
		||||
          notifyParent: refresh,
 | 
			
		||||
        );
 | 
			
		||||
      case 1:
 | 
			
		||||
        page = IncomePage();
 | 
			
		||||
      case 2:
 | 
			
		||||
        page = AssetPage();
 | 
			
		||||
      case 3:
 | 
			
		||||
        page = ProjectionPage();
 | 
			
		||||
      case 4:
 | 
			
		||||
        page = SettingsPage();
 | 
			
		||||
      case 5:
 | 
			
		||||
        page = HelpPage();
 | 
			
		||||
      default:
 | 
			
		||||
        throw UnimplementedError('No widget for page $pageSelected yet!');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Future<void> addNewValue(BuildContext context) {
 | 
			
		||||
      return showDialog(
 | 
			
		||||
        context: context,
 | 
			
		||||
        builder: (_) => AlertDialog(content: dialog),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Widget? floatingButton;
 | 
			
		||||
    if (dialog != null) {
 | 
			
		||||
      floatingButton = IconButton(
 | 
			
		||||
        onPressed: () {
 | 
			
		||||
          addNewValue(context);
 | 
			
		||||
        },
 | 
			
		||||
        icon: Icon(Icons.add),
 | 
			
		||||
        color: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return LayoutBuilder(builder: (context, constraints) {
 | 
			
		||||
      return Scaffold(
 | 
			
		||||
        appBar: AppBar(title: Text("Expense Tracker")),
 | 
			
		||||
        drawer: NavigationRail(
 | 
			
		||||
          extended: true,
 | 
			
		||||
          destinations: [
 | 
			
		||||
            NavigationRailDestination(
 | 
			
		||||
              icon: Icon(Icons.payment),
 | 
			
		||||
              label: Text('Expenses'),
 | 
			
		||||
            ),
 | 
			
		||||
            NavigationRailDestination(
 | 
			
		||||
              icon: Icon(Icons.account_balance),
 | 
			
		||||
              label: Text('Income'),
 | 
			
		||||
            ),
 | 
			
		||||
            NavigationRailDestination(
 | 
			
		||||
              icon: Icon(Icons.attach_money),
 | 
			
		||||
              label: Text('Liquid Assets'),
 | 
			
		||||
            ),
 | 
			
		||||
            NavigationRailDestination(
 | 
			
		||||
              icon: Icon(Icons.bar_chart),
 | 
			
		||||
              label: Text('Reports'),
 | 
			
		||||
            ),
 | 
			
		||||
            NavigationRailDestination(
 | 
			
		||||
              icon: Icon(Icons.settings),
 | 
			
		||||
              label: Text('Settings'),
 | 
			
		||||
            ),
 | 
			
		||||
            NavigationRailDestination(
 | 
			
		||||
              icon: Icon(Icons.help),
 | 
			
		||||
              label: Text('Help'),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
          selectedIndex: pageSelected,
 | 
			
		||||
          onDestinationSelected: (value) {
 | 
			
		||||
            setState(() {
 | 
			
		||||
              pageSelected = value;
 | 
			
		||||
              Navigator.pop(context);
 | 
			
		||||
            });
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        body: Container(
 | 
			
		||||
          color: Theme.of(context).colorScheme.primaryContainer,
 | 
			
		||||
          child: Center(child: page),
 | 
			
		||||
        ),
 | 
			
		||||
        floatingActionButton: floatingButton,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,24 @@
 | 
			
		||||
import '/models/frequency.dart';
 | 
			
		||||
 | 
			
		||||
class Expense {
 | 
			
		||||
  final int? id;
 | 
			
		||||
  final String name;
 | 
			
		||||
  final double cost;
 | 
			
		||||
  final Frequency frequency;
 | 
			
		||||
  final String description;
 | 
			
		||||
 | 
			
		||||
  const Expense(
 | 
			
		||||
      {required this.name,
 | 
			
		||||
  const Expense({
 | 
			
		||||
    this.id,
 | 
			
		||||
    required this.name,
 | 
			
		||||
    required this.cost,
 | 
			
		||||
    required this.frequency,
 | 
			
		||||
      required this.description});
 | 
			
		||||
    required this.description,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return "$name, $cost, ${frequency.title}, $description";
 | 
			
		||||
    //return "$name, $cost, ${frequency.title}, $description";
 | 
			
		||||
    return toMap().toString();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  double calcComparableCost() {
 | 
			
		||||
@@ -24,4 +28,24 @@ class Expense {
 | 
			
		||||
  double calcComparableCostDaily() {
 | 
			
		||||
    return cost / frequency.numDays;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  factory Expense.fromMap(Map<String, dynamic> json) => Expense(
 | 
			
		||||
        id: json['id'],
 | 
			
		||||
        name: json['name'],
 | 
			
		||||
        cost: json['cost'],
 | 
			
		||||
        frequency: Frequency.values
 | 
			
		||||
            .where((expense) => expense.title == json['frequency'])
 | 
			
		||||
            .first,
 | 
			
		||||
        description: json['description'],
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toMap() {
 | 
			
		||||
    return {
 | 
			
		||||
      'id': id,
 | 
			
		||||
      'name': name,
 | 
			
		||||
      'cost': cost,
 | 
			
		||||
      'frequency': frequency.title,
 | 
			
		||||
      'description': description,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,13 @@
 | 
			
		||||
// Flutter
 | 
			
		||||
import 'dart:nativewrappers/_internal/vm/lib/ffi_allocation_patch.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_expense_tracker/db.dart';
 | 
			
		||||
 | 
			
		||||
// Local
 | 
			
		||||
import '/models/expense.dart';
 | 
			
		||||
import '/models/frequency.dart';
 | 
			
		||||
 | 
			
		||||
List<Expense> expenses = [];
 | 
			
		||||
 | 
			
		||||
class ExpensePage extends StatefulWidget {
 | 
			
		||||
  const ExpensePage({
 | 
			
		||||
    super.key,
 | 
			
		||||
@@ -25,24 +26,38 @@ class _ExpensePageState extends State<ExpensePage> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final theme = Theme.of(context);
 | 
			
		||||
 | 
			
		||||
    expenses.sort(
 | 
			
		||||
    return FutureBuilder<List<Expense>>(
 | 
			
		||||
        future: DatabaseHelper.instance.getExpenses(),
 | 
			
		||||
        builder: (BuildContext context, AsyncSnapshot<List<Expense>> snapshot) {
 | 
			
		||||
          if (!snapshot.hasData) {
 | 
			
		||||
            return Center(child: Text('Loading...'));
 | 
			
		||||
          }
 | 
			
		||||
          snapshot.data!.sort(
 | 
			
		||||
            (a, b) => (b.calcComparableCost() - a.calcComparableCost()).toInt(),
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
    return expenses.isEmpty
 | 
			
		||||
        ? Text("Add expenses to get started!")
 | 
			
		||||
          return snapshot.data!.isEmpty
 | 
			
		||||
              ? Text(
 | 
			
		||||
                  "Add expenses to get started.",
 | 
			
		||||
                  softWrap: true,
 | 
			
		||||
                )
 | 
			
		||||
              : ListView.builder(
 | 
			
		||||
            itemCount: expenses.length,
 | 
			
		||||
                  itemCount: snapshot.data!.length,
 | 
			
		||||
                  itemBuilder: (_, index) {
 | 
			
		||||
                    List<Expense> expenses = snapshot.data!;
 | 
			
		||||
                    final Expense curr = expenses[index];
 | 
			
		||||
              final String estimateSymbolYearly = curr.frequency.timesPerYear
 | 
			
		||||
                    final String estimateSymbolYearly = curr
 | 
			
		||||
                                .frequency.timesPerYear
 | 
			
		||||
                                .toStringAsFixed(2)
 | 
			
		||||
                                .endsWith(".00") &&
 | 
			
		||||
                      curr.calcComparableCost().toStringAsFixed(3).endsWith("0")
 | 
			
		||||
                            curr
 | 
			
		||||
                                .calcComparableCost()
 | 
			
		||||
                                .toStringAsFixed(3)
 | 
			
		||||
                                .endsWith("0")
 | 
			
		||||
                        ? ""
 | 
			
		||||
                        : "~";
 | 
			
		||||
              final String estimateSymbolDaily =
 | 
			
		||||
                  curr.frequency.numDays.toStringAsFixed(2).endsWith(".00") &&
 | 
			
		||||
                    final String estimateSymbolDaily = curr.frequency.numDays
 | 
			
		||||
                                .toStringAsFixed(2)
 | 
			
		||||
                                .endsWith(".00") &&
 | 
			
		||||
                            curr
 | 
			
		||||
                                .calcComparableCostDaily()
 | 
			
		||||
                                .toStringAsFixed(3)
 | 
			
		||||
@@ -52,7 +67,7 @@ class _ExpensePageState extends State<ExpensePage> {
 | 
			
		||||
                    return Padding(
 | 
			
		||||
                      padding: const EdgeInsets.all(4.0),
 | 
			
		||||
                      child: Dismissible(
 | 
			
		||||
                  key: Key(curr.toString()),
 | 
			
		||||
                        key: Key(curr.id!.toString()),
 | 
			
		||||
                        background: Container(
 | 
			
		||||
                          color: Colors.red,
 | 
			
		||||
                          child: Row(
 | 
			
		||||
@@ -76,10 +91,9 @@ class _ExpensePageState extends State<ExpensePage> {
 | 
			
		||||
                        onDismissed: (direction) {
 | 
			
		||||
                          setState(() {
 | 
			
		||||
                            expenses.remove(curr);
 | 
			
		||||
                    });
 | 
			
		||||
                            switch (direction) {
 | 
			
		||||
                              case DismissDirection.startToEnd:
 | 
			
		||||
                        // Only remove the item from the list.
 | 
			
		||||
                                DatabaseHelper.instance.removeExpense(curr.id!);
 | 
			
		||||
                                break;
 | 
			
		||||
                              case DismissDirection.endToStart:
 | 
			
		||||
                                // Open an edit dialog, then remove the item from the list.
 | 
			
		||||
@@ -98,6 +112,7 @@ class _ExpensePageState extends State<ExpensePage> {
 | 
			
		||||
                                  "Direction ${direction.toString()} not recognized.",
 | 
			
		||||
                                );
 | 
			
		||||
                            }
 | 
			
		||||
                          });
 | 
			
		||||
                        },
 | 
			
		||||
                        child: Container(
 | 
			
		||||
                          decoration: BoxDecoration(
 | 
			
		||||
@@ -158,6 +173,7 @@ class _ExpensePageState extends State<ExpensePage> {
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                );
 | 
			
		||||
        });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -178,6 +194,7 @@ class ExpenseInputDialog extends StatefulWidget {
 | 
			
		||||
class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
 | 
			
		||||
  final _expenseFormKey = GlobalKey<FormState>();
 | 
			
		||||
 | 
			
		||||
  int? _id;
 | 
			
		||||
  String _name = "";
 | 
			
		||||
  double _cost = 0;
 | 
			
		||||
  Frequency _freq = Frequency.monthly;
 | 
			
		||||
@@ -186,6 +203,7 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if (widget.expense != null) {
 | 
			
		||||
      _id = widget.expense!.id;
 | 
			
		||||
      _name = widget.expense!.name;
 | 
			
		||||
      _cost = widget.expense!.cost;
 | 
			
		||||
      _freq = widget.expense!.frequency;
 | 
			
		||||
@@ -202,7 +220,7 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              if (widget.expense != null) {
 | 
			
		||||
                setState(() {
 | 
			
		||||
                  expenses.add(widget.expense!);
 | 
			
		||||
                  DatabaseHelper.instance.addExpense(widget.expense!);
 | 
			
		||||
                  widget.notifyParent();
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
@@ -237,9 +255,12 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
 | 
			
		||||
                    if (value!.isEmpty) {
 | 
			
		||||
                      return "Name must be provided.";
 | 
			
		||||
                    }
 | 
			
		||||
                    if (!expenses.every((expense) => expense.name != value)) {
 | 
			
		||||
                    // TODO: Figure out how to check the DB if a name already exists.
 | 
			
		||||
                    /*
 | 
			
		||||
                    if (DatabaseHelper.instance.checkExpenseNameExists(value)) {
 | 
			
		||||
                      return "Name must be unique, already in use.";
 | 
			
		||||
                    }
 | 
			
		||||
                    */
 | 
			
		||||
                    return null;
 | 
			
		||||
                  },
 | 
			
		||||
                  onSaved: (value) {
 | 
			
		||||
@@ -341,13 +362,22 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
 | 
			
		||||
                  if (_expenseFormKey.currentState!.validate()) {
 | 
			
		||||
                    _expenseFormKey.currentState!.save();
 | 
			
		||||
                    setState(() {
 | 
			
		||||
                      expenses.add(
 | 
			
		||||
                        Expense(
 | 
			
		||||
                      Expense expense = Expense(
 | 
			
		||||
                        id: _id,
 | 
			
		||||
                        name: _name,
 | 
			
		||||
                        cost: _cost,
 | 
			
		||||
                        frequency: _freq,
 | 
			
		||||
                            description: _desc),
 | 
			
		||||
                        description: _desc,
 | 
			
		||||
                      );
 | 
			
		||||
                      if (_id != null) {
 | 
			
		||||
                        DatabaseHelper.instance.updateExpense(
 | 
			
		||||
                          expense,
 | 
			
		||||
                        );
 | 
			
		||||
                      } else {
 | 
			
		||||
                        DatabaseHelper.instance.addExpense(
 | 
			
		||||
                          expense,
 | 
			
		||||
                        );
 | 
			
		||||
                      }
 | 
			
		||||
                    });
 | 
			
		||||
                    widget.notifyParent();
 | 
			
		||||
                    Navigator.of(context).pop();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										119
									
								
								lib/pages/home.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								lib/pages/home.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
			
		||||
// Flutter
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
// Local
 | 
			
		||||
import '/pages/expense.dart';
 | 
			
		||||
import '/pages/income.dart';
 | 
			
		||||
import '/pages/asset.dart';
 | 
			
		||||
import '/pages/report.dart';
 | 
			
		||||
import '/pages/settings.dart';
 | 
			
		||||
import '/pages/help.dart';
 | 
			
		||||
 | 
			
		||||
class HomePage extends StatefulWidget {
 | 
			
		||||
  const HomePage({
 | 
			
		||||
    super.key,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<HomePage> createState() => _HomePageState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _HomePageState extends State<HomePage> {
 | 
			
		||||
  var pageSelected = 0;
 | 
			
		||||
 | 
			
		||||
  refresh() {
 | 
			
		||||
    setState(() {});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    Widget page;
 | 
			
		||||
    Widget? dialog;
 | 
			
		||||
    switch (pageSelected) {
 | 
			
		||||
      case 0:
 | 
			
		||||
        page = ExpensePage();
 | 
			
		||||
        dialog = ExpenseInputDialog(
 | 
			
		||||
          notifyParent: refresh,
 | 
			
		||||
        );
 | 
			
		||||
      case 1:
 | 
			
		||||
        page = IncomePage();
 | 
			
		||||
      case 2:
 | 
			
		||||
        page = AssetPage();
 | 
			
		||||
      case 3:
 | 
			
		||||
        page = ProjectionPage();
 | 
			
		||||
      case 4:
 | 
			
		||||
        page = SettingsPage();
 | 
			
		||||
      case 5:
 | 
			
		||||
        page = HelpPage();
 | 
			
		||||
      default:
 | 
			
		||||
        throw UnimplementedError('No widget for page $pageSelected yet!');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Future<void> addNewValue(BuildContext context) {
 | 
			
		||||
      return showDialog(
 | 
			
		||||
        context: context,
 | 
			
		||||
        builder: (_) => AlertDialog(content: dialog),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Widget? floatingButton;
 | 
			
		||||
    if (dialog != null) {
 | 
			
		||||
      floatingButton = IconButton(
 | 
			
		||||
        onPressed: () {
 | 
			
		||||
          addNewValue(context);
 | 
			
		||||
        },
 | 
			
		||||
        icon: Icon(Icons.add),
 | 
			
		||||
        color: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return LayoutBuilder(builder: (context, constraints) {
 | 
			
		||||
      return Scaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          title: Text("Expense Tracker"),
 | 
			
		||||
        ),
 | 
			
		||||
        drawer: NavigationRail(
 | 
			
		||||
          extended: true,
 | 
			
		||||
          destinations: [
 | 
			
		||||
            NavigationRailDestination(
 | 
			
		||||
              icon: Icon(Icons.payment),
 | 
			
		||||
              label: Text('Expenses'),
 | 
			
		||||
            ),
 | 
			
		||||
            NavigationRailDestination(
 | 
			
		||||
              icon: Icon(Icons.account_balance),
 | 
			
		||||
              label: Text('Income'),
 | 
			
		||||
            ),
 | 
			
		||||
            NavigationRailDestination(
 | 
			
		||||
              icon: Icon(Icons.attach_money),
 | 
			
		||||
              label: Text('Liquid Assets'),
 | 
			
		||||
            ),
 | 
			
		||||
            NavigationRailDestination(
 | 
			
		||||
              icon: Icon(Icons.bar_chart),
 | 
			
		||||
              label: Text('Reports'),
 | 
			
		||||
            ),
 | 
			
		||||
            NavigationRailDestination(
 | 
			
		||||
              icon: Icon(Icons.settings),
 | 
			
		||||
              label: Text('Settings'),
 | 
			
		||||
            ),
 | 
			
		||||
            NavigationRailDestination(
 | 
			
		||||
              icon: Icon(Icons.help),
 | 
			
		||||
              label: Text('Help'),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
          selectedIndex: pageSelected,
 | 
			
		||||
          onDestinationSelected: (value) {
 | 
			
		||||
            setState(() {
 | 
			
		||||
              pageSelected = value;
 | 
			
		||||
              Navigator.pop(context);
 | 
			
		||||
            });
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        body: Container(
 | 
			
		||||
          color: Theme.of(context).colorScheme.primaryContainer,
 | 
			
		||||
          child: Center(child: page),
 | 
			
		||||
        ),
 | 
			
		||||
        floatingActionButton: floatingButton,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -5,8 +5,10 @@
 | 
			
		||||
import FlutterMacOS
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
import path_provider_foundation
 | 
			
		||||
import sqflite_darwin
 | 
			
		||||
 | 
			
		||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
 | 
			
		||||
  PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
 | 
			
		||||
  SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										98
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										98
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -49,6 +49,14 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.3.1"
 | 
			
		||||
  ffi:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: ffi
 | 
			
		||||
      sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.3"
 | 
			
		||||
  flutter:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description: flutter
 | 
			
		||||
@@ -124,13 +132,61 @@ packages:
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.15.0"
 | 
			
		||||
  path:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: path
 | 
			
		||||
      sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.9.0"
 | 
			
		||||
  path_provider:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: path_provider
 | 
			
		||||
      sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.5"
 | 
			
		||||
  path_provider_android:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: path_provider_android
 | 
			
		||||
      sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.2.15"
 | 
			
		||||
  path_provider_foundation:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: path_provider_foundation
 | 
			
		||||
      sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.4.1"
 | 
			
		||||
  path_provider_linux:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: path_provider_linux
 | 
			
		||||
      sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.2.1"
 | 
			
		||||
  path_provider_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: path_provider_platform_interface
 | 
			
		||||
      sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.2"
 | 
			
		||||
  path_provider_windows:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: path_provider_windows
 | 
			
		||||
      sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.3.0"
 | 
			
		||||
  platform:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -184,6 +240,14 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.5.4+6"
 | 
			
		||||
  sqflite_common_ffi:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: sqflite_common_ffi
 | 
			
		||||
      sha256: "883dd810b2b49e6e8c3b980df1829ef550a94e3f87deab5d864917d27ca6bf36"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.3.4+4"
 | 
			
		||||
  sqflite_darwin:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -200,6 +264,14 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.4.0"
 | 
			
		||||
  sqlite3:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: sqlite3
 | 
			
		||||
      sha256: "35d3726fe18ab1463403a5cc8d97dbc81f2a0b08082e8173851363fcc97b6627"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.7.2"
 | 
			
		||||
  stack_trace:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -248,6 +320,14 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.7.3"
 | 
			
		||||
  typed_data:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: typed_data
 | 
			
		||||
      sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.4.0"
 | 
			
		||||
  vector_math:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -264,6 +344,22 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "14.3.0"
 | 
			
		||||
  web:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: web
 | 
			
		||||
      sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.1.0"
 | 
			
		||||
  xdg_directories:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: xdg_directories
 | 
			
		||||
      sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.1.0"
 | 
			
		||||
sdks:
 | 
			
		||||
  dart: ">=3.6.1 <4.0.0"
 | 
			
		||||
  flutter: ">=3.24.0"
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,10 @@ environment:
 | 
			
		||||
dependencies:
 | 
			
		||||
  flutter:
 | 
			
		||||
    sdk: flutter
 | 
			
		||||
  path: ^1.9.0
 | 
			
		||||
  path_provider: ^2.1.5
 | 
			
		||||
  sqflite: ^2.4.1
 | 
			
		||||
  sqflite_common_ffi: ^2.3.4+4
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user