Pada tutorial ini, kita akan belajar membuat aplikasi mobile menggunakan Flutter yang terhubung dengan API backend berbasis Laravel. Studi kasusnya adalah aplikasi manajemen daftar mobil dengan fitur login, tampil data mobil, tambah mobil, edit mobil, dan hapus mobil.
Persiapan Backend Laravel
Backend Laravel harus sudah menyediakan API berikut:
-
Login & Register
Endpoint untuk autentikasi pengguna dan mendapatkan token JWT. -
CRUD Mobil
API untuk membuat, membaca, memperbarui, dan menghapus data mobil.
Pastikan Laravel berjalan di IP lokal yang dapat diakses device lain, contohnya:php artisan serve --host=0.0.0.0 --port=8000
dan base URL API misalnya:http://192.168.1.8:8000/api
Membuat Aplikasi Flutter
1. Setup Proyek Flutter
Buat proyek baru Flutter dan tambahkan dependencies:
|
1 2 3 4 |
dependencies: flutter: sdk: flutter http: ^0.13.5 |
Jalankan:
|
1 |
flutter pub get |
- ✅ Login Page →
login_screen.dart
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'car_list_screen.dart'; import 'register_screen.dart'; class LoginScreen extends StatefulWidget { @override _LoginScreenState createState() => _LoginScreenState(); } class _LoginScreenState extends State<LoginScreen> { final emailController = TextEditingController(); final passwordController = TextEditingController(); String error = ""; void login() async { final res = await http.post( Uri.parse('http://192.168.1.8:8000/api/login'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'email': emailController.text, 'password': passwordController.text, }), ); if (res.statusCode == 200) { final data = jsonDecode(res.body); final token = data['token']; Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => CarListScreen(token: token)), ); } else { setState(() => error = "Login gagal!"); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("Login")), body: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ TextField(controller: emailController, decoration: InputDecoration(labelText: "Email")), TextField(controller: passwordController, obscureText: true, decoration: InputDecoration(labelText: "Password")), ElevatedButton(onPressed: login, child: Text("Login")), TextButton( onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => RegisterScreen())), child: Text("Belum punya akun? Register"), ), Text(error, style: TextStyle(color: Colors.red)), ], ), ), ); } } |
- ✅ Register Page →
register_screen.dart
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'login_screen.dart'; class RegisterScreen extends StatefulWidget { @override _RegisterScreenState createState() => _RegisterScreenState(); } class _RegisterScreenState extends State<RegisterScreen> { final nameController = TextEditingController(); final emailController = TextEditingController(); final passwordController = TextEditingController(); final confirmPasswordController = TextEditingController(); String error = ""; void register() async { if (passwordController.text != confirmPasswordController.text) { setState(() => error = "Password tidak cocok"); return; } final res = await http.post( Uri.parse('http://192.168.1.8:8000/api/register'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'name': nameController.text, 'email': emailController.text, 'password': passwordController.text, 'password_confirmation': confirmPasswordController.text, }), ); if (res.statusCode == 200) { Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => LoginScreen())); } else { setState(() => error = "Register gagal!"); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("Register")), body: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ TextField(controller: nameController, decoration: InputDecoration(labelText: "Name")), TextField(controller: emailController, decoration: InputDecoration(labelText: "Email")), TextField(controller: passwordController, obscureText: true, decoration: InputDecoration(labelText: "Password")), TextField(controller: confirmPasswordController, obscureText: true, decoration: InputDecoration(labelText: "Confirm Password")), ElevatedButton(onPressed: register, child: Text("Register")), Text(error, style: TextStyle(color: Colors.red)), ], ), ), ); } } |
- ✅ Tampilan List Data Mobil →
car_list_screen.dart
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'car_add_screen.dart'; import 'car_edit_screen.dart'; class CarListScreen extends StatefulWidget { final String token; CarListScreen({required this.token}); @override _CarListScreenState createState() => _CarListScreenState(); } class _CarListScreenState extends State<CarListScreen> { List cars = []; Future<void> fetchCars() async { final response = await http.get( Uri.parse("http://192.168.1.8:8000/api/cars"), headers: {"Authorization": "Bearer ${widget.token}"}, ); if (response.statusCode == 200) { setState(() { cars = json.decode(response.body); }); } else { print("Failed to load cars"); } } void deleteCar(int id) async { final response = await http.delete( Uri.parse("http://192.168.1.8:8000/api/cars/$id"), headers: {"Authorization": "Bearer ${widget.token}"}, ); if (response.statusCode == 200) { fetchCars(); } else { print("Failed to delete car"); } } @override void initState() { super.initState(); fetchCars(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("Daftar Mobil")), body: ListView.builder( itemCount: cars.length, itemBuilder: (context, index) { final car = cars[index]; return Card( child: ListTile( title: Text(car['model'] ?? ''), subtitle: Text("${car['color']} - ${car['year']} - \$${car['price']}"), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: Icon(Icons.edit, color: Colors.blue), onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (_) => CarEditScreen( token: widget.token, car: car, ), ), ).then((_) => fetchCars()); }, ), IconButton( icon: Icon(Icons.delete, color: Colors.red), onPressed: () => deleteCar(car['id']), ), ], ), ), ); }, ), floatingActionButton: FloatingActionButton( onPressed: () async { await Navigator.push( context, MaterialPageRoute( builder: (_) => CarAddScreen(token: widget.token), ), ); fetchCars(); }, child: Icon(Icons.add), ), ); } } |
- ✅ Form Tambah Data Mobil →
car_add_screen.dart
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; class CarAddScreen extends StatefulWidget { final String token; CarAddScreen({required this.token}); @override _CarAddScreenState createState() => _CarAddScreenState(); } class _CarAddScreenState extends State<CarAddScreen> { final modelController = TextEditingController(); final colorController = TextEditingController(); final yearController = TextEditingController(); final priceController = TextEditingController(); final imageController = TextEditingController(); final merkIdController = TextEditingController(); void addCar() async { final res = await http.post( Uri.parse('http://192.168.1.8:8000/api/cars'), headers: { 'Authorization': 'Bearer ${widget.token}', 'Content-Type': 'application/json', }, body: jsonEncode({ 'merk_id': int.parse(merkIdController.text), 'model': modelController.text, 'color': colorController.text, 'year': int.parse(yearController.text), 'price': double.parse(priceController.text), 'image': imageController.text, }), ); if (res.statusCode == 201) { Navigator.pop(context); } else { print("Gagal tambah data"); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("Tambah Mobil")), body: Padding( padding: const EdgeInsets.all(16), child: SingleChildScrollView( child: Column( children: [ TextField(controller: merkIdController, decoration: InputDecoration(labelText: "Merk ID")), TextField(controller: modelController, decoration: InputDecoration(labelText: "Model")), TextField(controller: colorController, decoration: InputDecoration(labelText: "Color")), TextField(controller: yearController, keyboardType: TextInputType.number, decoration: InputDecoration(labelText: "Year")), TextField(controller: priceController, keyboardType: TextInputType.number, decoration: InputDecoration(labelText: "Price")), TextField(controller: imageController, decoration: InputDecoration(labelText: "Image URL")), SizedBox(height: 20), ElevatedButton(onPressed: addCar, child: Text("Simpan")), ], ), ), ), ); } } |
- ✅ Form Edit Data Mobil →
car_edit_screen.dart
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'car_add_screen.dart'; import 'car_edit_screen.dart'; class CarListScreen extends StatefulWidget { final String token; CarListScreen({required this.token}); @override _CarListScreenState createState() => _CarListScreenState(); } class _CarListScreenState extends State<CarListScreen> { List cars = []; Future<void> fetchCars() async { final response = await http.get( Uri.parse("http://192.168.1.8:8000/api/cars"), headers: {"Authorization": "Bearer ${widget.token}"}, ); if (response.statusCode == 200) { setState(() { cars = json.decode(response.body); }); } else { print("Failed to load cars"); } } void deleteCar(int id) async { final response = await http.delete( Uri.parse("http://192.168.1.8:8000/api/cars/$id"), headers: {"Authorization": "Bearer ${widget.token}"}, ); if (response.statusCode == 200) { fetchCars(); } else { print("Failed to delete car"); } } @override void initState() { super.initState(); fetchCars(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("Daftar Mobil")), body: ListView.builder( itemCount: cars.length, itemBuilder: (context, index) { final car = cars[index]; return Card( child: ListTile( title: Text(car['model'] ?? ''), subtitle: Text("${car['color']} - ${car['year']} - \$${car['price']}"), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: Icon(Icons.edit, color: Colors.blue), onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (_) => CarEditScreen(token: widget.token, car: car), ), ).then((_) => fetchCars()); }, ), IconButton( icon: Icon(Icons.delete, color: Colors.red), onPressed: () => deleteCar(car['id']), ), ], ), ), ); }, ), floatingActionButton: FloatingActionButton( onPressed: () async { await Navigator.push( context, MaterialPageRoute( builder: (_) => CarAddScreen(token: widget.token), ), ); fetchCars(); }, child: Icon(Icons.add), ), ); } } |






















