Dio API Integration - Login API with Provider and MVVM in Flutter

Learn how to integrate Dio (HTTP client) with a Login API in Flutter using the Provider package and MVVM architecture. 

Follow step-by-step tutorials and best practices to efficiently manage API calls and user authentication. 

Analyze log output and explore sample code for a seamless login experience using Dio, Provider, and MVVM.

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


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


// main.dart
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutteryfly/viewmodels/login_view_model.dart';
import 'package:flutteryfly/views/login_page.dart';
import 'package:provider/provider.dart';
import './data/services/api_service.dart';
import 'data/repositories/login_repository.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 LoginRepository instance with the ApiService instance
  final LoginRepository loginRepository = LoginRepository(apiService: apiService);

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Login', // Meta Title for the App
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: LoginPage(),
    );
  }
}

..

login_page.dart :To build a login page.


// views/login_page.dart
import 'package:flutter/material.dart';
import 'package:flutteryfly/views/widgets/common_text_field.dart';
import 'package:provider/provider.dart';
import '../viewmodels/login_view_model.dart';

class LoginPage extends StatelessWidget {
  LoginPage({Key? key}) : super(key: key);

  // Declare the TextEditingControllers at the class level
  final TextEditingController name = TextEditingController();
  final TextEditingController password = TextEditingController();

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

    // Set the initial value for the username text field
    name.text = 'eve.holt@reqres.in';

