Rest API and MySQL

1 App Development

1.1 MySQL Connection

To connect the Flutter app to a real MySQL database, you’ll need to use a RESTful API as an intermediary because of security reasons, Flutter can’t directly connect to MySQL. A backend API can act as a bridge between your Flutter app and the MySQL database. Common options include Laravel (PHP), Node.js (Javascript), Django (Python), Spring Boot (Java). We will use PHP to bridge Flutter app’s connections to MySQL database.

To transition from a local SQLite database to a MySQL database accessed via a PHP REST API, we’ll adjust our Flutter app code to use HTTP requests instead of SQLite queries. This involves removing the SQLite dependencies, setting up CRUD functions to communicate with PHP API, and updating your UI code accordingly.

We will need first to install a PHP server and a MySQL server. Apache is a common choice because it can handle PHP scripts and MySQL connections efficiently, but other servers like Nginx or LiteSpeed can also serve PHP.

For Windows development, you could use a setup like XAMPP that bundles Apache, PHP, and MySQL. These will handle HTTP requests from Flutter to interact with MySQL via the PHP REST API.

1.1.1 Creating the PHP Files

To fully implement the PHP backend that connects your Flutter app to a MySQL database, you’ll generally need multiple PHP files. Here’s a walkthrough of the essential files you’ll need.

  • config.php: Sets up the database connection.

  • fetch_battles.php: Retrieves all battle records.

  • insert_battle.php: Inserts a new battle record.

  • update_battle.php: Updates an existing battle.

  • delete_battle.php: Deletes a specified battle record.

1.1.1.1 config.php

This file manages the connection details for your MySQL database. It makes your configuration modular so other scripts can include it as needed.

<?php
// config.php
$host = 'localhost'; // Database host
$db = 'your_database_name'; // Database name
$user = 'your_username'; // Database username
$password = 'your_password'; // Database password

