UI Flutter Rest API
In my previous posts I wrote the backend for Items Inventory application and display the items with a simple Web interface using HTML and JavaScript. In this article I’ll show how to create a Mobile UI using the Flutter toolkit. This is the second post in a series and maybe the most exciting one for me as this is my first Flutter application. The app retrieve the Items by invoking the backend REST API.
What is Flutter
Flutter is Google’s UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase. What excites everyone is that you can use the same codebase for both IOS and Android apps which helps Mobile developers to achieve a higher velocity and productivity. In was unveiled in 2015 at Dart developer summit and it reached 1.0 version on December 4th 2018.
Flutter architecture is composed by 3 major components:
- Dart Platform. Flutter runs in the Dart virtual machine which features a just-in-time execution engine. While writing and debugging an app, Flutter uses Just In Time compilation, allowing for “hot reload”, with which modifications to source files can be injected into a running application
- Flutter Engine, is written primarily in C++. It implements Flutter’s core libraries, including animation and graphics, file and network I/O, accessibility support, plugin architecture, and a Dart runtime and toolchain for developing, compiling, and running Flutter applications. Flutter’s engine takes core technologies, Skia, a 2D graphics rendering library, and Dart, a VM for a garbage-collected object-oriented language, and hosts them in a shell. Different platforms have different shells, for example we have shells for Android and iOS.
- Framework is composed by Foundation library and Widgets. The Foundation library is written in Dart, provides basic classes and functions which are used to construct applications using Flutter, such as APIs to communicate with the engine. Flutter development is all around the Widgets. The central idea is that you build your UI out of widgets. Widgets describe what their view should look like given their current configuration and state. The are tons of Widgets, check out here the official Widget Catalog.
The Inventory App
Let’s start and build the Inventory App, once completed the APP will look as in the pictures below. It has a main page which contains a list of items with bottom navigation bar to show a subset of Items . A Detail page, to list Item detailed information and a New Item page that allow you to add new items into the inventory.
Before starting you need an editor which support Flutter SDK, I’m using Visual Studio Code, but you can choose also Android Studio. You need an Android/IOS physical device or an Android/IOS emulator. Follow the installation guide for your operating system. Once installation completes, select “New Flutter Project” and automatically will populate the project with a bunch of folders and files. Android and ios folders to be able to compile the app for respective platforms, the lib folder where the app is developed, a test folder to run tests. The pubspec.yaml file is very important as it holds project dependencies.
Let’s start and define the model class. In the lib/models directory I created item.dart, where the Item fields and Item.fromJson() constructor is defined. Item.fromJson() is used to create a new Item instance from a map structure. As the items are perishable (like food, drinks etc…), I’m defining below fields but feel free to modify those for your need. Make sure you send the expected information to the backend. I’m describing inline the intent of each field.
class Item {
final String id; //the item ID, autogenerated
final String created; //created date, autogenerated
final String name; //name of the product
final String expDate; //expiration date
final int expOpen; //expiration in days, once opened
final String comment; //any comment about the product
final String targetAge; //can be consumed by "child" or "adult"
final bool isOpen; //is the item opened
final String opened; //date when was opened
final bool isValid; //is the item valid or has expired
final int daysValid; //how many days of validity
Item({
this.id,
this.created,
this.name,
this.expDate,
this.expOpen,
this.comment,
this.targetAge,
this.isOpen,
this.opened,
this.isValid,
this.daysValid});
factory Item.fromJson(Map<String, dynamic> json) {
return Item(
id: json["id"],
created: json["Created"],
name: json["name"],
expDate: json["expdate"],
expOpen: json["expopen"],
comment: json["comment"],
targetAge: json["targetage"],
isOpen: json["isopen"],
opened: json["opened"],
isValid: json["isvalid"],
daysValid: json["daysvalid"]
);
}
}
The block.dart it is the repository, where we define the getters and setters. Notice that the BlockItem class is extended with ChangeNotifier which is used to notify its listeners when the notifyListeners() is called. The ChangeNotifier class is part of foundation.dart package that can be extended or mixed in that provides a change notification API.
class BlockItem with ChangeNotifier {
List<Item> _items;
List<Item> get listitems => _items;
List<Item> get childitems =>
_items.where((item) => item.targetAge == "child").toList();
List<Item> get adultitems =>
_items.where((item) => item.targetAge == "adult").toList();
set listitems(List<Item> val) {
_items = val;
notifyListeners();
}
Flutter has mechanisms for widgets to provide data and services to their descendants. I’m using Provider, which is a package that provides dependency injection and state management solution to a Flutter App. There are other mechanisms available but I’m not going to cover those in this article. Provider seems to be the one recommended by Flutter team as well.
The package is based on 3 concepts, and these will be applied in the app, but first some theory around them:
- ChangeNotifier is a simple class included in the Flutter SDK which provides change notification to its listeners. ChangeNotifier is one way to encapsulate your application state. It has notifyListeners() method, we call it any time the model changes in a way that might change the app’s UI. Check out above, we used it to extend BlockItem class.
- ChangeNotifierProvider is the widget that provides an instance of a ChangeNotifier to its descendants. ChangeNotifierProvider has to be placed above the widgets that will need to access it. If you want to provide more than one class, you can use MultiProvider.
Provider.of<T>(BuildContext context)
is used to read the data. The method will look up in the widget tree starting from the widget associated with the BuildContext passed and it will return the nearest variable of type T found.
In order to use Provider package you must first add it in the pubspec.yaml file. Besides provider, there are also http which will be used to make the requests to the backend API and intl to convert the date.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
provider: ^3.1.0
http: ^0.12.0+2
intl: ^0.16.0
Before moving to UI, we define a function which makes a request to API and fetch all Items from the database. Here we decode the json and add into a List of Items.
Future<List<Item>> fetchItems() async {
http.Response response = await http.get("http://10.0.2.2:8080/json/");
if (response.statusCode == 200) {
var mapResponse = jsonDecode(response.body);
List items = mapResponse.cast<Map<String, dynamic>>();
List<Item> dataAll = items.map<Item>((json) {
return Item.fromJson(json);
}).toList();
listitems = dataAll;
return listitems;
} else {
throw Exception('Failed to load from the Internet');
}
}
Home Screen
In the main.dart, the runApp() function takes the given Widget and makes it the root of the widget tree. In this example, the widget tree is ItemsListApp which extends the StateLessWidget.
A widget is either stateful or stateless. If a widget can change, when a user interacts with it, for example, it’s stateful. A stateless widget never changes, whereas a stateful widget is dynamic: for example, it can change its appearance in response to events triggered by user interactions or when it receives data.
As the HomeScreen class need access to data fetched from the backend API, we define the ChangeNotifierProvider at the top of the application. CurrentTab is a simple class, which is used to set the state of the BottomNavigationBar widget.
class CurrentTab with ChangeNotifier {
int _currentTab = 0;
get currentTab => this._currentTab;
set currentTab(int value) {
this._currentTab = value;
notifyListeners();
}
}
void main() => runApp(ItemsListApp());
class ItemsListApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
builder: (context) => BlockItem(),
),
ChangeNotifierProvider(
builder: (context) => CurrentTab(),
)
],
child: MaterialApp(
title: 'Item List',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
},
),
);
}
}
HomeScreen is a StatefulWidget as we are implementing the didChangeDependencies() method. This method is called immediately after initState on the first time the widget is built. It will also be called whenever an object that this widget depends on data from is called, in this case when notifier change. Within the method I call the fetchItems function to retrieve data from the API. As the App need access to the Items, the Provider.of<BlockItem>(context)
is called within the build function. While loading the CircularProgressIndicator is displayed on the screen, and we use the ItemList class to display the Items.
In order to classify the items, like items for adult or child, I’m adding a navigation bar at the bottom. BottomNavigationBar is a material widget that’s displayed at the bottom of an app for selecting among a small number of views.
Also we need a way to add items to database, therefore I have added FloatingActionButton, which is a circular icon button that hovers over content to promote a primary action in the application.
class HomeScreen extends StatefulWidget {
HomeScreen({Key key}) : super(key: key);
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
BlockItem notifier;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final notifier = Provider.of<BlockItem>(context);
if (this.notifier != notifier) {
this.notifier = notifier;
Future.microtask(() => notifier.fetchItems());
}
}
@override
Widget build(BuildContext context) {
BlockItem blockitem = Provider.of<BlockItem>(context);
return Scaffold(
appBar: AppBar(
title: Text("Item List"),
),
body: blockitem.listitems == null
? Center(child: CircularProgressIndicator())
: ItemList(),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NewItem() ),
);
},
child: Icon(Icons.add),
),
bottomNavigationBar: BottomNav(),
);
}
}
ItemList is a widget which returns a ListView.builder constructor. This constructor is appropriate for list views with a large (or infinite) number of children because the builder is called only for those children that are actually visible. You have to provide the itemCount (the number of items in the list) and itemBuilder that creates the widget instances when called.
We read the data from both classes using Provider and display the items belong to a category based on the selected tab from the bottom.
ListTile is a convenient class to use as it is a single fixed-height row that contains one to three lines of text optionally flanked by icons or other widgets, the icons are defined with the leading and trailing parameters.
getValidityIcon returns a different icon based on targetAge and validity of the product, checkout the widget here.
class ItemList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final BlockItem blockitem = Provider.of<BlockItem>(context);
final CurrentTab tab = Provider.of<CurrentTab>(context);
List<Item> selectItem() {
List<Item> selectedList;
switch (tab.currentTab) {
case 0:
selectedList = blockitem.listitems;
break;
case 1:
selectedList = blockitem.childitems;
break;
case 2:
selectedList = blockitem.adultitems;
break;
}
return selectedList;
}
List<Item>selectedList = selectItem();
return ListView.builder(
itemCount: blockitem.listitems[tab.currentTab] == null
? 0
: selectedList.length,
itemBuilder: (BuildContext context, int index) {
return Card(
elevation: 2.0,
color: Colors.blue.shade50,
child: ListTile(
leading: getValidityIcon(
selectedList[index].isValid,
selectedList[index].targetAge),
title: Text(selectedList[index].name),
subtitle: Text(
"Expires at: ${selectedList[index].expDate}"),
trailing: Icon(
Icons.arrow_forward_ios,
color: Colors.green.shade400,
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ItemDetail(itemDetailed: selectedList[index]),
),
);
},
),
);
},
);
}
}
Item Detail Screen
ItemDetail is a simple StatelessWidget class which display all the information about the selected item, with the option to delete the item from the database. Once you push delete Item an AlertDialog pop up that confirms the intent to remove the item.
I’m not going to copy and paste the entire script here, as it is pretty long and you can check it out here, but I’ll show below how I’m invoking the deleteItem method which is part of BlockItem class.
class ItemDetail extends StatelessWidget {
final Item itemDetailed;
ItemDetail({@required this.itemDetailed});
@override
Widget build(BuildContext context) {
final productProvider = Provider.of<BlockItem>(context);
void confirm() {
AlertDialog alertDialog = new AlertDialog(
content: new Text("Are You sure want to delete '${itemDetailed.name}'"),
actions: <Widget>[
new RaisedButton(
child: new Text(
"OK DELETE!",
style: new TextStyle(color: Colors.black),
),
color: Colors.red,
onPressed: () async {
await productProvider.deleteItem(itemDetailed.id);
Navigator.of(context).push(new MaterialPageRoute(
builder: (BuildContext context) => HomeScreen(),
));
},
),
new RaisedButton(
child:
new Text("CANCEL", style: new TextStyle(color: Colors.black)),
color: Colors.green,
onPressed: () => Navigator.pop(context),
),
],
);
showDialog(context: context, builder: (_) => alertDialog);
}
return Scaffold(
appBar: AppBar(
title: Text('${itemDetailed.name}'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.delete_forever),
onPressed: () => confirm(),
),
],
),
................
An alert dialog informs the user about situations that require acknowledgement, if user confirm the deletion than it execute the deleteItem method.
Future deleteItem(String id) async {
final response = await http.get('http://10.0.2.2:8080/json/del?id=$id');
if (response.statusCode == 200) {
final responseBody = await json.decode(response.body);
return SnackBar(content: Text(responseBody));
} else {
throw Exception('Failed to delete the Item');
}
}
Add Item Screen
NewItem it is a StatefulWidget class that builds a Form Widget. I got inspired by this great article, Building Forms with Flutter, published on https://codingwithjoe.com blog site. He goes in detail and explain very well the concepts behind building a form in Flutter.
A couple things to note. The Form widget acts as a container for grouping and validating multiple form fields. When creating the form, provide a GlobalKey. This uniquely identifies the Form, and allows validation of the form in a later step.
The TextFormField widget renders a material design text field and can display validation errors when they occur. Validate the input by providing a validator() function to the TextFormField. If the user’s input isn’t valid, the validator function returns a String containing an error message. If there are no errors, the validator must return null.
Once Form is saved the name variable takes the value and it is added to the method which construct the Item and post it to the API.
String name;
final _formKey = GlobalKey<FormState>();
child: Form(
key: _formKey,
child: ListView(
children: <Widget>[
//Element Name
TextFormField(
decoration: InputDecoration(
labelText: 'Product Title',
hintText: 'Example Name',
),
validator: (val) => val.isEmpty ? 'Name is required' : null,
onSaved: (value) => name = value),
......................................
RaisedButton(
splashColor: Colors.red,
onPressed: () async {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
await productProvider.addItem(Item(
name: name,
............));
Besides the TextFormFiled, I’m using DropDown menu and DatePicker.
Dropdowns are essentially a DropDownButton widget that contains a list of items. Items are represented by one or more DropDownMenuItem widgets. DropDownButton is a generic type meaning it is built as DropDownButton<T>
where the generic type T must represent the type of items in your dropdown.
List<String> _openStates = <String>['', 'true', 'false'];
String _isOpen = '';
String openState = 'false';
FormField(
builder: (FormFieldState state) {
return InputDecorator(
decoration: InputDecoration(
labelText: 'Is it Open ?',
errorText: state.hasError ? state.errorText : null,
),
isEmpty: _isOpen == '',
child: new DropdownButtonHideUnderline(
child: new DropdownButton(
value: _isOpen,
isDense: true,
onChanged: (String newValue) {
setState(() {
openState = newValue;
_isOpen = newValue;
state.didChange(newValue);
});
},
items: _openStates.map((String value) {
return new DropdownMenuItem(
value: value,
child: new Text(value),
);
}).toList(),
),
),
);
},
validator: (val) {
return val != '' ? null : 'Please select a state';
},
)
Making a Date Picker is thoroughly covered in the “Building Forms with Flutter” tutorial. Below is the method used to call the API to insert a new item.
//Add a new Item
Future addItem(Item item) async {
final _headers = {'Content-Type': 'application/json'};
Map<dynamic, dynamic> mapData = toJson(item);
String newjson = json.encode(mapData);
final response = await http.post('http://10.0.2.2:8080/json/add', headers: _headers, body: newjson);
if (response.statusCode == 200) {
// final responseBody = await json.decode(response.body);
return SnackBar(content: Text(response.body));
} else {
throw Exception('Failed to update the Item. Error: ${response.toString()}');
}
}
Map<dynamic, dynamic> toJson(Item item) {
var mapData = new Map();
mapData["name"] = item.name;
mapData["expdate"] = item.expDate;
mapData["expopen"] = item.expOpen;
mapData["comment"] = item.comment;
mapData["targetage"] = item.targetAge;
mapData["isopen"] = item.isOpen;
mapData["opened"] = item.opened;
return mapData;
}
Conclusion
The complete code is available HERE. As this is the my first Mobile APP using Flutter, I can say that the learning curve is steep. But, once you get accommodated with the Dart basics and learn a handful of widgets the development is fast. Most important is that you don’t have to create separate repositories for Android and IOS.