Flutter-writing-advanced-applications

提供:Dev Guides
移動先:案内検索

Flutter-高度なアプリケーションの作成

この章では、本格的なモバイルアプリケーション、expense_calculatorの作成方法を学習します。 Expense_Calculatorの目的は、経費情報を保存することです。 アプリケーションの完全な機能は次のとおりです-

  • 費用リスト。
  • 新しい費用を入力するフォーム。
  • 既存の費用を編集/削除するオプション。
  • 任意のインスタンスでの総費用。

Flutterフレームワークの下記の高度な機能を使用して、expense_calculatorアプリケーションをプログラミングします。

  • 経費リストを表示するためのListViewの高度な使用。

  • フォームプログラミング。

  • 経費を保存するSQLiteデータベースプログラミング。

  • プログラミングを簡素化するscoped_model状態管理。

    *expense_calculator* アプリケーションのプログラミングを始めましょう。
  • Androidスタジオで新しいFlutterアプリケーション、expense_calculatorを作成します。

  • pubspec.yamlを開き、パッケージの依存関係を追加します。

dependencies:
   flutter:
      sdk: flutter
   sqflite: ^1.1.0
   path_provider: ^0.5.0+1
   scoped_model: ^1.0.1
   intl: any

ここでこれらのポイントを観察します-

  • sqfliteは、SQLiteデータベースプログラミングに使用されます。
  • path_providerは、システム固有のアプリケーションパスを取得するために使用されます。
  • scoped_modelは状態管理に使用されます。
  • intlは日付のフォーマットに使用されます。

Androidスタジオは、pubspec.yamlが更新されたことを示す次のアラートを表示します。

高度なアプリケーションを作成するアラート

[依存関係の取得]オプションをクリックします。 Androidスタジオはインターネットからパッケージを取得し、アプリケーション用に適切に構成します。

main.dartの既存のコードを削除します。

新しいファイルExpense.dartを追加して、Expenseクラスを作成します。 経費クラスには、以下のプロパティとメソッドがあります。

  • property:id -SQLiteデータベースの経費エントリを表す一意のID。
  • property:amount -費やされた金額。
  • property:date -金額が費やされた日付。
  • プロパティ:カテゴリ-カテゴリは、金額が費やされる領域を表します。 例:食べ物、旅行など
  • formattedDate -日付プロパティのフォーマットに使用
  • fromMap -データベーステーブルのフィールドを経費オブジェクトのプロパティにマップし、新しい経費オブジェクトを作成するために使用します。
factory Expense.fromMap(Map<String, dynamic> data) {
   return Expense(
      data['id'],
      data['amount'],
      DateTime.parse(data['date']),
      data['category']
   );
}
  • toMap -経費オブジェクトをDart Mapに変換するために使用され、データベースプログラミングでさらに使用できます。
Map<String, dynamic> toMap() => {
   "id" : id,
   "amount" : amount,
   "date" : date.toString(),
   "category" : category,
};
  • columns -データベースフィールドを表すために使用される静的変数。

次のコードを入力してExpense.dartファイルに保存します。

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,
   };
}

上記のコードは単純で自明です。

新しいファイルDatabase.dartを追加して、SQLiteDbProviderクラスを作成します。 SQLiteDbProviderクラスの目的は次のとおりです-

  • getAllExpensesメソッドを使用して、データベースで利用可能なすべての費用を取得します。 すべてのユーザーの費用情報をリストするために使用されます。
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;
}
  • getExpenseByIdメソッドを使用して、データベースで使用可能な経費IDに基づいて特定の経費情報を取得します。 特定の経費情報をユーザーに表示するために使用されます。
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;
}
  • getTotalExpenseメソッドを使用して、ユーザーの総費用を取得します。 現在の総費用をユーザーに示すために使用されます。
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;
}
  • insertメソッドを使用して、新しい経費情報をデータベースに追加します。 ユーザーが新しい経費エントリをアプリケーションに追加するために使用されます。
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メソッドを使用して、既存の経費情報を削除します。 ユーザーがシステムで使用可能な既存の経費エントリを削除するために使用されます。
delete(int id) async {
   final db = await database;
   db.delete("Expense", where: "id = ?", whereArgs: [id]);
}
  • SQLiteDbProviderクラスの完全なコードは次のとおりです-
import 'dart:async';
import 'dart:io';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import 'Expense.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"]
            );
           /*await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image')
               values (?, ?, ?, ?, ?)", [
                  2, "Pixel", "Pixel is the most feature phone ever", 800, "pixel.png"
               ]
            );
            await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image')
               values (?, ?, ?, ?, ?)", [
                  3, "Laptop", "Laptop is most productive development tool", 2000, "laptop.png"
               ]
            );
            await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image')
               values (?, ?, ?, ?, ?)", [
                  4, "Tablet", "Laptop is most productive development tool", 1500, "tablet.png"
               ]
            );
            await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image')
               values (?, ?, ?, ?, ?)", [
                  5, "Pendrive", "iPhone is the stylist phone ever", 100, "pendrive.png"
               ]
            );
            await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image')
               values (?, ?, ?, ?, ?)", [
                  6, "Floppy Drive", "iPhone is the stylist phone ever", 20, "floppy.png"
               ]
            ); */
         }
      );
   }
   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]);
   }
}

ここに、

  • データベースは、SQLiteDbProviderオブジェクトを取得するプロパティです。
  • initDBは、SQLiteデータベースを選択して開くために使用されるメソッドです。

