In previous blogs we saw very basic things with flutter, now going forward i would like to take this forward by developing a real app for my personal project and use flutter for it.
Drawer
Let’s see how to implement AppDrawer as it is something which i need in the app.
It turns out its quite easy just follow this guide https://flutter.dev/docs/cookbook/design/drawer
Code Splitting
Next i want to show some content on the app similar to this
For this i need to make a separate widget. At this stage its better to split code in multiple files. Since the app will be quite big we cannot just keep all in one single main.dart file.
The way i will split it is
lib/screens == this will have all the main screens or pages
lib/widgets == this will have the different widgets used in the pages
lib/main.dart
This is how the code looks now
and our home.dart looks like this
import 'package:flutter/material.dart';
import "package:myapp/widget/home/amc.dart";
import 'package:myapp/widget/drawer.dart';
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(child: AMCSummary()),
drawer: AppDrawer(),
);
}
}
This way we are able to split the entire code base in more readable files
Data (JSON)
Most of the mobile app’s requires interaction with rest api and json data.
Even in the above widget we made, we need data from rest api. Let’s see by example how to parse JSON data first.
You can also read about it here https://flutter.dev/docs/development/data-and-backend
So to start let’s start with basic json parsing.
FlatButton(
child: Text("Test JSON"),
onPressed: () {
String jsonString =
"{\"name\": \"John Smith\",\"email\": \"[email protected]\"}";
Map<String, dynamic> user = jsonDecode(jsonString);
print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');
},
)
We added a “FlatButton” to our app and onPress did a print.
As dart is a strongly typed language this is not a very effective way of doing this. We should define a model class and map the json to a model class object.
First we define a User object like this
//model/user.dart
class User {
final String name;
final String email;
User(this.name, this.email); //constructor
User.fromJson(Map<String, dynamic> json)
: name = json["name"],
email = json["email"] {
print("fromJSON Called"); //just for debugging
}
Map<String, dynamic> toJson() => {"name": name, "email": email};
}
Since we are new to dart language, let’s see what this means exactly
Both User() and User.fromJson are constructors in dart read here https://dart.dev/guides/language/language-tour#constructors
https://dart.dev/guides/language/language-tour#final-and-const
In our code we include the file model/user.dart
FlatButton(
child: Text("Test JSON"),
onPressed: () {
String jsonString =
"{\"name\": \"John Smith\",\"email\": \"[email protected]\"}";
User user = User.fromJson(jsonDecode(jsonString));
print('Howdy, ${user.name}!');
print('We sent the verification link to ${user.email}.');
},
See full code here https://github.com/excellencetechnologies/flutter_start/commit/544b474354a4de829e01ad5a7978bbcb1bf71826
API Call
https://flutter.dev/docs/cookbook/networking/fetch-data
Read above how to do it and let’s see it in practice in our app.
First before understanding api calls, we need to understand the “Future” object in dart. Future object is used for doing any async operation and is managed via keywords “async”, “await” https://dart.dev/codelabs/async-await read about it in detail here.
Let’s use it in our code
Future<Post> fetchPost() async{
final response = await http
.get('https://jsonplaceholder.typicode.com/posts/1');
if (response.statusCode == 200) {
// If server returns an OK response, parse the JSON.
return Post.fromJson(json.decode(response.body));
} else {
// If that response was not OK, throw an error.
throw Exception('Failed to load post');
}
}
Post post = await fetchPost();
print(post.id);
For api’s we are using ” https://jsonplaceholder.typicode.com/ ” this is a popular website to try out different api’s
So basically we fire the api check if response is 200 i.e success. If yes, return the Post object, else we throw Exception.
Next, we call the function. Do note we are using “await” keyword when calling the function. Because we are calling the “await” function we are able to get Post object and not the Future
Also its best practice to move all api’s called to “services” folder in a separate file.
We define a service/post.dart
//services/post.dart
import 'package:myapp/model/post.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class PostService {
static Future<Post> fetchPost() async {
final response =
await http.get('https://jsonplaceholder.typicode.com/posts/1');
if (response.statusCode == 200) {
// If server returns an OK response, parse the JSON.
return Post.fromJson(json.decode(response.body));
} else {
// If that response was not OK, throw an error.
throw Exception('Failed to load post');
}
}
}
// our component
FlatButton(
child: Text("Test JSON"),
onPressed: () async {
Post post = await PostService.fetchPost();
print(post.id);
},
)
Cool! Next, lets see how to display this data in our Widget.
To show this data in our widget, we need a “statefull” widget
This is a sample widget for this
class MyDynamicData extends StatefulWidget {
@override
State<MyDynamicData> createState() {
return MyDynamicDataState();
}
}
class MyDynamicDataState extends State<MyDynamicData> {
String textToDisplay = "";
bool showLoader = false;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
showLoader ? CircularProgressIndicator() : Container(),
Text("$textToDisplay "),
FlatButton(
child: Text("Test JSON"),
onPressed: () async {
setState(() {
showLoader = true;
textToDisplay = "";
});
Post post = await PostService.fetchPost();
print(post.id);
setState(() {
showLoader = false;
textToDisplay = post.title;
});
},
)
],
);
}
}
Flutter comes with an out-of-box widget called FutureBuilder, https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html this is useful to make simple components with async features. This is mainly for very simple purposes
Also read this once https://flutter.dev/docs/cookbook/networking/fetch-data#5-moving-the-fetch-call-out-of-the-build-method this is relevant in cases when we want data to load automatically without button tap etc.
List View
Another important thing we use in app’s a lot is List (scrollable list).
In my app i need to show a List (data) after fetching the data from api. Let’ see how i have implemented it. But before doing see make sure to go through this https://flutter.dev/docs/cookbook#lists
This has examples of horizontal lists, grids, and other common uses for list view.
class AMCList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new FutureBuilder<List<AMC>>(
future: AMCService.getAMCList(),
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
List<AMC> amcs = snapshot.data;
return ListView.builder(
itemCount: amcs.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('${amcs[index].name}'),
);
});
},
);
}
}
One thing is very critical, listView needs a parent element which provides it a size. So either use it in body directly else if you are using it row, column make sure to use expanded or flexible etc and provide a height else you will get an error like this https://stackoverflow.com/questions/52801201/flutter-renderbox-was-not-laid-out
Routing
There is a very awesome guide by flutter on routing https://flutter.dev/docs/development/ui/navigation there is really nothing for me to explain on this. Just read the guide.
I will try to implement routing in the current app we have. I want to implement a feature in which when i click on an element in my ListView, i am able to see details about it.
First i will setup a new blank screen called “AMCScreen”
import 'package:flutter/material.dart';
import 'package:myapp/widget/drawer.dart';
class AMCScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('AMC'),
),
body: Center(
child: Text("AMC Page"),
) ,
drawer: AppDrawer(),
);
}
}
Next i add this screen to my route
import 'package:flutter/material.dart';
import 'package:myapp/screens/home.dart';
import 'package:myapp/screens/amc.dart';
void main() => runApp(MaterialApp(
title: 'My First Flutter App',
theme: ThemeData.light(),
initialRoute: "/",
routes: {
'/': (context) => HomeScreen(),
'/amc': (context) => AMCScreen()
}
));
Then in my listView i do this
return ListTile(
onTap: () {
print("tapped");
Navigator.pushNamed(context, '/amc');
},
title: Text('${amcs[index].name}'),
)
So when i click on ListView it takes me a new page and works!
See code here https://github.com/excellencetechnologies/flutter_start/commit/a63a9f7e6cd4f0062de47a4276ad1cac9357d381
Next, we need to pass AMC details to new screen to fetch data about the AMC.
The screen AMC requires AMC object to work. Without that it cannot work at all, so to make this happen we create AMCScreen object like this
import 'package:flutter/material.dart';
import 'package:myapp/widget/drawer.dart';
import 'package:myapp/model/amc.dart';
class AMCScreen extends StatelessWidget {
final String routeName = "/amcScreen";
final AMC amc;
AMCScreen({Key key, @required this.amc}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(amc.name),
),
body: Center(
child: Text(amc.name),
) ,
drawer: AppDrawer(),
);
}
}
Note how we have “amc” variable as required in the constructor. Also inline with this we need to remove AMCScreen() from the route object because we don’t have an amc object in routes.
Next in our ListView we make this change
return ListTile(
onTap: () {
print("tapped");
Navigator.push(context, MaterialPageRoute(
builder: (context) => AMCScreen(amc: amcs[index],)
));
},
title: Text('${amcs[index].name}'),
);
This means we are directly push the new screen to navigator.
Also on the AMCScreen we need a back button so we add it like this
appBar: AppBar(
automaticallyImplyLeading: true,
leading: IconButton(icon:Icon(Icons.arrow_back),
onPressed:() => Navigator.pop(context, false),
),
title: Text(amc.name),
)
current state of code looks like this https://github.com/excellencetechnologies/flutter_start/commit/f97b070bb2cbd271bc2ba4bcbcd4191db846662a
Next on the new screen, we need to fire api based on amc_id and get further detail to display on the screen.
To start with lets create a Statefull widget
class AMCFetchFund extends StatefulWidget {
final AMC amc;
AMCFetchFund({Key key, @required this.amc}) : super(key: key);
@override
_AMCFetchFundState createState() => new _AMCFetchFundState();
}
First notice how i have made this.amc @required and passing amc object to it. But then also notice, the State (AMCFetchFundState) i am not passing amc object??
class _AMCFetchFundState extends State<AMCFetchFund> {
List<Fund> funds;
@override
void initState() async {
super.initState();
List<Fund> newfunds = await AMCService.getFunds(widget.amc);
//.....
setState(() {
funds = newfunds;
});
}
.....//
Ok, lets pause here. First notice “widgets.amc” is used access the amc object! Second the above code give an error. It turn’s out we cannot use async function inside initState() or setState() at all!
https://stackoverflow.com/questions/49135632/flutter-best-practices-of-calling-async-codes-from-ui
So correct way to do this is
class _AMCFetchFundState extends State<AMCFetchFund> {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: AMCService.getFunds(widget.amc),
builder: (BuildContext context, AsyncSnapshot<List<Fund>> snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('${snapshot.data[index].name}'),
);
});
} else {
return CircularProgressIndicator();
}
},
);
}
}
after doing this, we are able to see all data of the amc.
Let’s continue further in next blog post