Networking in Flutter using Dio

Handling network requests is a critical aspect of application development, and it is essential to handle unexpected results gracefully to ensure a good user experience. 

In this article, we will explore how to use the Dio package to handle REST API requests in Flutter.

Dio is a powerful HTTP client for Dart that provides an intuitive API for performing advanced network tasks with ease. 

It offers support for interceptors, global configuration, FormData, request cancellation, file downloading, and timeout, among other features

While Flutter's built-in http package is suitable for performing basic network tasks, it can be challenging to use when handling more advanced features.

Therefore, Dio is an excellent choice for developers who want to perform advanced network tasks with ease. With its extensive documentation and active community support, developers can handle network requests in Flutter without difficulty.

In summary, Dio is a valuable package that simplifies the process of handling network requests in Flutter. By using its intuitive API, developers can easily handle complex network tasks and ensure that their applications provide an excellent user experience.

Here's a step-by-step tutorial on how to use Dio, MVVM architecture, Provider with Consumer, and ListView to fetch and display data from the JSONPlaceholder API in Flutter:


Add dependencies

First, you need to add the Dio and Provider packages to your Flutter project by adding the following lines to your pubspec.yaml file:



dependencies:
  flutter:
    sdk: flutter
  dio: ^5.3.0
  provider: ^6.0.5

Then, run flutter pub get in your terminal to install these packages.

MVVM folder structure - demo


lib/
├── data/
│   ├── models/
│   │   └── user.dart // Contains the User model class for representing user data.
│   ├── repositories/
│   │   └── user_repository.dart // Handles fetching user data from the API.
│   └── services/
│       └── api_service.dart // Provides methods to interact with the API using Dio or other HTTP clients.
│
├── utils/
│   ├── constants.dart // Contains constant values used throughout the app.
│   └── logger.dart // Utility for logging app events or errors.
│
├── view/
│   └── user_list.dart // View file for displaying the list of users.
│
├── view_models/
│   └── user_view_model.dart // ViewModel class for managing user data and API calls.
│
├── widgets/
│   ├── user_tile.dart // Reusable widget for displaying user details in a list.
│   └── loading_spinner.dart // Reusable loading spinner widget.
│
├── main.dart // The main entry point of the Flutter app.



The separate code layers of Model View ViewModel are:

Model: It represents the business logic and the data of an Application. It also consists of the business logic - local and remote data source, model classes, repository.

View: It contains the UI Code, Also sends the user action to the ViewModel but does not receive the response back directly. 

ViewModel: It acts as a connection between the View and the business logic. Furthermore, it doesn't have any idea about which View it has to use as it does not possess a direct reference with the View. 

..

Dio MVVM User List App

Build a Flutter user list app using Dio for API calls and MVVM architecture. Learn how to manage user data and API integration efficiently with sample code and best practices.


main.dart : The main entry point of the Flutter app.


// main.dart

import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:flutteryfly/viewmodels/user_view_model.dart';
import 'package:provider/provider.dart';
import './data/repositories/user_repository.dart';
import './data/services/api_service.dart';
import './views/user_list_consumer.dart';

void main() {
  // Create Dio instance for HTTP requests
  final Dio dio = Dio();

  // Create ApiService instance with the Dio instance
  final ApiService apiService = ApiService(dio: dio);

  // Create UserRepository instance with the ApiService instance
  final UserRepository userRepository = UserRepository(apiService: apiService);

  runApp(
    MultiProvider(
      providers: [
        // Provide the UserViewModel with UserRepository dependency to manage user data and API calls
        ChangeNotifierProvider<UserViewModel>(
          create: (context) => UserViewModel(userRepository: userRepository),
        ),
      ],
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'User List', // Meta Title for the App
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
      home: const UserListConsumer(),
    );
  }
}

..

user_list_consumer.dart : View file for displaying the list of users.

Create the UserListConsumer Widget

For larger applications with more complex state management, using Consumer could be a better choice for better control over widget rebuilds. 


// views/user_list_consumer.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../viewmodels/user_view_model.dart';

class UserListConsumer extends StatefulWidget {
  const UserListConsumer({super.key}); // A stateful widget to display the users

  @override
  State<UserListConsumer> createState() => _UserListConsumerState(); // Returns the state of the widget
}

class _UserListConsumerState extends State<UserListConsumer> {
  @override
  void initState() { // Called when the state object is inserted into the tree.
    super.initState();

    final userViewModel = Provider.of<UserViewModel>(context, listen: false); // Fetches userViewModel object
    userViewModel.fetchUsers(); // Calls the method to fetch the users

  }

  @override
  Widget build(BuildContext context) { // Build method which returns the UI
    return Scaffold(
      appBar: AppBar(
        title: const Text('Users'),
      ),
      body: Consumer<UserViewModel>( // Consumer widget to listen for changes in UserViewModel
        builder: (context, userViewModel, child) {
          if (userViewModel.loading) { // If data is still loading, show a progress indicator
            return const Center(
              child: CircularProgressIndicator(),
            );
          } else if (userViewModel.errorMessage.isNotEmpty) { // If there is an error, show the error message
            return Center(
              child: Text(userViewModel.errorMessage),
            );
          } else { // Otherwise, show the list of users
            return ListView.builder(
              itemCount: userViewModel.users.length,
              itemBuilder: (context, index) {
                final user = userViewModel.users[index];
                return ListTile(
                  title: Text(user.name),
                  subtitle: Text(user.email),
                  leading: CircleAvatar(
                    child: Text(user.id.toString()),
                  ),
                );
              },
            );
          }
        },
      ),
    );
  }
}

