By default most of what we do in Flutter is single threaded. This works fine for simple tasks, but complex tasks, or tasks that have lots of steps might cause the application to become “jerky”. Parsing a large amount of JSON data is an example of something that might cause an app to slow down, especially on a slower device.
Flutter provides a method to handle this however.
Building on our previous example where we got Post data from an Internet Server, we’re going to work on getting a list of posts, instead of a single post.
Making the Request
Depending upon your data, you will have a variety of ways to request data. Which one you use will depend upon the server you request from. One typical way is to have a URL which you access, however, you pass in an id as part of the URL if you want a specific item, or you don’t if you want the list.
For example:
// gets a single post, with an id of 1
https://jsonplaceholder.typicode.com/posts/1
// gets all posts
https://jsonplaceholder.typicode.com/posts/
Both of these will return a JSON set of data, but one returns a single post, while the second one returns a list of every post in the system. The system you use may implement limits to reduce issues with bandwidth and computing time on the server, but this is simple enough that it doesn’t.
The following is an example what will return approximately 5,000 items. (Which is why some systems will limit you.) This would definitely take a while to process.
Future<http.Response> fetchPhotos(http.Client client) async {
return client.get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));
}
This example only returns 100 items, so it should process faster, but it still may take time depending upon the size of the data.
Future<http.Response> fetchPhotos(http.Client client) async {
return client.get(Uri.parse('https://jsonplaceholder.typicode.com/poss'));
}
Note: We’re using an http.Client
to the function in this example. This makes the function easier to test and use in different environments.
Parsing the Large(r) Data Set
Since we are building upon our previous example, we already have our Post class to use, and we have a fetchPost()
function. However, we need to add a new function or two to fetch all of the posts, such as fetchPosts()
. The fetchPosts() will call parsePosts().
// A function that converts a response body into a List<Post>.
List<Post> parsePosts(String responseBody) {
final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<Post>((json) => Post.fromJson(json)).toList();
}
// called to get the JSON of posts from the server
Future<List<Post>> fetchPosts(http.Client client) async {
final response = await client
.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
return parsePosts(response.body);
}
Move Process to a Separate Thread
Running this on a slower device may cause the app to stutter and provide a poor user experience.
The solution is to use the compute()
method to a separate background isolate. This allows the function passed to compute to run in the background and not slow down the main app.
Now isolates work by sending messages back and forth. These messages can be primitive data types, and even lists, however more complex data types can cause issues.
To implement the isolate, we will simple add computer to the return statement in fetchPosts, like you see below.
// called to get the JSON of posts from the server
Future<List<Post>> fetchPhotos(http.Client client) async {
final response =
await client.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
// Use the compute function to run parsePosts in a separate isolate.
return compute(parsePosts, response.body);
}
We will also need to add an additional library to be able to use compute.
import 'package:flutter/foundation.dart';
Building the List to Display
Now we have built the list, we need to display it.
In our previous example, we had a single piece of data, but now we need to display a list of data. So we’re going to build a custom list Widget, like we did with our RandomName generator example.
class PostsList extends StatelessWidget {
const PostsList({Key? key, required this.posts}) : super(key: key);
final List<Post> posts;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
return Text(posts[index].title);
},
);
}
}
We’re going to call this widget within our FutureBuilder.
body: Center(
child: FutureBuilder<List<Post>>(
future: fetchPosts(http.Client()),
builder: (context, snapshot) {
if (snapshot.hasData) {
return PostsList(posts: snapshot.data!);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
// By default, show a loading spinner.
return const CircularProgressIndicator();
},
),
)
This will display each post title as a list item on the list.
If we save and reload, we can make this list to titles appear.
Formatting the List
The problem, is this content is crammed together, making it hard to read. In a previous example, we built a list which was better formatted. We can clean up the view by using this concept to display the list better.
This would make our ListBuild code look like:
Widget build(BuildContext context) {
return ListView.builder(
itemCount: posts.length,
padding: const EdgeInsets.all(16.0),
itemBuilder: (context, index) {
if (index.isOdd) return const Divider();
final itemIndex = index ~/ 2;
return Text(posts[itemIndex].title);
},
);
}
Notice we add padding to the elements, and provide a separator bar (Divider
) to make it easier to read the titles. If you Save and do a Hot Reload, you can see how much nicer the presentation of data is.
Linking to a Detail Screen
Creating a Detail Screen
Now, if you want to turn your list titles into into links which you can use to navigate to page which has the whole content displayed on it, not just the title, like when we started.
To do that we’ll first create a new screen (route
), which will be used to display the completed post.
class PostScreen extends StatelessWidget {
const PostScreen({Key? key}) : super(key: key);
static const routeName = '/post';
@override
Widget build(BuildContext context) {
// Extract the arguments from the current ModalRoute settings and
// cast them as ScreenArguments.
final args = ModalRoute.of(context)!.settings.arguments as Post;
return Scaffold(
appBar: AppBar(
title: Text("Single Post Display"),
),
body: Center(
child: Column(
children: <Widget>[
Text(
args.title,
style: Theme.of(context).textTheme.headline3,
),
Text(args.body)
],
),
),
);
}
}
Notice that we have a Post class already, so we’re going to pass that in as the argument. Now to set this up, we’re going to set up named routes.
Setting up Named Routes
So up in the main function we’re going to replace home with a series of routes, and an initialRoute. This is the same method that we used when we first looked at setting up Named Routes.
Widget build(BuildContext context) {
return MaterialApp(
title: 'External Internet Data Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
routes: {
// When navigating to the "/" route, go to the MyHomePage widget.
'/': (context) => MyHomePage(title: 'External Internet Data Demo'),
// When navigating to the "/post" route, go to the PostScreen widget.
// "/post" is defined in the PostScreen widget
PostScreen.routeName: (context) => const PostScreen(),
},
);
}
Now that the routes are setup, we’re going to create a link for each item in the list.
Creating a Link from our List
Right now in our ListView, we are putting a child of Text()
. While this works to display text, it is not “tappable”. So we’re going to wrap it within a ListTile()
. This also allows us to add icons, and other features if we want to in the future, but at this point we’re just going to add a onTap
method.
return ListTile(
title: Text(posts[itemIndex].title),
onTap: () {
Navigator.pushNamed(
context,
PostScreen.routeName,
arguments: posts[itemIndex],
);
},
);
Notice the title attribute is the Text widget, and then we have the onTap method, just like when we built the Name Generator with Favorites. And we are passing in arguments, like we manually did in our Navigating with Arguments previous example.
Since we have an array of Posts already, and we want to display the current post, we will just past the current Post as the argument. Doing so will allow the information to be passed.
Save the data, and do a hot reload, and you have a link on each title which takes you to a post detail page which you can read.
The back button is built in, so we don’t even have to add code or widgets for that.
Getting a List of Data from the Internet with Flutter was originally found on Access 2 Learn