Connecting APIs with Flutter UI
Connecting APIs with the Flutter UI means integrating a Flutter application's user interface with data and services provided by a backend server. A typical application sends an HTTP request, receives a response from an API, converts JSON data into Dart objects, manages asynchronous states, and displays the result using Flutter widgets.
Flutter provides the http package for common HTTP networking tasks, while widgets such as FutureBuilder can be used to connect asynchronous API results to the UI.
1. What is an API?
API stands for Application Programming Interface. An API allows a Flutter application to communicate with a backend server or another service.
For example, a mobile application may use APIs to:
- Fetch users.
- Load products.
- Display news.
- Fetch weather information.
- Login users.
- Register users.
- Create orders.
- Update profile information.
- Upload or download data.
- Delete records.
2. Flutter UI and API Connection
The Flutter UI is responsible for presenting information to the user, while API-related code communicates with the backend.
Flutter UI
↓
User Interaction
↓
API Service
↓
HTTP Request
↓
Backend API
↓
JSON Response
↓
Dart Model
↓
State Update
↓
Flutter UI
3. Why Connect APIs with Flutter?
- To display dynamic data instead of hardcoded information.
- To synchronize the application with a backend database.
- To allow users to create and update server-side records.
- To implement authentication.
- To display real-time or frequently changing information.
- To build production-level mobile applications.
4. Types of API Operations
| HTTP Method |
Common Purpose |
| GET |
Retrieve data |
| POST |
Create or submit data |
| PUT |
Update existing data |
| PATCH |
Partially update data |
| DELETE |
Remove data |
5. Adding the HTTP Package
The http package is commonly used to make HTTP requests from Flutter.
flutter pub add http
Then import it into your Dart file:
import 'package:http/http.dart' as http;
Flutter provides networking documentation covering HTTP requests and common networking patterns. Flutter Networking Documentation
6. Adding JSON Support
Most REST APIs return JSON data. Dart provides JSON conversion through dart:convert.
import 'dart:convert';
The main functions are:
jsonDecode() converts JSON text into Dart data.
jsonEncode() converts Dart data into JSON text.
7. Example API Response
Suppose an API returns the following JSON:
{
"id": 1,
"name": "John Doe",
"email": "[email protected]"
}
This response can be converted into a Dart model and displayed in the Flutter UI.
8. Creating a Dart Model
A model class represents the structure of API data in Dart. Models make API data easier to work with because the application can use strongly typed Dart objects instead of accessing raw JSON maps throughout the UI.
class User {
final int id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
}
9. Creating an API Service
Instead of putting HTTP requests directly inside UI widgets, create a service class that handles communication with the API.
class UserService {
Future<User> fetchUser() async {
final response = await http.get(
Uri.parse('https://example.com/api/users/1'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
return User.fromJson(data);
}
throw Exception('Failed to load user');
}
}
10. Understanding Future
Network operations are asynchronous. The application cannot know exactly when the server will respond, so Dart uses Future to represent a value that becomes available later.
Future<User> fetchUser() async {
// API request
}
The async keyword marks a function as asynchronous, while await waits for a Future to complete without blocking the application from continuing its asynchronous work.
11. Understanding async and await
Future<User> fetchUser() async {
final response = await http.get(
Uri.parse('https://example.com/api/users/1'),
);
// Continue processing after the response arrives.
}
Using async and await makes asynchronous API code easier to read and maintain.
12. Checking the API Response
Always check the HTTP status code before processing the response as successful data.
if (response.statusCode == 200) {
// Process successful response.
} else {
throw Exception('Failed to load data');
}
13. Common HTTP Status Codes
| Status Code |
Meaning |
| 200 |
Request successful |
| 201 |
Resource created successfully |
| 204 |
Request successful with no response body |
| 400 |
Bad request |
| 401 |
Unauthorized |
| 403 |
Forbidden |
| 404 |
Resource not found |
| 500 |
Internal server error |
14. Connecting the API to Flutter UI
Once the API service is ready, the result needs to be connected to a Flutter widget. FutureBuilder is useful for this purpose because it builds its UI according to the current state of a Future.
FutureBuilder<User>(
future: userService.fetchUser(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final user = snapshot.data!;
return Text(user.name);
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return const CircularProgressIndicator();
},
)
15. FutureBuilder States
| State |
UI Example |
| Loading |
CircularProgressIndicator |
| Success |
API data widgets |
| Error |
Error message |
| Empty |
No data message |
16. Why FutureBuilder is Useful
- Works naturally with asynchronous API operations.
- Provides access to the current Future state.
- Makes loading states easy to display.
- Makes error handling easy to implement.
- Allows successful API data to be rendered directly in the widget tree.
Flutter provides FutureBuilder as an asynchronous UI widget for building UI based on a Future. Flutter Async Widgets
17. Complete Single API Object Example
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class User {
final int id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
}
Future<User> fetchUser() async {
final response = await http.get(
Uri.parse('https://example.com/api/users/1'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
return User.fromJson(data);
}
throw Exception('Failed to load user');
}
class UserScreen extends StatefulWidget {
const UserScreen({super.key});
@override
State<UserScreen> createState() => _UserScreenState();
}
class _UserScreenState extends State<UserScreen> {
late Future<User> futureUser;
@override
void initState() {
super.initState();
futureUser = fetchUser();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('User Details'),
),
body: FutureBuilder<User>(
future: futureUser,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'),
);
}
if (!snapshot.hasData) {
return const Center(
child: Text('No user data'),
);
}
final user = snapshot.data!;
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user.name,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Text('ID: ${user.id}'),
Text('Email: ${user.email}'),
],
),
);
},
),
);
}
}
18. Important: Do Not Call API Directly in build()
A common mistake is creating a new API Future directly inside the build() method.
For example, avoid repeatedly creating requests like this:
FutureBuilder<User>(
future: fetchUser(),
builder: (context, snapshot) {
// UI
},
)
When a widget rebuilds, the Future may be recreated, which can result in another API request. A common approach is to initialize the Future in initState() and reuse it.
Recommended Approach
late Future<User> futureUser;
@override
void initState() {
super.initState();
futureUser = fetchUser();
}
Then use the stored Future:
FutureBuilder<User>(
future: futureUser,
builder: (context, snapshot) {
// UI
},
)
19. Connecting a List API with Flutter UI
Most applications need to display multiple API records. Suppose the API returns:
[
{
"id": 1,
"name": "John",
"email": "[email protected]"
},
{
"id": 2,
"name": "Jane",
"email": "[email protected]"
},
{
"id": 3,
"name": "Mike",
"email": "[email protected]"
}
]
20. Fetching a List from the API
Future<List<User>> fetchUsers() async {
final response = await http.get(
Uri.parse('https://example.com/api/users'),
);
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body) as List<dynamic>;
return data
.map(
(item) => User.fromJson(
item as Map<String, dynamic>,
),
)
.toList();
}
throw Exception('Failed to load users');
}
21. Displaying API List with ListView.builder
FutureBuilder<List<User>>(
future: futureUsers,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'),
);
}
final users = snapshot.data ?? [];
if (users.isEmpty) {
return const Center(
child: Text('No users found'),
);
}
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
leading: CircleAvatar(
child: Text(user.name[0]),
),
title: Text(user.name),
subtitle: Text(user.email),
);
},
);
},
)
22. Connecting API Data to Cards
API records can be displayed in cards for a more structured interface.
Card(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user.name,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(user.email),
Text('User ID: ${user.id}'),
],
),
),
)
23. Connecting API Data to GridView
Product catalogs, photo galleries, and dashboards can use GridView.builder.
GridView.builder(
padding: const EdgeInsets.all(12),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return Card(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.person,
size: 45,
),
const SizedBox(height: 8),
Text(user.name),
Text(user.email),
],
),
),
);
},
)
24. Connecting API Images to Flutter UI
If the API returns an image URL, Image.network can display the image.
Image.network(
user.profileImage,
width: 100,
height: 100,
fit: BoxFit.cover,
)
Image Loading State
Image.network(
user.profileImage,
loadingBuilder: (
context,
child,
loadingProgress,
) {
if (loadingProgress == null) {
return child;
}
return const CircularProgressIndicator();
},
)
Image Error State
Image.network(
user.profileImage,
errorBuilder: (
context,
error,
stackTrace,
) {
return const Icon(
Icons.broken_image,
);
},
)
25. Connecting API Data to a Detail Screen
A common UI pattern is to show API records in a list and open a detail page when the user taps a record.
ListTile(
title: Text(user.name),
subtitle: Text(user.email),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
UserDetailScreen(user: user),
),
);
},
)
User Detail Screen
class UserDetailScreen extends StatelessWidget {
final User user;
const UserDetailScreen({
super.key,
required this.user,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(user.name),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user.name,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text('ID: ${user.id}'),
Text('Email: ${user.email}'),
],
),
),
);
}
}
26. Connecting Forms with POST APIs
API integration is not limited to retrieving data. Flutter UI forms can collect user input and send it to an API using a POST request.
Future<User> createUser(
String name,
String email,
) async {
final response = await http.post(
Uri.parse('https://example.com/api/users'),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode({
'name': name,
'email': email,
}),
);
if (response.statusCode == 201) {
final data = jsonDecode(response.body)
as Map<String, dynamic>;
return User.fromJson(data);
}
throw Exception('Failed to create user');
}
Flutter's networking recipes demonstrate using http.post() and JSON encoding for sending data. Flutter Send Data Documentation
27. Connecting TextField with an API
A TextField can collect data that will be sent to an API.
final TextEditingController nameController =
TextEditingController();
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Enter Name',
),
)
Use the entered value when the user presses a button:
ElevatedButton(
onPressed: () {
createUser(
nameController.text,
emailController.text,
);
},
child: const Text('Create User'),
)
28. Displaying POST Response in the UI
FutureBuilder<User>(
future: futureUser,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (snapshot.hasData) {
return Text(
'Created: ${snapshot.data!.name}',
);
}
return const SizedBox();
},
)
29. Updating API Data from Flutter UI
An edit screen can retrieve an existing record, allow the user to modify it, and then send an update request to the API.
Future<User> updateUser(
int id,
String name,
String email,
) async {
final response = await http.put(
Uri.parse(
'https://example.com/api/users/$id',
),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode({
'name': name,
'email': email,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body)
as Map<String, dynamic>;
return User.fromJson(data);
}
throw Exception('Failed to update user');
}
30. Deleting Data from Flutter UI
A delete button can trigger an HTTP DELETE request.
Future<void> deleteUser(int id) async {
final response = await http.delete(
Uri.parse(
'https://example.com/api/users/$id',
),
);
if (response.statusCode != 200 &&
response.statusCode != 204) {
throw Exception('Failed to delete user');
}
}
31. Refreshing API Data
After creating, updating, or deleting data, the UI may need to reload the API data.
setState(() {
futureUsers = fetchUsers();
});
This creates a new Future and allows the UI to rebuild with updated information.
32. Pull-to-Refresh
RefreshIndicator can be used to allow users to manually refresh API data.
RefreshIndicator(
onRefresh: () async {
setState(() {
futureUsers = fetchUsers();
});
await futureUsers;
},
child: ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(users[index].name),
);
},
),
)
33. Handling Loading, Success, Empty, and Error States
A professional API-driven UI should provide clear feedback for each state.
FutureBuilder<List<User>>(
future: futureUsers,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return const Center(
child: Text('Unable to load users'),
);
}
final users = snapshot.data ?? [];
if (users.isEmpty) {
return const Center(
child: Text('No users available'),
);
}
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(users[index].name),
subtitle: Text(users[index].email),
);
},
);
},
)
34. Creating a Reusable API State UI
For larger projects, loading, error, empty, and success states can be represented using reusable widgets.
class LoadingView extends StatelessWidget {
const LoadingView({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
}
}
class EmptyView extends StatelessWidget {
const EmptyView({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text('No data available'),
);
}
}
class ErrorView extends StatelessWidget {
final String message;
const ErrorView({
super.key,
required this.message,
});
@override
Widget build(BuildContext context) {
return Center(
child: Text(message),
);
}
}
35. Separating UI and API Logic
A clean Flutter application should avoid putting all networking code inside widgets.
lib/
├── models/
│ └── user.dart
├── services/
│ └── user_service.dart
├── screens/
│ └── user_screen.dart
├── widgets/
│ └── user_card.dart
└── main.dart
Recommended Responsibilities
| Layer |
Responsibility |
| Model |
Represent API data |
| Service |
Make HTTP requests |
| Repository |
Coordinate and provide data sources |
| ViewModel/Controller |
Manage UI state and application logic |
| Widget/View |
Display information and handle UI interaction |
36. API Service Example
class UserService {
Future<List<User>> getUsers() async {
final response = await http.get(
Uri.parse('https://example.com/api/users'),
);
if (response.statusCode != 200) {
throw Exception('Failed to fetch users');
}
final List<dynamic> jsonList =
jsonDecode(response.body) as List<dynamic>;
return jsonList
.map(
(item) => User.fromJson(
item as Map<String, dynamic>,
),
)
.toList();
}
}
37. ViewModel-Style API Integration
For larger applications, a ViewModel or another state-management layer can sit between the service and the UI.
class UserViewModel extends ChangeNotifier {
final UserService service;
UserViewModel(this.service);
List<User> users = [];
bool isLoading = false;
String? errorMessage;
Future<void> loadUsers() async {
isLoading = true;
errorMessage = null;
notifyListeners();
try {
users = await service.getUsers();
} catch (e) {
errorMessage = e.toString();
} finally {
isLoading = false;
notifyListeners();
}
}
}
38. UI Using ViewModel State
if (viewModel.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (viewModel.errorMessage != null) {
return Center(
child: Text(viewModel.errorMessage!),
);
}
if (viewModel.users.isEmpty) {
return const Center(
child: Text('No users found'),
);
}
return ListView.builder(
itemCount: viewModel.users.length,
itemBuilder: (context, index) {
final user = viewModel.users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
);
},
);
39. Authentication with APIs
Many APIs require authentication. An authentication token is commonly sent through an HTTP header.
final response = await http.get(
Uri.parse('https://example.com/api/profile'),
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'Accept': 'application/json',
},
);
Flutter's networking documentation demonstrates adding authorization information to HTTP headers when accessing protected services. Flutter Authenticated Requests
40. Handling Authentication Errors
if (response.statusCode == 401) {
throw Exception(
'Session expired. Please login again.',
);
}
The UI can respond by showing a login screen or an appropriate authentication message.
41. Using Uri for API URLs
Instead of manually concatenating complex URLs, Dart's Uri APIs can be used to construct URLs safely, especially when query parameters are involved.
final uri = Uri.https(
'example.com',
'/api/users',
{
'page': '1',
'limit': '20',
'search': 'john',
},
);
final response = await http.get(uri);
42. API Search from Flutter UI
A search field can send the entered keyword to an API.
Future<List<User>> searchUsers(
String keyword,
) async {
final uri = Uri.https(
'example.com',
'/api/users',
{
'search': keyword,
},
);
final response = await http.get(uri);
if (response.statusCode != 200) {
throw Exception('Search failed');
}
final List<dynamic> data =
jsonDecode(response.body) as List<dynamic>;
return data
.map(
(item) => User.fromJson(
item as Map<String, dynamic>,
),
)
.toList();
}
43. Handling Nested API Data
API responses may contain nested objects and arrays.
{
"id": 1,
"name": "John",
"address": {
"city": "Mumbai",
"country": "India"
},
"orders": [
{
"id": 101,
"product": "Laptop"
},
{
"id": 102,
"product": "Phone"
}
]
}
Nested data should be represented using separate Dart model classes when the structure is complex.
44. Displaying Nested Data
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Name: ${user.name}'),
Text('City: ${user.address.city}'),
Text('Orders: ${user.orders.length}'),
],
)
45. Handling Null API Values
External API data should not always be assumed to be complete. A field may be missing or have a null value.
class User {
final int id;
final String name;
final String? phone;
User({
required this.id,
required this.name,
this.phone,
});
factory User.fromJson(
Map<String, dynamic> json,
) {
return User(
id: json['id'] as int,
name: json['name'] as String,
phone: json['phone'] as String?,
);
}
}
Display it safely:
Text(
user.phone ?? 'Phone not available',
)
46. Handling JSON Parsing Errors
If an API returns invalid JSON, decoding can fail. Handle parsing errors instead of allowing the application to fail unexpectedly.
try {
final data = jsonDecode(response.body);
return User.fromJson(
data as Map<String, dynamic>,
);
} on FormatException {
throw Exception(
'Invalid JSON response',
);
}
47. Handling Network Exceptions
try {
final response = await http.get(
Uri.parse('https://example.com/api/users'),
);
if (response.statusCode != 200) {
throw Exception('Server error');
}
} catch (e) {
throw Exception(
'Network request failed: $e',
);
}
48. Displaying a User-Friendly Error
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 50,
),
const SizedBox(height: 12),
const Text(
'Unable to load data',
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () {
setState(() {
futureUsers = fetchUsers();
});
},
child: const Text('Retry'),
),
],
),
);
}
49. API Data and UI State
The UI should represent the current state of the API operation.
API Request
↓
Loading State
↓
Server Response
↓
┌───────────────┐
│ │
Success Error
│ │
↓ ↓
Data UI Error UI
│
↓
Empty or Data
50. Complete Architecture Example
lib/
├── models/
│ └── user.dart
│
├── services/
│ └── user_service.dart
│
├── viewmodels/
│ └── user_view_model.dart
│
├── screens/
│ ├── user_screen.dart
│ └── user_detail_screen.dart
│
├── widgets/
│ ├── user_card.dart
│ ├── loading_view.dart
│ └── error_view.dart
│
└── main.dart
51. Complete API-to-UI Flow
User opens screen
↓
initState()
↓
Call API Service
↓
HTTP GET Request
↓
API Server
↓
JSON Response
↓
Check Status Code
↓
jsonDecode()
↓
Model.fromJson()
↓
Future completes
↓
FutureBuilder/ViewModel
↓
Update UI
↓
ListView/Card/Text/Image
↓
User sees API data
52. API Integration with Loading State
class UserScreen extends StatefulWidget {
const UserScreen({super.key});
@override
State<UserScreen> createState() => _UserScreenState();
}
class _UserScreenState extends State<UserScreen> {
late Future<List<User>> futureUsers;
@override
void initState() {
super.initState();
futureUsers = fetchUsers();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Users'),
),
body: FutureBuilder<List<User>>(
future: futureUsers,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return const Center(
child: Text('Unable to load users'),
);
}
final users = snapshot.data ?? [];
if (users.isEmpty) {
return const Center(
child: Text('No users found'),
);
}
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return Card(
margin: const EdgeInsets.all(8),
child: ListTile(
leading: CircleAvatar(
child: Text(user.name[0]),
),
title: Text(user.name),
subtitle: Text(user.email),
),
);
},
);
},
),
);
}
}
53. Performance Considerations
- Avoid making unnecessary API requests.
- Do not create API Futures repeatedly during widget rebuilds.
- Use
ListView.builder for large lists.
- Use pagination for very large datasets.
- Cache data where appropriate.
- Separate networking logic from UI code.
- Use background JSON parsing for sufficiently large JSON responses when appropriate.
Flutter provides a recipe for moving large JSON parsing work to a separate isolate when appropriate. Flutter Background JSON Parsing
54. Pagination with Flutter UI
When an API contains thousands of records, loading everything at once may be inefficient. Pagination allows the application to load a smaller number of records at a time.
GET /api/users?page=1&limit=20
When the user reaches the end of the list:
GET /api/users?page=2&limit=20
The UI can append the new results to the existing list.
55. Refreshing After CRUD Operations
After creating, updating, or deleting an item, the UI may need to retrieve the latest API data.
Future<void> refreshUsers() async {
setState(() {
futureUsers = fetchUsers();
});
await futureUsers;
}
56. Best Practices for Connecting APIs with Flutter UI
- Keep API calls outside the UI whenever practical.
- Create model classes for API responses.
- Use a dedicated service or repository layer.
- Use
async and await for asynchronous operations.
- Check HTTP status codes.
- Handle loading, success, empty, and error states.
- Do not unnecessarily create API requests inside
build().
- Use
FutureBuilder for straightforward Future-based screens.
- Use appropriate state management for complex applications.
- Handle nullable API fields safely.
- Use pagination for large datasets.
- Use background parsing when large JSON responses require it.
- Show meaningful error messages to users.
- Keep authentication tokens and sensitive configuration secure.
57. Common Mistakes
- Forgetting to add the
http package.
- Forgetting required network permissions on platforms that need them.
- Not checking the response status code.
- Trying to display raw JSON without parsing it.
- Calling API functions directly from
build().
- Not showing a loading indicator.
- Not handling API errors.
- Not handling empty results.
- Assuming every JSON field is non-null.
- Putting all networking logic inside one widget.
- Loading extremely large datasets without pagination.
- Ignoring authentication requirements.
58. Interview Questions
- What is an API?
- Why are APIs used in Flutter applications?
- How do you make an HTTP request in Flutter?
- What is the purpose of the
http package?
- What is the purpose of
jsonDecode()?
- What is a Dart model class?
- What is the purpose of
fromJson()?
- What is a Future?
- What is
FutureBuilder?
- What states should an API-driven UI handle?
- Why should API calls generally not be created directly inside
build()?
- How do you display API data using
ListView.builder?
- How do you display API images?
- How do you send data from a Flutter form to an API?
- How do you update API data from Flutter?
- How do you delete API data?
- How do you handle authentication tokens?
- What is the purpose of separating API services from UI widgets?
- What is pagination?
- When should JSON parsing be performed in a background isolate?
59. Quick Revision Table
| Concept |
Purpose |
http |
Make HTTP requests |
http.get() |
Retrieve API data |
http.post() |
Send or create data |
http.put() |
Update data |
http.delete() |
Delete data |
jsonDecode() |
Convert JSON into Dart data |
jsonEncode() |
Convert Dart data into JSON |
Future |
Represent asynchronous results |
FutureBuilder |
Connect Future results to UI |
ListView.builder |
Display dynamic API lists |
GridView.builder |
Display API data in grids |
Image.network |
Display remote images |
RefreshIndicator |
Refresh API data |
| Model |
Represent structured API data |
| Service |
Handle API communication |
| ViewModel |
Manage UI state and connect data to views |
60. Practice Exercise
Create a Flutter application that connects to a users API and displays the returned data.
Requirements
- Add the
http package.
- Create a
User model.
- Create a
UserService.
- Create a
fetchUsers() method.
- Decode the JSON response.
- Convert JSON objects into
User objects.
- Create a StatefulWidget for the user screen.
- Initialize the API Future in
initState().
- Use
FutureBuilder.
- Display a loading indicator.
- Display an error message.
- Display an empty-state message.
- Display users using
ListView.builder.
- Add a retry button.
- Add pull-to-refresh functionality.
- Create a user detail screen.
- Add a POST form to create a new user.
- Refresh the list after creating a user.
61. Official Flutter Resources
62. Learn Flutter with JustAcademy
For structured Flutter training and practical API integration learning, explore the following resources:
63. Summary
Connecting APIs with Flutter UI involves combining networking, JSON processing, Dart models, asynchronous programming, and Flutter widgets. A typical implementation starts by creating an API service with the http package, sending a request with async and await, checking the HTTP response, decoding JSON, converting it into Dart models, and connecting the resulting Future or application state to the UI.
FutureBuilder is useful for straightforward asynchronous screens, while service, repository, and ViewModel-based structures can help organize larger applications. A well-designed API-connected UI should handle loading, success, empty, and error states and should keep networking logic separated from presentation logic.
64. Learning Outcome
After completing this topic, you should be able to:
- Understand how APIs communicate with Flutter applications.
- Use the
http package to make GET, POST, PUT, and DELETE requests.
- Convert JSON responses into Dart model objects.
- Use
async, await, and Future for asynchronous networking.
- Connect API results to Flutter widgets using
FutureBuilder.
- Display API data using lists, grids, cards, images, and detail screens.
- Send form data to backend APIs.
- Handle authentication headers and API errors.
- Implement refresh and pagination patterns.
- Separate API services from UI and application state.
- Build a structured API-driven Flutter application.