新しいファイルExpenseListModel.dartを作成して、ExpenseListModelを作成します。 モデルの目的は、ユーザーの費用の完全な情報をメモリに保持し、ユーザーの費用がメモリで変更されるたびにアプリケーションのユーザーインターフェイスを更新することです。 scoped_modelパッケージのModelクラスに基づいています。 次のプロパティとメソッドがあります-

  • _items-費用のプライベートリスト。
  • items-リストへの予期しないまたは偶発的な変更を防ぐためのUnmodifiableListView <Expense>としての_itemsのゲッター。
  • totalExpense-アイテム変数に基づいた総費用のゲッター。
double get totalExpense {
   double amount = 0.0;
   for(var i = 0; i < _items.length; i++) {
      amount = amount + _items[i].amount;
   }
   return amount;
}
  • load-経費をデータベースから_items変数にロードするために使用されます。 また、UIを更新するためにnotifyListenersを呼び出します。
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();
   });
}
  • byId-_items変数から特定の費用を取得するために使用されます。
Expense byId(int id) {
   for(var i = 0; i < _items.length; i++) {
      if(_items[i].id == id) {
         return _items[i];
      }
   }
   return null;
}
  • add-新しい経費項目を_items変数とデータベースに追加するために使用します。 また、UIを更新するためにnotifyListenersを呼び出します。
void add(Expense item) {
   SQLiteDbProvider.db.insert(item).then((val) {
      _items.add(val); notifyListeners();
   });
}
  • 更新-経費項目を_items変数およびデータベースに更新するために使用されます。 また、UIを更新するために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();
}
  • delete-データベースからだけでなく、_items変数内の既存の経費項目を削除するために使用されます。 また、UIを更新するために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();
}
  • ExpenseListModelクラスの完全なコードは次のとおりです-
import 'dart:collection';
import 'package:scoped_model/scoped_model.dart';
import 'Expense.dart';
import 'Database.dart';

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

  /*Future<double> 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();
   }
}
  • main.dartファイルを開きます。 以下に指定されているようにクラスをインポートします-
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'ExpenseListModel.dart';
import 'Expense.dart';
  • メイン関数を追加し、ScopedModel <ExpenseListModel>ウィジェットを渡してrunAppを呼び出します。
void main() {
   final expenses = ExpenseListModel();
   runApp(
      ScopedModel<ExpenseListModel>(model: expenses, child: MyApp(),)
   );
}

ここに、

  • 経費オブジェクトは、データベースからすべてのユーザー経費情報をロードします。 また、アプリケーションを初めて開いたときに、適切なテーブルを使用して必要なデータベースが作成されます。
  • ScopedModelは、アプリケーションのライフサイクル全体で費用情報を提供し、どのインスタンスでもアプリケーションの状態を維持します。 これにより、StatefulWidgetの代わりにStatelessWidgetを使用できます。

MaterialAppウィジェットを使用して簡単な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.blue,
         ),
         home: MyHomePage(title: 'Expense calculator'),
      );
   }
}
  • MyHomePageウィジェットを作成して、すべてのユーザーの費用情報と合計費用を上部に表示します。 右下隅のフローティングボタンは、新しい費用を追加するために使用されます。
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.italic),))
                        );
                     }
                  },
                  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, );
                           }
                        )
                     )
                  );
                 //expenses.add(new Expense(
                    //2, 1000, DateTime.parse('2019-04-01 11:00:00'), 'Food')
                  );
                 //print(expenses.items.length);
               },
               tooltip: 'Increment', child: Icon(Icons.add), );
            }
         )
      );
   }
}

ここに、

  • ScopedModelDescendantは、費用モデルをListViewおよびFloatingActionButtonウィジェットに渡すために使用されます。
  • ListView.separatedおよびListTileウィジェットは、経費情報をリストするために使用されます。
  • Dismissibleウィジェットは、スワイプジェスチャーを使用して経費エントリを削除するために使用されます。
  • ナビゲータは、経費エントリの編集インターフェイスを開くために使用されます。 経費入力をタップすることで有効にできます。

FormPageウィジェットを作成します。 FormPageウィジェットの目的は、経費エントリを追加または更新することです。 経費入力の検証も処理します。

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',
                           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'),
                     ),
                  ],
               ),
            ),
         ),
      );
   }
}

ここに、

  • TextFormFieldは、フォームエントリの作成に使用されます。
  • TextFormFieldのvalidatorプロパティは、RegExパターンとともにフォーム要素を検証するために使用されます。
  • _submit関数は、経費オブジェクトとともに使用して、経費をデータベースに追加または更新します。
  • main.dartファイルの完全なコードは次のとおりです-
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'ExpenseListModel.dart';
import 'Expense.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.blue,
         ),
         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.italic),))
                        );
                     }
                  },
                  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, );
                              }
                           )
                        )
                     );
                    //expenses.add(
                        new Expense(
                          //2, 1000, DateTime.parse('2019-04-01 11:00:00'), 'Food'
                        )
                     );
                    //print(expenses.items.length);
                  },
                  tooltip: 'Increment', child: Icon(Icons.add),
               );
            }
         )
      );
   }
}
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',
                           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'),
                     ),
                  ],
               ),
            ),
         ),
      );
   }
}
  • 次に、アプリケーションを実行します。
  • フローティングボタンを使用して新しい費用を追加します。
  • 経費入力をタップして、既存の経費を編集します。
  • 経費エントリをいずれかの方向にスワイプして、既存の経費を削除します。

アプリケーションのスクリーンショットのいくつかは次のとおりです-

経費計算ツール

経費詳細の入力

総費用