Flutter MVVM Pattern with Dio API Integration and Provider for ListView App

Learn how to integrate Dio (HTTP client) with MVVM architecture to perform Get API calls efficiently.

Explore sample code, step-by-step tutorial, and best practices for seamless API integration. 




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


MVVM folder structure


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

..

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


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

class UserList extends StatefulWidget {
  const UserList({Key? key}) : super(key: key);

  @override
  State<UserList> createState() => _UserListState();
}

class _UserListState extends State<UserList> {
  @override
  void initState() {
    super.initState();

    // Fetch user data when the state object is inserted into the tree.
    final userViewModel = Provider.of<UserViewModel>(context, listen: false);
    userViewModel.fetchUsers();
  }

  @override
  Widget build(BuildContext context) {
    final userViewModel = Provider.of<UserViewModel>(context);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Users'),
      ),
      body: userViewModel.loading
          ? const Center(
        child: CircularProgressIndicator(),
      )
          : userViewModel.errorMessage.isNotEmpty
          ? Center(
        child: Text(userViewModel.errorMessage),
      )
          : ListView.builder(
        itemCount: userViewModel.users.length,
        itemBuilder: (context, index) {
          final user = userViewModel.users[index];

          // Display a list of users with their name, email, and ID.
          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.


// 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.


// 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"
    }
  },
  
]
..

This Example without Consumer:

We directly uses the Provider.of method to access the UserViewModel instance and retrieve the data directly. 

When the data is loading, it shows a progress indicator, and if there's an error, the error message is displayed; otherwise, the list of users is shown.


..
If you have a more complex application with multiple widgets dependent on the UserViewModel, using Consumer might be more efficient. 

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.

Read more

Comments