Debug School

rakesh kumar
rakesh kumar

Posted on

How to Secure CRUD Operations in Flutter MVMM architecture with Login Authentication

Secure some operation of CRUD in Flutter

To enable viewing CRUD data without login but restrict actions like creating, editing, and deleting to only logged-in users, you need to manage the visibility and accessibility of these actions based on the authentication state. Here's the complete implementation:

Step-by-Step Guide

Setup Dependencies: Use http for REST API calls and provider for state management.
Authentication Provider: Manage authentication state.
CRUD Operations: Implement fetching, creating, updating, and deleting data.
UI Logic: Enable or disable actions based on authentication state.
Step 1: Setup Dependencies
Add the following dependencies to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  provider: ^5.0.0
  http: ^0.14.0
Enter fullscreen mode Exit fullscreen mode

Run flutter pub get to install the packages.

Step 2: Authentication Provider

Create a class to manage the authentication state.

Create the Authentication Service
Create a file named auth_service.dart in your services directory (create one if it doesn't exist).

// services/auth_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;

class AuthService {
  final String baseUrl = 'https://yourapi.com'; // Replace with your API base URL

  Future<Map<String, dynamic>> login(String username, String password) async {
    final url = Uri.parse('$baseUrl/login'); // Replace with your API login endpoint

    try {
      final response = await http.post(
        url,
        headers: {
          'Content-Type': 'application/json',
        },
        body: json.encode({
          'username': username,
          'password': password,
        }),
      );

      if (response.statusCode == 200) {
        return json.decode(response.body);
      } else {
        throw Exception('Failed to authenticate. Please check your credentials.');
      }
    } catch (error) {
      throw Exception('Failed to authenticate. Please try again later.');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Update the AuthProvider to Use the AuthService Modify your AuthProvider to use the AuthService for the login functionality.
// providers/auth_provider.dart
import 'package:flutter/material.dart';
import '../services/auth_service.dart';

class AuthProvider with ChangeNotifier {
  final AuthService _authService = AuthService();
  bool _isAuthenticated = false;
  String _authToken = '';

  bool get isAuthenticated => _isAuthenticated;

  Future<void> login(String username, String password) async {
    try {
      final responseData = await _authService.login(username, password);
      _authToken = responseData['token'];
      _isAuthenticated = true;
      notifyListeners();
    } catch (error) {
      _isAuthenticated = false;
      throw error;
    }
  }

  void logout() {
    _authToken = '';
    _isAuthenticated = false;
    notifyListeners();
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: CRUD Operations

Create a service class to handle API calls.

import 'dart:convert';
import 'package:http/http.dart' as http;

class ApiService {
  final String baseUrl = 'https://jsonplaceholder.typicode.com';

  Future<List<dynamic>> fetchData() async {
    final response = await http.get(Uri.parse('$baseUrl/posts'));
    if (response.statusCode == 200) {
      return json.decode(response.body);
    } else {
      throw Exception('Failed to load data');
    }
  }

  Future<void> createData(Map<String, dynamic> data) async {
    final response = await http.post(
      Uri.parse('$baseUrl/posts'),
      headers: {"Content-Type": "application/json"},
      body: json.encode(data),
    );
    if (response.statusCode != 201) {
      throw Exception('Failed to create data');
    }
  }

  Future<void> updateData(int id, Map<String, dynamic> data) async {
    final response = await http.put(
      Uri.parse('$baseUrl/posts/$id'),
      headers: {"Content-Type": "application/json"},
      body: json.encode(data),
    );
    if (response.statusCode != 200) {
      throw Exception('Failed to update data');
    }
  }

  Future<void> deleteData(int id) async {
    final response = await http.delete(Uri.parse('$baseUrl/posts/$id'));
    if (response.statusCode != 200) {
      throw Exception('Failed to delete data');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Main Entry Point and Routing

Set up the main entry point and define routes.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'auth_provider.dart';
import 'crud_screen.dart';
import 'login_screen.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => AuthProvider(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter CRUD App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Consumer<AuthProvider>(
        builder: (context, auth, child) {
          return auth.isAuthenticated ? CrudScreen() : CrudScreen(); // Always show CrudScreen, but control actions
        },
      ),
      routes: {
        '/crud': (context) => CrudScreen(),
        '/login': (context) => LoginScreen(),
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Login Screen

Create a login screen.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'auth_provider.dart';

class LoginScreen extends StatelessWidget {
  final TextEditingController _usernameController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: <Widget>[
            TextField(
              controller: _usernameController,
              decoration: InputDecoration(labelText: 'Username'),
            ),
            TextField(
              controller: _passwordController,
              decoration: InputDecoration(labelText: 'Password'),
              obscureText: true,
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                String username = _usernameController.text;
                String password = _passwordController.text;
                Provider.of<AuthProvider>(context, listen: false).login(username, password);
              },
              child: Text('Login'),
            ),
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: CRUD Screen

Create a screen to display and manage CRUD operations.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'auth_provider.dart';
import 'api_service.dart';

class CrudScreen extends StatefulWidget {
  @override
  _CrudScreenState createState() => _CrudScreenState();
}

class _CrudScreenState extends State<CrudScreen> {
  final ApiService apiService = ApiService();
  late Future<List<dynamic>> _data;

  @override
  void initState() {
    super.initState();
    _data = apiService.fetchData();
  }

  void _showCreateDialog() {
    showDialog(
      context: context,
      builder: (context) {
        return _DataDialog(
          onSubmit: (data) async {
            await apiService.createData(data);
            setState(() {
              _data = apiService.fetchData();
            });
          },
        );
      },
    );
  }

  void _showEditDialog(int id, Map<String, dynamic> data) {
    showDialog(
      context: context,
      builder: (context) {
        return _DataDialog(
          initialData: data,
          onSubmit: (updatedData) async {
            await apiService.updateData(id, updatedData);
            setState(() {
              _data = apiService.fetchData();
            });
          },
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    bool isAuthenticated = Provider.of<AuthProvider>(context).isAuthenticated;

    return Scaffold(
      appBar: AppBar(
        title: Text('CRUD Operations'),
        actions: <Widget>[
          isAuthenticated
              ? IconButton(
                  icon: Icon(Icons.logout),
                  onPressed: () {
                    Provider.of<AuthProvider>(context, listen: false).logout();
                    Navigator.pushReplacementNamed(context, '/login');
                  },
                )
              : IconButton(
                  icon: Icon(Icons.login),
                  onPressed: () {
                    Navigator.pushNamed(context, '/login');
                  },
                ),
        ],
      ),
      body: FutureBuilder<List<dynamic>>(
        future: _data,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Center(child: Text('Error: ${snapshot.error}'));
          } else if (!snapshot.hasData || snapshot.data!.isEmpty) {
            return Center(child: Text('No data available'));
          } else {
            return ListView.builder(
              itemCount: snapshot.data!.length,
              itemBuilder: (context, index) {
                var item = snapshot.data![index];
                return ListTile(
                  title: Text(item['title']),
                  subtitle: Text(item['body']),
                  trailing: isAuthenticated
                      ? Row(
                          mainAxisSize: MainAxisSize.min,
                          children: <Widget>[
                            IconButton(
                              icon: Icon(Icons.edit),
                              onPressed: () => _showEditDialog(item['id'], item),
                            ),
                            IconButton(
                              icon: Icon(Icons.delete),
                              onPressed: () async {
                                await apiService.deleteData(item['id']);
                                setState(() {
                                  _data = apiService.fetchData();
                                });
                              },
                            ),
                          ],
                        )
                      : null,
                );
              },
            );
          }
        },
      ),
      floatingActionButton: isAuthenticated
          ? FloatingActionButton(
              onPressed: _showCreateDialog,
              child: Icon(Icons.add),
            )
          : null,
    );
  }
}

class _DataDialog extends StatefulWidget {
  final Map<String, dynamic>? initialData;
  final Function(Map<String, dynamic>) onSubmit;

  _DataDialog({this.initialData, required this.onSubmit});

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

class __DataDialogState extends State<_DataDialog> {
  final TextEditingController _titleController = TextEditingController();
  final TextEditingController _bodyController = TextEditingController();

  @override
  void initState() {
    super.initState();
    if (widget.initialData != null) {
      _titleController.text = widget.initialData!['title'];
      _bodyController.text = widget.initialData!['body'];
    }
  }

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text(widget.initialData == null ? 'Create Data' : 'Edit Data'),
content: Column(
    mainAxisSize: MainAxisSize.min,
    children: <Widget>[
      TextField(
        controller: _titleController,
        decoration: InputDecoration(labelText: 'Title'),
      ),
      TextField(
        controller: _bodyController,
        decoration: InputDecoration(labelText: 'Body'),
      ),
    ],
  ),
  actions: <Widget>[
    ElevatedButton(
      onPressed: () {
        final data = {
          'title': _titleController.text,
          'body': _bodyController.text,
        };
        widget.onSubmit(data);
        Navigator.of(context).pop();
      },
      child: Text('Submit'),
    ),
    ElevatedButton(
      onPressed: () {
        Navigator.of(context).pop();
      },
      child: Text('Cancel'),
    ),
  ],
);
}
}
Enter fullscreen mode Exit fullscreen mode

Secure All operation of CRUD in Flutter

To implement an application where CRUD operations (Create, Read, Update, Delete) are displayed using a RESTful API but actions like create, edit, and delete are only enabled after login in Flutter, you can follow these steps:

Setup Dependencies: Use http for REST API calls and provider for state management.
Authentication Provider: Manage authentication state.
CRUD Operations: Implement fetching, creating, updating, and deleting data.
UI Logic: Enable or disable actions based on authentication state.
Here's a complete example:

Step 1: Setup Dependencies
Add the following dependencies to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  provider: ^5.0.0
  http: ^0.14.0
Enter fullscreen mode Exit fullscreen mode

Run flutter pub get to install the packages.

Step 2: Authentication Provider
Create a class to manage the authentication state.

import 'package:flutter/material.dart';

class AuthProvider with ChangeNotifier {
  bool _isAuthenticated = false;

  bool get isAuthenticated => _isAuthenticated;

  void login(String username, String password) {
    // Implement your authentication logic here.
    // For demo purposes, we just set it to true.
    _isAuthenticated = true;
    notifyListeners();
  }

  void logout() {
    _isAuthenticated = false;
    notifyListeners();
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: CRUD Operations
Create a service class to handle API calls.

import 'dart:convert';
import 'package:http/http.dart' as http;

class ApiService {
  final String baseUrl = 'https://jsonplaceholder.typicode.com';

  Future<List<dynamic>> fetchData() async {
    final response = await http.get(Uri.parse('$baseUrl/posts'));
    if (response.statusCode == 200) {
      return json.decode(response.body);
    } else {
      throw Exception('Failed to load data');
    }
  }

  Future<void> createData(Map<String, dynamic> data) async {
    final response = await http.post(
      Uri.parse('$baseUrl/posts'),
      headers: {"Content-Type": "application/json"},
      body: json.encode(data),
    );
    if (response.statusCode != 201) {
      throw Exception('Failed to create data');
    }
  }

  Future<void> updateData(int id, Map<String, dynamic> data) async {
    final response = await http.put(
      Uri.parse('$baseUrl/posts/$id'),
      headers: {"Content-Type": "application/json"},
      body: json.encode(data),
    );
    if (response.statusCode != 200) {
      throw Exception('Failed to update data');
    }
  }

  Future<void> deleteData(int id) async {
    final response = await http.delete(Uri.parse('$baseUrl/posts/$id'));
    if (response.statusCode != 200) {
      throw Exception('Failed to delete data');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Main Entry Point and Routing
Set up the main entry point and define routes.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'auth_provider.dart';
import 'crud_screen.dart';
import 'login_screen.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => AuthProvider(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter CRUD App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Consumer<AuthProvider>(
        builder: (context, auth, child) {
          return auth.isAuthenticated ? CrudScreen() : LoginScreen();
        },
      ),
      routes: {
        '/crud': (context) => CrudScreen(),
        '/login': (context) => LoginScreen(),
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Login Screen
Create a login screen.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'auth_provider.dart';

class LoginScreen extends StatelessWidget {
  final TextEditingController _usernameController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: <Widget>[
            TextField(
              controller: _usernameController,
              decoration: InputDecoration(labelText: 'Username'),
            ),
            TextField(
              controller: _passwordController,
              decoration: InputDecoration(labelText: 'Password'),
              obscureText: true,
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                String username = _usernameController.text;
                String password = _passwordController.text;
                Provider.of<AuthProvider>(context, listen: false).login(username, password);
              },
              child: Text('Login'),
            ),
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: CRUD Screen
Create a screen to display and manage CRUD operations.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'auth_provider.dart';
import 'api_service.dart';

class CrudScreen extends StatefulWidget {
  @override
  _CrudScreenState createState() => _CrudScreenState();
}

class _CrudScreenState extends State<CrudScreen> {
  final ApiService apiService = ApiService();
  late Future<List<dynamic>> _data;

  @override
  void initState() {
    super.initState();
    _data = apiService.fetchData();
  }

  void _showCreateDialog() {
    showDialog(
      context: context,
      builder: (context) {
        return _DataDialog(
          onSubmit: (data) async {
            await apiService.createData(data);
            setState(() {
              _data = apiService.fetchData();
            });
          },
        );
      },
    );
  }

  void _showEditDialog(int id, Map<String, dynamic> data) {
    showDialog(
      context: context,
      builder: (context) {
        return _DataDialog(
          initialData: data,
          onSubmit: (updatedData) async {
            await apiService.updateData(id, updatedData);
            setState(() {
              _data = apiService.fetchData();
            });
          },
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    bool isAuthenticated = Provider.of<AuthProvider>(context).isAuthenticated;

    return Scaffold(
      appBar: AppBar(
        title: Text('CRUD Operations'),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.logout),
            onPressed: () {
              Provider.of<AuthProvider>(context, listen: false).logout();
              Navigator.pushReplacementNamed(context, '/login');
            },
          ),
        ],
      ),
      body: FutureBuilder<List<dynamic>>(
        future: _data,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Center(child: Text('Error: ${snapshot.error}'));
          } else if (!snapshot.hasData || snapshot.data!.isEmpty) {
            return Center(child: Text('No data available'));
          } else {
            return ListView.builder(
              itemCount: snapshot.data!.length,
              itemBuilder: (context, index) {
                var item = snapshot.data![index];
                return ListTile(
                  title: Text(item['title']),
                  subtitle: Text(item['body']),
                  trailing: isAuthenticated
                      ? Row(
                          mainAxisSize: MainAxisSize.min,
                          children: <Widget>[
                            IconButton(
                              icon: Icon(Icons.edit),
                              onPressed: () => _showEditDialog(item['id'], item),
                            ),
                            IconButton(
                              icon: Icon(Icons.delete),
                              onPressed: () async {
                                await apiService.deleteData(item['id']);
                                setState(() {
                                  _data = apiService.fetchData();
                                });
                              },
                            ),
                          ],
                        )
                      : null,
                );
              },
            );
          }
        },
      ),
      floatingActionButton: isAuthenticated
          ? FloatingActionButton(
              onPressed: _showCreateDialog,
              child: Icon(Icons.add),
            )
          : null,
    );
  }
}

class _DataDialog extends StatefulWidget {
  final Map<String, dynamic>? initialData;
  final Function(Map<String, dynamic>) onSubmit;

  _DataDialog({this.initialData, required this.onSubmit});

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

class __DataDialogState extends State<_DataDialog> {
  final TextEditingController _titleController = TextEditingController();
  final TextEditingController _bodyController = TextEditingController();

  @override
  void initState() {
    super.initState();
    if (widget.initialData != null) {
      _titleController.text = widget.initialData!['title'];
      _bodyController.text = widget.initialData!['body'];
    }
  }

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text(widget.initialData == null ? 'Create Data' : 'Edit Data'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          TextField(
            controller: _titleController,
            decoration: InputDecoration(labelText: 'Title'),
          ),
Enter fullscreen mode Exit fullscreen mode

Top comments (0)