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');
}
}
}
..
..
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"
}
},
]