try {
    $pdo = new PDO("mysql:host=$host;dbname=$db", $user, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die("Database connection failed: " . $e->getMessage());
}
?>
1.1.1.2 fetch_battles.php

This file retrieves all Battle entries from the database and sends them as JSON data to the Flutter app.

<?php
// fetch_battles.php
include 'config.php';

try {
    $stmt = $pdo->query('SELECT * FROM battles');
    $battles = $stmt->fetchAll(PDO::FETCH_ASSOC);

    echo json_encode(['status' => 'success', 'data' => $battles]);
} catch (PDOException $e) {
    echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
?>
1.1.1.3 insert_battle.php

This file inserts a new battle record into the database, using POST data received from the Flutter app.

<?php
// insert_battle.php
include 'config.php';

$name = $_POST['name'] ?? '';
$date = $_POST['date'] ?? '';
$summary = $_POST['summary'] ?? '';

try {
    $stmt = $pdo->prepare("INSERT INTO battles (name, date, summary) VALUES (?, ?, ?)");
    $stmt->execute([$name, $date, $summary]);

    echo json_encode(['status' => 'success', 'message' => 'Battle added successfully']);
} catch (PDOException $e) {
    echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
?>
1.1.1.4 update_battle.php

This file updates an existing battle entry based on an ID received via POST from the Flutter app.

<?php
// update_battle.php
include 'config.php';

$id = $_POST['id'] ?? null;
$name = $_POST['name'] ?? '';
$date = $_POST['date'] ?? '';
$summary = $_POST['summary'] ?? '';

try {
    $stmt = $pdo->prepare("UPDATE battles SET name = ?, date = ?, summary = ? WHERE id = ?");
    $stmt->execute([$name, $date, $summary, $id]);

    echo json_encode(['status' => 'success', 'message' => 'Battle updated successfully']);
} catch (PDOException $e) {
    echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
?>
1.1.1.5 delete_battle.php

This file deletes a battle from the database, identified by an ID received from the Flutter app.

<?php
// delete_battle.php
include 'config.php';

$id = $_POST['id'] ?? null;

try {
    $stmt = $pdo->prepare("DELETE FROM battles WHERE id = ?");
    $stmt->execute([$id]);

    echo json_encode(['status' => 'success', 'message' => 'Battle deleted successfully']);
} catch (PDOException $e) {
    echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
?>

1.1.2 Setting Up the Server

To connect your Flutter application to a MySQL database via PHP, follow these steps:

1.1.2.1 Step 1: Set Up a Web Server

To use PHP, you will need a web server that supports PHP. Options include XAMPP (for Windows and Mac), MAMP (for Mac), or LAMP (for Linux). These packages include Apache, MySQL, and PHP, allowing you to host and test PHP files locally.

1.1.2.2 Step 2: Place PHP Files in the Server Directory
  • If you are using XAMPP, place the PHP files in the htdocs folder within the XAMPP installation directory (e.g., C:\xampp\htdocs\course_app on Windows).

  • If you are using MAMP, place the files in the htdocs folder within the MAMP directory (e.g., /Applications/MAMP/htdocs/course_app on macOS).

  • If using a live server, upload the files to a directory within the web server’s root folder (often named public_html or www).

For example, if your project is named course_app, create a directory called course_app within the server’s root directory and place all PHP files (such as config.php, fetch_battles.php, etc.) inside this folder.

1.1.2.3 Step 3: Set Folder Permissions

Ensure that the server has permission to read and execute the PHP files, and that the database credentials in config.php are configured with the appropriate access level.

1.1.2.4 Step 4: Access PHP Files via URLs

Once the PHP files are placed on the server, you can access them through URLs.

  • For local testing: If files are placed in htdocs/course_app, the URL might look like:
    http://localhost/course_app/fetch_battles.php
    From an Android device/emulator, you will need to put your computer’s internal IP address instead of localhost. On Linux, open a terminal and type ifconfig, then look for your local IP address. On Windows, open a Command Prompt and type ipconfig, then look for your IPv4 address. The address is usually something like 192.168.x.y. Then input your address e.g.:
    http://192.168.1.6/course_app/fetch_battles.php

  • For a live server, the URL might look like:
    https://yourdomain.com/course_app/fetch_battles.php

1.1.2.5 Step 5: Update Flutter App

In your Flutter app, ensure the HTTP requests (e.g., http.get() or http.post()) are directed to the correct URLs for these PHP files.

1.1.3 Modifying the Flutter Code

Now, we need to modify the Flutter code to use HTTP requests instead of SQLite methods.

1.1.3.1 database_helper.dart

You’ll use the http package in Dart, so be sure to add it to your pubspec.yaml:

dependencies:
  http: ^0.13.4
1.1.3.2 database_helper.dart

Replace this file with new methods to call the PHP API.

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

class DatabaseHelper {
  static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
  DatabaseHelper._privateConstructor();

  final String baseUrl = "http://192.168.1.8/apps/course_app"; // Update with your server URL

  Future<List<Battle>> getAllBattles() async {
  print(Uri.parse('$baseUrl/fetch_battles.php'));
  final response = await http.get(Uri.parse('$baseUrl/fetch_battles.php'));
  print(response);

  if (response.statusCode == 200) {
    // Decode the JSON response
    final Map<String, dynamic> decoded = json.decode(response.body);

    // Extract the 'data' list from the decoded JSON map
    List<dynamic> data = decoded["data"];

    // Map each item in 'data' to a Battle object
    return data.map((json) => Battle.fromMap(json)).toList();
  } else {
    throw Exception("Failed to load battles");
  }
}

  Future<int> updateBattle(Battle battle) async {
  try {
    final response = await http.post(
      Uri.parse('$baseUrl/update_battle.php'),
      body: battle.toMap(),
    );

    print("Response status: ${response.statusCode}");
    print("Response body: ${response.body}");

    if (response.statusCode == 200) {
      final Map<String, dynamic> data = json.decode(response.body);
      if (data['status'] == 'success') {
        return 1; // Success
      } else {
        throw Exception("Failed to update battle: ${data['message']}");
      }
    } else {
      throw Exception("Failed to update battle with status code: ${response.statusCode}");
    }
  } catch (e) {
    print("Error occurred: $e");
    throw Exception("Failed to update battle: $e");
  }
}

  Future<int> insertBattle(Battle battle) async {
    final response = await http.post(
      Uri.parse('$baseUrl/add_battle.php'),
      body: battle.toMap(),
    );

    if (response.statusCode == 200) {
      return 1; // Success
    } else {
      throw Exception("Failed to insert battle");
    }
  }

  Future<int> deleteBattle(int id) async {
    final response = await http.post(
      Uri.parse('$baseUrl/delete_battle.php'),
      body: {'id': id.toString()},
    );

    if (response.statusCode == 200) {
      return 1; // Success
    } else {
      throw Exception("Failed to delete battle");
    }
  }
}
1.1.3.3 battle_view.dart

Update battle_view.dart to Fetch Data from the API.

Replace SQLite-based calls in BattleView with calls to DatabaseHelper’s getAllBattles method.

import 'package:flutter/material.dart';
import 'package:course_app/utils/battle.dart';
import 'package:course_app/utils/database_helper.dart';

class BattleView extends StatelessWidget {
  BattleView();

  Future<List<Battle>> fetchBattles() async {
    final dbHelper = DatabaseHelper.instance;
    return await dbHelper.getAllBattles();
  }

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 8.0),
        child: FutureBuilder<List<Battle>>(
          future: fetchBattles(),
          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) {
              final battles = snapshot.data ?? [];

              return ListView.builder(
                itemCount: battles.length,
                itemBuilder: (context, index) {
                  final battle = battles[index];
                  return InkWell(
                    onTap: () {
                      Navigator.pushNamed(
                        context,
                        '/battle',
                        arguments: {'battle': battle},
                      );
                    },
                    child: Container(
                      margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
                      padding: EdgeInsets.all(10),
                      decoration: BoxDecoration(
                        color: Colors.grey[200],
                        border: Border.all(color: Colors.grey),
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            battle.name,
                            style: TextStyle(
                                fontWeight: FontWeight.bold, fontSize: 16),
                          ),
                          SizedBox(height: 5),
                          Text(
                            battle.date,
                            style: TextStyle(
                                fontSize: 14, fontStyle: FontStyle.italic),
                          ),
                        ],
                      ),
                    ),
                  );
                },
              );
            } else {
              return Center(child: Text('No battles available.'));
            }
          },
        ),
      ),
    );
  }
}
1.1.3.4 battle_screen.dart

