Popular Searches
Popular Course Categories
Popular Courses

Displaying API Data in Flutter

Displaying API Data in Flutter

Flutter APIs & Networking

 


Displaying API Data in Flutter


Displaying API data in Flutter means fetching data from a backend server, processing the response, converting it into Dart objects, and presenting it through Flutter widgets. This is a fundamental concept for applications that display dynamic information such as users, products, news, orders, profiles, posts, and other server-side data.


Flutter commonly uses the http package for network requests, dart:convert for JSON decoding, Dart model classes for structured data, and widgets such as FutureBuilder, ListView.builder, and GridView.builder to display API results.

 

 


 

 

1. What Does Displaying API Data Mean?


An API allows a Flutter application to communicate with a backend server. The server usually returns data in JSON format, which Flutter processes before displaying it on the screen.


Flutter Application
        ↓
HTTP Request
        ↓
Backend API
        ↓
JSON Response
        ↓
JSON Decoding
        ↓
Dart Model
        ↓
Flutter State
        ↓
Flutter Widgets
        ↓
User Interface

 

 

For example, an API may return:


{
  "id": 1,
  "name": "Rahul Sharma",
  "email": "[email protected]"
}

 

 

The Flutter application can convert this JSON into a User object and display the values using widgets such as Text.

 

 


 

 

2. Why Display API Data in Flutter?



  • To show real-time information from a backend.

  • To display user profiles and account information.

  • To show product catalogs.

  • To display orders and transactions.

  • To create news and social media feeds.

  • To display search results.

  • To build dashboards and reports.

  • To create applications whose content changes without updating the application itself.

 

 


 

 

3. Required Packages


The http package provides a convenient way to make HTTP requests in Flutter. Flutter's networking documentation uses it for fetching data from the internet. :contentReference[oaicite:0]{index=0}


flutter pub add http

 

 

Import the packages:


import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

 

 


 

 

4. Basic API Data Display Flow


The general process can be divided into several steps:



  1. Create the API request.

  1. Send the request to the server.

  1. Receive the HTTP response.

  1. Check the HTTP status code.

  1. Decode the JSON response.

  1. Convert JSON into Dart model objects.

  1. Store or manage the data in application state.

  1. Build Flutter widgets using the data.

  1. Handle loading, success, empty, and error states.

 

 


 

 

5. Creating a Model Class


A model class represents the structure of the API data. Using model classes makes API data easier to manage and provides stronger type safety than working with dynamic maps throughout the UI.


class User {
  final int id;
  final String name;
  final String email;

 


  const User({
    required this.id,
    required this.name,
    required this.email,
  });


  factory User.fromJson(Map json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
    );
  }
}

 

 


 

 

6. Fetching API Data


A GET request can be used to retrieve data from an API.


Future fetchUser() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
  );

 


  if (response.statusCode == 200) {
    final data =
        jsonDecode(response.body) as Map;


    return User.fromJson(data);
  }


  throw Exception('Failed to load user');
}

 

 

The Flutter networking cookbook recommends checking the HTTP response before converting the response body into the application model. :contentReference[oaicite:1]{index=1}

 

 


 

 

7. Understanding Future


API calls are asynchronous because the application needs to wait for a response from the server.


Future fetchUser() async {
  // API request
}

 

 

Here:



  • Future represents a value that will be available later.

  • User represents the expected result.

  • async allows asynchronous code.

  • await waits for the API operation to complete.

 

 


 

 

8. Using FutureBuilder


FutureBuilder is one of the simplest Flutter widgets for displaying data returned from a Future. It can build different UI states depending on whether the operation is loading, successful, or has failed. :contentReference[oaicite:2]{index=2}


FutureBuilder(
  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(snapshot.data!.name);
    }


    return const Text('No data available');
  },
)

 

 


 

 

9. Storing the Future in State


The Future used by FutureBuilder should generally be stored rather than creating a new API request during every build() call.


