Flutter berkomunikasi dengan ESP32 via HTTP REST API di jaringan lokal.
Flutter HTTP Client ↔ ESP32 HTTP Server (lokal network, tanpa internet)
Terminalflutter pub add http
YAMLdependencies:
flutter:
sdk: flutter
http: ^1.2.0
Buka android/app/src/main/AndroidManifest.xml:
XML<manifest ...>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<application
android:usesCleartextTraffic="true"
...>
android:usesCleartextTraffic="true" diperlukan karena ESP32 menggunakan HTTP (bukan HTTPS). Ini hanya untuk development / jaringan lokal.
Dartimport 'dart:convert';
import 'package:http/http.dart' as http;
class Esp32Service {
// IP default ESP32 AP mode
static const String baseUrl = 'http://192.168.4.1';
static const Duration timeout = Duration(seconds: 5);
/// GET /api/status — baca status sensor + LED
static Future<Map<String, dynamic>?> getStatus() async {
try {
final response = await http
.get(Uri.parse('$baseUrl/api/status'))
.timeout(timeout);
if (response.statusCode == 200) {
return jsonDecode(response.body);
}
} catch (e) {
print('Error getStatus: $e');
}
return null;
}
/// POST /api/led — kontrol LED ON/OFF
static Future<bool> setLed(bool isOn) async {
try {
final response = await http
.post(
Uri.parse('$baseUrl/api/led'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'state': isOn ? 'ON' : 'OFF'}),
)
.timeout(timeout);
return response.statusCode == 200;
} catch (e) {
print('Error setLed: $e');
}
return false;
}
/// Cek apakah ESP32 online
static Future<bool> ping() async {
try {
final response = await http
.get(Uri.parse('$baseUrl/api/status'))
.timeout(const Duration(seconds: 2));
return response.statusCode == 200;
} catch (_) {
return false;
}
}
}
Dartimport 'dart:async';
import 'package:flutter/material.dart';
// import esp32_service.dart dari step sebelumnya
class WifiControlPage extends StatefulWidget {
const WifiControlPage({super.key});
@override
State<WifiControlPage> createState() => _WifiControlPageState();
}
class _WifiControlPageState extends State<WifiControlPage> {
bool _isConnected = false;
bool _ledOn = false;
int _cahaya = 0;
int _uptime = 0;
int _clients = 0;
Timer? _pollTimer;
@override
void initState() {
super.initState();
_checkConnection();
}
@override
void dispose() {
_pollTimer?.cancel();
super.dispose();
}
// Cek koneksi & mulai polling
Future<void> _checkConnection() async {
final online = await Esp32Service.ping();
setState(() => _isConnected = online);
if (online) {
_fetchStatus();
// Polling setiap 2 detik
_pollTimer?.cancel();
_pollTimer = Timer.periodic(
const Duration(seconds: 2),
(_) => _fetchStatus(),
);
}
}
// Ambil data sensor dari ESP32
Future<void> _fetchStatus() async {
final data = await Esp32Service.getStatus();
if (data != null && mounted) {
setState(() {
_ledOn = data['led'] == 'ON';
_cahaya = data['cahaya'] ?? 0;
_uptime = data['uptime'] ?? 0;
_clients = data['clients'] ?? 0;
});
}
}
// Toggle LED
Future<void> _toggleLed() async {
final success = await Esp32Service.setLed(!_ledOn);
if (success) {
setState(() => _ledOn = !_ledOn);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0f172a),
appBar: AppBar(
title: const Text('WiFi Controller'),
backgroundColor: const Color(0xFF1e293b),
actions: [
Icon(
_isConnected ? Icons.wifi : Icons.wifi_off,
color: _isConnected ? Colors.green : Colors.red,
),
const SizedBox(width: 16),
],
),
body: _isConnected ? _buildDashboard() : _buildOffline(),
);
}
Widget _buildOffline() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.wifi_off, size: 80, color: Colors.red),
const SizedBox(height: 16),
const Text(
'Tidak terhubung ke ESP32',
style: TextStyle(color: Colors.white, fontSize: 18),
),
const SizedBox(height: 8),
Text(
'Pastikan HP terkoneksi ke WiFi\n"ESP32_IoT_AP"',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[500]),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _checkConnection,
icon: const Icon(Icons.refresh),
label: const Text('Coba Lagi'),
),
],
),
);
}
Widget _buildDashboard() {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Status cards
Row(
children: [
_card('☀️ Cahaya', '$_cahaya', Colors.amber),
const SizedBox(width: 12),
_card('⏱️ Uptime', '${_uptime}s', Colors.cyan),
const SizedBox(width: 12),
_card('📱 Clients', '$_clients', Colors.purple),
],
),
const SizedBox(height: 32),
// LED Control
GestureDetector(
onTap: _toggleLed,
child: Container(
padding: const EdgeInsets.all(40),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _ledOn
? Colors.amber.withValues(alpha: 0.2)
: const Color(0xFF1e293b),
border: Border.all(
color: _ledOn ? Colors.amber : Colors.grey,
width: 3,
),
),
child: Icon(
Icons.lightbulb,
size: 60,
color: _ledOn ? Colors.amber : Colors.grey,
),
),
),
const SizedBox(height: 16),
Text(
'LED: ${_ledOn ? "ON" : "OFF"}',
style: TextStyle(
color: _ledOn ? Colors.amber : Colors.grey,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const Text(
'Tap untuk toggle',
style: TextStyle(color: Colors.grey),
),
],
),
);
}
Widget _card(String label, String value, Color color) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF1e293b),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withValues(alpha: 0.3)),
),
child: Column(
children: [
Text(label,
style: TextStyle(color: color, fontSize: 12)),
const SizedBox(height: 8),
Text(value,
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold)),
],
),
),
);
}
}
Sequence Diagram: Polling + kontrol LED via HTTP