Update battle_screen.dart to save data via the API. Replace the _saveBattle function to use the API update method.

Future<void> _saveBattle() async {
  final dbHelper = DatabaseHelper.instance;
  battle = Battle(
    id: battle.id,
    name: _nameController.text,
    date: _dateController.text,
    summary: _summaryController.text,
  );

  await dbHelper.updateBattle(battle);
  ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Battle updated!')));
  Navigator.pop(context);
}

1.1.4 Test

Start Your PHP Server and make sure all API endpoints are accessible from your Flutter app.

Run the Flutter App and test each feature (fetching, adding, updating, and deleting battles) to ensure everything is connected to the MySQL database through the API.

2 Instant Updates

If you make changes to the content inside BattleScreen, you will notice that the changes reflect in the MySQL database, but do not show up immediately when you navigate back to Home. If you visit BattleScreen again, it won’t show the previous changes.

We therefore need to force the screens to reload after the change to the database.

To make the updated battle information reflect immediately without requiring a hot reload, you can use setState() in BattleView to re-fetch the data when returning from BattleScreen. This can be achieved by refreshing the battles list when navigating back.

import 'package:flutter/material.dart';
import 'package:course_app/utils/battle.dart';
import 'package:course_app/utils/database_helper.dart';

class BattleView extends StatefulWidget {
  @override
  _BattleViewState createState() => _BattleViewState();
}

class _BattleViewState extends State<BattleView> {
  Future<List<Battle>>? _battlesFuture;

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

