Flutter - Application State



Application State - scoped_model

Flutter provides an easy way to manage the state of the application using scoped_model package. Flutter package are simply library of reusable functionality. We will learn about Flutter packages in detail in the upcoming chapters.

scoped_model provides three main class to enable robust state management in an application which are discussed in detail here −

Model

Model encapsulates the state of an application. We can use as many Model (by inheriting Model class) as needed to maintain the application state. It has a single method, notifyListeners, which needs to be called whenever the Model state changes. notifyListeners will do necessary things to update the UI.

class Product extends Model { 
   final String name; 
   final String description; 
   final int price;
   final String image; 
   int rating; 
   
   Product(this.name, this.description, this.price, this.image, this.rating); 
   factory Product.fromMap(Map<String, dynamic> json) { 
      return Product( 
         json['name'], 
         json['description'], 
         json['price'], 
         json['image'], 
         json['rating'], 
      ); 
   } 
   void updateRating(int myRating) { 
      rating = myRating; notifyListeners(); 
   }
}

ScopedModel

ScopedModel is a widget, which holds the given model and then passes it to all the descendant widget if requested. If more than one model is needed, then we need to nest the ScopedModel.

  • Single model

ScopedModel<Product>(
   model: item, child: AnyWidget() 
)
  • Multiple model

ScopedModel<Product>( 
   model: item1, 
   child: ScopedModel<Product>( 
      model: item2, child: AnyWidget(),
   ),
)

ScopedModel.of is a method used to get the model underlying the ScopedModel. It can be used when no UI changes are necessary even though the model is going to change. The following will not change the UI (rating) of the product.

ScopedModel.of<Product>(context).updateRating(2);

ScopedModelDescendant

ScopedModelDescendant is a widget, which gets the model from the upper level widget, ScopedModel and build its user interface whenever the model changes.

ScopedModelDescendant has a two properties – builder and child. child is the UI part which does not change and will be passed to builder. builder accepts a function with three arguments −

  • content − ScopedModelDescendant pass the context of the application.

  • child − A part of UI, which does not change based on the model.

  • model − The actual model at that instance.

return ScopedModelDescendant<ProductModel>( 
   builder: (context, child, cart) => { ... Actual UI ... }, 
   child: PartOfTheUI(), 
);

Let us change our previous sample to use the scoped_model instead of StatefulWidget

  • Create a new Flutter application in Android studio, product_scoped_model_app

  • Replace the default startup code (main.dart) with our product_state_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
  • Configure scoped_model package in the pubspec.yaml file as shown below: −

dependencies: scoped_model: ^1.0.1

Here, you should use the latest version of the http package

  • Android studio will alert that the pubspec.yaml is updated.

PubSpec YAML
  • Click Get dependencies option. Android studio will get the package from Internet and properly configure it for the application.

  • Replace the default startup code (main.dart) with our startup code.

import 'package:flutter/material.dart'; 
void main() => runApp(MyApp()); 

class MyApp extends StatelessWidget { 
   // This widget is the root of your application. 
   @override 
   Widget build(BuildContext context) { 
      return MaterialApp( 
         title: 'Flutter Demo', 
         theme: ThemeData(primarySwatch: Colors.blue,), 
         home: MyHomePage(title: 'Product state 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: Text( 'Hello World', )
         ), 
      );
   }
}
  • Import scoped_model package in the main.dart file.

import 'package:scoped_model/scoped_model.dart';
  • Let us create a Product class, Product.dart to organize the product information.

import 'package:scoped_model/scoped_model.dart'; 
class Product extends Model { 
   final String name; 
   final String description; 
   final int price; 
   final String image; 
   int rating;

   Product(this.name, this.description, this.price, this.image, this.rating); 
   factory Product.fromMap(Map<String, dynamic> json) { 
      return Product( 
         json['name'], 
         json['description'], 
         json['price'], 
         json['image'], 
         json['rating'], 
      ); 
   } 
   void updateRating(int myRating) {
      rating = myRating; 
      notifyListeners(); 
   }
}

Here, we have used notifyListeners to change the UI whenever the rating is changed.

  • Let us write a method getProducts in the Product class to generate our dummy product records.

static List<Product> getProducts() { 
   List<Product> items = <Product>[]; 
   
   items.add(
      Product(
         "Pixel",
         "Pixel is the most feature-full phone ever", 800,
         "pixel.png", 0
      )
   ); 
   items.add(
      Product(
         "Laptop", "Laptop is most productive development tool", 2000, 
         "laptop.png", 0
      )
   );
   items.add(
      Product(
         "Tablet", 
         "Tablet is the most useful device ever for meeting", 1500, 
         "tablet.png", 0
      )
   );
   items.add(
      Product(
         "Pendrive", 
         "Pendrive is useful storage medium", 
         100, "pendrive.png", 0
      )
   );
   items.add(
      Product(
         "Floppy Drive", 
         "Floppy drive is useful rescue storage medium", 20, 
         "floppy.png", 0
      )
   );
   return items; 
}
import product.dart in main.dart
import 'Product.dart';
  • Let us change our new widget, RatingBox to support scoped_model concept.

