494 lines
14 KiB
Markdown
494 lines
14 KiB
Markdown
# Mobile App Integration Guide
|
|
|
|
Panduan integrasi API Classification dengan aplikasi mobile Flutter.
|
|
|
|
## 📱 Flutter Integration
|
|
|
|
### 1. Dependencies
|
|
|
|
Tambahkan ke `pubspec.yaml` di `mobile_TA/padi_app/`:
|
|
|
|
```yaml
|
|
dependencies:
|
|
flutter:
|
|
sdk: flutter
|
|
http: ^1.1.0
|
|
image_picker: ^1.0.0
|
|
permission_handler: ^11.4.0
|
|
image: ^4.0.0
|
|
cached_network_image: ^3.3.0
|
|
```
|
|
|
|
### 2. Service Class
|
|
|
|
Buat file `lib/services/classification_service.dart`:
|
|
|
|
```dart
|
|
import 'package:http/http.dart' as http;
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:image_picker/image_picker.dart';
|
|
|
|
class ClassificationService {
|
|
static const String _baseUrl = 'http://YOUR_LARAVEL_IP:8000/api/classification';
|
|
|
|
// Get IP lokal Laravel server
|
|
// Jika testing di device lokal: http://127.0.0.1:8000
|
|
// Jika testing di emulator: http://10.0.2.2:8000
|
|
// Jika testing di device fisik: http://192.168.x.x:8000
|
|
|
|
final http.Client _httpClient;
|
|
|
|
ClassificationService({http.Client? httpClient})
|
|
: _httpClient = httpClient ?? http.Client();
|
|
|
|
/// Klasifikasi dari file gambar
|
|
Future<ClassificationResult> classifyImage(File imageFile) async {
|
|
try {
|
|
final request = http.MultipartRequest('POST', Uri.parse('$_baseUrl/classify'));
|
|
|
|
request.files.add(
|
|
await http.MultipartFile.fromPath('image', imageFile.path),
|
|
);
|
|
|
|
final response = await request.send().timeout(
|
|
const Duration(seconds: 30),
|
|
onTimeout: () {
|
|
throw Exception('Request timeout');
|
|
},
|
|
);
|
|
|
|
final responseBody = await response.stream.bytesToString();
|
|
final jsonResponse = jsonDecode(responseBody);
|
|
|
|
if (response.statusCode == 200 && jsonResponse['success'] == true) {
|
|
return ClassificationResult.fromJson(jsonResponse['data']);
|
|
} else {
|
|
throw Exception(jsonResponse['message'] ?? 'Classification failed');
|
|
}
|
|
} on SocketException {
|
|
throw Exception('Network error - pastikan server berjalan');
|
|
} on TimeoutException {
|
|
throw Exception('Request timeout');
|
|
} catch (e) {
|
|
throw Exception('Error: $e');
|
|
}
|
|
}
|
|
|
|
/// Klasifikasi dan simpan
|
|
Future<ClassificationResult> classifyAndSave(
|
|
File imageFile, {
|
|
String? notes,
|
|
}) async {
|
|
try {
|
|
final request = http.MultipartRequest(
|
|
'POST',
|
|
Uri.parse('$_baseUrl/classify-and-save'),
|
|
);
|
|
|
|
request.files.add(
|
|
await http.MultipartFile.fromPath('image', imageFile.path),
|
|
);
|
|
|
|
if (notes != null && notes.isNotEmpty) {
|
|
request.fields['notes'] = notes;
|
|
}
|
|
|
|
final response = await request.send().timeout(
|
|
const Duration(seconds: 30),
|
|
onTimeout: () {
|
|
throw Exception('Request timeout');
|
|
},
|
|
);
|
|
|
|
final responseBody = await response.stream.bytesToString();
|
|
final jsonResponse = jsonDecode(responseBody);
|
|
|
|
if (response.statusCode == 200 && jsonResponse['success'] == true) {
|
|
return ClassificationResult.fromJson(jsonResponse['data']);
|
|
} else {
|
|
throw Exception(jsonResponse['message'] ?? 'Classification failed');
|
|
}
|
|
} on SocketException {
|
|
throw Exception('Network error');
|
|
} catch (e) {
|
|
throw Exception('Error: $e');
|
|
}
|
|
}
|
|
|
|
/// Test koneksi ke API
|
|
Future<bool> testConnection() async {
|
|
try {
|
|
final response = await _httpClient
|
|
.get(Uri.parse('$_baseUrl/test'))
|
|
.timeout(const Duration(seconds: 10));
|
|
|
|
if (response.statusCode == 200) {
|
|
final jsonResponse = jsonDecode(response.body);
|
|
return jsonResponse['success'] == true;
|
|
}
|
|
return false;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
class ClassificationResult {
|
|
final String predictedClass;
|
|
final String confidence;
|
|
final double confidenceValue;
|
|
final Map<String, dynamic> allPredictions;
|
|
final DiseaseInfo diseaseInfo;
|
|
final String? imagePath;
|
|
final String? notes;
|
|
final DateTime timestamp;
|
|
|
|
ClassificationResult({
|
|
required this.predictedClass,
|
|
required this.confidence,
|
|
required this.confidenceValue,
|
|
required this.allPredictions,
|
|
required this.diseaseInfo,
|
|
required this.timestamp,
|
|
this.imagePath,
|
|
this.notes,
|
|
});
|
|
|
|
factory ClassificationResult.fromJson(Map<String, dynamic> json) {
|
|
return ClassificationResult(
|
|
predictedClass: json['predicted_class'] ?? '',
|
|
confidence: json['confidence'] ?? '0%',
|
|
confidenceValue: json['confidence_value'] ?? 0.0,
|
|
allPredictions: Map<String, dynamic>.from(
|
|
json['all_predictions'] ?? {},
|
|
),
|
|
diseaseInfo: DiseaseInfo.fromJson(
|
|
Map<String, dynamic>.from(json['disease_info'] ?? {}),
|
|
),
|
|
imagePath: json['image_path'],
|
|
notes: json['notes'],
|
|
timestamp: DateTime.parse(
|
|
json['timestamp'] ?? DateTime.now().toIso8601String(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class DiseaseInfo {
|
|
final String name;
|
|
final String description;
|
|
final List<String> symptoms;
|
|
final List<String> treatment;
|
|
final String severity;
|
|
|
|
DiseaseInfo({
|
|
required this.name,
|
|
required this.description,
|
|
required this.symptoms,
|
|
required this.treatment,
|
|
required this.severity,
|
|
});
|
|
|
|
factory DiseaseInfo.fromJson(Map<String, dynamic> json) {
|
|
return DiseaseInfo(
|
|
name: json['name'] ?? 'Unknown',
|
|
description: json['description'] ?? '',
|
|
symptoms: List<String>.from(json['symptoms'] ?? []),
|
|
treatment: List<String>.from(json['treatment'] ?? []),
|
|
severity: json['severity'] ?? 'Unknown',
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. UI Screen
|
|
|
|
Buat file `lib/screens/classification_screen.dart`:
|
|
|
|
```dart
|
|
import 'package:flutter/material.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'dart:io';
|
|
import '../services/classification_service.dart';
|
|
|
|
class ClassificationScreen extends StatefulWidget {
|
|
const ClassificationScreen({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
State<ClassificationScreen> createState() => _ClassificationScreenState();
|
|
}
|
|
|
|
class _ClassificationScreenState extends State<ClassificationScreen> {
|
|
final ClassificationService _service = ClassificationService();
|
|
final ImagePicker _imagePicker = ImagePicker();
|
|
|
|
File? _selectedImage;
|
|
ClassificationResult? _result;
|
|
bool _isLoading = false;
|
|
String? _errorMessage;
|
|
|
|
Future<void> _pickImage() async {
|
|
try {
|
|
final pickedFile = await _imagePicker.pickImage(
|
|
source: ImageSource.camera,
|
|
);
|
|
|
|
if (pickedFile != null) {
|
|
setState(() {
|
|
_selectedImage = File(pickedFile.path);
|
|
_result = null;
|
|
_errorMessage = null;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
setState(() {
|
|
_errorMessage = 'Error picking image: $e';
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _classify() async {
|
|
if (_selectedImage == null) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Pilih gambar terlebih dahulu')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isLoading = true;
|
|
_errorMessage = null;
|
|
});
|
|
|
|
try {
|
|
final result = await _service.classifyAndSave(_selectedImage!);
|
|
setState(() {
|
|
_result = result;
|
|
});
|
|
} catch (e) {
|
|
setState(() {
|
|
_errorMessage = e.toString();
|
|
});
|
|
} finally {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Klasifikasi Penyakit Padi')),
|
|
body: SingleChildScrollView(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
children: [
|
|
// Image Preview
|
|
Container(
|
|
width: double.infinity,
|
|
height: 300,
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: _selectedImage != null
|
|
? Image.file(_selectedImage!, fit: BoxFit.cover)
|
|
: const Center(child: Text('Tidak ada gambar dipilih')),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Buttons
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: _pickImage,
|
|
icon: const Icon(Icons.camera_alt),
|
|
label: const Text('Ambil Foto'),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: _isLoading ? null : _classify,
|
|
icon: const Icon(Icons.check),
|
|
label: _isLoading
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
),
|
|
)
|
|
: const Text('Klasifikasi'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Error Message
|
|
if (_errorMessage != null)
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red[100],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
_errorMessage!,
|
|
style: const TextStyle(color: Colors.red),
|
|
),
|
|
),
|
|
|
|
// Results
|
|
if (_result != null)
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Hasil Klasifikasi',
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text('Diagnosis: ${_result!.diseaseInfo.name}',
|
|
style: const TextStyle(
|
|
fontSize: 18, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 8),
|
|
Text('Confidence: ${_result!.confidence}'),
|
|
Text('Severity: ${_result!.diseaseInfo.severity}'),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Gejala:',
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
..._result!.diseaseInfo.symptoms
|
|
.map((s) => Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 8.0, top: 4.0),
|
|
child: Text('• $s'),
|
|
)),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Penanganan:',
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
..._result!.diseaseInfo.treatment
|
|
.map((t) => Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 8.0, top: 4.0),
|
|
child: Text('• $t'),
|
|
)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Update main.dart
|
|
|
|
Edit `lib/main.dart`:
|
|
|
|
```dart
|
|
import 'package:flutter/material.dart';
|
|
import 'screens/classification_screen.dart';
|
|
|
|
void main() {
|
|
runApp(const MyApp());
|
|
}
|
|
|
|
class MyApp extends StatelessWidget {
|
|
const MyApp({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'Padi Disease Classification',
|
|
theme: ThemeData(
|
|
primarySwatch: Colors.green,
|
|
useMaterial3: true,
|
|
),
|
|
home: const ClassificationScreen(),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## 🌐 Konfigurasi IP Server
|
|
|
|
Tergantung di mana aplikasi dijalankan:
|
|
|
|
| Environment | URL |
|
|
|---|---|
|
|
| **Emulator Android** | `http://10.0.2.2:8000` |
|
|
| **Device Fisik (WiFi)** | `http://192.168.x.x:8000` |
|
|
| **Testing Lokal** | `http://127.0.0.1:8000` |
|
|
| **Production** | `http://production-domain.com/api` |
|
|
|
|
Edit di `classification_service.dart`:
|
|
|
|
```dart
|
|
static const String _baseUrl = 'http://10.0.2.2:8000/api/classification';
|
|
```
|
|
|
|
## 🔧 Testing
|
|
|
|
```bash
|
|
cd mobile_TA/padi_app
|
|
|
|
# Install dependencies
|
|
flutter pub get
|
|
|
|
# Run app
|
|
flutter run
|
|
|
|
# Atau dengan specific device
|
|
flutter run -d emulator-5554
|
|
```
|
|
|
|
## 📝 Contoh Response
|
|
|
|
```json
|
|
{
|
|
"predicted_class": "Bacterialblight",
|
|
"confidence": "95.23%",
|
|
"confidence_value": 0.9523,
|
|
"all_predictions": {
|
|
"Bacterialblight": 0.9523,
|
|
"Brownspot": 0.0380,
|
|
"Leafsmut": 0.0097
|
|
},
|
|
"disease_info": {
|
|
"name": "Bercak Bakteri (Bacterial Blight)",
|
|
"description": "...",
|
|
"symptoms": [...],
|
|
"treatment": [...],
|
|
"severity": "Sedang hingga Tinggi"
|
|
},
|
|
"image_path": "/storage/classifications/...",
|
|
"timestamp": "2024-03-09T10:30:45.000000Z"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
**Next:** Integrasikan dengan dashboard aplikasi Anda.
|