Implementing Secure Login Functionality in Flutter Using the MVC Pattern

MVC stands for Model-View-Controller, a popular software design pattern used to create scalable and maintainable applications. Flutter, being a popular framework for building mobile applications, also follows the MVC architecture.

Here's a brief overview of how the MVC architecture works in Flutter:

  • Model: The model represents the data and business logic of the application. In Flutter, you can create models using Dart classes that define the structure and behaviour of your data.
  • View: The view is responsible for rendering the user interface of the application. In Flutter, you can create views using widgets, which are building blocks that compose the UI.
  • Controller: The controller is the intermediary between the model and the view. It handles user input, updates the model accordingly, and triggers UI updates in the view. In Flutter, you can create controllers using stateful widgets, which are widgets that can maintain their own state and update themselves when needed.

In Flutter, the MVC architecture is often implemented using the StatefulWidget class. The StatefulWidget class allows you to define a widget that has a mutable state, which can be used to represent the model of the application. The StatefulWidget class also allows you to define a separate widget to represent the view of the application, and a separate class to represent the controller.

login_page_flutter_MVC


Here's an example of how you might implement the MVC architecture in Flutter:

You can test this Login page with this Username and Password:

    Username: eve.holt@reqres.in

    Password: cityslicka

Now, let's see how we can split the code:

Model:- user.dart

In this code, the data that needs to be stored in the Model is the user's email and password.

class User {
  final String email;
  final String password;

  User({required this.email, required this.password});
}

Controller:- sign_in.dart

The signIn function handles user input and updates the Model and View accordingly. It can be considered as the controller.

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../modal/user.dart';

class LoginController {
  Future<String?> signIn(User user, String text) async {
    SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
    Map data = {'email': user.email, 'password': user.password};
    var jsonResponse = null;
    var response =
        await http.post(Uri.parse('https://reqres.in/api/login'), body: data);
    if (response.statusCode == 200) {
      jsonResponse = json.decode(response.body);
      if (jsonResponse != null) {
        sharedPreferences.setString("token", jsonResponse['token']);
        return jsonResponse['token'];
      }
    } else {
      print(response.body);
      return null;
    }
    return null;
  }
}

View:- login.dart

The LoginPage class represents the view. It is responsible for displaying the user interface to the user.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import '../main.dart';
import '../modal/user.dart';
import '../services/sign_in.dart';

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

  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final LoginController _loginController = LoginController();
  bool _isLoading = false;
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  void _signIn() async {
    setState(() {
      _isLoading = true;
    });
    final user =
        User(email: _emailController.text, password: _passwordController.text);
    final token = await _loginController.signIn(user, 'text');

    if (token != null) {
      Navigator.of(context).pushAndRemoveUntil(
        MaterialPageRoute(builder: (_) => const MainPage()),
        (Route<dynamic> route) => false,
      );
    } else {
      setState(() {
        _isLoading = false;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Invalid email or password')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light.copyWith(
      statusBarColor: Colors.transparent,
    ));
    return Scaffold(
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : Column(
              children: [
                Container(
                  margin: const EdgeInsets.only(top: 50.0),
                  padding: const EdgeInsets.symmetric(
                      horizontal: 20.0, vertical: 30.0),
                  child: Image.network(
                      'https://blogger.googleusercontent.com/img/a/AVvXsEhdOsFuVngageLIphCbJ2cXqgdfhifZMKj3l01OlrtPkN2g-UMW0yOEnWGr_p8bVZ7iMAqIFpmQ3zLH3XxyGq4_LxUFVsvaznnw4fBqP81B6MnmV2BFU0HK-ypuHxvGkrFdGjprTyW22c6ApXwg9ByNFdVjMyh9PHYSpf7SL27xS4K52br8R4LhV2h2rA=s1600'),
                ),
                Container(
                  padding: const EdgeInsets.symmetric(
                      horizontal: 15.0, vertical: 20.0),
                  child: Column(
                    children: <Widget>[
                      TextFormField(
                        controller: _emailController,
                        cursorColor: Colors.black,
                        style: const TextStyle(color: Colors.black54),
                        decoration: const InputDecoration(
                          icon: Icon(Icons.email, color: Colors.black54),
                          hintText: "Email",
                          border: UnderlineInputBorder(
                            borderSide: BorderSide(color: Colors.black54),
                          ),
                          hintStyle: TextStyle(color: Colors.black54),
                        ),
                      ),
                      const SizedBox(height: 30.0),
                      TextFormField(
                        controller: _passwordController,
                        cursorColor: Colors.black,
                        obscureText: true,
                        style: const TextStyle(color: Colors.black54),
                        decoration: const InputDecoration(
                          icon: Icon(Icons.lock, color: Colors.black54),
                          hintText: "Password",
                          border: UnderlineInputBorder(
                            borderSide: BorderSide(color: Colors.black54),
                          ),
                          hintStyle: TextStyle(color: Colors.black54),
                        ),
                      ),
                    ],
                  ),
                ),
                Container(
                  width: MediaQuery.of(context).size.width,
                  height: 40.0,
                  padding: const EdgeInsets.symmetric(horizontal: 15.0),
                  margin: const EdgeInsets.only(top: 15.0),
                  child: ElevatedButton(
                    onPressed: _emailController.text == '' ||
                            _passwordController.text == ''
                        ? null
                        : _signIn,
                    child: const Text(
                      'Sign In',
                      style: TextStyle(color: Colors.black54),
                    ),
                  ),
                ),
              ],
            ),
    );
  }
}

So, to split this code into MVC architecture, we need to create a separate file for the Model and a separate file for the Controller.

Main:- main.dart

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:test_a_s/view/login.dart';

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Login Page",
      debugShowCheckedModeBanner: false,
      home: const MainPage(),
      theme: ThemeData(primarySwatch: Colors.orange),
    );
  }
}

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

  @override
  // ignore: library_private_types_in_public_api
  _MainPageState createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  late SharedPreferences sharedPreferences;

  @override
  void initState() {
    super.initState();
    checkLoginStatus();
  }

  checkLoginStatus() async {
    sharedPreferences = await SharedPreferences.getInstance();
    if (sharedPreferences.getString("token") == null) {
      Navigator.of(context).pushAndRemoveUntil(
          MaterialPageRoute(builder: (BuildContext context) => LoginPage()),
          (Route<dynamic> route) => false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Logged in Screen",
            style: TextStyle(color: Colors.white)),
        actions: <Widget>[
          ElevatedButton(
            onPressed: () {
              sharedPreferences.clear();
              // ignore: deprecated_member_use
              sharedPreferences.commit();
              Navigator.of(context).pushAndRemoveUntil(
                  MaterialPageRoute(
                      builder: (BuildContext context) => LoginPage()),
                  (Route<dynamic> route) => false);
            },
            child: const Text("Log Out", style: TextStyle(color: Colors.white)),
          ),
        ],
      ),
      body: const Center(child: Text("Logged In Screen")),
      drawer: const Drawer(),
    );
  }
}


GitHub Links

Follow @PradeepTheDeveloper Star Issue Sponsor


Comments

Popular posts from this blog

Error Handling in Flutter - Gradle issue

How to Make a Dynamic and Trending ListView with Flutter Widgets?

Understanding API integration with Getx State management