class RatingBox extends StatelessWidget {
   RatingBox({Key key, this.item}) : super(key: key); 
   final Product item; 
   
   Widget build(BuildContext context) {
      double _size = 20; 
      print(item.rating); 
      return Row(
         mainAxisAlignment: MainAxisAlignment.end, 
         crossAxisAlignment: CrossAxisAlignment.end, 
         mainAxisSize: MainAxisSize.max, 
         children: <Widget>[ 
            Container(
               padding: EdgeInsets.all(0), 
               child: IconButton(
                  icon: (
                     item.rating >= 1 
                     ? Icon( Icons.star, size: _size, )
                     : Icon( Icons.star_border, size: _size, )
                  ), color: Colors.red[500], 
                  onPressed: () => this.item.updateRating(1), 
                  iconSize: _size, 
               ), 
            ), 
            Container(
               padding: EdgeInsets.all(0),
               child: IconButton(
                  icon: (item.rating >= 2
                     ? Icon(
                        Icons.star,
                        size: _size,
                     ) : Icon(
                        Icons.star_border,
                        size: _size,
                     )
                  ), 
                  color: Colors.red[500],
                  onPressed: () => this.item.updateRating(2),
                  iconSize: _size,
               ),
            ),
            Container(
               padding: EdgeInsets.all(0),
               child: IconButton(
                  icon: (
                     item.rating >= 3? Icon(
                        Icons.star,
                        size: _size,
                     )
                     : Icon(
                        Icons.star_border,
                        size: _size,
                     )
                  ), 
                  color: Colors.red[500], 
                  onPressed: () => this.item.updateRating(3), 
                  iconSize: _size, 
               ), 
            ), 
         ], 
      ); 
   }
}

Here, we have extended the RatingBox from StatelessWidget instead of StatefulWidget. Also, we have used Product model’s updateRating method to set the rating.

  • Let us modify our ProductBox widget to work with Product, ScopedModel and ScopedModelDescendant class.

class ProductBox extends StatelessWidget { 
   ProductBox({Key key, this.item}) : super(key: key);
   final Product item; 

   Widget build(BuildContext context) {
      return Container(
         padding: EdgeInsets.all(2), 
         height: 140, 
         child: Card(
            child: Row(
               mainAxisAlignment: MainAxisAlignment.spaceEvenly, 
               children: <Widget>[  
                  Image.asset("assets/appimages/" + this.item.image), 
                  Expanded(
                     child: Container(
                        padding: EdgeInsets.all(5),
                        child: ScopedModel<Product>(
                           model: this.item,
                           child: Column(
                              mainAxisAlignment: MainAxisAlignment.spaceEvenly, 
                              children: <Widget>[
                                 Text(this.item.name, 
                                    style: TextStyle(fontWeight: FontWeight.bold)), 
                                 Text(this.item.description), 
                                    Text("Price: " + 
                                 this.item.price.toString()), 
                                 ScopedModelDescendant<Product>(
                                    builder: (context, child, item) 
                                    { return RatingBox(item: item); }
                                 ) 
                              ], 
                           )
                        )
                     )
                  )
               ]
            ), 
         )
      ); 
   } 
}

Here, we have wrapped the RatingBox widget within ScopedModel and ScopedModelDecendant.

  • Change the MyHomePage widget to use our ProductBox widget.

class MyHomePage extends StatelessWidget {
   MyHomePage({Key key, this.title}) : super(key: key); 
   final String title; 
   final items = Product.getProducts(); 

   @override 
   Widget build(BuildContext context) {
      return Scaffold(
         appBar: AppBar(title: Text("Product Navigation")),
         body: ListView.builder(
            itemCount: items.length,
            itemBuilder: (context, index) {
               return ProductBox(item: items[index]);
            }, 
         )
      ); 
   }
}

Here, we have used ListView.builder to dynamically build our product list.

  • The complete code of the application is as follows −

Product.dart
import 'package:scoped_model/scoped_model.dart'; 
class Product extends Model {
   final String name; 
   final String description; 
   final int price; 
   final String image; 
   int rating; 
   