late Future futureUser;

 


@override
void initState() {
  super.initState();
  futureUser = fetchUser();
}

 

 

The API request can then be passed to FutureBuilder.


FutureBuilder(
  future: futureUser,
  builder: (context, snapshot) {
    // Build UI
  },
)

 

 

Calling the API directly from build() can result in repeated requests because Flutter may call build() multiple times. Flutter's official networking recipe recommends initiating the Future in initState() or didChangeDependencies() when appropriate. :contentReference[oaicite:3]{index=3}

 

 


 

 

10. Complete Example: Displaying One API Object


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;


  const User({
    required this.id,
    required this.name,
    required this.email,
  });


  factory User.fromJson(Map json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
    );
  }
}


Future fetchUser() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
  );


  if (response.statusCode == 200) {
    final data =
        jsonDecode(response.body) as Map;


    return User.fromJson(data);
  }


  throw Exception('Failed to load user');
}


class UserScreen extends StatefulWidget {
  const UserScreen({super.key});


  @override
  State createState() => _UserScreenState();
}


class _UserScreenState extends State {
  late Future futureUser;


  @override
  void initState() {
    super.initState();
    futureUser = fetchUser();
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('User Details'),
      ),
      body: Center(
        child: FutureBuilder(
          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 const Text('No user found');
            }


            final user = snapshot.data!;


            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  'ID: ${user.id}',
                  style: const TextStyle(fontSize: 18),
                ),
                Text(
                  'Name: ${user.name}',
                  style: const TextStyle(fontSize: 22),
                ),
                Text(
                  'Email: ${user.email}',
                  style: const TextStyle(fontSize: 18),
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

 

 


 

 

11. Displaying API Data Using Text


For a single object, individual fields can be displayed using Text.


Column(
  children: [
    Text(user.name),
    Text(user.email),
    Text('${user.id}'),
  ],
)

 

 

You can also use labels:


Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text('Name: ${user.name}'),
    Text('Email: ${user.email}'),
    Text('User ID: ${user.id}'),
  ],
)

 

 


 

 

12. Displaying API Data Using Card


Card is useful for presenting API data in a visually organized format.


Card(
  margin: const EdgeInsets.all(12),
  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),
      ],
    ),
  ),
)

 

 


 

 

13. Displaying a List of API Data


Many APIs return an array of objects.


[
  {
    "id": 1,
    "name": "Rahul",
    "email": "[email protected]"
  },
  {
    "id": 2,
    "name": "Priya",
    "email": "[email protected]"
  },
  {
    "id": 3,
    "name": "Amit",
    "email": "[email protected]"
  }
]

 

 

First, create a function that returns a list of users.


Future> fetchUsers() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/users'),
  );

 


  if (response.statusCode != 200) {
    throw Exception('Failed to load users');
  }


  final List data = jsonDecode(response.body);


  return data
      .map(
        (item) => User.fromJson(
          item as Map,
        ),
      )
      .toList();
}

 

 


 

 

14. Displaying a List with ListView.builder


ListView.builder is useful for displaying dynamic API lists because it creates list items as needed.


FutureBuilder>(
  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),
        );
      },
    );
  },
)

 

 


 

 

15. Complete List API Example


class UsersScreen extends StatefulWidget {
  const UsersScreen({super.key});

 


  @override
  State createState() => _UsersScreenState();
}


class _UsersScreenState extends State {
  late Future> futureUsers;


