Django ve Flutter 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:

  1. Flutter UI: Kullanıcı düğmeye basar (Örn: Öğrenci Listele).

  2. Repository/Service: Flutter, Django API'sine bir HTTP isteği (GET/POST) gönderir.

  3. Django URL & View: İstek karşılanır, veritabanından veri çekilir.

  4. Serializer: Veritabanı nesneleri (Queryset) JSON formatına dönüştürülür.

  5. Response: JSON veri Flutter'a döner ve arayüz güncellenir.



Önemli Not: django-cors-headers paketini ekledik çünkü Flutter (Client) ve Django (Server) farklı portlarda çalıştığı için CORS hatası almamak gerekir.


Aşağıda, sıfırdan başlayarak hem Django (Backend & Web) hem de Flutter (Mobil Frontend) kısımlarını ayağa kaldırabileceğin tam proje kodlarını ve kurulum adımlarını derledim.

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:

Bash:
# 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):

Python:
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

Python:
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)

Python:
from rest_framework import serializers
from .models import Ogrenci

class OgrenciSerializer(serializers.ModelSerializer):
    class Meta:
        model = Ogrenci
        fields = '__all__'

ogrenci/views.py

Python:
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

Python:
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

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

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

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

Bash:
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.

Kaynak Kod : https://github.com/nuritiras/okul_projesi_api.git


BÖLÜM 2: FRONTEND (Flutter) Kurulumu

Yeni bir terminal açın (Django terminali çalışmaya devam etsin).

1. Kurulum ve Paketler

Bash:
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

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

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

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

Bash:
flutter run

Projeyi Nasıl Test Edeceksiniz?

  1. Web Arayüzü için: Tarayıcınızdan http://127.0.0.1:8000/ adresine gidin. Buradan ekleme/silme yapın.

  2. 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.

  • Sağ alt köşedeki (+) butonuna basarak yeni öğrenci ekleyebilirsiniz.
  • Listedeki bir öğrencinin yanındaki Kalem ikonuna basarak bilgilerini güncelleyebilir, Çöp Kutusu ikonuna basarak silebilirsiniz.
  • Sağ üstteki Yenile ikonuna basarak sunucudaki güncel verileri anında çekebilirsiniz.

Yorumlar

Bu blogdaki popüler yayınlar

Pardus Üzerine Django Kurulumu

Python ile Web Geliştirme: Django App Oluşturma