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
htdocsfolder within the XAMPP installation directory (e.g.,C:\xampp\htdocs\course_appon Windows). -
If you are using MAMP, place the files in the
htdocsfolder within the MAMP directory (e.g.,/Applications/MAMP/htdocs/course_appon macOS). -
If using a live server, upload the files to a directory within the web server’s root folder (often named
public_htmlorwww).
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 oflocalhost. On Linux, open a terminal and typeifconfig, then look for your local IP address. On Windows, open a Command Prompt and typeipconfig, then look for your IPv4 address. The address is usually something like192.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.
-
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. -
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:-
GETto retrieve information, -
POSTto create new resources, -
PUTorPATCHto update resources, and -
DELETEto 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.
-
-
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. -
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. -
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.