  @override
  void initState() {
    super.initState();
    futureUsers = fetchUsers();
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Users'),
      ),
      body: FutureBuilder>(
        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 available'),
            );
          }


          return ListView.builder(
            itemCount: users.length,
            itemBuilder: (context, index) {
              final user = users[index];


              return Card(
                margin: const EdgeInsets.symmetric(
                  horizontal: 12,
                  vertical: 6,
                ),
                child: ListTile(
                  leading: CircleAvatar(
                    child: Text(user.name[0]),
                  ),
                  title: Text(user.name),
                  subtitle: Text(user.email),
                  trailing: Text('#${user.id}'),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

 

 


 

 

16. Displaying API Images


APIs frequently return image URLs.


{
  "id": 1,
  "name": "Laptop",
  "image": "https://example.com/images/laptop.jpg"
}

 

 

Add the image field to the model:


class Product {
  final int id;
  final String name;
  final String image;

 


  const Product({
    required this.id,
    required this.name,
    required this.image,
  });


  factory Product.fromJson(Map json) {
    return Product(
      id: json['id'] as int,
      name: json['name'] as String,
      image: json['image'] as String,
    );
  }
}

 

 

Display the image:


Image.network(
  product.image,
  width: 100,
  height: 100,
  fit: BoxFit.cover,
)

 

 

A product card could combine the image and API data:


Card(
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Image.network(
        product.image,
        height: 180,
        width: double.infinity,
        fit: BoxFit.cover,
      ),
      Padding(
        padding: const EdgeInsets.all(12),
        child: Text(product.name),
      ),
    ],
  ),
)

 

 


 

 

17. Displaying Product Data


A common Flutter API screen is an e-commerce product list.


class Product {
  final int id;
  final String title;
  final double price;
  final String image;

 


  const Product({
    required this.id,
    required this.title,
    required this.price,
    required this.image,
  });


  factory Product.fromJson(Map json) {
    return Product(
      id: json['id'] as int,
      title: json['title'] as String,
      price: (json['price'] as num).toDouble(),
      image: json['image'] as String,
    );
  }
}

 

 

Display the product:


Card(
  child: Padding(
    padding: const EdgeInsets.all(12),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Image.network(
          product.image,
          height: 160,
          width: double.infinity,
          fit: BoxFit.cover,
        ),
        const SizedBox(height: 10),
        Text(
          product.title,
          style: const TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.bold,
          ),
        ),
        const SizedBox(height: 6),
        Text(
          '₹${product.price.toStringAsFixed(2)}',
          style: const TextStyle(fontSize: 16),
        ),
      ],
    ),
  ),
)

 

 


 

 

18. Displaying API Data in GridView


For products, categories, images, or dashboards, a grid can provide a better layout.


GridView.builder(
  padding: const EdgeInsets.all(12),
  gridDelegate:
      const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    crossAxisSpacing: 12,
    mainAxisSpacing: 12,
    childAspectRatio: 0.75,
  ),
  itemCount: products.length,
  itemBuilder: (context, index) {
    final product = products[index];

 


    return Card(
      child: Column(
        children: [
          Expanded(
            child: Image.network(
              product.image,
              fit: BoxFit.cover,
              width: double.infinity,
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8),
            child: Text(product.title),
          ),
        ],
      ),
    );
  },
)

 

 


 

 

19. Loading State


API data does not appear immediately. The application should provide feedback while the request is running.


if (snapshot.connectionState ==
    ConnectionState.waiting) {
  return const Center(
    child: CircularProgressIndicator(),
  );
}

 

 

Other loading indicators can also be used:



  • CircularProgressIndicator

  • LinearProgressIndicator

  • Skeleton loading UI

  • Shimmer placeholders

  • Inline loading indicators

 

 


 

 

20. Success State


When data is successfully received, display the content.


if (snapshot.hasData) {
  final user = snapshot.data!;

 


  return Text(user.name);
}

 

 

For a list:


if (snapshot.hasData) {
  final users = snapshot.data!;

 


  return ListView.builder(
    itemCount: users.length,
    itemBuilder: (context, index) {
      return Text(users[index].name);
    },
  );
}

 

 


 

 

21. Empty State


An API request can succeed while returning an empty list. This should be handled separately from an error.


final users = snapshot.data ?? [];

 


if (users.isEmpty) {
  return const Center(
    child: Text('No users found'),
  );
}

 

 

