Tutorial 8: Flutter Networking, Authentication, and Integration
Pemrograman Berbasis Platform (CSGE602022) — diselenggarakan oleh Fakultas Ilmu Komputer Universitas Indonesia, Semester Ganjil 2025/2026
Tujuan Pembelajaran
Setelah menyelesaikan tutorial ini, mahasiswa diharapkan untuk dapat:
- Memahami struktur dan pembuatan model pada Flutter.
- Memahami cara mengambil, mengolah, dan menampilkan data dari web service.
- Memahami state management dasar menggunakan Provider pada Flutter.
- Dapat melakukan autentikasi dengan web service Django dengan aplikasi Flutter.
Model pada Flutter
Pada tutorial kali ini, kita akan memanggil web service dan menampilkan hasil yang didapatkan ke halaman Flutter yang kita buat. Akan tetapi, sebelum melakukan pemanggilan web service, kita perlu mendefinisikan model yang akan kita gunakan ketika melakukan pemanggilan web service. Model pada Flutter menggunakan prinsip class seperti layaknya yang sudah dipelajari pada DDP2 bagian OOP.
Kode di bawah ini adalah contoh dan tidak wajib diikuti. Akan tetapi, contoh ini sangat disarankan untuk dibaca karena konsepnya akan digunakan pada bagian-bagian selanjutnya.
Berikut merupakan contoh class pada Flutter.
class Car {
Car({
required this.id,
required this.model,
required this.brand,
required this.color,
this.releaseDate
});
int id;
String model;
String brand;
String color;
DateTime releaseDate;
}
Jika kamu mengalami error saat membuat class, tambahkan keyword required pada setiap parameter class pada bagian constructor.
Sampai saat ini, kita telah berhasil membuat class. Selanjutnya, beberapa kode lain dapat ditambahkan sehingga terbentuk sebuah model Car. Car ini merupakan suatu model yang merepresentasikan respons dari pemanggilan web service.
Untuk mewujudkan hal tersebut, diperlukan library dart:convert yang dapat diimpor pada bagian paling atas berkas.
import 'dart:convert';
...
Pada class Car, tambahkan kode berikut.
factory Car.fromJson(Map<String, dynamic> json) => Car(
id: json["id"],
model: json["model"],
brand: json["brand"],
color: json["color"],
releaseDate: json["releaseDate"],
);
Map<String, dynamic> toJson() => {
"id": id,
"model": model,
"brand": brand,
"color": color,
"releaseDate": releaseDate,
};
Tambahkan kode berikut di luar class Car.
Car carFromJson(String str) => Car.fromJson(json.decode(str));
String carToJson(Car data) => json.encode(data.toJson());
Pada akhirnya, kode akan terbentuk seperti berikut untuk menampilkan satu objek Car dari web service.
import 'dart:convert';
Car carFromJson(String str) => Car.fromJson(json.decode(str));
String carToJson(Car data) => json.encode(data.toJson());
class Car {
Car({
required this.id,
required this.model,
required this.brand,
required this.color,
this.releaseDate
});
int id;
String model;
String brand;
String color;
DateTime releaseDate;
factory Car.fromJson(Map<String, dynamic> json) => Car(
id: json["id"],
model: json["model"],
brand: json["brand"],
color: json["color"],
releaseDate: json["releaseDate"],
);
Map<String, dynamic> toJson() => {
"id": id,
"model": model,
"brand": brand,
"color": color,
"releaseDate": releaseDate,
};
}
Penjelasan
Terdapat beberapa kode-kode tambahan seperti method toJson dan fromJson di dalam class Car. Hal tersebut akan diperlukan karena ketika kita melakukan request ke suatu web service/API dengan method GET, umumnya kita mendapatkan hasil pemanggilan berupa JSON. Tentunya, hal ini tergantung pada implementasi web service tersebut, tetapi pada contoh ini kita akan menggunakan JSON. Oleh karena itu, kita perlu melakukan konversi data dengan method fromJson agar Flutter kemudian dapat mengenali JSON tersebut sebagai objek dari class Car. Selain itu, terdapat juga method toJson yang dapat digunakan ketika kita ingin melakukan pengiriman data ke web service dalam bentuk JSON (seperti POST atau PUT).
Berikut adalah contoh respons dari web service dengan method GET yang dapat dikonversi ke class model Car.
{
"id": 1,
"model": "Mercedes-AMG F1 W16 E PERFORMANCE",
"brand": "Mercedes-AMG PETRONAS",
"color": "Black",
"releaseDate": "2025-02-24T00:00:00+0000"
}
Lalu, bagaimana jika respons dari web service berupa kumpulan (list) berisi objek JSON? Sebenarnya sama saja dengan kode di atas, hanya saja terdapat sedikit perubahan pada method carFromJson dan carToJson.
Kodenya adalah sebagai berikut.
List<Car> carFromJson(String str) => List<Car>.from(json.decode(str).map((car) => Car.fromJson(car)));
String carToJson(List<Car> data) => json.encode(List<dynamic>.from(data.map((car) => car.toJson())));
Berikut adalah contoh respons dari web service dengan method GET yang dapat dikonversi ke model Car.
[
{
"id": 1,
"model": "Mercedes-AMG F1 W16 E PERFORMANCE",
"brand": "Mercedes-AMG PETRONAS",
"color": "Black",
"releaseDate": "2025-02-24T00:00:00+0000"
},
{
"id": 2,
"model": "FW47",
"brand": "Williams Racing",
"color": "Blue",
"releaseDate": "2025-02-14T00:00:00+0000"
},
{
"id": 3,
"model": "SF-25",
"brand": "Scuderia Ferrari HP",
"color": "Red",
"releaseDate": "2025-02-18T00:00:00+0000"
}
]
Fetch Data dari Web Service pada Flutter
Pada saat pengembangan aplikasi, ada kalanya kita perlu mengambil data eksternal dari luar aplikasi kita (Internet) untuk ditampilkan di aplikasi kita. Tutorial ini bertujuan untuk memahami cara melakukan fetching data dari sebuah web service pada Flutter.
Secara umum terdapat beberapa langkah ketika ingin menampilkan data dari web service lain ke aplikasi Flutter, yaitu:
-
Menambahkan dependensi
httpke proyek; dependensi ini digunakan untuk bertukar HTTP request. -
Membuat model sesuai dengan respons dari data yang berasal dari web service tersebut.
-
Membuat http request ke web service menggunakan dependensi
http. -
Mengkonversikan objek yang didapatkan dari web service ke model yang telah kita buat di langkah kedua.
-
Menampilkan data yang telah dikonversi ke aplikasi dengan
FutureBuilder.
Penjelasan lebih lanjut dapat dibaca pada tautan ini.
State Management Dasar menggunakan Provider
Provider adalah sebuah pembungkus di sekitar InheritedWidget agar InheritedWidget lebih mudah digunakan dan lebih dapat digunakan kembali. InheritedWidget sendiri adalah kelas dasar untuk widget Flutter yang secara efisien menyebarkan informasi ke widget lainnya yang berada pada satu tree.
Manfaat menggunakan Provider adalah sebagai berikut.
- Mengalokasikan resource menjadi lebih sederhana.
- Lazy-loading.
- Mengurangi boilerplate tiap kali membuat class baru.
- Didukung oleh Flutter Devtool sehingga
providerdapat dilacak dari Devtool. - Peningkatan skalabilitas untuk class yang memanfaatkan mekanisme listen yang dibangun secara kompleks.
Untuk mengetahui provider secara lebih lanjut, silakan buka halaman package Provider.
Tutorial: Integrasi Autentikasi Django-Flutter
Setup Autentikasi pada Django untuk Flutter
Ikuti langkah-langkah berikut untuk melakukan integrasi sistem autentikasi pada Django.
-
Buatlah
django-appbernamaauthenticationpada project Django yang telah kamu buat sebelumnya. -
Tambahkan
authenticationkeINSTALLED_APPSpada main projectsettings.pyaplikasi Django kamu.infoApabila kamu lupa cara untuk langkah 1 dan 2, coba baca lagi Tutorial 1.
-
Jalankan perintah
pip install django-cors-headersuntuk menginstal library yang dibutuhkan. Jangan lupa untuk menyalakan virtual environment Python terlebih dahulu. Jangan lupa juga untuk menambahkandjango-cors-headerskerequirements.txt. -
Tambahkan
corsheaderskeINSTALLED_APPSpada main projectsettings.pyaplikasi Django kamu. -
Tambahkan
corsheaders.middleware.CorsMiddlewarekeMIDDLEWAREpada main projectsettings.pyaplikasi Django kamu. -
Tambahkan beberapa variabel berikut ini pada main project
settings.pyaplikasi Django kamu....
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = 'None'
SESSION_COOKIE_SAMESITE = 'None'
... -
Untuk keperluan integrasi ke Django dari emulator Android, tambahkan
10.0.2.2padaALLOWED_HOSTSdi berkassettings.py.ALLOWED_HOSTS = [..., ..., "10.0.2.2"]
Ingat tanda "..." artinya adalah kode kamu yang sudah ada saat ini, jadi cukup tambahkan yang diminta saja.
-
Buatlah sebuah metode view untuk login pada
authentication/views.py.from django.contrib.auth import authenticate, login as auth_login
...
@csrf_exempt
def login(request):
username = request.POST['username']
password = request.POST['password']
user = authenticate(username=username, password=password)
if user is not None:
if user.is_active:
auth_login(request, user)
# Login status successful.
return JsonResponse({
"username": user.username,
"status": True,
"message": "Login successful!"
# Add other data if you want to send data to Flutter.
}, status=200)
else:
return JsonResponse({
"status": False,
"message": "Login failed, account is disabled."
}, status=401)
else:
return JsonResponse({
"status": False,
"message": "Login failed, please check your username or password."
}, status=401) -
Buat file
urls.pypada folderauthenticationdan tambahkan URL routing terhadap fungsi yang sudah dibuat dengan endpointlogin/.from django.urls import path
from authentication.views import login
app_name = 'authentication'
urlpatterns = [
path('login/', login, name='login'),
] -
Terakhir, tambahkan
path('auth/', include('authentication.urls')),pada filefootball_news/urls.py.
Integrasi Sistem Autentikasi pada Flutter
Untuk memudahkan pembuatan sistem autentikasi, tim asisten dosen telah membuatkan package Flutter yang dapat dipakai untuk melakukan kontak dengan web service Django (termasuk operasi GET dan POST).
Package dapat diakses melalui tautan berikut: pbp_django_auth
Ikuti langkah-langkah berikut untuk melakukan integrasi sistem autentikasi pada Flutter.
-
Instal package yang telah disediakan oleh tim asisten dosen dengan menjalankan perintah berikut di Terminal. Jalankan pada direktori root dari proyek Flutter kamu.
flutter pub add provider
flutter pub add pbp_django_auth -
Untuk menggunakan package tersebut, kamu perlu memodifikasi root widget untuk menyediakan
CookieRequestlibrary ke semua child widgets dengan menggunakanProvider.Sebagai contoh, jika aplikasimu pada berkas
main.dartsebelumnya seperti ini:import 'package:flutter/material.dart';
import 'package:football_news/screens/menu.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue)
.copyWith(secondary: Colors.blueAccent[400]),
),
home: MyHomePage(),
);
}
}Ubahlah menjadi:
import 'package:flutter/material.dart';
import 'package:football_news/screens/menu.dart';
import 'package:pbp_django_auth/pbp_django_auth.dart';
import 'package:provider/provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
Widget build(BuildContext context) {
return Provider(
create: (_) {
CookieRequest request = CookieRequest();
return request;
},
child: MaterialApp(
title: 'Football News',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue)
.copyWith(secondary: Colors.blueAccent[400]),
),
home: MyHomePage(),
),
);
}
}Hal ini akan membuat objek
Providerbaru yang akan membagikan instanceCookieRequestdengan semua komponen yang ada di aplikasi.infoPastikan kamu menambahkan
import 'package:pbp_django_auth/pbp_django_auth.dart';danimport 'package:provider/provider.dart';pada bagian atas berkas -
Buatlah berkas baru pada didalam folder
screensdengan namalogin.dart. -
Isilah berkas
login.dartdengan kode berikut.import 'package:football_news/screens/menu.dart';
import 'package:flutter/material.dart';
import 'package:pbp_django_auth/pbp_django_auth.dart';
import 'package:provider/provider.dart';
void main() {
runApp(const LoginApp());
}
class LoginApp extends StatelessWidget {
const LoginApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Login',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue)
.copyWith(secondary: Colors.blueAccent[400]),
),
home: const LoginPage(),
);
}
}
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
Widget build(BuildContext context) {
final request = context.watch<CookieRequest>();
return Scaffold(
appBar: AppBar(
title: const Text('Login'),
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Card(
elevation: 8,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Login',
style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 30.0),
TextField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Username',
hintText: 'Enter your username',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
),
contentPadding:
EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
),
),
const SizedBox(height: 12.0),
TextField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
),
contentPadding:
EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
),
obscureText: true,
),
const SizedBox(height: 24.0),
ElevatedButton(
onPressed: () async {
String username = _usernameController.text;
String password = _passwordController.text;
// Check credentials
// TODO: Change the URL and don't forget to add trailing slash (/) at the end of URL!
// To connect Android emulator with Django on localhost, use URL http://10.0.2.2/
// If you using chrome, use URL http://localhost:8000
final response = await request
.login("http://[YOUR_APP_URL]/auth/login/", {
'username': username,
'password': password,
});
if (request.loggedIn) {
String message = response['message'];
String uname = response['username'];
if (context.mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => MyHomePage()),
);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content:
Text("$message Welcome, $uname.")),
);
}
} else {
if (context.mounted) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Login Failed'),
content: Text(response['message']),
actions: [
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.pop(context);
},
),
],
),
);
}
}
},
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
minimumSize: Size(double.infinity, 50),
backgroundColor: Theme.of(context).colorScheme.primary,
padding: const EdgeInsets.symmetric(vertical: 16.0),
),
child: const Text('Login'),
),
const SizedBox(height: 36.0),
GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Info'),
content: const Text(
'We will create the register page later',
),
actions: [
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.pop(context);
},
),
],
),
);
},
child: Text(
'Don\'t have an account? Register',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 16.0,
),
),
),
],
),
),
),
),
),
);
}
} -
Pada file
main.dart, pada WidgetMaterialApp(...), ubahhome: MyHomePage()menjadihome: const LoginPage() -
Pada langkah ini, kamu akan menambahkan fungsi register pada proyek kamu. Sebelum itu, kamu harus memodifikasi modul
authenticationpada proyek Django yang kamu kerjakan sebelumnya. Tambahkan metode view berikut padaauthentication/views.pyyang sudah kamu buat.
from django.contrib.auth.models import User
import json
...
@csrf_exempt
def register(request):
if request.method == 'POST':
data = json.loads(request.body)
username = data['username']
password1 = data['password1']
password2 = data['password2']
# Check if the passwords match
if password1 != password2:
return JsonResponse({
"status": False,
"message": "Passwords do not match."
}, status=400)
# Check if the username is already taken
if User.objects.filter(username=username).exists():
return JsonResponse({
"status": False,
"message": "Username already exists."
}, status=400)
# Create the new user
user = User.objects.create_user(username=username, password=password1)
user.save()
return JsonResponse({
"username": user.username,
"status": 'success',
"message": "User created successfully!"
}, status=200)
else:
return JsonResponse({
"status": False,
"message": "Invalid request method."
}, status=400)
- Tambahkan path baru pada
authentication/urls.pydengan kode berikut
from authentication.views import login, register # Tambahkan register di baris ini
...
path('register/', register, name='register'),
-
Pada proyek Flutter, buatlah berkas baru pada folder
screensdengan namaregister.dart. -
Isilah berkas
register.dartdengan kode berikut.import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:football_news/screens/login.dart';
import 'package:pbp_django_auth/pbp_django_auth.dart';
import 'package:provider/provider.dart';
class RegisterPage extends StatefulWidget {
const RegisterPage({super.key});
State<RegisterPage> createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
Widget build(BuildContext context) {
final request = context.watch<CookieRequest>();
return Scaffold(
appBar: AppBar(
title: const Text('Register'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
),
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Card(
elevation: 8,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text(
'Register',
style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 30.0),
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Username',
hintText: 'Enter your username',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
),
contentPadding:
EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your username';
}
return null;
},
),
const SizedBox(height: 12.0),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
),
contentPadding:
EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
return null;
},
),
const SizedBox(height: 12.0),
TextFormField(
controller: _confirmPasswordController,
decoration: const InputDecoration(
labelText: 'Confirm Password',
hintText: 'Confirm your password',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
),
contentPadding:
EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please confirm your password';
}
return null;
},
),
const SizedBox(height: 24.0),
ElevatedButton(
onPressed: () async {
String username = _usernameController.text;
String password1 = _passwordController.text;
String password2 = _confirmPasswordController.text;
// Check credentials
// TODO: Change the URL and don't forget to add trailing slash (/) at the end of URL!
// To connect Android emulator with Django on localhost, use URL http://10.0.2.2/
// If you using chrome, use URL http://localhost:8000
final response = await request.postJson(
"http://[YOUR_APP_URL]/auth/register/",
jsonEncode({
"username": username,
"password1": password1,
"password2": password2,
}));
if (context.mounted) {
if (response['status'] == 'success') {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Successfully registered!'),
),
);
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const LoginPage()),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Failed to register!'),
),
);
}
}
},
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
minimumSize: Size(double.infinity, 50),
backgroundColor: Theme.of(context).colorScheme.primary,
padding: const EdgeInsets.symmetric(vertical: 16.0),
),
child: const Text('Register'),
),
],
),
),
),
),
),
);
}
}
Sebelum melanjutkan ke bagian berikutnya, pastikan kalian sudah mengganti url kalian di bagian "[YOUR_APP_URL]" agar aplikasi kalian dapat dijalankan.
- Pada file
screens/login.dart, import fileregister.dartdan update fungsionTappada widgetGestureDetectordi bagian Register untuk menavigasikan ke halamanRegisterPage.
...
import 'package:football_news/screens/register.dart';
...
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const RegisterPage(),
),
);
},
child: Text(
'Don\'t have an account? Register',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 16.0,
),
),
),
...
- Jalankan aplikasi Flutter kamu dan coba lakukan flow register dan login untuk memastikan integrasi autentikasi berfungsi dengan baik.
Sebelum mencoba register dan login pastikan website yang telah kamu buat pada tutorial sebelumnya sudah kamu jalankan di terminal!
Jangan lanjut ke bagian berikutnya jika flow register dan login kalian masih belum berfungsi dengan baik, silakan lakukan pengecekan ulang penempatan kode kalian sebelum meminta bantuan asdos.
Tutorial: Pembuatan Model Kustom
Dalam membuat model yang menyesuaikan dengan data JSON, kita dapat memanfaatkan website Quicktype dengan tahapan sebagai berikut.
- Bukalah endpoint
JSONyang sudah kamu buat sebelumnya pada tutorial 2.
Yang dimaksud endpoint JSON adalah ketika url web kalian menampilkan data json di url http://localhost:8000/json/. Contoh data JSON dapat dilihat pada gambar sebelah kiri di poin no 3.
-
Salinlah data
JSONdan buka situs web Quicktype. -
Pada situs web Quicktype, ubahlah name menjadi
NewsEntry, source type menjadiJSON, dan language menjadiDart.
-
Tempel data JSON yang telah disalin sebelumnya ke dalam textbox yang tersedia pada Quicktype.
-
Klik pilihan
Copy Codepada Quicktype.
Setelah mendapatkan kode model melalui Quicktype, buka kembali proyek Flutter dan buatlah folder baru models/ pada subdirektori lib/. Buatlah file baru pada folder tersebut dengan nama news_entry.dart, dan tempel kode yang sudah disalin dari Quicktype.
Tutorial: Penerapan Fetch Data dari Django Untuk Ditampilkan ke Flutter
Menambahkan Dependensi HTTP
Untuk melakukan perintah HTTP request, kita membutuhkan package tambahan yakni package http.
-
Lakukan
flutter pub add httppada terminal proyek Flutter untuk menambahkan packagehttp. -
Pada file
android/app/src/main/AndroidManifest.xml, tambahkan kode berikut untuk memperbolehkan akses Internet pada aplikasi Flutter yang sedang dibuat....
<application>
...
</application>
<!-- Required to fetch data from the Internet. -->
<uses-permission android:name="android.permission.INTERNET" />
...
Mengintegrasikan Data News List dari Django
Langkah Django
Sebelum memulai integrasi di Flutter, kita perlu menambahkan endpoint proxy untuk mengatasi masalah CORS pada gambar.
- Tambahkan import berikut pada bagian atas file
main/views.py.
import requests
- Tambahkan fungsi view berikut di
main/views.py.
def proxy_image(request):
image_url = request.GET.get('url')
if not image_url:
return HttpResponse('No URL provided', status=400)
try:
# Fetch image from external source
response = requests.get(image_url, timeout=10)
response.raise_for_status()
# Return the image with proper content type
return HttpResponse(
response.content,
content_type=response.headers.get('Content-Type', 'image/jpeg')
)
except requests.RequestException as e:
return HttpResponse(f'Error fetching image: {str(e)}', status=500)
- Tambahkan path baru pada
main/urls.py.
from main.views import ..., proxy_image
app_name = 'main'
urlpatterns = [
# ... other paths
path('proxy-image/', proxy_image, name='proxy_image'),
]
- Jalankan ulang aplikasi Django kamu.
Langkah Flutter
- Buatlah berkas baru pada direktori
lib/widgetsdengan namanews_entry_card.dart.
import 'package:flutter/material.dart';
import 'package:football_news/models/news_entry.dart';
class NewsEntryCard extends StatelessWidget {
final NewsEntry news;
final VoidCallback onTap;
const NewsEntryCard({
super.key,
required this.news,
required this.onTap,
});
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: InkWell(
onTap: onTap,
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
side: BorderSide(color: Colors.grey.shade300),
),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Thumbnail
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.network(
'http://localhost:8000/proxy-image/?url=${Uri.encodeComponent(news.thumbnail)}',
height: 150,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Container(
height: 150,
color: Colors.grey[300],
child: const Center(child: Icon(Icons.broken_image)),
),
),
),
const SizedBox(height: 8),
// Title
Text(
news.title,
style: const TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 6),
// Category
Text('Category: ${news.category}'),
const SizedBox(height: 6),
// Content preview
Text(
news.content.length > 100
? '${news.content.substring(0, 100)}...'
: news.content,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.black54),
),
const SizedBox(height: 6),
// Featured indicator
if (news.isFeatured)
const Text(
'Featured',
style: TextStyle(
color: Colors.amber,
fontWeight: FontWeight.bold
),
),
],
),
),
),
),
);
}
}
- Buatlah berkas baru pada direktori
lib/screensdengan namanews_entry_list.dart.
import 'package:flutter/material.dart';
import 'package:football_news/models/news_entry.dart';
import 'package:football_news/widgets/left_drawer.dart';
import 'package:football_news/screens/news_detail.dart';
import 'package:football_news/widgets/news_entry_card.dart';
import 'package:provider/provider.dart';
import 'package:pbp_django_auth/pbp_django_auth.dart';
class NewsEntryListPage extends StatefulWidget {
const NewsEntryListPage({super.key});
State<NewsEntryListPage> createState() => _NewsEntryListPageState();
}
class _NewsEntryListPageState extends State<NewsEntryListPage> {
Future<List<NewsEntry>> fetchNews(CookieRequest request) async {
// TODO: Replace the URL with your app's URL and don't forget to add a trailing slash (/)!
// To connect Android emulator with Django on localhost, use URL http://10.0.2.2/
// If you using chrome, use URL http://localhost:8000
final response = await request.get('http://[YOUR_APP_URL]/json/');
// Decode response to json format
var data = response;
// Convert json data to NewsEntry objects
List<NewsEntry> listNews = [];
for (var d in data) {
if (d != null) {
listNews.add(NewsEntry.fromJson(d));
}
}
return listNews;
}
Widget build(BuildContext context) {
final request = context.watch<CookieRequest>();
return Scaffold(
appBar: AppBar(
title: const Text('News Entry List'),
),
drawer: const LeftDrawer(),
body: FutureBuilder(
future: fetchNews(request),
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.data == null) {
return const Center(child: CircularProgressIndicator());
} else {
if (!snapshot.hasData) {
return const Column(
children: [
Text(
'There are no news in football news yet.',
style: TextStyle(fontSize: 20, color: Color(0xff59A5D8)),
),
SizedBox(height: 8),
],
);
} else {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (_, index) => NewsEntryCard(
news: snapshot.data![index],
onTap: () {
// Show a snackbar when news card is clicked
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text("You clicked on ${snapshot.data![index].title}"),
),
);
},
),
);
}
}
},
),
);
}
}
- Tambahkan halaman
news_entry_list.dartkewidgets/left_drawer.dartdengan menambahkan kode berikut.
// Add this import at the top
import 'package:football_news/screens/news_entry_list.dart';
// Add this ListTile in your drawer
ListTile(
leading: const Icon(Icons.add_reaction_rounded),
title: const Text('News List'),
onTap: () {
// Route to news list page
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const NewsEntryListPage()),
);
},
),
- Ubah fungsi tombol
See Football Newspada halaman utama agar mengarahkan ke halamanNewsEntryListPage. Kamu dapat melakukan redirection dengan menambahkanelse ifsetelah kodeif(...){...}di bagian akhironTap: () { }yang ada pada filewidgets/news_card.dart.
// Add this import at the top
import 'package:football_news/screens/news_entry_list.dart';
// Add this condition in your onTap handler
else if (item.name == "See Football News") {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const NewsEntryListPage()
),
);
}
Mengintegrasikan Data News Detail dari Django
- Buatlah berkas baru pada direktori
lib/screensdengan namanews_detail.dart.
import 'package:flutter/material.dart';
import 'package:football_news/models/news_entry.dart';
class NewsDetailPage extends StatelessWidget {
final NewsEntry news;
const NewsDetailPage({super.key, required this.news});
String _formatDate(DateTime date) {
// Simple date formatter without intl package
final months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return '${date.day} ${months[date.month - 1]} ${date.year}, ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('News Detail'),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Thumbnail image
if (news.thumbnail.isNotEmpty)
Image.network(
'http://localhost:8000/proxy-image/?url=${Uri.encodeComponent(news.thumbnail)}',
width: double.infinity,
height: 250,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Container(
height: 250,
color: Colors.grey[300],
child: const Center(
child: Icon(Icons.broken_image, size: 50),
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Featured badge
if (news.isFeatured)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12.0, vertical: 6.0),
margin: const EdgeInsets.only(bottom: 12.0),
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.circular(20.0),
),
child: const Text(
'Featured',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
// Title
Text(
news.title,
style: const TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
// Category and Date
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10.0, vertical: 4.0),
decoration: BoxDecoration(
color: Colors.indigo.shade100,
borderRadius: BorderRadius.circular(12.0),
),
child: Text(
news.category.toUpperCase(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.indigo.shade700,
),
),
),
const SizedBox(width: 12),
Text(
_formatDate(news.createdAt),
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
const SizedBox(height: 8),
// Views count
Row(
children: [
Icon(Icons.visibility, size: 16, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'${news.newsViews} views',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
const Divider(height: 32),
// Full content
Text(
news.content,
style: const TextStyle(
fontSize: 16.0,
height: 1.6,
),
textAlign: TextAlign.justify,
),
const SizedBox(height: 24),
],
),
),
],
),
),
);
}
}
- Update file
lib/screens/news_entry_list.dartuntuk menambahkan navigasi ke halaman detail. Ubah bagianonTappadaNewsEntryCarddengan kode berikut.
// Add this import at the top
import 'package:football_news/screens/news_detail.dart';
...
// Update the ListView.builder section
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (_, index) => NewsEntryCard(
news: snapshot.data![index],
onTap: () {
// Navigate to news detail page
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NewsDetailPage(
news: snapshot.data![index],
),
),
);
},
),
);
...
Jalankan aplikasi dan cobalah untuk menambahkan beberapa NewsEntry di situs web kamu. Kemudian, coba lihat hasilnya melalui halaman Daftar News dan News Detail yang baru saja kamu buat di aplikasi Flutter.
Tutorial: Integrasi Form Flutter Dengan Layanan Django
Langkah Django
Langkah-langkah berikut akan dilakukan pada kode proyek Django.
- Buatlah sebuah fungsi view baru pada
main/views.pyaplikasi Django kamu dengan potongan kode berikut. Tambahkan import-import berikut pada bagian atas file.
from django.views.decorators.csrf import csrf_exempt
from django.utils.html import strip_tags
import json
from django.http import JsonResponse
Kemudian tambahkan fungsi view berikut:
@csrf_exempt
def create_news_flutter(request):
if request.method == 'POST':
data = json.loads(request.body)
title = strip_tags(data.get("title", "")) # Strip HTML tags
content = strip_tags(data.get("content", "")) # Strip HTML tags
category = data.get("category", "")
thumbnail = data.get("thumbnail", "")
is_featured = data.get("is_featured", False)
user = request.user
new_news = News(
title=title,
content=content,
category=category,
thumbnail=thumbnail,
is_featured=is_featured,
user=user
)
new_news.save()
return JsonResponse({"status": "success"}, status=200)
else:
return JsonResponse({"status": "error"}, status=401)
- Tambahkan path baru pada
main/urls.pydengan kode berikut.
path('create-flutter/', create_news_flutter, name='create_news_flutter'),
- Jalankan ulang aplikasi Django kamu.
Langkah Flutter
Setelah sisi Django selesai, selanjutnya kita beralih ke formulir di sisi Flutter. Langkah-langkah berikut akan dilakukan pada kode proyek Flutter.
- Tambahkan import berikut pada bagian atas file
newslist_form.dart.
import 'dart:convert';
import 'package:provider/provider.dart';
import 'package:pbp_django_auth/pbp_django_auth.dart';
import 'package:football_news/screens/menu.dart';
- Hubungkan halaman
newslist_form.dartdenganCookieRequestdengan menambahkan baris kode berikut di dalam methodbuild.
Widget build(BuildContext context) {
final request = context.watch<CookieRequest>();
return Scaffold(
// ... rest of your code
);
}
- Ubahlah perintah pada
onPressed: ()button tambah menjadi kode berikut.
onPressed: () async {
if (_formKey.currentState!.validate()) {
// TODO: Replace the URL with your app's URL
// To connect Android emulator with Django on localhost, use URL http://10.0.2.2/
// If you using chrome, use URL http://localhost:8000
final response = await request.postJson(
"http://[Your_APP_URL]/create-flutter/",
jsonEncode({
"title": _title,
"content": _content,
"thumbnail": _thumbnail,
"category": _category,
"is_featured": _isFeatured,
}),
);
if (context.mounted) {
if (response['status'] == 'success') {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(
content: Text("News successfully saved!"),
));
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => MyHomePage()),
);
} else {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(
content: Text("Something went wrong, please try again."),
));
}
}
}
},
- Jalankan ulang aplikasi dan coba untuk menambahkan berita baru dari aplikasi Flutter kamu.
Tutorial: Implementasi Fitur Logout
Langkah Django
Langkah-langkah berikut akan dilakukan pada kode proyek Django.
- Buatlah sebuah metode view untuk logout pada
authentication/views.py. Tambahkan import berikut pada bagian atas file.
from django.contrib.auth import logout as auth_logout
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
Kemudian tambahkan fungsi view berikut:
@csrf_exempt
def logout(request):
username = request.user.username
try:
auth_logout(request)
return JsonResponse({
"username": username,
"status": True,
"message": "Logged out successfully!"
}, status=200)
except:
return JsonResponse({
"status": False,
"message": "Logout failed."
}, status=401)
- Tambahkan path baru pada
authentication/urls.pydengan kode berikut.
from django.urls import path
from authentication.views import login,register,logout
app_name = 'authentication'
urlpatterns = [
path('login/', login, name='login'),
path('register/', register, name='register'),
path('logout/', logout, name='logout')
]
Langkah Flutter
Langkah-langkah berikut akan dilakukan pada kode proyek Flutter.
- Buka file
lib/widgets/news_card.dartdan tambahkan import berikut pada bagian atas file.
import 'package:football_news/screens/login.dart';
import 'package:pbp_django_auth/pbp_django_auth.dart';
import 'package:provider/provider.dart';
- Ubah method
builduntuk menambahkanCookieRequest.
Widget build(BuildContext context) {
final request = context.watch<CookieRequest>();
return Material(
// ... rest of your code
);
}
-
Ubahlah perintah
onTap: () {...}pada widgetInkwellmenjadionTap: () async {...}agar widgetInkwelldapat melakukan proses logout secara asinkronus. -
Tambahkan kode berikut ke dalam
async {...}di bagian akhir (setelah statement if sebelumnya).
// Add this after your previous if statements
else if (item.name == "Logout") {
// TODO: Replace the URL with your app's URL and don't forget to add a trailing slash (/)!
// To connect Android emulator with Django on localhost, use URL http://10.0.2.2/
// If you using chrome, use URL http://localhost:8000
final response = await request.logout(
"http://[YOUR_APP_URL]/auth/logout/");
String message = response["message"];
if (context.mounted) {
if (response['status']) {
String uname = response["username"];
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text("$message See you again, $uname."),
));
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
),
);
}
}
}
- Jalankan ulang aplikasi dan coba untuk melakukan logout.
Akhir Kata
Selamat! Kamu telah menyelesaikan Tutorial 8! Semoga dengan tutorial ini, kalian dapat memahami mengenai model, fetch data, state management dasar, dan integrasi Django-Flutter dengan baik. 😄
- Pelajari dan pahami kembali kode yang sudah kamu tuliskan di atas dengan baik. Jangan lupa untuk menyelesaikan semua TODO yang ada!
Jangan lupa juga untuk menjalankan flutter analyze untuk melihat apakah ada bagian kode kamu yang dapat dioptimisasi.
-
Lakukan
add,commitdanpushuntuk memperbarui repositori GitHub.git add .
git commit -m "<pesan_commit>"
git push -u origin <branch_utama>- Ubah
<pesan_commit>sesuai dengan keinginan. Contoh:git commit -m "tutorial 8 selesai". - Ubah
<branch_utama>sesuai dengan nama branch utamamu. Contoh:git push -u origin mainataugit push -u origin master.
- Ubah
Referensi Tambahan
- Fetch Data From the Internet
- How to create models in Flutter Dart
- Simple app state management | Flutter
- Flutter State Management with Provider
- Pengenalan State Management Flutter dan Jenis-jenisnya
Kontributor
- Fiona Ratu Maheswari (FIO)
- Meutia Fajriyah (MEW)
- Yeshua Marco Gracia (ACO)
- Anthony Edbert Feriyanto (ANT)
Credits
Tutorial ini dikembangkan berdasarkan PBP Genap 2025/2026 yang ditulis oleh Tim Pengajar Pemrograman Berbasis Platform 2025/2026. Segala tutorial serta instruksi yang dicantumkan pada repositori ini dirancang sedemikian rupa sehingga mahasiswa yang sedang mengambil mata kuliah Pemrograman Berbasis Platform dapat menyelesaikan tutorial saat sesi lab berlangsung.