Flutter ve Django ile Client-Server Mimarisi
Modern yazılım dünyasında en çok tercih edilen Client-Server (İstemci-Sunucu) mimarisini kullanarak basit bir öğrenci yönetim sisteminin nasıl inşa edileceğini anlatır.
1. Mimari Yapı ve Veri Akışı
Profesyonel projelerde verinin izlediği yol şöyledir:
Flutter UI: Kullanıcı düğmeye basar (Örn: Öğrenci Listele).
Repository/Service: Flutter, Django API'sine bir HTTP isteği (GET/POST) gönderir.
Django URL & View: İstek karşılanır, veritabanından veri çekilir.
Serializer: Veritabanı nesneleri (Queryset) JSON formatına dönüştürülür.
Response: JSON veri Flutter'a döner ve arayüz güncellenir.
Önemli Not:
django-cors-headerspaketini ekledik çünkü Flutter (Client) ve Django (Server) farklı portlarda çalıştığı için CORS hatası almamak gerekir.
BÖLÜM 1: BACKEND (Django & API & Web Arayüzü) Kurulumu
1. Ortam Kurulumu
Terminalinizi (veya komut satırını) açın ve sırasıyla şu komutları çalıştırın:
# 1. Ana klasörü oluştur ve içine gir
mkdir okul_projesi
cd okul_projesi
# 2. Sanal ortam (virtual environment) oluştur ve aktif et
python -m venv venv
# Windows için: venv\Scripts\activate
# Mac/Linux için: source venv/bin/activate
# 3. Gerekli paketleri kur
pip install django djangorestframework django-cors-headers
# 4. Django projesini ve uygulamasını başlat
django-admin startproject backend .
python manage.py startapp ogrenci
2. Dosya İçerikleri
backend/settings.py
İlgili kısımları aşağıdaki gibi güncelleyin (diğer ayarları bozmadan eklemeleri yapın):
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Eklenenler
'rest_framework',
'corsheaders',
'ogrenci',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # EN ÜSTE EKLENMELİ
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# Geliştirme ortamı için tüm kaynaklara izin veriyoruz
CORS_ALLOW_ALL_ORIGINS = True
ogrenci/models.py
from django.db import models
class Ogrenci(models.Model):
ad = models.CharField(max_length=100)
soyad = models.CharField(max_length=100)
numara = models.IntegerField(unique=True)
def __str__(self):
return f"{self.ad} {self.soyad}"
ogrenci/serializers.py (Bu dosyayı kendiniz oluşturun)
from rest_framework import serializers
from .models import Ogrenci
class OgrenciSerializer(serializers.ModelSerializer):
class Meta:
model = Ogrenci
fields = '__all__'
ogrenci/views.py
from rest_framework import viewsets
from django.urls import reverse_lazy
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
from .models import Ogrenci
from .serializers import OgrenciSerializer
# 1. API GÖRÜNÜMLERİ (Flutter İçin)
class OgrenciViewSet(viewsets.ModelViewSet):
queryset = Ogrenci.objects.all()
serializer_class = OgrenciSerializer
# 2. WEB ARAYÜZÜ GÖRÜNÜMLERİ (Tarayıcı İçin)
class OgrenciListView(ListView):
model = Ogrenci
template_name = 'ogrenci/liste.html'
context_object_name = 'ogrenciler'
class OgrenciCreateView(CreateView):
model = Ogrenci
fields = ['ad', 'soyad', 'numara']
template_name = 'ogrenci/form.html'
success_url = reverse_lazy('ogrenci_liste')
class OgrenciUpdateView(UpdateView):
model = Ogrenci
fields = ['ad', 'soyad', 'numara']
template_name = 'ogrenci/form.html'
success_url = reverse_lazy('ogrenci_liste')
class OgrenciDeleteView(DeleteView):
model = Ogrenci
template_name = 'ogrenci/sil_onay.html'
success_url = reverse_lazy('ogrenci_liste')
backend/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from ogrenci.views import (
OgrenciViewSet,
OgrenciListView, OgrenciCreateView, OgrenciUpdateView, OgrenciDeleteView
)
# API Rotaları
router = DefaultRouter()
router.register(r'ogrenciler', OgrenciViewSet)
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include(router.urls)), # Flutter buraya bağlanacak
# Web Arayüzü Rotaları
path('', OgrenciListView.as_view(), name='ogrenci_liste'),
path('ekle/', OgrenciCreateView.as_view(), name='ogrenci_ekle'),
path('guncelle/<int:pk>/', OgrenciUpdateView.as_view(), name='ogrenci_guncelle'),
path('sil/<int:pk>/', OgrenciDeleteView.as_view(), name='ogrenci_sil'),
]
3. HTML Şablonları (Templates)
ogrenci klasörünün içine sırasıyla templates ve onun da içine ogrenci klasörü oluşturun. (Yol: ogrenci/templates/ogrenci/) İçine şu 3 dosyayı ekleyin:
liste.html
<!DOCTYPE html>
<html>
<head><title>Öğrenci Sistemi</title></head>
<body style="font-family: Arial, sans-serif; padding: 20px;">
<h2>Öğrenci Listesi</h2>
<a href="{% url 'ogrenci_ekle' %}" style="padding: 10px; background: green; color: white; text-decoration: none;">+ Yeni Öğrenci Ekle</a>
<br><br>
<table border="1" cellpadding="10" cellspacing="0" width="100%">
<tr><th>Ad</th><th>Soyad</th><th>Numara</th><th>İşlemler</th></tr>
{% for ogrenci in ogrenciler %}
<tr>
<td>{{ ogrenci.ad }}</td><td>{{ ogrenci.soyad }}</td><td>{{ ogrenci.numara }}</td>
<td>
<a href="{% url 'ogrenci_guncelle' ogrenci.id %}">Düzenle</a> |
<a href="{% url 'ogrenci_sil' ogrenci.id %}" style="color: red;">Sil</a>
</td>
</tr>
{% empty %}
<tr><td colspan="4">Kayıtlı öğrenci yok.</td></tr>
{% endfor %}
</table>
</body>
</html>
form.html
<!DOCTYPE html>
<html>
<head><title>Öğrenci Formu</title></head>
<body style="font-family: Arial, sans-serif; padding: 20px;">
<h2>{% if object %}Öğrenciyi Güncelle{% else %}Yeni Öğrenci Ekle{% endif %}</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" style="padding: 8px 15px; background: blue; color: white;">Kaydet</button>
<a href="{% url 'ogrenci_liste' %}">İptal</a>
</form>
</body>
</html>
sil_onay.html
<!DOCTYPE html>
<html>
<head><title>Silme Onayı</title></head>
<body style="font-family: Arial, sans-serif; padding: 20px;">
<h2>Emin misiniz?</h2>
<p><strong>{{ object.ad }} {{ object.soyad }}</strong> isimli öğrenciyi silmek istediğinize emin misiniz?</p>
<form method="post">
{% csrf_token %}
<button type="submit" style="padding: 8px 15px; background: red; color: white;">Evet, Sil</button>
<a href="{% url 'ogrenci_liste' %}">İptal</a>
</form>
</body>
</html>
4. Veritabanını Hazırlama ve Backend'i Çalıştırma
python manage.py makemigrations
python manage.py migrate
python manage.py runserver
Sunucu çalıştıktan sonra:
- Web Arayüzü:
http://127.0.0.1:8000/ - API Uç Noktası (Endpoint):
http://127.0.0.1:8000/api/ogrenciler/adreslerinden projeye erişebilirsiniz.
BÖLÜM 2: FRONTEND (Flutter) Kurulumu
Yeni bir terminal açın (Django terminali çalışmaya devam etsin).
1. Kurulum ve Paketler
flutter create okul_app
cd okul_app
flutter pub add http
2. Dosya İçerikleri
Tüm Flutter kodlarını lib klasörü altında oluşturacağız.
lib/ogrenci_model.dart
class Ogrenci {
final int? id;
final String ad;
final String soyad;
final int numara;
Ogrenci({this.id, required this.ad, required this.soyad, required this.numara});
factory Ogrenci.fromJson(Map<String, dynamic> json) => Ogrenci(
id: json['id'],
ad: json['ad'],
soyad: json['soyad'],
numara: json['numara'],
);
Map<String, dynamic> toJson() => {
"ad": ad,
"soyad": soyad,
"numara": numara,
};
}
lib/api_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'ogrenci_model.dart';
class ApiService {
// DİKKAT: Android Emülatör kullanıyorsanız "10.0.2.2" yazın.
// Chrome (Web) veya iOS Simulator kullanıyorsanız "127.0.0.1" yazın.
static const String baseUrl = "http://127.0.0.1:8000/api/ogrenciler/";
Future<List<Ogrenci>> fetchOgrenciler() async {
final response = await http.get(Uri.parse(baseUrl));
if (response.statusCode == 200) {
List jsonResponse = json.decode(utf8.decode(response.bodyBytes));
return jsonResponse.map((data) => Ogrenci.fromJson(data)).toList();
} else {
throw Exception('Veriler alınamadı');
}
}
Future<bool> ogrenciEkle(Ogrenci ogrenci) async {
final response = await http.post(
Uri.parse(baseUrl),
headers: {"Content-Type": "application/json"},
body: json.encode(ogrenci.toJson()),
);
return response.statusCode == 201;
}
Future<bool> ogrenciGuncelle(int id, Ogrenci ogrenci) async {
final response = await http.put(
Uri.parse('$baseUrl$id/'),
headers: {"Content-Type": "application/json"},
body: json.encode(ogrenci.toJson()),
);
return response.statusCode == 200;
}
Future<bool> ogrenciSil(int id) async {
final response = await http.delete(Uri.parse('$baseUrl$id/'));
return response.statusCode == 204;
}
}
lib/main.dart
import 'package:flutter/material.dart';
import 'api_service.dart';
import 'ogrenci_model.dart';
void main() {
runApp(const OkulApp());
}
class OkulApp extends StatelessWidget {
const OkulApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Okul Yönetim Sistemi',
debugShowCheckedModeBanner: false,
theme: ThemeData(primarySwatch: Colors.indigo, useMaterial3: true),
home: const OgrenciListeEkrani(),
);
}
}
class OgrenciListeEkrani extends StatefulWidget {
const OgrenciListeEkrani({super.key});
@override
State<OgrenciListeEkrani> createState() => _OgrenciListeEkraniState();
}
class _OgrenciListeEkraniState extends State<OgrenciListeEkrani> {
final ApiService apiService = ApiService();
late Future<List<Ogrenci>> ogrenciListesi;
@override
void initState() {
super.initState();
_listeyiYenile();
}
void _listeyiYenile() {
setState(() {
ogrenciListesi = apiService.fetchOgrenciler();
});
}
void _ogrenciFormDialog({Ogrenci? ogrenci}) {
final isUpdate = ogrenci != null;
final adController = TextEditingController(text: isUpdate ? ogrenci.ad : '');
final soyadController = TextEditingController(text: isUpdate ? ogrenci.soyad : '');
final numaraController = TextEditingController(text: isUpdate ? ogrenci.numara.toString() : '');
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(isUpdate ? "Öğrenci Güncelle" : "Yeni Öğrenci Ekle"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(controller: adController, decoration: const InputDecoration(labelText: "Ad")),
TextField(controller: soyadController, decoration: const InputDecoration(labelText: "Soyad")),
TextField(controller: numaraController, decoration: const InputDecoration(labelText: "Okul No"), keyboardType: TextInputType.number),
],
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("İptal")),
ElevatedButton(
onPressed: () async {
if (adController.text.isNotEmpty && numaraController.text.isNotEmpty) {
final yeniVeri = Ogrenci(
id: isUpdate ? ogrenci.id : null,
ad: adController.text,
soyad: soyadController.text,
numara: int.parse(numaraController.text),
);
bool basarili = isUpdate
? await apiService.ogrenciGuncelle(ogrenci.id!, yeniVeri)
: await apiService.ogrenciEkle(yeniVeri);
if (mounted) {
Navigator.pop(context);
if (basarili) {
_listeyiYenile();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(isUpdate ? "Güncellendi!" : "Eklendi!")));
}
}
}
},
child: Text(isUpdate ? "Güncelle" : "Kaydet"),
),
],
),
);
}
void _ogrenciSil(int id) async {
bool basarili = await apiService.ogrenciSil(id);
if (basarili) {
_listeyiYenile();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Silindi!"), backgroundColor: Colors.red));
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Öğrenci Yönetimi"),
actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: _listeyiYenile)],
),
body: FutureBuilder<List<Ogrenci>>(
future: ogrenciListesi,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
if (snapshot.hasError) return Center(child: Text("Hata: ${snapshot.error}"));
if (!snapshot.hasData || snapshot.data!.isEmpty) return const Center(child: Text("Kayıtlı öğrenci yok."));
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
final ogrenci = snapshot.data![index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: ListTile(
leading: CircleAvatar(child: Text(ogrenci.ad[0].toUpperCase())),
title: Text("${ogrenci.ad} ${ogrenci.soyad}"),
subtitle: Text("Numara: ${ogrenci.numara}"),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(icon: const Icon(Icons.edit, color: Colors.blue), onPressed: () => _ogrenciFormDialog(ogrenci: ogrenci)),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Emin misiniz?"),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("İptal")),
TextButton(onPressed: () { Navigator.pop(context); _ogrenciSil(ogrenci.id!); }, child: const Text("Sil", style: TextStyle(color: Colors.red))),
],
),
);
},
),
],
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _ogrenciFormDialog(),
child: const Icon(Icons.add),
),
);
}
}
Kaynak Kod : https://github.com/nuritiras/okul_app.git
3. Flutter'ı Çalıştırma
flutter runProjeyi Nasıl Test Edeceksiniz?
Web Arayüzü için: Tarayıcınızdan
http://127.0.0.1:8000/adresine gidin. Buradan ekleme/silme yapın.Flutter Arayüzü için: Cihazınızda/Emülatörünüzde açılan uygulamaya bakın. Yenile (refresh) ikonuna bastığınızda web'den eklediğiniz verilerin geldiğini göreceksiniz. Oradan sildiğinizde web'den de silinecektir. Ortak bir veritabanı (Client-Server mantığı) tıkır tıkır çalışıyor olacaktır.

Yorumlar
Yorum Gönder