Useful empty-state messages include:



  • No products found.

  • No users available.

  • No orders found.

  • No search results.

  • No notifications available.

 

 


 

 

22. Error State


API requests can fail because of network problems, server errors, invalid responses, authentication issues, or other conditions.


if (snapshot.hasError) {
  return Center(
    child: Text(
      'Unable to load data: ${snapshot.error}',
    ),
  );
}

 

 

A better application can display a user-friendly message rather than exposing technical error details.


return const Center(
  child: Text(
    'Unable to load data. Please try again.',
  ),
);

 

 


 

 

23. Retry API Request


A retry button can allow the user to request the data again after a failure.


if (snapshot.hasError) {
  return Center(
    child: ElevatedButton(
      onPressed: () {
        setState(() {
          futureUsers = fetchUsers();
        });
      },
      child: const Text('Retry'),
    ),
  );
}

 

 


 

 

24. Pull-to-Refresh


RefreshIndicator can be used to allow users to refresh API data manually.


RefreshIndicator(
  onRefresh: () async {
    setState(() {
      futureUsers = fetchUsers();
    });

 


    await futureUsers;
  },
  child: ListView.builder(
    itemCount: users.length,
    itemBuilder: (context, index) {
      return ListTile(
        title: Text(users[index].name),
      );
    },
  ),
)

 

 


 

 

25. Search API Data Locally


Once API data is loaded into a list, it can be filtered locally.


final filteredUsers = users.where((user) {
  return user.name
      .toLowerCase()
      .contains(searchText.toLowerCase());
}).toList();

 

 

Display the filtered list:


ListView.builder(
  itemCount: filteredUsers.length,
  itemBuilder: (context, index) {
    final user = filteredUsers[index];

 


    return ListTile(
      title: Text(user.name),
      subtitle: Text(user.email),
    );
  },
)

 

 


 

 

26. API Search Using Query Parameters


For large datasets, searching can be performed by the backend instead of downloading all records.


Future> searchProducts(
  String query,
) async {
  final uri = Uri.https(
    'example.com',
    '/api/products',
    {'search': query},
  );

 


  final response = await http.get(uri);


  if (response.statusCode != 200) {
    throw Exception('Failed to search products');
  }


  final List data = jsonDecode(response.body);


  return data
      .map(
        (item) => Product.fromJson(
          item as Map,
        ),
      )
      .toList();
}

 

 


 

 

27. Displaying Nested API Data


APIs often return nested objects.


{
  "id": 1,
  "name": "Rahul",
  "address": {
    "city": "Mumbai",
    "country": "India"
  }
}

 

 

Create a nested model:


class Address {
  final String city;
  final String country;

 


  const Address({
    required this.city,
    required this.country,
  });


  factory Address.fromJson(Map json) {
    return Address(
      city: json['city'] as String,
      country: json['country'] as String,
    );
  }
}

 

 

Use it inside the user model:


class User {
  final int id;
  final String name;
  final Address address;

 


  const User({
    required this.id,
    required this.name,
    required this.address,
  });


  factory User.fromJson(Map json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      address: Address.fromJson(
        json['address'] as Map,
      ),
    );
  }
}

 

 

Display nested data:


Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text(user.name),
    Text(user.address.city),
    Text(user.address.country),
  ],
)

 

 


 

 

28. Displaying API Data in a Reusable Widget


Reusable widgets make large API-based applications easier to maintain.


class UserCard extends StatelessWidget {
  final User user;

 


  const UserCard({
    super.key,
    required this.user,
  });


  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        leading: CircleAvatar(
          child: Text(user.name[0]),
        ),
        title: Text(user.name),
        subtitle: Text(user.email),
      ),
    );
  }
}

 

 

Use it inside the list:


ListView.builder(
  itemCount: users.length,
  itemBuilder: (context, index) {
    return UserCard(
      user: users[index],
    );
  },
)

 

 


 

 

29. Separating API Service from UI