..

user_view_model.dart : ViewModel class for managing user data and API calls.

In this code, the UserViewModel class extends the ChangeNotifier class, which means it can notify its listeners when its state changes. 

The UserViewModel class has a single method called fetchUsers(), which calls the getUsers() method of the ApiService class to fetch a list of users from the API. It then converts the response data into a list of User instances and sets the _users property to the loaded users. 

Finally, it calls notifyListeners() to notify its listeners that its state has changed.

// view_models/user_view_model.dart
import 'package:flutter/material.dart';

import '../data/models/user.dart';
import '../data/repositories/user_repository.dart';

class UserViewModel extends ChangeNotifier {
  final UserRepository userRepository;

  UserViewModel({required this.userRepository});

  List<User> _users = []; // List to store user data fetched from the API.
  bool _loading = false; // Boolean flag to track if data is currently being fetched.
  String _errorMessage = ''; // String to store any error message that occurs during data fetching.

  List<User> get users => _users; // Getter method to access the list of users.
  bool get loading => _loading; // Getter method to access the loading flag.
  String get errorMessage => _errorMessage; // Getter method to access the error message.

  Future<void> fetchUsers() async {
    _loading = true; // Set loading flag to true before making the API call.
    _errorMessage = ''; // Clear any previous error message.

    try {
      // Call the getUsers() method from the UserRepository to fetch user data from the API.
      _users = await userRepository.getUsers();

    } catch (e) {
      // If an exception occurs during the API call, set the error message to display the error.
      _errorMessage = 'Failed to fetch users';
    } finally {
      // After API call is completed, set loading flag to false and notify listeners of data change.
      _loading = false;
      notifyListeners();
    }
  }
}

..

user_repository.dart : Handles fetching user data from the API.


// data/repositories/user_repository.dart
import '../models/user.dart';
import '../services/api_service.dart';

class UserRepository {
  final ApiService apiService; // Instance of the ApiService class to perform API requests.

  UserRepository({required this.apiService});

  Future<List<User>> getUsers() async {
    try {
      // Call the getUsers() method from the ApiService to fetch user data from the API.
      final data = await apiService.getUsers();

      // Map the API response data to a List of User objects using the User.fromJson() constructor.
      return data.map((json) => User.fromJson(json)).toList();

    } catch (e) {
      // If an exception occurs during the API call, throw an exception with an error message.
      throw Exception('Failed to fetch users');
    }
  }
}

..

api_service.dart : Provides methods to interact with the API using Dio or other HTTP clients.

In this code, the ApiService class has a single method called getUsers()

This method uses the Dio package to make an HTTP GET request to the JSONPlaceholder API to fetch a list of users. It returns the response data as a list of dynamic objects.

// data/services/api_service.dart
import 'package:dio/dio.dart';

class ApiService {
  final Dio _dio; // Dio instance to perform HTTP requests.

  ApiService({required Dio dio}) : _dio = dio;

  Future<List<dynamic>> getUsers() async {
    try {
      // Make a GET request to the API endpoint to fetch user data.
      final response = await _dio.get('https://jsonplaceholder.typicode.com/users');

      // Check if the response status code is 200 (OK).
      if (response.statusCode == 200) {
        return response.data; // If successful, return the response data (List of dynamic).
      } else {
        // If the response status code is not 200, throw an exception with an error message.
        throw Exception('API failed with status code: ${response.statusCode}');
      }
    } catch (e) {
      // If any exception occurs during the API call, throw an exception with the error message.
      throw Exception('An error occurred: $e');
    }
  }
}

..

..

Json Array:

https://jsonplaceholder.typicode.com/users

[
  {
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "Sincere@april.biz",
    "address": {
      "street": "Kulas Light",
      "suite": "Apt. 556",
      "city": "Gwenborough",
      "zipcode": "92998-3874",
      "geo": {
        "lat": "-37.3159",
        "lng": "81.1496"
      }
    },
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org",
    "company": {
      "name": "Romaguera-Crona",
      "catchPhrase": "Multi-layered client-server neural-net",
      "bs": "harness real-time e-markets"
    }
  },
  {
    "id": 2,
    "name": "Ervin Howell",
    "username": "Antonette",
    "email": "Shanna@melissa.tv",
    "address": {
      "street": "Victor Plains",
      "suite": "Suite 879",
      "city": "Wisokyburgh",
      "zipcode": "90566-7771",
      "geo": {
        "lat": "-43.9509",
        "lng": "-34.4618"
      }
    },
    "phone": "010-692-6593 x09125",
    "website": "anastasia.net",
    "company": {
      "name": "Deckow-Crist",
      "catchPhrase": "Proactive didactic contingency",
      "bs": "synergize scalable supply-chains"
    }
  },
  
]
..
Consumer allows you to limit the rebuilds to only the relevant parts of the UI tree when the relevant data changes. This can help avoid unnecessary rebuilds and improve performance.

..

Testing with API data
We will use{JSON} Placeholder to test our network data because it provides you with a hosted REST API consisting of sample user data and allows you to perform a variety of network operation tests.

Output:




..

..


Comments