Category: 1. Tutorial

https://cdn3d.iconscout.com/3d/premium/thumb/blog-writing-3d-icon-download-in-png-blend-fbx-gltf-file-formats–creative-content-article-marketing-pack-business-icons-6578772.png?f=webp

  • Conclusion

    Flutter framework does a great job by providing an excellent framework to build mobile applications in a truly platform independent way. By providing simplicity in the development process, high performance in the resulting mobile application, rich and relevant user interface for both Android and iOS platform, Flutter framework will surely enable a lot of new developers to develop high performance and feature-full mobile application in the near future.

  • Writting Advanced Applications

    In this chapter, we are going to learn how to write a full fledged mobile application, expense_calculator. The purpose of the expense_calculator is to store our expense information. The complete feature of the application is as follows −

    • Expense list.
    • Form to enter new expenses.
    • Option to edit / delete the existing expenses.
    • Total expenses at any instance.

    We are going to program the expense_calculator application using below mentioned advanced features of Flutter framework.

    • Advanced use of ListView to show the expense list.
    • Form programming.
    • SQLite database programming to store our expenses.
    • scoped_model state management to simplify our programming.

    Let us start programming the expense_calculator application.

    • Create a new Flutter application, expense_calculator in Android studio.
    • Open pubspec.yaml and add package dependencies.
    dependencies: 
       flutter: 
    
      sdk: flutter 
    sqflite: ^1.1.0 path_provider: ^0.5.0+1 scoped_model: ^1.0.1 intl: any
    • Observe these points here −
      • sqflite is used for SQLite database programming.
      • path_provider is used to get system specific application path.
      • scoped_model is used for state management.
      • intl is used for date formatting.
    • Android studio will display the following alert that the pubspec.yaml is updated.
    Alert Writing Advanced Applications
    • Click Get dependencies option. Android studio will get the package from Internet and properly configure it for the application.
    • Remove the existing code in main.dart.
    • Add new file, Expense.dart to create Expense class. Expense class will have the below properties and methods.
      • 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 the area in which the amount is spent. e.g Food, Travel, etc.,
      • formattedDate − Used to format the date property
      • fromMap − Used to map the field from database table to the property in the expense object and to create a new expense object.
    factory Expense.fromMap(Map<String, dynamic> data) { 
       return Expense( 
    
      data&#91;'id'], 
      data&#91;'amount'], 
      DateTime.parse(data&#91;'date']),    
      data&#91;'category'] 
    ); }
    • toMap − Used to convert the expense object to Dart Map, which can be further used in database programming
    Map<String, dynamic> toMap() => { 
       "id" : id, 
       "amount" : amount, 
       "date" : date.toString(), 
       "category" : category, 
    };
    • columns − Static variable used to represent the database field.
    • Enter and save the following code into the Expense.dart file.
    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&#91;'id'], 
         data&#91;'amount'], 
         DateTime.parse(data&#91;'date']), data&#91;'category'] 
      ); 
    } Map<String, dynamic> toMap() => {
      "id" : id, 
      "amount" : amount, 
      "date" : date.toString(), 
      "category" : category, 
    }; }
    • The above code is simple and self explanatory.
    • Add new file, Database.dart to create SQLiteDbProvider class. The purpose of the SQLiteDbProvider class is as follows −
      • Get all expenses available in the database using getAllExpenses method. It will be used to list all the user’s expense information.
    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; }
    • Get a specific expense information based on expense identity available in the database using getExpenseById method. It will be used to show the particular expense information to the user.
    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; 
    }
    • Get the total expenses of the user using getTotalExpense method. It will be used to show the current total expense to the user.
    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; }
    • Add new expense information into the database using insert method. It will be used to add new expense entry into the application by the user.
    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 (?, ?, ?, ?)", &#91;
         id, expense.amount, expense.date.toString(), expense.category
      ]
    ); return Expense(id, expense.amount, expense.date, expense.category); }
    • Update existing expense information using update method. It will be used to edit and update existing expense entry available in the system by the user.
    update(Expense product) async {
       final db = await database; 
       
       var result = await db.update("Expense", product.toMap(), 
       where: "id = ?", whereArgs: [product.id]); 
       return result; 
    }
    • Delete existing expense information using delete method. It will be used remove the existing expense entry available in the system by the user.
    delete(int id) async {
       final db = await database;
       db.delete("Expense", where: "id = ?", whereArgs: [id]); 
    }
    • The complete code of the SQLiteDbProvider class is as follows −
    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 (?, ?, ?, ?)",&#91;1, 1000, '2019-04-01 10:00:00', "Food"]
            );
            /*await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", &#91;
                  2, "Pixel", "Pixel is the most feature phone ever", 800, "pixel.png"
               ]
            ); 
            await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", &#91;
                  3, "Laptop", "Laptop is most productive development tool", 2000, "laptop.png"
               ]
            );
            await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", &#91;
                  4, "Tablet", "Laptop is most productive development tool", 1500, "tablet.png"
               ]
            );
            await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", &#91;
                  5, "Pendrive", "iPhone is the stylist phone ever", 100, "pendrive.png"
               ]
            ); 
            await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", &#91;
                  6, "Floppy Drive", "iPhone is the stylist phone ever", 20, "floppy.png"
               ]
            ); */ 
         }
      );
    } Future<List<Expense>> getAllExpenses() async {
      final db = await database; 
      List&lt;Map&gt; 
      results = await db.query(
         "Expense", columns: Expense.columns, orderBy: "date DESC"
      );
      List&lt;Expense&gt; 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: &#91;id]); 
      return result.isNotEmpty ? Expense.fromMap(result.first) : Null; 
    } Future<double> getTotalExpense() async {
      final db = await database;
      List&lt;Map&gt; list = await db.rawQuery(
         "Select SUM(amount) as amount from expense"
      );
      return list.isNotEmpty ? list&#91;0]&#91;"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&#91;"last_inserted_id"]; 
      var result = await db.rawInsert(
         "INSERT Into Expense (id, amount, date, category)" 
         " VALUES (?, ?, ?, ?)", &#91;
            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: &#91;product.id]
      ); 
      return result; 
    } delete(int id) async {
      final db = await database;
      db.delete("Expense", where: "id = ?", whereArgs: &#91;id]);
    } }
    • Here,
      • database is the property to get the SQLiteDbProvider object.
      • initDB is a method used to select and open the SQLite database.
    • Create a new file, ExpenseListModel.dart to create ExpenseListModel. The purpose of the model is to hold the complete information of the user expenses in the memory and updating the user interface of the application whenever user’s expense changes in the memory. It is based on Model class from scoped_model package. It has the following properties and methods −
      • _items − private list of expenses.
      • items − getter for _items as UnmodifiableListView<Expense> to prevent unexpected or accidental changes to the list.
      • totalExpense − getter for Total expenses based on the items variable.
    double get totalExpense {
       double amount = 0.0; 
       for(var i = 0; i < _items.length; i++) { 
    
      amount = amount + _items&#91;i].amount; 
    } return amount; }
    • load − Used to load the complete expenses from database and into the _items variable. It also calls notifyListeners to update the UI.
    void load() {
       Future<List<Expense>> 
       list = SQLiteDbProvider.db.getAllExpenses(); 
       list.then( (dbItems) {
    
      for(var i = 0; i &lt; dbItems.length; i++) { 
         _items.add(dbItems&#91;i]); 
      } notifyListeners(); 
    }); }
    • byId − Used to get a particular expenses from _items variable.
    Expense byId(int id) { 
       for(var i = 0; i < _items.length; i++) { 
    
      if(_items&#91;i].id == id) { 
         return _items&#91;i]; 
      } 
    } return null; }
    • add − Used to add a new expense item into the _items variable as well as into the database. It also calls notifyListeners to update the UI.
    void add(Expense item) {
       SQLiteDbProvider.db.insert(item).then((val) { 
    
      _items.add(val); notifyListeners(); 
    }); }
    • Update − Used to Update expense item into the _items variable as well as into the database. It also calls notifyListeners to update the UI.
    void update(Expense item) {
       bool found = false;
       for(var i = 0; i < _items.length; i++) {
    
      if(_items&#91;i].id == item.id) {
         _items&#91;i] = item; 
         found = true; 
         SQLiteDbProvider.db.update(item); break; 
      } 
    } if(found) notifyListeners(); }
    • delete − Used to remove an existing expense item in the _items variable as well as from the database. It also calls notifyListeners to update the UI.
    void delete(Expense item) { 
       bool found = false; 
       for(var i = 0; i < _items.length; i++) {
    
      if(_items&#91;i].id == item.id) {
         found = true; 
         SQLiteDbProvider.db.delete(item.id); 
         _items.removeAt(i); break; 
      }
    } if(found) notifyListeners(); }
    • The complete code of the ExpenseListModel class is as follows −
    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 &lt; _items.length; i++) { 
         amount = amount + _items&#91;i].amount; 
      } 
      return amount; 
    } void load() {
      Future&lt;List&lt;Expense&gt;&gt; list = SQLiteDbProvider.db.getAllExpenses(); 
      list.then( (dbItems) {
         for(var i = 0; i &lt; dbItems.length; i++) {
            _items.add(dbItems&#91;i]); 
         } 
         notifyListeners(); 
      }); 
    } Expense byId(int id) {
      for(var i = 0; i &lt; _items.length; i++) { 
         if(_items&#91;i].id == id) { 
            return _items&#91;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 &lt; _items.length; i++) {
         if(_items&#91;i].id == item.id) {
            _items&#91;i] = item; 
            found = true; 
            SQLiteDbProvider.db.update(item); 
            break; 
         }
      }
      if(found) notifyListeners(); 
    } void delete(Expense item) {
      bool found = false; 
      for(var i = 0; i &lt; _items.length; i++) {
         if(_items&#91;i].id == item.id) {
            found = true; 
            SQLiteDbProvider.db.delete(item.id); 
            _items.removeAt(i); break; 
         }
      }
      if(found) notifyListeners(); 
    } }
    • Open main.dart file. Import the classes as specified below −
    import 'package:flutter/material.dart'; 
    import 'package:scoped_model/scoped_model.dart'; 
    import 'ExpenseListModel.dart'; 
    import 'Expense.dart';
    
    • Add main function and call runApp by passing ScopedModel<ExpenseListModel> widget.
    void main() { 
       final expenses = ExpenseListModel(); 
       runApp(
    
      ScopedModel&lt;ExpenseListModel&gt;(model: expenses, child: MyApp(),)
    ); }
    • Here,
      • expenses object loads all the user expenses information from the database. Also, when the application is opened for the first time, it will create the required database with proper tables.
      • ScopedModel provides the expense information during the whole life cycle of the application and ensures the maintenance of state of the application at any instance. It enables us to use StatelessWidget instead of StatefulWidget.
    • Create a simple MyApp using MaterialApp widget.
    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'), 
      );
    } }
    • Create MyHomePage widget to display all the user’s expense information along with total expenses at the top. Floating button at the bottom right corner will be used to add new expenses.
    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&lt;ExpenseListModel&gt;(
            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&#91;index].id.toString()), 
                              onDismissed: (direction) { 
                              expenses.delete(expenses.items&#91;index]); 
                              Scaffold.of(context).showSnackBar(
                                 SnackBar(
                                    content: Text(
                                       "Item with id, " 
                                       + expenses.items&#91;index].id.toString() + 
                                       " is dismissed"
                                    )
                                 )
                              ); 
                           },
                           child: ListTile( onTap: () { 
                              Navigator.push(
                                 context, MaterialPageRoute(
                                    builder: (context) =&gt; FormPage(
                                       id: expenses.items&#91;index].id,
                                       expenses: expenses, 
                                    )
                                 )
                              );
                           }, 
                           leading: Icon(Icons.monetization_on), 
                           trailing: Icon(Icons.keyboard_arrow_right), 
                           title: Text(expenses.items&#91;index].category + ": " + 
                           expenses.items&#91;index].amount.toString() + 
                           " \nspent on " + expenses.items&#91;index].formattedDate, 
                           style: TextStyle(fontSize: 18, fontStyle: FontStyle.italic),))
                        ); 
                     }
                  },
                  separatorBuilder: (context, index) { 
                     return Divider(); 
                  }, 
               );
            },
         ),
         floatingActionButton: ScopedModelDescendant&lt;ExpenseListModel&gt;(
            builder: (context, child, expenses) {
               return FloatingActionButton( onPressed: () {
                  Navigator.push( 
                     context, MaterialPageRoute(
                        builder: (context) =&gt; ScopedModelDescendant&lt;ExpenseListModel&gt;(
                           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), ); 
            }
         )
      );
    } }
    • Here,
      • ScopedModelDescendant is used to pass the expense model into the ListView and FloatingActionButton widget.
      • ListView.separated and ListTile widget is used to list the expense information.
      • Dismissible widget is used to delete the expense entry using swipe gesture.
      • Navigator is used to open edit interface of an expense entry. It can be activated by tapping an expense entry.
    • Create a FormPage widget. The purpose of the FormPage widget is to add or update an expense entry. It handles expense entry validation as well.
    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: &#91;
                     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'^&#91;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) =&gt; _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)&#91;- /.]
                              (0&#91;1-9]|1&#91;012])&#91;- /.](0&#91;1-9]|&#91;12]&#91;0-9]|3&#91;01])$'; 
                           RegExp regex = new RegExp(pattern); 
                           if (!regex.hasMatch(val)) 
                              return 'Enter a valid date'; 
                           else return null; 
                        },
                        onSaved: (val) =&gt; _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) =&gt; _category = val, 
                        initialValue: id == 0 ? '' 
                        : expenses.byId(id).category.toString(),
                     ), 
                     RaisedButton( 
                        onPressed: _submit, 
                        child: new Text('Submit'), 
                     ), 
                  ],
               ),
            ),
         ),
      );
    } }
    • Here,
      • TextFormField is used to create form entry.
      • validator property of TextFormField is used to validate the form element along with RegEx patterns.
      • _submit function is used along with expenses object to add or update the expenses into the database.
    • The complete code of the main.dart file is as follows −
    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&lt;ExpenseListModel&gt;(
         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&lt;ExpenseListModel&gt;(
            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&#91;index].id.toString()), 
                           onDismissed: (direction) {
                              expenses.delete(expenses.items&#91;index]); 
                              Scaffold.of(context).showSnackBar(
                                 SnackBar(
                                    content: Text(
                                       "Item with id, " + 
                                       expenses.items&#91;index].id.toString() 
                                       + " is dismissed"
                                    )
                                 )
                              );
                           }, 
                           child: ListTile( onTap: () {
                              Navigator.push( context, MaterialPageRoute(
                                 builder: (context) =&gt; FormPage(
                                    id: expenses.items&#91;index].id, expenses: expenses, 
                                 )
                              ));
                           }, 
                           leading: Icon(Icons.monetization_on), 
                           trailing: Icon(Icons.keyboard_arrow_right), 
                           title: Text(expenses.items&#91;index].category + ": " + 
                           expenses.items&#91;index].amount.toString() + " \nspent on " + 
                           expenses.items&#91;index].formattedDate, 
                           style: TextStyle(fontSize: 18, fontStyle: FontStyle.italic),))
                        );
                     }
                  }, 
                  separatorBuilder: (context, index) {
                     return Divider(); 
                  },
               ); 
            },
         ),
         floatingActionButton: ScopedModelDescendant&lt;ExpenseListModel&gt;(
            builder: (context, child, expenses) {
               return FloatingActionButton(
                  onPressed: () {
                     Navigator.push(
                        context, MaterialPageRoute(
                           builder: (context)
                           =&gt; ScopedModelDescendant&lt;ExpenseListModel&gt;(
                              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: &#91;
                     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'^&#91;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) =&gt; _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)&#91;- /.]
                           (0&#91;1-9]|1&#91;012])&#91;- /.](0&#91;1-9]|&#91;12]&#91;0-9]|3&#91;01])$'; 
                           RegExp regex = new RegExp(pattern); 
                           if (!regex.hasMatch(val)) return 'Enter a valid date'; 
                           else return null; 
                        },
                        onSaved: (val) =&gt; _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) =&gt; _category = val, 
                        initialValue: id == 0 ? '' : expenses.byId(id).category.toString(), 
                     ),
                     RaisedButton(
                        onPressed: _submit, 
                        child: new Text('Submit'), 
                     ),
                  ],
               ),
            ),
         ),
      );
    } }
    • Now, run the application.
    • Add new expenses using floating button.
    • Edit existing expenses by tapping the expense entry.
    • Delete the existing expenses by swiping the expense entry in either direction.

    Some of the screen shots of the application are as follows −

    Expense Calculator
    Enter Expense Details
    Total Expenses
  • Deployment

    This chapter explains how to deploy Flutter application in both Android and iOS platforms.

    Android Application

    • Change the application name using android:label entry in android manifest file. Android app manifest file, AndroidManifest.xml is located in <app dir>/android/app/src/main. It contains entire details about an android application. We can set the application name using android:label entry.
    • Change launcher icon using android:icon entry in manifest file.
    • Sign the app using standard option as necessary.
    • Enable Proguard and Obfuscation using standard option, if necessary.
    • Create a release APK file by running below command −
    cd /path/to/my/application 
    flutter build apk
    
    • You can see an output as shown below −
    Initializing gradle...                                            8.6s 
    Resolving dependencies...                                        19.9s 
    Calling mockable JAR artifact transform to create file: 
    /Users/.gradle/caches/transforms-1/files-1.1/android.jar/ 
    c30932f130afbf3fd90c131ef9069a0b/android.jar with input 
    /Users/Library/Android/sdk/platforms/android-28/android.jar 
    Running Gradle task 'assembleRelease'... 
    Running Gradle task 'assembleRelease'... 
    Done                                                             85.7s 
    Built build/app/outputs/apk/release/app-release.apk (4.8MB).
    
    • Install the APK on a device using the following command −
    flutter install
    
    • Publish the application into Google Playstore by creating an appbundle and push it into playstore using standard methods.
    flutter build appbundle
    

    iOS Application

    • Register the iOS application in App Store Connect using standard method. Save the =Bundle ID used while registering the application.
    • Update Display name in the XCode project setting to set the application name.
    • Update Bundle Identifier in the XCode project setting to set the bundle id, which we used in step 1.
    • Code sign as necessary using standard method.
    • Add a new app icon as necessary using standard method.
    • Generate IPA file using the following command −
    flutter build ios
    
    • Now, you can see the following output −
    Building com.example.MyApp for device (ios-release)... 
    Automatically signing iOS for device deployment 
    using specified development team in Xcode project: 
    Running Xcode build...                                   23.5s 
    ......................
    
    • Test the application by pushing the application, IPA file into TestFlight using standard method.
    • Finally, push the application into App Store using standard method.
  • Testing

    Testing is very important phase in the development life cycle of an application. It ensures that the application is of high quality. Testing requires careful planning and execution. It is also the most time consuming phase of the development.

    Dart language and Flutter framework provides extensive support for the automated testing of an application.

    Types of Testing

    Generally, three types of testing processes are available to completely test an application. They are as follows −

    Unit Testing

    Unit testing is the easiest method to test an application. It is based on ensuring the correctness of a piece of code (a function, in general) o a method of a class. But, it does not reflect the real environment and subsequently, is the least option to find the bugs.

    Widget Testing

    Widget testing is based on ensuring the correctness of the widget creation, rendering and interaction with other widgets as expected. It goes one step further and provides near real-time environment to find more bugs.

    Integration Testing

    Integration testing involves both unit testing and widget testing along with external component of the application like database, web service, etc., It simulates or mocks the real environment to find nearly all bugs, but it is the most complicated process.

    Flutter provides support for all types of testing. It provides extensive and exclusive support for Widget testing. In this chapter, we will discuss widget testing in detail.

    Widget Testing

    Flutter testing framework provides testWidgets method to test widgets. It accepts two arguments −

    • Test description
    • Test code
    testWidgets('test description: find a widget', '<test code>');
    

    Explore our latest online courses and learn new skills at your own pace. Enroll and become a certified expert to boost your career.

    Steps Involved

    Widget Testing involves three distinct steps −

    • Render the widget in the testing environment.
    • WidgetTester is the class provided by Flutter testing framework to build and renders the widget. pumpWidget method of the WidgetTester class accepts any widget and renders it in the testing environment.
    testWidgets('finds a specific instance', (WidgetTester tester) async { 
       await tester.pumpWidget(MaterialApp( 
    
      home: Scaffold( 
         body: Text('Hello'), 
      ), 
    )); });
    • Finding the widget, which we need to test.
      • Flutter framework provides many options to find the widgets rendered in the testing environment and they are generally called Finders. The most frequently used finders are find.text, find.byKey and find.byWidget.
        • find.text finds the widget that contains the specified text.
    find.text('Hello')
    
    • find.byKey find the widget by its specific key.
    find.byKey('home')
    
    • find.byWidget find the widget by its instance variable.
    find.byWidget(homeWidget)
    
    • Ensuring the widget works as expected.
    • Flutter framework provides many options to match the widget with the expected widget and they are normally called Matchers. We can use the expect method provided by the testing framework to match the widget, which we found in the second step with our our expected widget by choosing any of the matchers. Some of the important matchers are as follows.
      • findsOneWidget − verifies a single widget is found.
    expect(find.text('Hello'), findsOneWidget);
    
    • findsNothing − verifies no widgets are found
    expect(find.text('Hello World'), findsNothing);
    
    • findsWidgets − verifies more than a single widget is found.
    expect(find.text('Save'), findsWidgets);
    
    • findsNWidgets − verifies N number of widgets are found.
    expect(find.text('Save'), findsNWidgets(2));
    

    The complete test code is as follows −

    testWidgets('finds hello widget', (WidgetTester tester) async { 
       await tester.pumpWidget(MaterialApp( 
    
      home: Scaffold( 
         body: Text('Hello'), 
      ), 
    )); expect(find.text('Hello'), findsOneWidget); });

    Here, we rendered a MaterialApp widget with text Hello using Text widget in its body. Then, we used find.text to find the widget and then matched it using findsOneWidget.

    Working Example

    Let us create a simple flutter application and write a widget test to understand better the steps involved and the concept.

    • Create a new flutter application, flutter_test_app in Android studio.
    • Open widget_test.dart in test folder. It has a sample testing code as given below −
    testWidgets('Counter increments smoke test', (WidgetTester tester) async {
       // Build our app and trigger a frame. 
       await tester.pumpWidget(MyApp()); 
       
       // Verify that our counter starts at 0. 
       expect(find.text('0'), findsOneWidget); 
       expect(find.text('1'), findsNothing); 
       
       // Tap the '+' icon and trigger a frame. 
       await tester.tap(find.byIcon(Icons.add)); 
       await tester.pump(); 
       
       // Verify that our counter has incremented. 
       expect(find.text('0'), findsNothing); 
       expect(find.text('1'), findsOneWidget); 
    });
    • Here, the test code does the following functionalities −
      • Renders MyApp widget using tester.pumpWidget.
      • Ensures that the counter is initially zero using findsOneWidget and findsNothing matchers.
      • Finds the counter increment button using find.byIcon method.
      • Taps the counter increment button using tester.tap method.
      • Ensures that the counter is increased using findsOneWidget and findsNothing matchers.
    • Let us again tap the counter increment button and then check whether the counter is increased to two.
    await tester.tap(find.byIcon(Icons.add)); 
    await tester.pump(); 
    
    expect(find.text('2'), findsOneWidget);
    
    • Click Run menu.
    • Click tests in widget_test.dart option. This will run the test and report the result in the result window.
    Flutter Testing
  • Internationalization

    Nowadays, mobile applications are used by customers from different countries and as a result, applications are required to display the content in different languages. Enabling an application to work in multiple languages is called Internationalizing the application.

    For an application to work in different languages, it should first find the current locale of the system in which the application is running and then need to show it’s content in that particular locale, and this process is called Localization.

    Flutter framework provides three base classes for localization and extensive utility classes derived from base classes to localize an application.

    The base classes are as follows −

    • Locale − Locale is a class used to identify the user’s language. For example, en-us identifies the American English and it can be created as.
    Locale en_locale = Locale('en', 'US')
    

    Here, the first argument is language code and the second argument is country code. Another example of creating Argentina Spanish (es-ar) locale is as follows −

    Locale es_locale = Locale('es', 'AR')
    
    • Localizations − Localizations is a generic widget used to set the Locale and the localized resources of its child.
    class CustomLocalizations { 
       CustomLocalizations(this.locale); 
       final Locale locale; 
       static CustomLocalizations of(BuildContext context) { 
    
      return Localizations.of&lt;CustomLocalizations&gt;(context, CustomLocalizations); 
    } static Map<String, Map<String, String>> _resources = {
      'en': {
         'title': 'Demo', 
         'message': 'Hello World' 
      }, 
      'es': {
         'title': 'Manifestación', 
         'message': 'Hola Mundo', 
      }, 
    }; String get title {
      return _resources&#91;locale.languageCode]&#91;'title']; 
    } String get message {
      return _resources&#91;locale.languageCode]&#91;'message']; 
    } }
    • Here, CustomLocalizations is a new custom class created specifically to get certain localized content (title and message) for the widget. of method uses the Localizations class to return new CustomLocalizations class.
    • LocalizationsDelegate − LocalizationsDelegate is a factory class through which Localizations widget is loaded. It has three over-ridable methods −
      • isSupported − Accepts a locale and return whether the specified locale is supported or not.
    @override 
    bool isSupported(Locale locale) => ['en', 'es'].contains(locale.languageCode);
    
    • load − Accepts a locale and start loading the resources for the specified locale.
    @override 
    Future<CustomLocalizations> load(Locale locale) { 
       return SynchronousFuture<CustomLocalizations>(CustomLocalizations(locale)); 
    }
    
    • shouldReload − Specifies whether reloading of CustomLocalizations is necessary when its Localizations widget is rebuild.
    @override 
    bool shouldReload(CustomLocalizationsDelegate old) => false;
    
    • The complete code of CustomLocalizationDelegate is as follows −
    class CustomLocalizationsDelegate extends 
    LocalizationsDelegate<CustomLocalizations> { 
       const CustomLocalizationsDelegate(); 
       @override 
       bool isSupported(Locale locale) => ['en', 'es'].contains(locale.languageCode);
       @override 
       Future<CustomLocalizations> load(Locale locale) { 
    
      return SynchronousFuture&lt;CustomLocalizations&gt;(CustomLocalizations(locale));
    } @override bool shouldReload(CustomLocalizationsDelegate old) => false; }

    In general, Flutter applications are based on two root level widgets, MaterialApp or WidgetsApp. Flutter provides ready made localization for both widgets and they are MaterialLocalizations and WidgetsLocaliations. Further, Flutter also provides delegates to load MaterialLocalizations and WidgetsLocaliations and they are GlobalMaterialLocalizations.delegate and GlobalWidgetsLocalizations.delegate respectively.

    Let us create a simple internationalization enabled application to test and understand the concept.

    • Create a new flutter application, flutter_localization_app.
    • Flutter supports the internationalization using exclusive flutter package, flutter_localizations. The idea is to separate the localized content from the main SDK. Open the pubspec.yaml and add below code to enable the internationalization package −
    dependencies: 
       flutter: 
    
      sdk: flutter 
    flutter_localizations:
      sdk: flutter
    • Android studio will display the following alert that the pubspec.yaml is updated.
    Alert
    • Click Get dependencies option. Android studio will get the package from Internet and properly configure it for the application.
    • Import flutter_localizations package in the main.dart as follows −
    import 'package:flutter_localizations/flutter_localizations.dart'; 
    import 'package:flutter/foundation.dart' show SynchronousFuture;
    
    • Here, the purpose of SynchronousFuture is to load the custom localizations synchronously.
    • Create a custom localizations and its corresponding delegate as specified below −
    class CustomLocalizations { 
       CustomLocalizations(this.locale); 
       final Locale locale; 
       static CustomLocalizations of(BuildContext context) {
    
      return Localizations.of&lt;CustomLocalizations&gt;(context, CustomLocalizations); 
    } static Map<String, Map<String, String>> _resources = {
      'en': {
         'title': 'Demo', 
         'message': 'Hello World' 
      }, 
      'es': { 
         'title': 'Manifestación', 
         'message': 'Hola Mundo', 
      }, 
    }; String get title {
      return _resources&#91;locale.languageCode]&#91;'title']; 
    } String get message {
      return _resources&#91;locale.languageCode]&#91;'message']; 
    } } class CustomLocalizationsDelegate extends LocalizationsDelegate<CustomLocalizations> { const CustomLocalizationsDelegate(); @override bool isSupported(Locale locale) => ['en', 'es'].contains(locale.languageCode); @override Future<CustomLocalizations> load(Locale locale) {
      return SynchronousFuture&lt;CustomLocalizations&gt;(CustomLocalizations(locale)); 
    } @override bool shouldReload(CustomLocalizationsDelegate old) => false; }
    • Here, CustomLocalizations is created to support localization for title and message in the application and CustomLocalizationsDelegate is used to load CustomLocalizations.
    • Add delegates for MaterialApp, WidgetsApp and CustomLocalization using MaterialApp properties, localizationsDelegates and supportedLocales as specified below −
    localizationsDelegates: [
       const CustomLocalizationsDelegate(),   
       GlobalMaterialLocalizations.delegate, 
       GlobalWidgetsLocalizations.delegate, 
    ], 
    supportedLocales: [
       const Locale('en', ''),
       const Locale('es', ''), 
    ],
    • Use CustomLocalizations method, of to get the localized value of title and message and use it in appropriate place as specified below −
    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(CustomLocalizations .of(context) .title), ), 
         body: Center(
            child: Column(
               mainAxisAlignment: MainAxisAlignment.center, 
               children: &lt;Widget&gt;&#91; 
                  Text( CustomLocalizations .of(context) .message, ), 
               ], 
            ), 
         ),
      );
    } }
    • Here, we have modified the MyHomePage class from StatefulWidget to StatelessWidget for simplicity reason and used the CustomLocalizations to get title and message.
    • Compile and run the application. The application will show its content in English.
    • Close the application. Go to Settings → System → Languages and Input → Languages*.
    • Click Add a language option and select Spanish. This will install Spanish language and then list it as one of the option.
    • Select Spanish and move it above English. This will set as Spanish as first language and everything will be changed to Spanish text.
    • Now relaunch the internationalization application and you will see the title and message in Spanish language.
    • We can revert the language to English by move the English option above Spanish option in the setting.
    • The result of the application (in Spanish) is shown in the screenshot given below −
    Manifestacion

    Using intl Package

    Flutter provides intl package to further simplify the development of localized mobile application. intl package provides special methods and tools to semi-auto generate language specific messages.

    Let us create a new localized application by using intl package and understand the concept.

    • Create a new flutter application, flutter_intl_app.
    • Open pubspec.yaml and add the package details.
    dependencies: 
       flutter: 
    
      sdk: flutter 
    flutter_localizations:
      sdk: flutter 
    intl: ^0.15.7 intl_translation: ^0.17.3
    • Android studio will display the alert as shown below informing that the pubspec.yaml is updated.
    Informing Updation
    • Click Get dependencies option. Android studio will get the package from Internet and properly configure it for the application.
    • Copy the main.dart from previous sample, flutter_internationalization_app.
    • Import the intl pacakge as shown below −
    import 'package:intl/intl.dart';
    
    • Update the CustomLocalization class as shown in the code given below −
    class CustomLocalizations { 
       static Future<CustomLocalizations> load(Locale locale) {
    
      final String name = locale.countryCode.isEmpty ? locale.languageCode : locale.toString(); 
      final String localeName = Intl.canonicalizedLocale(name); 
      
      return initializeMessages(localeName).then((_) {
         Intl.defaultLocale = localeName; 
         return CustomLocalizations(); 
      }); 
    } static CustomLocalizations of(BuildContext context) {
      return Localizations.of&lt;CustomLocalizations&gt;(context, CustomLocalizations); 
    } String get title {
      return Intl.message( 
         'Demo', 
         name: 'title', 
         desc: 'Title for the Demo application', 
      ); 
    } String get message{
      return Intl.message(
         'Hello World', 
         name: 'message', 
         desc: 'Message for the Demo application', 
      ); 
    } } class CustomLocalizationsDelegate extends LocalizationsDelegate<CustomLocalizations> { const CustomLocalizationsDelegate(); @override bool isSupported(Locale locale) => ['en', 'es'].contains(locale.languageCode); @override Future<CustomLocalizations> load(Locale locale) {
      return CustomLocalizations.load(locale); 
    } @override bool shouldReload(CustomLocalizationsDelegate old) => false; }
    • Here, we have used three methods from the intl package instead of custom methods. Otherwise, the concepts are same.
      • Intl.canonicalizedLocale − Used to get correct locale name.
      • Intl.defaultLocale − Used to set current locale
      • Intl.message − Used to define new messages.
    • import l10n/messages_all.dart file. We will generate this file shortly
    import 'l10n/messages_all.dart';
    
    • Now, create a folder, lib/l10n
    • Open a command prompt and go to application root directory (where pubspec.yaml is available) and run the following command −
    flutter packages pub run intl_translation:extract_to_arb --output-
       dir=lib/l10n lib/main.dart
    
    • Here, the command will generate, intl_message.arb file, a template to create message in different locale. The content of the file is as follows −
    {
       "@@last_modified": "2019-04-19T02:04:09.627551", 
       "title": "Demo", 
       "@title": {
    
      "description": "Title for the Demo application", 
      "type": "text", 
      "placeholders": {} 
    }, "message": "Hello World", "@message": {
      "description": "Message for the Demo 
      application", 
      "type": "text", 
      "placeholders": {} 
    } }
    • Copy intl_message.arb and create new file, intl_en.arb.
    • Copy intl_message.arb and create new file, intl_es.arb and change the content to Spanish language as shown below −
    {
       "@@last_modified": "2019-04-19T02:04:09.627551",  
       "title": "Manifestación", 
       "@title": {
    
      "description": "Title for the Demo application", 
      "type": "text", 
      "placeholders": {} 
    }, "message": "Hola Mundo", "@message": {
      "description": "Message for the Demo application", 
      "type": "text", 
      "placeholders": {} 
    } }
    • Now, run the following command to create final message file, messages_all.dart.
    flutter packages pub run intl_translation:generate_from_arb 
    --output-dir=lib\l10n --no-use-deferred-loading 
    lib\main.dart lib\l10n\intl_en.arb lib\l10n\intl_es.arb
    
    • Compile and run the application. It will work similar to above application, flutter_localization_app.
  • Database Concepts

    Flutter provides many advanced packages to work with databases. The most important packages are −

    • sqflite − Used to access and manipulate SQLite database, and
    • firebase_database − Used to access and manipulate cloud hosted NoSQL database from Google.

    In this chapter, let us discuss each of them in detail.

    SQLite

    SQLite database is the de-facto and standard SQL based embedded database engine. It is small and time-tested database engine. sqflite package provides a lot of functionality to work efficiently with SQLite database. It provides standard methods to manipulate SQLite database engine. The core functionality provided by sqflite package is as follows −

    • Create / Open (openDatabase method) a SQLite database.
    • Execute SQL statement (execute method) against SQLite database.
    • Advanced query methods (query method) to reduce to code required to query and get information from SQLite database.

    Let us create a product application to store and fetch product information from a standard SQLite database engine using sqflite package and understand the concept behind the SQLite database and sqflite package.

    • Create a new Flutter application in Android studio, product_sqlite_app.
    • Replace the default startup code (main.dart) with our product_rest_app code.
    • Copy the assets folder from product_nav_app to product_rest_app and add assets inside the *pubspec.yaml` file.
    flutter: 
       assets: 
    
      - assets/appimages/floppy.png 
      - assets/appimages/iphone.png 
      - assets/appimages/laptop.png 
      - assets/appimages/pendrive.png 
      - assets/appimages/pixel.png 
      - assets/appimages/tablet.png</code></pre>
    • Configure sqflite package in the pubspec.yaml file as shown below −
    dependencies: sqflite: any
    

    Use the latest version number of sqflite in place of any

    • Configure path_provider package in the pubspec.yaml file as shown below −
    dependencies: path_provider: any
    
    • Here, path_provider package is used to get temporary folder path of the system and path of the application. Use the latest version number of sqflite in place of any.
    • Android studio will alert that the pubspec.yaml is updated.
    Updated
    • Click Get dependencies option. Android studio will get the package from Internet and properly configure it for the application.
    • In database, we need primary key, id as additional field along with Product properties like name, price, etc., So, add id property in the Product class. Also, add a new method, toMap to convert product object into Map object. fromMap and toMap are used to serialize and de- serialize the Product object and it is used in database manipulation methods.
    class Product { 
       final int id; 
       final String name; 
       final String description; 
       final int price; 
       final String image; 
       static final columns = ["id", "name", "description", "price", "image"]; 
       Product(this.id, this.name, this.description, this.price, this.image); 
       factory Product.fromMap(Map<String, dynamic> data) {
    
      return Product( 
         data&#91;'id'], 
         data&#91;'name'], 
         data&#91;'description'], 
         data&#91;'price'], 
         data&#91;'image'], 
      ); 
    } Map<String, dynamic> toMap() => {
      "id": id, 
      "name": name, 
      "description": description, 
      "price": price, 
      "image": image 
    }; }
    • Create a new file, Database.dart in the lib folder to write SQLite related functionality.
    • Import necessary import statement in Database.dart.
    import 'dart:async'; 
    import 'dart:io'; 
    import 'package:path/path.dart'; 
    import 'package:path_provider/path_provider.dart'; 
    import 'package:sqflite/sqflite.dart'; 
    import 'Product.dart';
    • Note the following points here −
      • async is used to write asynchronous methods.
      • io is used to access files and directories.
      • path is used to access dart core utility function related to file paths.
      • path_provider is used to get temporary and application path.
      • sqflite is used to manipulate SQLite database.
    • Create a new class SQLiteDbProvider
    • Declare a singleton based, static SQLiteDbProvider object as specified below −
    class SQLiteDbProvider { 
       SQLiteDbProvider._(); 
       static final SQLiteDbProvider db = SQLiteDbProvider._(); 
       static Database _database; 
    }
    
    • SQLiteDBProvoider object and its method can be accessed through the static db variable.
    SQLiteDBProvoider.db.<emthod>
    
    • Create a method to get database (Future option) of type Future<Database>. Create product table and load initial data during the creation of the database itself.
    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, "ProductDB.db"); 
       return await openDatabase(
    
      path, 
      version: 1,
      onOpen: (db) {}, 
      onCreate: (Database db, int version) async {
         await db.execute(
            "CREATE TABLE Product ("
            "id INTEGER PRIMARY KEY,"
            "name TEXT,"
            "description TEXT,"
            "price INTEGER," 
            "image TEXT" ")"
         ); 
         await db.execute(
            "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
            values (?, ?, ?, ?, ?)", 
            &#91;1, "iPhone", "iPhone is the stylist phone ever", 1000, "iphone.png"]
         ); 
         await db.execute(
            "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
            values (?, ?, ?, ?, ?)", 
            &#91;2, "Pixel", "Pixel is the most feature phone ever", 800, "pixel.png"]
         ); 
         await db.execute(
            "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
            values (?, ?, ?, ?, ?)", 
            &#91;3, "Laptop", "Laptop is most productive development tool", 2000, "laptop.png"]\
         ); 
         await db.execute( 
            "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
            values (?, ?, ?, ?, ?)", 
            &#91;4, "Tablet", "Laptop is most productive development tool", 1500, "tablet.png"]
         );
         await db.execute( 
            "INSERT INTO Product 
            ('id', 'name', 'description', 'price', 'image') 
            values (?, ?, ?, ?, ?)", 
            &#91;5, "Pendrive", "Pendrive is useful storage medium", 100, "pendrive.png"]
         );
         await db.execute( 
            "INSERT INTO Product 
            ('id', 'name', 'description', 'price', 'image') 
            values (?, ?, ?, ?, ?)", 
            &#91;6, "Floppy Drive", "Floppy drive is useful rescue storage medium", 20, "floppy.png"]
         ); 
      }
    ); }
    • Here, we have used the following methods −
      • getApplicationDocumentsDirectory − Returns application directory path
      • join − Used to create system specific path. We have used it to create database path.
      • openDatabase − Used to open a SQLite database
      • onOpen − Used to write code while opening a database
      • onCreate − Used to write code while a database is created for the first time
      • db.execute − Used to execute SQL queries. It accepts a query. If the query has placeholder (?), then it accepts values as list in the second argument.
    • Write a method to get all products in the database −
    Future<List<Product>> getAllProducts() async { 
       final db = await database; 
       List<Map> 
       results = await db.query("Product", columns: Product.columns, orderBy: "id ASC"); 
       
       List<Product> products = new List(); 
       results.forEach((result) { 
    
      Product product = Product.fromMap(result); 
      products.add(product); 
    }); return products; }
    • Here, we have done the following −
      • Used query method to fetch all the product information. query provides shortcut to query a table information without writing the entire query. query method will generate the proper query itself by using our input like columns, orderBy, etc.,
      • Used Product’s fromMap method to get product details by looping the results object, which holds all the rows in the table.
    • Write a method to get product specific to id
    Future<Product> getProductById(int id) async {
       final db = await database; 
       var result = await db.query("Product", where: "id = ", whereArgs: [id]); 
       return result.isNotEmpty ? Product.fromMap(result.first) : Null; 
    }
    • Here, we have used where and whereArgs to apply filters.
    • Create three methods - insert, update and delete method to insert, update and delete product from the database.
    insert(Product product) async { 
       final db = await database; 
       var maxIdResult = await db.rawQuery(
    
      "SELECT MAX(id)+1 as last_inserted_id FROM Product");
    var id = maxIdResult.first["last_inserted_id"]; var result = await db.rawInsert(
      "INSERT Into Product (id, name, description, price, image)" 
      " VALUES (?, ?, ?, ?, ?)", 
      &#91;id, product.name, product.description, product.price, product.image] 
    ); return result; } update(Product product) async { final db = await database; var result = await db.update("Product", product.toMap(), where: "id = ?", whereArgs: [product.id]); return result; } delete(int id) async { final db = await database; db.delete("Product", where: "id = ?", whereArgs: [id]); }
    • The final code of the Database.dart is as follows −
    import 'dart:async'; 
    import 'dart:io'; 
    import 'package:path/path.dart'; 
    import 'package:path_provider/path_provider.dart'; 
    import 'package:sqflite/sqflite.dart'; 
    import 'Product.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, "ProductDB.db"); 
      return await openDatabase(
         path, version: 1, 
         onOpen: (db) {}, 
         onCreate: (Database db, int version) async {
            await db.execute(
               "CREATE TABLE Product (" 
               "id INTEGER PRIMARY KEY," 
               "name TEXT," 
               "description TEXT," 
               "price INTEGER," 
               "image TEXT"")"
            ); 
            await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", 
               &#91;1, "iPhone", "iPhone is the stylist phone ever", 1000, "iphone.png"]
            ); 
            await db.execute( 
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", 
               &#91;2, "Pixel", "Pixel is the most feature phone ever", 800, "pixel.png"]
            );
            await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", 
               &#91;3, "Laptop", "Laptop is most productive development tool", 2000, "laptop.png"]
            ); 
            await db.execute( 
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", 
               &#91;4, "Tablet", "Laptop is most productive development tool", 1500, "tablet.png"]
            ); 
            await db.execute( 
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", 
               &#91;5, "Pendrive", "Pendrive is useful storage medium", 100, "pendrive.png"]
            );
            await db.execute( 
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", 
               &#91;6, "Floppy Drive", "Floppy drive is useful rescue storage medium", 20, "floppy.png"]
            ); 
         }
      ); 
    } Future<List<Product>> getAllProducts() async {
      final db = await database; 
      List&lt;Map&gt; results = await db.query(
         "Product", columns: Product.columns, orderBy: "id ASC"
      ); 
      List&lt;Product&gt; products = new List();   
      results.forEach((result) {
         Product product = Product.fromMap(result); 
         products.add(product); 
      }); 
      return products; 
    } Future<Product> getProductById(int id) async {
      final db = await database; 
      var result = await db.query("Product", where: "id = ", whereArgs: &#91;id]); 
      return result.isNotEmpty ? Product.fromMap(result.first) : Null; 
    } insert(Product product) async {
      final db = await database; 
      var maxIdResult = await db.rawQuery("SELECT MAX(id)+1 as last_inserted_id FROM Product"); 
      var id = maxIdResult.first&#91;"last_inserted_id"]; 
      var result = await db.rawInsert(
         "INSERT Into Product (id, name, description, price, image)" 
         " VALUES (?, ?, ?, ?, ?)", 
         &#91;id, product.name, product.description, product.price, product.image] 
      ); 
      return result; 
    } update(Product product) async {
      final db = await database; 
      var result = await db.update(
         "Product", product.toMap(), where: "id = ?", whereArgs: &#91;product.id]
      ); 
      return result; 
    } delete(int id) async {
      final db = await database; 
      db.delete("Product", where: "id = ?", whereArgs: &#91;id]);
    } }
    • Change the main method to get the product information.
    void main() {
       runApp(MyApp(products: SQLiteDbProvider.db.getAllProducts())); 
    }
    • Here, we have used the getAllProducts method to fetch all products from the database.
    • Run the application and see the results. It will be similar to previous example, Accessing Product service API, except the product information is stored and fetched from the local SQLite database.

    Cloud Firestore

    Firebase is a BaaS app development platform. It provides many feature to speed up the mobile application development like authentication service, cloud storage, etc., One of the main feature of Firebase is Cloud Firestore, a cloud based real time NoSQL database.

    Flutter provides a special package, cloud_firestore to program with Cloud Firestore. Let us create an online product store in the Cloud Firestore and create a application to access the product store.

    • Create a new Flutter application in Android studio, product_firebase_app.
    • Replace the default startup code (main.dart) with our product_rest_app code.
    • Copy Product.dart file from product_rest_app into the lib folder.
    class Product { 
       final String name; 
       final String description; 
       final int price; 
       final String image; 
       
       Product(this.name, this.description, this.price, this.image); 
       factory Product.fromMap(Map<String, dynamic> json) {
    
      return Product( 
         json&#91;'name'], 
         json&#91;'description'], 
         json&#91;'price'], 
         json&#91;'image'], 
      ); 
    } }
    • Copy the assets folder from product_rest_app to product_firebase_app and add assets inside the pubspec.yaml file.
    flutter:
       assets: 
       - assets/appimages/floppy.png 
       - assets/appimages/iphone.png 
       - assets/appimages/laptop.png 
       - assets/appimages/pendrive.png 
       - assets/appimages/pixel.png 
       - assets/appimages/tablet.png
    • Configure cloud_firestore package in the pubspec.yaml file as shown below −
    dependencies: cloud_firestore: ^0.9.13+1
    
    • Here, use the latest version of the cloud_firestore package.
    • Android studio will alert that the pubspec.yaml is updated as shown here −
    Cloud Firestore Package
    • Click Get dependencies option. Android studio will get the package from Internet and properly configure it for the application.
    • Create a project in the Firebase using the following steps −
      • Create a Firebase account by selecting Free plan at https://firebase.google.com/pricing/.
      • Once Firebase account is created, it will redirect to the project overview page. It list all the Firebase based project and provides an option to create a new project.
      • Click Add project and it will open a project creation page.
      • Enter products app db as project name and click Create project option.
      • Go to *Firebase console.
      • Click Project overview. It opens the project overview page.
      • Click android icon. It will open project setting specific to Android development.
      • Enter Android Package name, com.tutorialspoint.flutterapp.product_firebase_app.
      • Click Register App. It generates a project configuration file, google_service.json.
      • Download google_service.json and then move it into the project’s android/app directory. This file is the connection between our application and Firebase.
      • Open android/app/build.gradle and include the following code −
    apply plugin: 'com.google.gms.google-services'
    
    • Open android/build.gradle and include the following configuration −
    buildscript {
       repositories { 
    
      // ... 
    } dependencies {
      // ... 
      classpath 'com.google.gms:google-services:3.2.1' // new 
    } }
    • Open android/app/build.gradle and include the following code as well.
    android {
       defaultConfig { 
    
      ... 
      multiDexEnabled true 
    } ... } dependencies { ... compile 'com.android.support: multidex:1.0.3' }
    • Follow the remaining steps in the Firebase Console or just skip it.
    • Create a product store in the newly created project using the following steps −
      • Go to Firebase console.
      • Open the newly created project.
      • Click the Database option in the left menu.
      • Click Create database option.
      • Click Start in test mode and then Enable.
      • Click Add collection. Enter product as collection name and then click Next.
      • Enter the sample product information as shown in the image here −
    Sample Product Information
    • Add addition product information using Add document options.
    • Open main.dart file and import Cloud Firestore plugin file and remove http package.
    import 'package:cloud_firestore/cloud_firestore.dart';
    
    • Remove parseProducts and update fetchProducts to fetch products from Cloud Firestore instead of Product service API.
    Stream<QuerySnapshot> fetchProducts() { 
       return Firestore.instance.collection('product').snapshots(); }
    
    • Here, Firestore.instance.collection method is used to access product collection available in the cloud store. Firestore.instance.collection provides many option to filter the collection to get the necessary documents. But, we have not applied any filter to get all product information.
    • Cloud Firestore provides the collection through Dart Stream concept and so modify the products type in MyApp and MyHomePage widget from Future<list<Product>> to Stream<QuerySnapshot>.
    • Change the build method of MyHomePage widget to use StreamBuilder instead of FutureBuilder.
    @override 
    Widget build(BuildContext context) {
       return Scaffold(
    
      appBar: AppBar(title: Text("Product Navigation")), 
      body: Center(
         child: StreamBuilder&lt;QuerySnapshot&gt;(
            stream: products, builder: (context, snapshot) {
               if (snapshot.hasError) print(snapshot.error); 
               if(snapshot.hasData) {
                  List&lt;DocumentSnapshot&gt; 
                  documents = snapshot.data.documents; 
                  
                  List&lt;Product&gt; 
                  items = List&lt;Product&gt;(); 
                  
                  for(var i = 0; i &lt; documents.length; i++) { 
                     DocumentSnapshot document = documents&#91;i]; 
                     items.add(Product.fromMap(document.data)); 
                  } 
                  return ProductBoxList(items: items);
               } else { 
                  return Center(child: CircularProgressIndicator()); 
               }
            }, 
         ), 
      )
    ); }
    • Here, we have fetched the product information as List<DocumentSnapshot> type. Since, our widget, ProductBoxList is not compatible with documents, we have converted the documents into List<Product> type and further used it.
    • Finally, run the application and see the result. Since, we have used the same product information as that of SQLite application and changed the storage medium only, the resulting application looks identical to SQLite application application.
  • Introduction to Package

    Dart’s way of organizing and sharing a set of functionality is through Package. Dart Package is simply sharable libraries or modules. In general, the Dart Package is same as that of Dart Application except Dart Package does not have application entry point, main.

    The general structure of Package (consider a demo package, my_demo_package) is as below −

    • lib/src/* − Private Dart code files.
    • lib/my_demo_package.dart − Main Dart code file. It can be imported into an application as −
    import 'package:my_demo_package/my_demo_package.dart'
    
    • Other private code file may be exported into the main code file (my_demo_package.dart), if necessary as shown below −
    export src/my_private_code.dart
    
    • lib/* − Any number of Dart code files arranged in any custom folder structure. The code can be accessed as,
    import 'package:my_demo_package/custom_folder/custom_file.dart'
    
    • pubspec.yaml − Project specification, same as that of application,

    All Dart code files in the Package are simply Dart classes and it does not have any special requirement for a Dart code to include it in a Package.

    Types of Packages

    Since Dart Packages are basically a small collection of similar functionality, it can be categorized based on its functionality.

    Dart Package

    Generic Dart code, which can be used in both web and mobile environment. For example, english_words is one such package which contains around 5000 words and has basic utility functions like nouns (list nouns in the English), syllables (specify number of syllables in a word.

    Flutter Package

    Generic Dart code, which depends on Flutter framework and can be used only in mobile environment. For example, fluro is a custom router for flutter. It depends on the Flutter framework.

    Flutter Plugin

    Generic Dart code, which depends on Flutter framework as well as the underlying platform code (Android SDK or iOS SDK). For example, camera is a plugin to interact with device camera. It depends on the Flutter framework as well as the underlying framework to get access to camera.

    Using a Dart Package

    Dart Packages are hosted and published into the live server, https://pub.dartlang.org. Also, Flutter provides simple tool, pub to manage Dart Packages in the application. The steps needed to use as Package is as follows −

    • Include the package name and the version needed into the pubspec.yaml as shown below −
    dependencies: english_words: ^3.1.5
    
    • The latest version number can be found by checking the online server.
    • Install the package into the application by using the following command −
    flutter packages get
    
    • While developing in the Android studio, Android Studio detects any change in the pubspec.yaml and displays an Android studio package alert to the developer as shown below −
    Package Alert
    • Dart Packages can be installed or updated in Android Studio using the menu options.
    • Import the necessary file using the command shown below and start working −
    import 'package:english_words/english_words.dart';
    
    • Use any method available in the package,
    nouns.take(50).forEach(print);
    
    • Here, we have used nouns function to get and print the top 50 words.

    Explore our latest online courses and learn new skills at your own pace. Enroll and become a certified expert to boost your career.

    Develop a Flutter Plugin Package

    Developing a Flutter Plugin is similar to developing a Dart application or Dart Package. The only exception is that the plugin is going to use System API (Android or iOS) to get the required platform specific functionality.

    As we have already learned how to access platform code in the previous chapters, let us develop a simple plugin, my_browser to understand the plugin development process. The functionality of the my_browser plugin is to allow the application to open the given website in the platform specific browser.

    • Start Android Studio.
    • Click File → New Flutter Project and select Flutter Plugin option.
    • You can see a Flutter plugin selection window as shown here −
    Flutter Plugin
    • Enter my_browser as project name and click Next.
    • Enter the plugin name and other details in the window as shown here −
    Configure New Flutter Plugin
    • Enter company domain, flutterplugins.tutorialspoint.com in the window shown below and then click on Finish. It will generate a startup code to develop our new plugin.
    Package Name
    • Open my_browser.dart file and write a method, openBrowser to invoke platform specific openBrowser method.
    Future<void> openBrowser(String urlString) async { 
       try {
    
      final int result = await _channel.invokeMethod(
         'openBrowser', &lt;String, String&gt;{ 'url': urlString }
      );
    } on PlatformException catch (e) {
      // Unable to open the browser print(e); 
    } }
    • Open MyBrowserPlugin.java file and import the following classes −
    import android.app.Activity; 
    import android.content.Intent; 
    import android.net.Uri; 
    import android.os.Bundle;
    
    • Here, we have to import library required for opening a browser from Android.
    • Add new private variable mRegistrar of type Registrar in MyBrowserPlugin class.
    private final Registrar mRegistrar;
    
    • Here, Registrar is used to get context information of the invoking code.
    • Add a constructor to set Registrar in MyBrowserPlugin class.
    private MyBrowserPlugin(Registrar registrar) { 
       this.mRegistrar = registrar; 
    }
    
    • Change registerWith to include our new constructor in MyBrowserPlugin class.
    public static void registerWith(Registrar registrar) { 
       final MethodChannel channel = new MethodChannel(registrar.messenger(), "my_browser"); 
       MyBrowserPlugin instance = new MyBrowserPlugin(registrar); 
       channel.setMethodCallHandler(instance); 
    }
    • Change the onMethodCall to include openBrowser method in MyBrowserPlugin class.
    @Override 
    public void onMethodCall(MethodCall call, Result result) { 
       String url = call.argument("url");
       if (call.method.equals("getPlatformVersion")) { 
    
      result.success("Android " + android.os.Build.VERSION.RELEASE); 
    } else if (call.method.equals("openBrowser")) {
      openBrowser(call, result, url); 
    } else {
      result.notImplemented(); 
    } }
    • Write the platform specific openBrowser method to access browser in MyBrowserPlugin class.
    private void openBrowser(MethodCall call, Result result, String url) { 
       Activity activity = mRegistrar.activity(); 
       if (activity == null) {
    
      result.error("ACTIVITY_NOT_AVAILABLE", 
      "Browser cannot be opened without foreground activity", null); 
      return; 
    } Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); activity.startActivity(intent); result.success((Object) true); }
    • The complete source code of the my_browser plugin is as follows −

    my_browser.dart

    import 'dart:async'; 
    import 'package:flutter/services.dart'; 
    
    class MyBrowser {
       static const MethodChannel _channel = const MethodChannel('my_browser'); 
       static Future<String> get platformVersion async { 
    
      final String version = await _channel.invokeMethod('getPlatformVersion'); return version; 
    } Future<void> openBrowser(String urlString) async {
      try {
         final int result = await _channel.invokeMethod(
            'openBrowser', &lt;String, String&gt;{'url': urlString}); 
      } 
      on PlatformException catch (e) { 
         // Unable to open the browser print(e); 
      }
    } }

    MyBrowserPlugin.java

    package com.tutorialspoint.flutterplugins.my_browser; 
    
    import io.flutter.plugin.common.MethodCall; 
    import io.flutter.plugin.common.MethodChannel; 
    import io.flutter.plugin.common.MethodChannel.MethodCallHandler; 
    import io.flutter.plugin.common.MethodChannel.Result; 
    import io.flutter.plugin.common.PluginRegistry.Registrar; 
    import android.app.Activity; 
    import android.content.Intent; 
    import android.net.Uri; 
    import android.os.Bundle; 
    
    /** MyBrowserPlugin */ 
    public class MyBrowserPlugin implements MethodCallHandler {
       private final Registrar mRegistrar; 
       private MyBrowserPlugin(Registrar registrar) { 
    
      this.mRegistrar = registrar; 
    } /** Plugin registration. */ public static void registerWith(Registrar registrar) {
      final MethodChannel channel = new MethodChannel(
         registrar.messenger(), "my_browser"); 
      MyBrowserPlugin instance = new MyBrowserPlugin(registrar); 
      channel.setMethodCallHandler(instance); 
    } @Override public void onMethodCall(MethodCall call, Result result) {
      String url = call.argument("url"); 
      if (call.method.equals("getPlatformVersion")) { 
         result.success("Android " + android.os.Build.VERSION.RELEASE); 
      } 
      else if (call.method.equals("openBrowser")) { 
         openBrowser(call, result, url); 
      } else { 
         result.notImplemented(); 
      } 
    } private void openBrowser(MethodCall call, Result result, String url) {
      Activity activity = mRegistrar.activity(); 
      if (activity == null) {
         result.error("ACTIVITY_NOT_AVAILABLE",
            "Browser cannot be opened without foreground activity", null); 
         return; 
      }
      Intent intent = new Intent(Intent.ACTION_VIEW); 
      intent.setData(Uri.parse(url)); 
      activity.startActivity(intent); 
      result.success((Object) true); 
    } }
    • Create a new project, my_browser_plugin_test to test our newly created plugin.
    • Open pubspec.yaml and set my_browser as a plugin dependency.
    dependencies: 
       flutter: 
    
      sdk: flutter 
    my_browser:
      path: ../my_browser</code></pre>
    • Android studio will alert that the pubspec.yaml is updated as shown in the Android studio package alert given below −
    Android Studio Package Alert
    • Click Get dependencies option. Android studio will get the package from Internet and properly configure it for the application.
    • Open main.dart and include my_browser plugin as below −
    import 'package:my_browser/my_browser.dart';
    
    • Call the openBrowser function from my_browser plugin as shown below −
    onPressed: () => MyBrowser().openBrowser("https://flutter.dev"),
    
    • The complete code of the main.dart is as follows −
    import 'package:flutter/material.dart'; 
    import 'package:my_browser/my_browser.dart'; 
    
    void main() => runApp(MyApp()); 
    
    class MyApp extends StatelessWidget { 
       @override 
       Widget build(BuildContext context) {
    
      return MaterialApp( 
         title: 'Flutter Demo', 
         theme: ThemeData( 
            primarySwatch: Colors.blue, 
         ), 
         home: MyHomePage(
            title: 'Flutter Demo Home Page'
         ), 
      );,
    } } 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: Center(
            child: RaisedButton(
               child: Text('Open Browser'), 
               onPressed: () =&gt; MyBrowser().openBrowser("https://flutter.dev"), 
            ),
         ), 
      ); 
    } }
    • Run the application and click the Open Browser button and see that the browser is launched. You can see a Browser app - Home page as shown in the screenshot shown below −
    Open Browser

    You can see a Browser app – Browser screen as shown in the screenshot shown below −

    Flutter Infrastructure
  • Writing IOS Specific Code

    Accessing iOS specific code is similar to that on Android platform except that it uses iOS specific languages – Objective-C or Swift and iOS SDK. Otherwise, the concept is same as that of the Android platform.

    Let us write the same application as in the previous chapter for iOS platform as well.

    • Let us create a new application in Android Studio (macOS), flutter_browser_ios_app
    • Follow steps 2 – 6 as in previous chapter.
    • Start XCode and click File → Open
    • Choose the xcode project under ios directory of our flutter project.
    • Open AppDelegate.m under Runner → Runner path. It contains the following code −
    #include "AppDelegate.h" 
    #include "GeneratedPluginRegistrant.h" 
    @implementation AppDelegate 
    
    - (BOOL)application:(UIApplication *)application
       didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
      // &#91;GeneratedPluginRegistrant registerWithRegistry:self];
      // Override point for customization after application launch.
      return &#91;super application:application didFinishLaunchingWithOptions:launchOptions];
    } @end
    • We have added a method, openBrowser to open browser with specified url. It accepts single argument, url.
    - (void)openBrowser:(NSString *)urlString { 
       NSURL *url = [NSURL URLWithString:urlString]; 
       UIApplication *application = [UIApplication sharedApplication]; 
       [application openURL:url]; 
    }
    • In didFinishLaunchingWithOptions method, find the controller and set it in controller variable.
    FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
    
    • In didFinishLaunchingWithOptions method, set the browser channel as flutterapp.tutorialspoint.com/browse −
    FlutterMethodChannel* browserChannel = [
       FlutterMethodChannel methodChannelWithName:
       @"flutterapp.tutorialspoint.com/browser" binaryMessenger:controller];
    • Create a variable, weakSelf and set current class −
    __weak typeof(self) weakSelf = self;
    
    • Now, implement setMethodCallHandler. Call openBrowser by matching call.method. Get url by invoking call.arguments and pass it while calling openBrowser.
    [browserChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
       if ([@"openBrowser" isEqualToString:call.method]) { 
    
      NSString *url = call.arguments&#91;@"url"];   
      &#91;weakSelf openBrowser:url]; 
    } else { result(FlutterMethodNotImplemented); } }];
    • The complete code is as follows −
    #include "AppDelegate.h" 
    #include "GeneratedPluginRegistrant.h" 
    @implementation AppDelegate 
    
    - (BOOL)application:(UIApplication *)application 
       didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
       
       // custom code starts 
       FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController; 
       FlutterMethodChannel* browserChannel = [
    
      FlutterMethodChannel methodChannelWithName:
      @"flutterapp.tutorialspoint.com /browser" binaryMessenger:controller]; 
    __weak typeof(self) weakSelf = self; [browserChannel setMethodCallHandler:^(
      FlutterMethodCall* call, FlutterResult result) { 
      
      if (&#91;@"openBrowser" isEqualToString:call.method]) { 
         NSString *url = call.arguments&#91;@"url"];
         &#91;weakSelf openBrowser:url]; 
      } else { result(FlutterMethodNotImplemented); } 
    }]; // custom code ends [GeneratedPluginRegistrant registerWithRegistry:self]; // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } - (void)openBrowser:(NSString *)urlString { NSURL *url = [NSURL URLWithString:urlString]; UIApplication *application = [UIApplication sharedApplication]; [application openURL:url]; } @end
    • Open project setting.
    • Go to Capabilities and enable Background Modes.
    • Add *Background fetch and Remote Notification**.
    • Now, run the application. It works similar to Android version but the Safari browser will be opened instead of chrome.
  • Writing Android Specific Code

    Flutter provides a general framework to access platform specific feature. This enables the developer to extend the functionality of the Flutter framework using platform specific code. Platform specific functionality like camera, battery level, browser, etc., can be accessed easily through the framework.

    The general idea of accessing the platform specific code is through simple messaging protocol. Flutter code, Client and the platform code and Host binds to a common Message Channel. Client sends message to the Host through the Message Channel. Host listens on the Message Channel, receives the message and does the necessary functionality and finally, returns the result to the Client through Message Channel.

    The platform specific code architecture is shown in the block diagram given below −

    Specific Code Architecture

    The messaging protocol uses a standard message codec (StandardMessageCodec class) that supports binary serialization of JSON-like values such as numbers, strings, boolean, etc., The serialization and de-serialization works transparently between the client and the host.

    Let us write a simple application to open a browser using Android SDK and understand how

    • Create a new Flutter application in Android studio, flutter_browser_app
    • Replace main.dart code with below code −
    import 'package:flutter/material.dart'; 
    void main() => runApp(MyApp()); 
    class MyApp extends StatelessWidget { 
       @override 
       Widget build(BuildContext context) {
    
      return MaterialApp(
         title: 'Flutter Demo', 
         theme: ThemeData( 
            primarySwatch: Colors.blue, 
         ), 
         home: MyHomePage(title: 'Flutter Demo Home Page'),
      );
    } } 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: Center(
            child: RaisedButton( 
               child: Text('Open Browser'), 
               onPressed: null, 
            ), 
         ), 
      ); 
    } }
    • Here, we have created a new button to open the browser and set its onPressed method as null.
    • Now, import the following packages −
    import 'dart:async'; 
    import 'package:flutter/services.dart';
    
    • Here, services.dart include the functionality to invoke platform specific code.
    • Create a new message channel in the MyHomePage widget.
    static const platform = const 
    MethodChannel('flutterapp.tutorialspoint.com/browser');
    • Write a method, _openBrowser to invoke platform specific method, openBrowser method through message channel.
    Future<void> _openBrowser() async { 
       try {
    
      final int result = await platform.invokeMethod(
         'openBrowser', &lt;String, String&gt;{ 
            'url': "https://flutter.dev" 
         }
      ); 
    } on PlatformException catch (e) {
      // Unable to open the browser 
      print(e); 
    } }

    Here, we have used platform.invokeMethod to invoke openBrowser (explained in coming steps). openBrowser has an argument, url to open a specific url.

    • Change the value of onPressed property of the RaisedButton from null to _openBrowser.
    onPressed: _openBrowser,
    
    • Open MainActivity.java (inside the android folder) and import the required library −
    import android.app.Activity; 
    import android.content.Intent; 
    import android.net.Uri; 
    import android.os.Bundle; 
    
    import io.flutter.app.FlutterActivity; 
    import io.flutter.plugin.common.MethodCall; 
    import io.flutter.plugin.common.MethodChannel; 
    import io.flutter.plugin.common.MethodChannel.MethodCallHandler; 
    import io.flutter.plugin.common.MethodChannel.Result; 
    import io.flutter.plugins.GeneratedPluginRegistrant;
    • Write a method, openBrowser to open a browser
    private void openBrowser(MethodCall call, Result result, String url) { 
       Activity activity = this; 
       if (activity == null) { 
    
      result.error("ACTIVITY_NOT_AVAILABLE", 
      "Browser cannot be opened without foreground 
      activity", null); 
      return; 
    } Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); activity.startActivity(intent); result.success((Object) true); }
    • Now, set channel name in the MainActivity class −
    private static final String CHANNEL = "flutterapp.tutorialspoint.com/browser";
    
    • Write android specific code to set message handling in the onCreate method −
    new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler( 
       new MethodCallHandler() { 
       @Override 
       public void onMethodCall(MethodCall call, Result result) { 
    
      String url = call.argument("url"); 
      if (call.method.equals("openBrowser")) {
         openBrowser(call, result, url); 
      } else { 
         result.notImplemented(); 
      } 
    } });

    Here, we have created a message channel using MethodChannel class and used MethodCallHandler class to handle the message. onMethodCall is the actual method responsible for calling the correct platform specific code by the checking the message. onMethodCall method extracts the url from message and then invokes the openBrowser only when the method call is openBrowser. Otherwise, it returns notImplemented method.

    The complete source code of the application is as follows −

    main.dart

    MainActivity.java

    package com.tutorialspoint.flutterapp.flutter_browser_app; 
    
    import android.app.Activity; 
    import android.content.Intent; 
    import android.net.Uri; 
    import android.os.Bundle; 
    import io.flutter.app.FlutterActivity; 
    import io.flutter.plugin.common.MethodCall; 
    import io.flutter.plugin.common.MethodChannel.Result; 
    import io.flutter.plugins.GeneratedPluginRegistrant; 
    
    public class MainActivity extends FlutterActivity { 
       private static final String CHANNEL = "flutterapp.tutorialspoint.com/browser"; 
       @Override 
       protected void onCreate(Bundle savedInstanceState) { 
    
      super.onCreate(savedInstanceState); 
      GeneratedPluginRegistrant.registerWith(this); 
      new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
         new MethodCallHandler() {
            @Override 
            public void onMethodCall(MethodCall call, Result result) {
               String url = call.argument("url"); 
               if (call.method.equals("openBrowser")) { 
                  openBrowser(call, result, url); 
               } else { 
                  result.notImplemented(); 
               }
            }
         }
      ); 
    } private void openBrowser(MethodCall call, Result result, String url) {
      Activity activity = this; if (activity == null) {
         result.error(
            "ACTIVITY_NOT_AVAILABLE", "Browser cannot be opened without foreground activity", null
         ); 
         return; 
      } 
      Intent intent = new Intent(Intent.ACTION_VIEW); 
      intent.setData(Uri.parse(url)); 
      activity.startActivity(intent); 
      result.success((Object) true); 
    } }

    main.dart

    import 'package:flutter/material.dart'; 
    import 'dart:async'; 
    import 'package:flutter/services.dart'; 
    
    void main() => runApp(MyApp()); 
    class MyApp extends StatelessWidget {
       @override 
       Widget build(BuildContext context) {
    
      return MaterialApp(
         title: 'Flutter Demo', 
         theme: ThemeData( 
            primarySwatch: Colors.blue, 
         ), 
         home: MyHomePage(
            title: 'Flutter Demo Home Page'
         ), 
      ); 
    } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; static const platform = const MethodChannel('flutterapp.tutorialspoint.com/browser'); Future<void> _openBrowser() async {
      try {
         final int result = await platform.invokeMethod('openBrowser', &lt;String, String&gt;{ 
            'url': "https://flutter.dev" 
         });
      }
      on PlatformException catch (e) { 
         // Unable to open the browser print(e); 
      } 
    } @override Widget build(BuildContext context) {
      return Scaffold( 
         appBar: AppBar( 
            title: Text(this.title), 
         ), 
         body: Center(
            child: RaisedButton( 
               child: Text('Open Browser'), 
               onPressed: _openBrowser, 
            ), 
         ),
      );
    } }

    Run the application and click the Open Browser button and you can see that the browser is launched. The Browser app – Home page is as shown in the screenshot here −

    Flutter Demo Home Page
    Productively Build Apps
  • Animation

    Animation is a complex procedure in any mobile application. In spite of its complexity, Animation enhances the user experience to a new level and provides a rich user interaction. Due to its richness, animation becomes an integral part of modern mobile application. Flutter framework recognizes the importance of Animation and provides a simple and intuitive framework to develop all types of animations.

    Introduction

    Animation is a process of showing a series of images / picture in a particular order within a specific duration to give an illusion of movement. The most important aspects of the animation are as follows −

    • Animation have two distinct values: Start value and End value. The animation starts from Start value and goes through a series of intermediate values and finally ends at End values. For example, to animate a widget to fade away, the initial value will be the full opacity and the final value will be the zero opacity.
    • The intermediate values may be linear or non-linear (curve) in nature and it can be configured. Understand that the animation works as it is configured. Each configuration provides a different feel to the animation. For example, fading a widget will be linear in nature whereas bouncing of a ball will be non-linear in nature.
    • The duration of the animation process affects the speed (slowness or fastness) of the animation.
    • The ability to control the animation process like starting the animation, stopping the animation, repeating the animation to set number of times, reversing the process of animation, etc.,
    • In Flutter, animation system does not do any real animation. Instead, it provides only the values required at every frame to render the images.

    Animation Based Classes

    Flutter animation system is based on Animation objects. The core animation classes and its usage are as follows −

    Animation

    Generates interpolated values between two numbers over a certain duration. The most common Animation classes are −

    • Animation<double> − interpolate values between two decimal number
    • Animation<Color> − interpolate colors between two color
    • Animation<Size> − interpolate sizes between two size
    • AnimationController − Special Animation object to control the animation itself. It generates new values whenever the application is ready for a new frame. It supports linear based animation and the value starts from 0.0 to 1.0
    controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
    

    Here, controller controls the animation and duration option controls the duration of the animation process. vsync is a special option used to optimize the resource used in the animation.

    CurvedAnimation

    Similar to AnimationController but supports non-linear animation. CurvedAnimation can be used along with Animation object as below −

    controller = AnimationController(duration: const Duration(seconds: 2), vsync: this); 
    animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)

    Tween<T>

    Derived from Animatable<T> and used to generate numbers between any two numbers other than 0 and 1. It can be used along with Animation object by using animate method and passing actual Animation object.

    AnimationController controller = AnimationController( 
       duration: const Duration(milliseconds: 1000), 
    vsync: this); Animation<int> customTween = IntTween(
       begin: 0, end: 255).animate(controller);
    • Tween can also used along with CurvedAnimation as below −
    AnimationController controller = AnimationController(
       duration: const Duration(milliseconds: 500), vsync: this); 
    final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut); 
    Animation<int> customTween = IntTween(begin: 0, end: 255).animate(curve);

    Here, controller is the actual animation controller. curve provides the type of non-linearity and the customTween provides custom range from 0 to 255.

    Explore our latest online courses and learn new skills at your own pace. Enroll and become a certified expert to boost your career.

    Work flow of the Flutter Animation

    The work flow of the animation is as follows −

    • Define and start the animation controller in the initState of the StatefulWidget.
    AnimationController(duration: const Duration(seconds: 2), vsync: this); 
    animation = Tween<double>(begin: 0, end: 300).animate(controller); 
    controller.forward();
    • Add animation based listener, addListener to change the state of the widget.
    animation = Tween<double>(begin: 0, end: 300).animate(controller) ..addListener(() {
       setState(() { 
    
      // The state that has changed here is the animation object’s value. 
    }); });
    • Build-in widgets, AnimatedWidget and AnimatedBuilder can be used to skip this process. Both widget accepts Animation object and get current values required for the animation.
    • Get the animation values during the build process of the widget and then apply it for width, height or any relevant property instead of the original value.
    child: Container( 
       height: animation.value, 
       width: animation.value, 
       child: <Widget>, 
    )

    Working Application

    Let us write a simple animation based application to understand the concept of animation in Flutter framework.

    • Create a new Flutter application in Android studio, product_animation_app.
    • Copy the assets folder from product_nav_app to product_animation_app and add assets inside the pubspec.yaml file.
    flutter: 
       assets: 
       - assets/appimages/floppy.png 
       - assets/appimages/iphone.png 
       - assets/appimages/laptop.png 
       - assets/appimages/pendrive.png 
       - assets/appimages/pixel.png 
       - assets/appimages/tablet.png
    • Remove the default startup code (main.dart).
    • Add import and basic main function.
    import 'package:flutter/material.dart'; 
    void main() => runApp(MyApp());
    • Create the MyApp widget derived from StatefulWidgtet.
    class MyApp extends StatefulWidget { 
       _MyAppState createState() => _MyAppState(); 
    }
    • Create _MyAppState widget and implement initState and dispose in addition to default build method.
    class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin { 
       Animation<double> animation; 
       AnimationController controller; 
       @override void initState() {
    
      super.initState(); 
      controller = AnimationController(
         duration: const Duration(seconds: 10), vsync: this
      ); 
      animation = Tween&lt;double&gt;(begin: 0.0, end: 1.0).animate(controller); 
      controller.forward(); 
    } // This widget is the root of your application. @override Widget build(BuildContext context) {
      controller.forward(); 
      return MaterialApp(
         title: 'Flutter Demo',
         theme: ThemeData(primarySwatch: Colors.blue,), 
         home: MyHomePage(title: 'Product layout demo home page', animation: animation,)
      ); 
    } @override void dispose() {
      controller.dispose();
      super.dispose();
    } }

    Here,

    • In initState method, we have created an animation controller object (controller), an animation object (animation) and started the animation using controller.forward.
    • In dispose method, we have disposed the animation controller object (controller).
    • In build method, send animation to MyHomePage widget through constructor. Now, MyHomePage widget can use the animation object to animate its content.
    • Now, add ProductBox widget
    class ProductBox extends StatelessWidget {
       ProductBox({Key key, this.name, this.description, this.price, this.image})
    
      : super(key: key);
    final String name; final String description; final int price; final String image; Widget build(BuildContext context) {
      return Container(
         padding: EdgeInsets.all(2), 
         height: 140, 
         child: Card( 
            child: Row( 
               mainAxisAlignment: MainAxisAlignment.spaceEvenly, 
               children: &lt;Widget&gt;&#91; 
                  Image.asset("assets/appimages/" + image), 
                  Expanded( 
                     child: Container( 
                        padding: EdgeInsets.all(5), 
                        child: Column( 
                           mainAxisAlignment: MainAxisAlignment.spaceEvenly, 
                           children: &lt;Widget&gt;&#91; 
                              Text(this.name, style: 
                                 TextStyle(fontWeight: FontWeight.bold)), 
                              Text(this.description), 
                                 Text("Price: " + this.price.toString()), 
                           ], 
                        )
                     )
                  )
               ]
            )
         )
      ); 
    } }
    • Create a new widget, MyAnimatedWidget to do simple fade animation using opacity.
    class MyAnimatedWidget extends StatelessWidget { 
       MyAnimatedWidget({this.child, this.animation}); 
    
      
    final Widget child; final Animation<double> animation; Widget build(BuildContext context) => Center( child: AnimatedBuilder(
      animation: animation, 
      builder: (context, child) =&gt; Container( 
         child: Opacity(opacity: animation.value, child: child), 
      ), 
      child: child), 
    ); }
    • Here, we have used AniatedBuilder to do our animation. AnimatedBuilder is a widget which build its content while doing the animation at the same time. It accepts a animation object to get current animation value. We have used animation value, animation.value to set the opacity of the child widget. In effect, the widget will animate the child widget using opacity concept.
    • Finally, create the MyHomePage widget and use the animation object to animate any of its content.
    class MyHomePage extends StatelessWidget {
       MyHomePage({Key key, this.title, this.animation}) : super(key: key); 
       
       final String title; 
       final Animation<double> 
       animation; 
       
       @override 
       Widget build(BuildContext context) {
    
      return Scaffold(
         appBar: AppBar(title: Text("Product Listing")),body: ListView(
            shrinkWrap: true,
            padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0), 
            children: &lt;Widget&gt;&#91;
               FadeTransition(
                  child: ProductBox(
                     name: "iPhone", 
                     description: "iPhone is the stylist phone ever", 
                     price: 1000, 
                     image: "iphone.png"
                  ), opacity: animation
               ), 
               MyAnimatedWidget(child: ProductBox(
                  name: "Pixel", 
                  description: "Pixel is the most featureful phone ever", 
                  price: 800, 
                  image: "pixel.png"
               ), animation: animation), 
               ProductBox(
                  name: "Laptop", 
                  description: "Laptop is most productive development tool", 
                  price: 2000, 
                  image: "laptop.png"
               ), 
               ProductBox(
                  name: "Tablet", 
                  description: "Tablet is the most useful device ever for meeting", 
                  price: 1500, 
                  image: "tablet.png"
               ), 
               ProductBox(
                  name: "Pendrive", 
                  description: "Pendrive is useful storage medium", 
                  price: 100, 
                  image: "pendrive.png"
               ),
               ProductBox(
                  name: "Floppy Drive", 
                  description: "Floppy drive is useful rescue storage medium", 
                  price: 20, 
                  image: "floppy.png"
               ),
            ],
         )
      );
    } }

    Here, we have used FadeAnimation and MyAnimationWidget to animate the first two items in the list. FadeAnimation is a build-in animation class, which we used to animate its child using opacity concept.

    • The complete code is as follows −
    import 'package:flutter/material.dart'; 
    void main() => runApp(MyApp()); 
    
    class MyApp extends StatefulWidget { 
       _MyAppState createState() => _MyAppState(); 
    } 
    class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
       Animation<double> animation; 
       AnimationController controller; 
       
       @override 
       void initState() {
    
      super.initState(); 
      controller = AnimationController(
         duration: const Duration(seconds: 10), vsync: this); 
      animation = Tween&lt;double&gt;(begin: 0.0, end: 1.0).animate(controller); 
      controller.forward(); 
    } // This widget is the root of your application. @override Widget build(BuildContext context) {
      controller.forward(); 
      return MaterialApp( 
         title: 'Flutter Demo', theme: ThemeData(primarySwatch: Colors.blue,), 
         home: MyHomePage(title: 'Product layout demo home page', animation: animation,) 
      ); 
    } @override void dispose() {
      controller.dispose();
      super.dispose(); 
    } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title, this.animation}): super(key: key); final String title; final Animation<double> animation; @override Widget build(BuildContext context) {
      return Scaffold(
         appBar: AppBar(title: Text("Product Listing")), 
         body: ListView(
            shrinkWrap: true, 
            padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0), 
            children: &lt;Widget&gt;&#91;
               FadeTransition(
                  child: ProductBox(
                     name: "iPhone", 
                     description: "iPhone is the stylist phone ever", 
                     price: 1000, 
                     image: "iphone.png"
                  ), 
                  opacity: animation
               ), 
               MyAnimatedWidget(
                  child: ProductBox( 
                     name: "Pixel", 
                     description: "Pixel is the most featureful phone ever", 
                     price: 800, 
                     image: "pixel.png"
                  ), 
                  animation: animation
               ), 
               ProductBox( 
                  name: "Laptop", 
                  description: "Laptop is most productive development tool", 
                  price: 2000, 
                  image: "laptop.png"
               ), 
               ProductBox(
                  name: "Tablet",
                  description: "Tablet is the most useful device ever for meeting",
                  price: 1500, 
                  image: "tablet.png"
               ), 
               ProductBox(
                  name: "Pendrive", 
                  description: "Pendrive is useful storage medium", 
                  price: 100, 
                  image: "pendrive.png"
               ), 
               ProductBox(
                  name: "Floppy Drive", 
                  description: "Floppy drive is useful rescue storage medium", 
                  price: 20, 
                  image: "floppy.png"
               ), 
            ], 
         )
      ); 
    } } class ProductBox extends StatelessWidget { ProductBox({Key key, this.name, this.description, this.price, this.image}) :
      super(key: key);
    final String name; final String description; final int price; final String image; Widget build(BuildContext context) {
      return Container(
         padding: EdgeInsets.all(2), 
         height: 140, 
         child: Card(
            child: Row(
               mainAxisAlignment: MainAxisAlignment.spaceEvenly, 
               children: &lt;Widget&gt;&#91; 
                  Image.asset("assets/appimages/" + image), 
                  Expanded(
                     child: Container( 
                        padding: EdgeInsets.all(5), 
                        child: Column( 
                           mainAxisAlignment: MainAxisAlignment.spaceEvenly, 
                           children: &lt;Widget&gt;&#91; 
                              Text(
                                 this.name, style: TextStyle(
                                    fontWeight: FontWeight.bold
                                 )
                              ), 
                              Text(this.description), Text(
                                 "Price: " + this.price.toString()
                              ), 
                           ], 
                        )
                     )
                  ) 
               ]
            )
         )
      ); 
    } } class MyAnimatedWidget extends StatelessWidget { MyAnimatedWidget({this.child, this.animation}); final Widget child; final Animation<double> animation; Widget build(BuildContext context) => Center(
      child: AnimatedBuilder(
         animation: animation, 
         builder: (context, child) =&gt; Container( 
            child: Opacity(opacity: animation.value, child: child), 
         ), 
         child: child
      ), 
    ); }
    • Compile and run the application to see the results. The initial and final version of the application is as follows −
    Initial Version
    Final Version