For larger projects, API requests should be separated from UI code.


lib/
├── models/
│   └── user.dart
├── services/
│   └── user_service.dart
├── screens/
│   └── users_screen.dart
├── widgets/
│   └── user_card.dart
└── main.dart

 

 

Model


Represents the API data.

 

 

Service


Makes HTTP requests and converts responses into model objects.

 

 

Screen


Manages the screen and decides which UI state should be displayed.

 

 

Widget


Displays reusable pieces of API data.

 

 


 

 

30. Example of a User Service


class UserService {
  Future> getUsers() async {
    final response = await http.get(
      Uri.parse(
        'https://jsonplaceholder.typicode.com/users',
      ),
    );

 


    if (response.statusCode != 200) {
      throw Exception('Failed to load users');
    }


    final List data = jsonDecode(response.body);


    return data
        .map(
          (item) => User.fromJson(
            item as Map,
          ),
        )
        .toList();
  }
}

 

 

The screen can then use the service:


final userService = UserService();

 


late Future> futureUsers;


@override
void initState() {
  super.initState();
  futureUsers = userService.getUsers();
}

 

 


 

 

31. Displaying API Data with Authentication


Some APIs require authorization headers. Flutter's networking documentation demonstrates using HTTP headers to provide authorization information. :contentReference[oaicite:4]{index=4}


Future> fetchUsers() async {
  final response = await http.get(
    Uri.parse('https://example.com/api/users'),
    headers: {
      'Authorization': 'Bearer YOUR_TOKEN',
      'Accept': 'application/json',
    },
  );

 


  if (response.statusCode != 200) {
    throw Exception('Failed to load users');
  }


  final List data = jsonDecode(response.body);


  return data
      .map(
        (item) => User.fromJson(
          item as Map,
        ),
      )
      .toList();
}

 

 

Authentication tokens should be handled securely and should not be hard-coded into production applications.

 

 


 

 

32. Displaying API Data After POST Request


API data can also be displayed after sending data to the server. For example, a POST request can create a new record and return the created object.


Future createUser(
  String name,
  String email,
) async {
  final response = await http.post(
    Uri.parse('https://example.com/api/users'),
    headers: {
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'name': name,
      'email': email,
    }),
  );

 


  if (response.statusCode == 201) {
    final data =
        jsonDecode(response.body) as Map;


    return User.fromJson(data);
  }


  throw Exception('Failed to create user');
}

 

 

The returned model can then be displayed on the screen. Flutter's official networking recipe uses http.post(), JSON encoding, status-code validation, and model conversion for this type of workflow. :contentReference[oaicite:5]{index=5}

 

 


 

 

33. Handling API Status States


A robust API screen normally has at least four states:








State Meaning Example UI
Loading Request is in progress. CircularProgressIndicator
Success Data was received. ListView or Card
Empty Request succeeded but no records exist. No data message
Error Request or processing failed. Error message and retry button

 

 


 

 

34. Complete API Data Display Architecture


User Action
    ↓
Screen
    ↓
Service
    ↓
HTTP Request
    ↓
API Server
    ↓
JSON Response
    ↓
JSON Decoder
    ↓
Model.fromJson()
    ↓
Future / State
    ↓
Widget
    ↓
Flutter UI

 

 

This separation makes it easier to test, maintain, and reuse API-related code.

 

 


 

 

35. Avoid API Calls Inside build()


A common mistake is performing network requests directly inside build().


@override
Widget build(BuildContext context) {
  fetchUsers();

 


  return const Scaffold();
}

 

 

This can cause repeated requests whenever the widget rebuilds.

 

 

A better approach is:


late Future> futureUsers;

 


@override
void initState() {
  super.initState();
  futureUsers = fetchUsers();
}

 

 


 

 

36. Handling Large API Responses