    return Scaffold(
      appBar: AppBar(
        title: const Text('Login'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(30.0),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // Show CircularProgressIndicator while loading
              if (viewModel.isLoading) const CircularProgressIndicator(),
              // Show error message if login fails
              if (!viewModel.isLoading && viewModel.loginError != null)
                Text(
                  viewModel.loginError!,
                  style: const TextStyle(color: Colors.red),
                ),
              // Show response message if login is successful
              if (viewModel.response != null)
                Text(
                  viewModel.response!.toString(),
                  style: const TextStyle(color: Colors.green),
                ),
              const SizedBox(height: 16),
              // Username text field
              CommonTextField(
                controller: name,
                hintText: 'Enter your username',
                keyboardType: TextInputType.text,
                onChanged: (value) {
                  // Handle username changes
                },
              ),
              Container(height: 40),
              // Password text field
              CommonTextField(
                controller: password,
                obscureText: true,
                hintText: 'Enter your password',
                keyboardType: TextInputType.text,
                onChanged: (value) {
                  // Handle password changes
                },
              ),
              Container(height: 40),
              // Login button
              ElevatedButton(
                onPressed: () {
                  // Construct login request with the entered username and password
                  Map<dynamic, dynamic> req = {
                    "email": name.text,
                    "password": password.text,
                  };
                  // Call the login method in the view model with the request
                  viewModel.login(req);
                },
                child: const Text('Login'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

..

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


// view_models/login_view_model.dart
import 'package:flutter/material.dart';
import '../data/models/login_response.dart';
import '../data/repositories/login_repository.dart';

// Create the LoginViewModel class and extend it with ChangeNotifier to enable state management.
class LoginViewModel with ChangeNotifier {
  // Instance of the LoginRepository class to interact with the user login data.
  final LoginRepository userRepository;

  // Constructor to initialize the LoginViewModel with the LoginRepository instance.
  LoginViewModel({required this.userRepository});

  // Private variables to store response and error data from the login process.
  String? _response; // Response from the login API call.
  String? get response => _response; // Getter to access the login response.

  String? _loginError; // Error message in case the login process fails.
  bool _isLoading = false; // A flag to track the loading state during the login process.

  // Getters to access the error message and loading state.
  String? get loginError => _loginError;
  bool get isLoading => _isLoading;

  // Method to handle the login process asynchronously.
  Future<void> login(Map<dynamic, dynamic> req) async {
    // Set the isLoading flag to true to indicate that the login process is ongoing.
    _isLoading = true;
    // Notify the listeners (usually UI elements) that the state has changed.
    notifyListeners();

    try {
      // Call the login method from the userRepository to initiate the login process.
      _response = await userRepository.login(req);
      // If the login is successful, set the loginError to null.
      _loginError = null;
    } catch (e) {
      // If an error occurs during the login process, catch the error and set the loginError with the error message.
      _loginError = e.toString();
    }

    // Set the isLoading flag back to false to indicate that the login process has completed.
    _isLoading = false;
    // Notify the listeners that the state has changed after completing the login process.
    notifyListeners();
  }
}

..

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


// data/repositories/login_repository.dart
import '../services/api_service.dart';

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

  LoginRepository({required this.apiService});

  Future<String?> login(Map<dynamic, dynamic> req) async {
    try {
      // Attempt to make the API call to login the user using the apiService.
      final response = await apiService.loginUser(req);
      // Store the response from the API call in the 'response' variable.

      return response;
      // If the API call is successful, return the response (String) to the caller.
    } catch (e) {
      // If an exception occurs during the API call, catch it and handle the error.

      throw Exception('Failed to login');
      // Throw a new Exception with the message 'Failed to login', indicating that the login process failed.
      // The caller of this function can catch this exception and handle the error appropriately.
    }
  }
}

..

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';
import '../../utils/logger_interceptor.dart';

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

  // ApiService({required Dio dio}) : _dio = dio;
  ApiService({required Dio dio}) {
    _dio = Dio(BaseOptions(
      //baseUrl: "https://dummyjson.com/products/",
      // connectTimeout: const Duration(seconds:5),
      // receiveTimeout: const Duration(seconds: 3),
      responseType: ResponseType.json,
    ))
      ..interceptors.addAll([
        //LoggerInterceptor(), //custom logger interceptor.
      ]);
  }

 

  Future<String?> loginUser(Map<dynamic, dynamic> req) async {
    try {
      final response =
          await _dio.post('https://reqres.in/api/login', data: req);
      if (response.statusCode == 200) {
        // Success
        //return response.data['token'];
        // Check if 'token' key exists in the response data
        if (response.data.containsKey('token')) {
          return response.data['token'];
        }
        // If 'error' key doesn't exist, return a generic error message
        return 'An error occurred';
      } else {
        // Handle other status codes as needed
        return 'Failed to login';
      }
    } catch (e) {
      if (e is DioException) {
        if (e.response != null &&
            e.response!.data != null &&
            e.response!.data['error'] != null) {
          final errorMessage = e.response!.data['error'];
          print('Error Message: $errorMessage');
          return errorMessage;
        }
      }
      // Handle other errors or exceptions
      print('Error: $e');
      return null;
    }
  }
}

..

..

Json Login Request & Response:


https://reqres.in/api/login

Request:
{
    "email": "eve.holt@reqres.in",
    "password": "cityslicka"
}


success: 201 response
{
    "error": "user not found"
}



failure: 404 response
{
    "token": "QpwL5tke4Pnpja7X4"
}
..
common_text_field.dart : It is a custom text field that allows users to enter text with various customization options for decoration and input type.


// view/widget/common_text_field.dart
import 'package:flutter/material.dart';

class CommonTextField extends StatelessWidget {
  final TextEditingController controller;
  final String? hintText;
  final TextInputType? keyboardType;
  final bool obscureText;
  final Function(String)? onChanged;

  const CommonTextField({
    Key? key,
    required this.controller,
    this.hintText,
    this.keyboardType,
    this.obscureText = false,
    this.onChanged,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return TextField(
      controller: controller,
      keyboardType: keyboardType,
      obscureText: obscureText,
      onChanged: onChanged,
      decoration: InputDecoration(
        hintText: hintText,
        contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(8),
          borderSide: BorderSide(color: theme.primaryColor, width: 1),
        ),
        focusedBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(8),
          borderSide: BorderSide(color: theme.colorScheme.secondary, width: 2),
        ),
        enabledBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(8),
          borderSide: BorderSide(color: theme.disabledColor, width: 1),
        ),
        errorBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(8),
          borderSide: const BorderSide(color: Colors.red, width: 1),
        ),
        focusedErrorBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(8),
          borderSide: const BorderSide(color: Colors.red, width: 2),
        ),
        // You can add more customization to the decoration as needed
        // For example, adding icons, labels, etc.
      ),
    );
  }
}

..

Comments