   Product(this.name, this.description, this.price, this.image, this.rating); 
   factory Product.fromMap(Map<String, dynamic> json) {
      return Product(
         json['name'], 
         json['description'], 
         json['price'], 
         json['image'], 
         json['rating'], 
      );n 
   } void cn "Laptop is most productive development tool", 2000, "laptop.png", 0));
   items.add(
      Product(
         "Tablet"cnvn, 
         "Tablet is the most useful device ever for meeting", 1500, 
         "tablet.png", 0
      )
   ); 
   items.add(
      Product(
         "Pendrive", 
         "Pendrive is useful storage medium", 100, 
         "pendrive.png", 0
      )
   ); 
   items.add(
      Product( 
         "Floppy Drive", 
         "Floppy drive is useful rescue storage medium", 20, 
         "floppy.png", 0
      )
   )
   ; return items; 
}
main.dart
import 'package:flutter/material.dart'; 
import 'package:scoped_model/scoped_model.dart'; 
import 'Product.dart'; 

void main() => runApp(MyApp()); 
class MyApp extends StatelessWidget {
   // This widget is the root of your application

   @override 
   Widget build(BuildContext context) {
      return MaterialApp(
         title: 'Flutter Demo', 
         theme: ThemeData( 
            primarySwatch: Colors.blue, 
         ), 
         home: MyHomePage(title: 'Product state demo home page'), 
      ); 
   } 
}
class MyHomePage extends StatelessWidget { 
   MyHomePage({Key key, this.title}) : super(key: key); 
   final String title; 
   final items = Product.getProducts(); 
   
   @override 
   Widget build(BuildContext context) {
      return Scaffold(
         appBar: AppBar(title: Text("Product Navigation")), 
         body: ListView.builder(
            itemCount: items.length, 
            itemBuilder: (context, index) { 
               return ProductBox(item: items[index]); 
            }, 
         )
      ); 
   } 
}
class RatingBox extends StatelessWidget {
   RatingBox({Key key, this.item}) : super(key: key);
   final Product item;
   Widget build(BuildContext context) {
      double _size = 20; 
      print(item.rating); 
      return Row(
         mainAxisAlignment: MainAxisAlignment.end,
         crossAxisAlignment: CrossAxisAlignment.end,
         mainAxisSize: MainAxisSize.max,
         children: <Widget>[
            Container(
               padding: EdgeInsets.all(0),
               child: IconButton(
                  icon: (
                     item.rating >= 1? Icon( Icons.star, size: _size, )
                     : Icon( Icons.star_border, size: _size, )
                  ), 
                  color: Colors.red[500], 
                  onPressed: () => this.item.updateRating(1), 
                  iconSize: _size, 
               ), 
            ), 
            Container(
               padding: EdgeInsets.all(0), 
               child: IconButton(
                  icon: (item.rating >= 2 
                     ? Icon( 
                        Icons.star, 
                        size: _size, 
                     ) 
                     : Icon( 
                        Icons.star_border, 
                        size: _size, 
                     )
                  ), 
                  color: Colors.red[500], 
                  onPressed: () => this.item.updateRating(2), 
                  iconSize: _size, 
               ), 
            ), 
            Container(
               padding: EdgeInsets.all(0),
               child: IconButton(
                  icon: (
                     item.rating >= 3 ? 
                     Icon( Icons.star, size: _size, )
                     : Icon( Icons.star_border, size: _size, )
                  ), 
                  color: Colors.red[500], 
                  onPressed: () => this.item.updateRating(3), 
                  iconSize: _size, 
               ), 
            ), 
         ], 
      ); 
   } 
}
class ProductBox extends StatelessWidget { 
   ProductBox({Key key, this.item}) : super(key: key); 
   final Product item; 
   Widget build(BuildContext context) {
      return Container(
         padding: EdgeInsets.all(2),
         height: 140,
         child: Card( 
            child: Row(
               mainAxisAlignment: MainAxisAlignment.spaceEvenly, 
               children: <Widget>[ 
                  Image.asset("assets/appimages/" + this.item.image),
                  Expanded(
                     child: Container( 
                        padding: EdgeInsets.all(5), 
                        child: ScopedModel<Product>(
                           model: this.item, child: Column(
                              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                              children: <Widget>[
                                 Text(
                                    this.item.name, style: TextStyle(
                                       fontWeight: FontWeight.bold
                                    )
                                 ), 
                                 Text(this.item.description), 
                                 Text("Price: " + this.item.price.toString()), 
                                 ScopedModelDescendant<Product>(
                                    builder: (context, child, item) {
                                       return RatingBox(item: item); 
                                    }
                                 )
                              ],
                           )
                        )
                     )
                  )
               ]
            ), 
         )
      );
   }
}

Finally, compile and run the application to see its result. It will work similar to previous example except the application uses the scoped_model concept.

flutter_state_management.htm
Advertisements