When an API returns a very large JSON response, decoding and model conversion can require significant processing. Flutter provides techniques such as moving parsing work to another isolate using compute(). The official Flutter documentation demonstrates this approach for large JSON responses. :contentReference[oaicite:6]{index=6}


List parseUsers(String responseBody) {
  final List data = jsonDecode(responseBody);

 


  return data
      .map(
        (item) => User.fromJson(
          item as Map,
        ),
      )
      .toList();
}

 

 

For sufficiently large datasets, this parsing function can be executed with compute() so that expensive processing does not unnecessarily block the main UI isolate.

 

 


 

 

37. Performance Best Practices



  • Do not make unnecessary API requests.

  • Do not fetch the same data repeatedly without a reason.

  • Use ListView.builder for large dynamic lists.

  • Use GridView.builder for dynamic grids.

  • Convert JSON into model objects.

  • Use pagination for very large datasets.

  • Consider caching frequently used data.

  • Use background parsing for sufficiently large JSON responses.

  • Keep expensive operations away from the build() method.

  • Use reusable widgets for repeated UI structures.

 

 


 

 

38. Pagination


When an API contains thousands of records, loading everything at once can be inefficient. Pagination allows the application to request smaller groups of records.


GET /api/products?page=1&limit=20
GET /api/products?page=2&limit=20
GET /api/products?page=3&limit=20

 

 

Pagination is commonly used for:



  • Product catalogs

  • Social feeds

  • News applications

  • Order history

  • Search results

  • Large user lists

 

 


 

 

39. API Data and State Management


For small applications, FutureBuilder can be enough for displaying asynchronous API data. Larger applications may use state-management solutions to manage API data, loading states, errors, caching, and updates.

 

 

Common approaches include:



  • setState

  • FutureBuilder

  • ChangeNotifier

  • Provider

  • Riverpod

  • Bloc/Cubit

  • Other application-specific state-management architectures

 

 


 

 

40. API Data with setState


For simpler screens, API data can also be stored in state variables.


List users = [];
bool isLoading = false;
String? errorMessage;

 


Future loadUsers() async {
  setState(() {
    isLoading = true;
    errorMessage = null;
  });


  try {
    final result = await fetchUsers();


    if (!mounted) return;


    setState(() {
      users = result;
      isLoading = false;
    });
  } catch (error) {
    if (!mounted) return;


    setState(() {
      errorMessage = 'Unable to load users';
      isLoading = false;
    });
  }
}

 

 


 

 

41. Displaying API Data with Loading, Error, Empty, and Success UI


Widget buildUserContent() {
  if (isLoading) {
    return const Center(
      child: CircularProgressIndicator(),
    );
  }

 


  if (errorMessage != null) {
    return Center(
      child: Text(errorMessage!),
    );
  }


  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(
        title: Text(user.name),
        subtitle: Text(user.email),
      );
    },
  );
}

 

 


 

 

42. API Data with Refresh and Retry


A production-style screen should provide a way to recover from temporary failures.


Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    const Text(
      'Unable to load data',
    ),
    const SizedBox(height: 12),
    ElevatedButton(
      onPressed: loadUsers,
      child: const Text('Try Again'),
    ),
  ],
)

 

 


 

 

43. Common Mistakes


Mistake 1: Ignoring the Status Code


final data = jsonDecode(response.body);

Always verify whether the server returned a successful response before assuming the expected JSON structure.

 

 

Mistake 2: Calling the API in build()


This can trigger repeated requests during widget rebuilds.

 

 

Mistake 3: Not Handling Empty Data


An empty list should not necessarily be treated as an error.

 

 

Mistake 4: Not Handling Errors


Network requests can fail, so the UI should provide a useful error state.

 

 

Mistake 5: Using Unstructured dynamic Data Everywhere


Model classes are generally easier to maintain than passing raw maps through multiple UI layers.

 

 

Mistake 6: Loading Too Much Data


For large datasets, consider pagination, filtering, caching, and appropriate API endpoints.

 

 

Mistake 7: Ignoring Image Loading Errors