  void _loadBattles() {
    final dbHelper = DatabaseHelper.instance;
    setState(() {
      _battlesFuture = dbHelper.getAllBattles();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 8.0),
        child: FutureBuilder<List<Battle>>(
          future: _battlesFuture,
          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) {
              final battles = snapshot.data ?? [];

              return ListView.builder(
                itemCount: battles.length,
                itemBuilder: (context, index) {
                  final battle = battles[index];
                  return InkWell(
                    onTap: () {
                      Navigator.pushNamed(
                        context,
                        '/battle',
                        arguments: {'battle': battle},
                      ).then((_) {
                        // Refresh battles after returning from BattleScreen
                        _loadBattles();
                      });
                    },
                    child: Container(
                      margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
                      padding: EdgeInsets.all(10),
                      decoration: BoxDecoration(
                        color: Colors.grey[200],
                        border: Border.all(color: Colors.grey),
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            battle.name,
                            style: TextStyle(
                                fontWeight: FontWeight.bold, fontSize: 16),
                          ),
                          SizedBox(height: 5),
                          Text(
                            battle.date,
                            style: TextStyle(
                                fontSize: 14, fontStyle: FontStyle.italic),
                          ),
                        ],
                      ),
                    ),
                  );
                },
              );
            } else {
              return Center(child: Text('No battles available.'));
            }
          },
        ),
      ),
    );
  }
}

Navigator.pushNamed is used with the specified route name and the battle object as an argument. .then((_) => _loadBattles()) refreshes the list by calling _loadBattles after returning from BattleScreen.

The underscore (‘_‘) in Dart (and in many other programming languages) is often used as a placeholder for a variable whose value you don’t need to use. In the context of ‘then((_) => ...)‘, it is typically used in a ‘Future‘’s ‘then‘ callback, where the function expects a parameter but you don’t actually need that parameter.

For example:

someFuture.then((_) {
  // Code here runs after the future completes, but
  // the result of the future is not needed.
});

In this case:

  • The ‘then‘ method takes a callback function that executes after ‘someFuture‘ completes.

  • The underscore (‘_‘) is a convention meaning "I don’t care about this value," so the callback is saying, "I need to run some code when this future completes, but I don’t need the future’s result."

If the future has a meaningful return value you want to use, you would replace ‘_‘ with a named variable:

someFuture.then((result) {
  print(result); // Do something with the result
});

Using ‘_‘ helps make it clear to anyone reading the code that the return value is intentionally ignored.

3 Explanation of Concepts

.1 REST

In the context of a REST API, REST stands for REpresentational State Transfer. REST is an architectural style for designing networked applications and is widely used for building APIs. The key components of REST are the following.

  1. Representation:
    In a REST API, resources (such as a “User” or “Product”) are represented in a specific format, typically JSON or XML, which is transferred between the client and server. This allows different clients (web browsers, mobile apps, etc.) to interact with the same data in a consistent format.

  2. State Transfer:
    When a client interacts with a RESTful service, it changes the “state” of a resource (like adding a new record or updating an entry) using specific HTTP methods:

    • GET to retrieve information,

    • POST to create new resources,

    • PUT or PATCH to update resources, and

    • DELETE to remove resources.

    Each request is independent (stateless), meaning no session state is stored on the server between requests. This makes REST APIs more scalable and easier to manage.

  3. Statelessness:
    REST APIs are stateless. This means each API call from the client to the server must contain all the information needed to understand and process the request. The server does not store any session data, so each request is independent, which simplifies scaling and reduces server overhead.

  4. Uniform Interface:
    REST relies on a standard, uniform way of interacting with resources. This includes well-defined HTTP methods (verbs) and consistent use of URIs (Uniform Resource Identifiers) to identify resources.

  5. Client-Server Separation:
    RESTful systems separate client and server concerns. The client interacts with the user, while the server handles the database and business logic. This allows them to evolve independently and makes the system easier to maintain.

Last modified: Tuesday, 3 December 2024, 5:20 AM