When displaying remote images, provide an appropriate placeholder or error widget when necessary.

 

 


 

 

44. Best Practices



  • Use the http package for HTTP communication.

  • Decode JSON using jsonDecode().

  • Use model classes for API data.

  • Keep API logic in service classes.

  • Keep presentation logic inside widgets or appropriate state-management layers.

  • Check HTTP status codes.

  • Handle loading, success, empty, and error states.

  • Avoid API calls directly inside build().

  • Use ListView.builder for dynamic lists.

  • Use GridView.builder for dynamic grids.

  • Use pagination for large datasets.

  • Use refresh and retry functionality where appropriate.

  • Handle nullable fields carefully.

  • Use secure authentication mechanisms for protected APIs.

  • Consider background parsing for sufficiently large JSON responses.

  • Keep reusable API widgets separate from networking code.

 

 


 

 

45. Practical Project Example


Create a Flutter product application with the following flow:



  1. Create a Product model.

  1. Create ProductService.

  1. Make a GET API request.

  1. Decode the JSON response.

  1. Convert the response into List.

  1. Store the Future or application state.

  1. Display products using GridView.builder.

  1. Display product images using Image.network.

  1. Display product name and price.

  1. Show a loading indicator while data is being fetched.

  1. Show an error message if the request fails.

  1. Show an empty state when there are no products.

  1. Add retry functionality.

  1. Add pull-to-refresh functionality.

  1. Add search or filtering functionality.

 

 


 

 

46. Practice Exercise


Build a Flutter application that displays a list of users from an API.


Requirements



  • Create a User model.

  • Create a UserService.

  • Fetch users using an HTTP GET request.

  • Decode the JSON response.

  • Convert the response into model objects.

  • Display users using ListView.builder.

  • Show a circular progress indicator while loading.

  • Show an error message when the request fails.

  • Show a "No users found" message when the list is empty.

  • Add a retry button.

  • Add pull-to-refresh.

  • Add a search field for filtering users.

 

 


 

 

47. Interview Questions



  1. What is API data in Flutter?

  1. How do you make a GET request in Flutter?

  1. What package is commonly used for HTTP requests?

  1. What does jsonDecode() do?

  1. Why should API responses be converted into model classes?

  1. What is a Future?

  1. What is FutureBuilder?

  1. Why should an API request not normally be created directly inside build()?

  1. How do you display a list of API records?

  1. What is the difference between ListView and ListView.builder?

  1. How can API images be displayed in Flutter?

  1. How do you handle API loading states?

  1. How do you handle API errors?

  1. How do you handle an empty API response?

  1. How can a user retry a failed API request?

  1. How can pull-to-refresh be implemented?

  1. What is pagination?

  1. How can API data be searched or filtered?

  1. How do you display nested JSON data?

  1. How can large JSON responses be processed efficiently?

 

 


 

 

48. Quick Revision















Concept Purpose
http.get() Fetches data from an API.
response.body Contains the response data.
jsonDecode() Converts JSON text into Dart data.
fromJson() Converts JSON data into a model object.
Future Represents an asynchronous result.
FutureBuilder Builds UI based on asynchronous data.
ListView.builder Displays dynamic list data efficiently.
GridView.builder Displays dynamic grid data.
Image.network() Displays an image from a network URL.
RefreshIndicator Provides pull-to-refresh functionality.
compute() Can move expensive parsing work to another isolate.

 

 


 

 

49. Useful Resources









 

 


 

 

Conclusion


Displaying API data in Flutter involves more than simply making an HTTP request. A complete implementation should fetch the data, validate the response, decode JSON, convert it into Dart models, manage asynchronous state, and display the result using suitable Flutter widgets.


For simple API screens, FutureBuilder with ListView.builder or GridView.builder can provide a straightforward solution. As an application grows, separating models, services, state management, and reusable UI components makes the application easier to maintain and extend.

 

whatsapp