Dua Paylaşım Uygulaması: Django & Flutter

Bu uygulama, hem temel CRUD (Oluşturma, Okuma, Güncelleme, Silme) işlemlerini hem de kullanıcı doğrulama (authentication) gibi kritik konuları öğretmek veya kapsamlı bir eğitim serisi haline getirmek için mükemmel bir senaryo.

1. Backend: Django API Kurulumu

Django REST Framework (DRF) kullanarak API'yi hızlıca ayağa kaldırabiliriz. Kimlik doğrulama için Token Authentication (örneğin djangorestframework-simplejwt veya Djoser) kullanmak, Flutter tarafında oturum yönetimini çok daha standart ve güvenli hale getirecektir.

Veritabanı Modelleri (models.py):

Python:
from django.db import models
from django.contrib.auth.models import User

class Dua(models.Model):
    kullanici = models.ForeignKey(User, on_delete=models.CASCADE)
    icerik = models.TextField()
    olusturulma_tarihi = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.kullanici.username} - {self.icerik[:20]}"

class Begeni(models.Model):
    kullanici = models.ForeignKey(User, on_delete=models.CASCADE)
    dua = models.ForeignKey(Dua, related_name='begeniler', on_delete=models.CASCADE)

    class Meta:
        # Bir kullanıcı bir duayı sadece bir kez beğenebilir
        unique_together = ('kullanici', 'dua') 

class Yorum(models.Model):
    kullanici = models.ForeignKey(User, on_delete=models.CASCADE)
    dua = models.ForeignKey(Dua, related_name='yorumlar', on_delete=models.CASCADE)
    icerik = models.TextField()
    olusturulma_tarihi = models.DateTimeField(auto_now_add=True)

Temel API Uç Noktaları (Endpoints):

  • GET /api/dualar/ $\rightarrow$ Tüm duaları listeler.

  • POST /api/dualar/ $\rightarrow$ Yeni dua ekler (Auth gerektirir).

  • POST /api/dualar/<id>/begen/ $\rightarrow$ Duayı beğenir/beğenmekten vazgeçer (Auth gerektirir).

  • GET /api/dualar/<id>/yorumlar/ $\rightarrow$ Bir duanın yorumlarını listeler.

  • POST /api/dualar/<id>/yorumlar/ $\rightarrow$ Yorum ekler (Auth gerektirir).


2. Frontend: Flutter Entegrasyonu

Flutter tarafında uygulamanın kalbini Form işlemleri ve HTTP istekleri oluşturacak.

A. Authentication (Kimlik Doğrulama):

  • Kullanıcı giriş yaptığında Django'dan dönen JWT (JSON Web Token) cihazda güvenli bir şekilde saklanmalıdır (flutter_secure_storage paketi idealdir).

  • Giriş yapıldıktan sonra tüm yetki gerektiren API isteklerinde bu token HTTP başlığına (Header) eklenmelidir.

B. Form Gönderme (Yeni Dua / Yorum Ekleme):

  • Form ve TextFormField widget'ları kullanılarak kullanıcıdan veri alınır.

  • GlobalKey<FormState> ile form doğrulama (validation) işlemleri yapılarak boş veya çok kısa içeriklerin API'ye gönderilmesinin önüne geçilir.

C. POST API Kullanımı (http paketi ile örnek):

Dart:
import 'package:http/http.dart' as http;
import 'dart:convert';

Future<void> duaPaylas(String icerik, String token) async {
  final url = Uri.parse('http://10.0.2.2:8000/api/dualar/'); // Emülatör için localhost
  final response = await http.post(
    url,
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer $token',
    },
    body: jsonEncode({
      'icerik': icerik,
    }),
  );

  if (response.statusCode == 201) {
    print('Dua başarıyla paylaşıldı!');
  } else {
    throw Exception('Dua paylaşılamadı: ${response.body}');
  }
}

Geliştirme İçin Ekstra İpuçları

  • State Management: Duaların listesi, beğenilerin kalp ikonunda anlık olarak güncellenmesi ve kullanıcının oturum durumu için Provider veya Riverpod kullanarak veri akışını yönetebilirsiniz.

  • Sunucu Dağıtımı: Uygulamayı canlıya alırken API'yi kendi yönettiğiniz bir sunucuda (örneğin Pardus üzerinde) Gunicorn ve Nginx ikilisiyle ayağa kaldırıp, veritabanı olarak PostgreSQL bağlayabilirsiniz.

Harika! O halde temeli sağlam atmak adına backend tarafıyla, yani Django'da JWT (JSON Web Token) Kurulumu ve İç İçe (Nested) Serializer Yazımı ile başlayalım. Bu yapı, derslerinizde öğrencilerinize anlatırken veya bloglarınızda adım adım bir rehber olarak paylaşmak için çok verimli bir öğretim materyali olacaktır.

1. Simple JWT Kurulumu ve Ayarları

Flutter tarafında güvenli bir giriş sistemi (Login) yapmak için token bazlı bir yapı şarttır. Django'da bu iş için en popüler ve modern paket djangorestframework-simplejwt'dir.

Kurulum:

Bash
pip install djangorestframework-simplejwt

settings.py Ayarları:

Django'ya varsayılan kimlik doğrulama yöntemi olarak JWT'yi kullanacağını söylememiz gerekiyor:

Python:
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
}

urls.py (Token Uç Noktaları):

Flutter'dan kullanıcı adı ve şifre gönderildiğinde token üretecek URL'leri tanımlıyoruz:

Python:
from django.urls import path
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    # Flutter bu adrese POST isteği atarak token alacak
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    # Token süresi dolduğunda yenilemek için
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

2. İç İçe (Nested) Serializer Mimarisi

Dua uygulamasında, ana ekranda duaları listelerken o duaya ait yorumları ve beğeni sayısını da tek bir API isteğiyle Flutter'a göndermek hem performansı artırır hem de mobil taraftaki kod karmaşasını azaltır. Bunun için serializers.py dosyamızı iç içe tasarlamalıyız.

Python:
from rest_framework import serializers
from .models import Dua, Yorum, Begeni
from django.contrib.auth.models import User

# Sadece ID ve Username döndüren sadeleştirilmiş kullanıcı serializer'ı
class KullaniciSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username']

class YorumSerializer(serializers.ModelSerializer):
    kullanici = KullaniciSerializer(read_only=True) # Yorumu yapanın bilgileri

    class Meta:
        model = Yorum
        fields = ['id', 'kullanici', 'icerik', 'olusturulma_tarihi']

class DuaSerializer(serializers.ModelSerializer):
    kullanici = KullaniciSerializer(read_only=True)
    
    # İç içe Serializer: Duaya ait yorumları liste halinde çekiyoruz
    yorumlar = YorumSerializer(many=True, read_only=True) 
    
    # Özel alanlar: Beğeni sayısı ve aktif kullanıcının beğenme durumu
    begeni_sayisi = serializers.SerializerMethodField() 
    kullanici_begendi_mi = serializers.SerializerMethodField() 

    class Meta:
        model = Dua
        fields = ['id', 'kullanici', 'icerik', 'olusturulma_tarihi', 'yorumlar', 'begeni_sayisi', 'kullanici_begendi_mi']

    # begeni_sayisi alanını dolduran fonksiyon
    def get_begeni_sayisi(self, obj):
        return obj.begeniler.count()

    # kullanici_begendi_mi alanını dolduran fonksiyon
    def get_kullanici_begendi_mi(self, obj):
        request = self.context.get('request')
        if request and request.user.is_authenticated:
            # İsteği yapan kullanıcı bu duayı beğenmiş mi kontrol et
            return obj.begeniler.filter(kullanici=request.user).exists()
        return False

Bu Yapının Geliştiriciye Avantajı Nedir?

Flutter tarafında GET /api/dualar/ isteği yapıldığında, dönen JSON verisi sadece duanın metnini vermez. Aynı paketin içinde; o duayı yazanı, altına yapılan tüm yorumları, toplam beğeni sayısını ve en önemlisi Flutter'daki "Kalp" ikonunu dolu mu yoksa boş mu göstereceğimizi belirleyen kullanici_begendi_mi true/false bilgisini tek seferde verir.

Mobil uygulama güvenliğinin temelini atan bu konu, hem sınıfta öğrencilerinize mimariyi anlatırken hem de blog yazılarınızda "Flutter'da Güvenli Oturum Yönetimi" gibi başlıklar altında işlemek için çok güçlü bir içerik olacaktır.

Kullanıcı adı ve şifre ile Django'ya istek atıp, dönen JWT token'ını cihazda şifreli bir şekilde saklayan yapıyı adım adım kuralım.

1. Gerekli Paketlerin Eklenmesi

pubspec.yaml dosyamıza HTTP istekleri ve güvenli depolama için gerekli paketleri ekliyoruz:

YAML:
dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.0
  flutter_secure_storage: ^9.0.0

2. Token Yönetimi İçin Servis Sınıfı

Kod okunabilirliğini artırmak ve işlemleri tek bir merkezden yönetmek iyi bir programlama pratiğidir. Token'ı kaydetmek, okumak ve silmek (çıkış yapmak) için bir TokenServisi sınıfı oluşturalım:

Dart:
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class TokenServisi {
  // secure storage nesnesini oluşturuyoruz
  final _storage = const FlutterSecureStorage();
  
  // Anahtar kelimemiz
  static const _tokenKey = 'jwt_access_token';

  // Token'ı şifreli olarak cihaza kaydetme
  Future<void> tokenKaydet(String token) async {
    await _storage.write(key: _tokenKey, value: token);
  }

  // Cihazdan token'ı okuma
  Future<String?> tokenGetir() async {
    return await _storage.read(key: _tokenKey);
  }

  // Çıkış yaparken token'ı silme
  Future<void> tokenSil() async {
    await _storage.delete(key: _tokenKey);
  }
}

3. Login Ekranı ve API Entegrasyonu

Şimdi kullanıcıdan verileri alacağımız Form yapısını ve Django'daki /api/token/ ucuna POST isteği atacak fonksiyonumuzu içeren ekranı tasarlayalım.

Dart:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'token_servisi.dart'; // Yukarıda oluşturduğumuz sınıf

class LoginEkrani extends StatefulWidget {
  @override
  _LoginEkraniState createState() => _LoginEkraniState();
}

class _LoginEkraniState extends State<LoginEkrani> {
  final _formKey = GlobalKey<FormState>();
  final TextEditingController _kullaniciAdiController = TextEditingController();
  final TextEditingController _sifreController = TextEditingController();
  final TokenServisi _tokenServisi = TokenServisi();
  
  bool _yukleniyor = false;

  Future<void> _girisYap() async {
    // Form doğrulamasından geçemezse işlemi durdur
    if (!_formKey.currentState!.validate()) return;

    setState(() { _yukleniyor = true; });

    // Android emülatör için 10.0.2.2, iOS veya web için localhost / 127.0.0.1 kullanılır
    final url = Uri.parse('http://10.0.2.2:8000/api/token/');

    try {
      final response = await http.post(
        url,
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode({
          'username': _kullaniciAdiController.text,
          'password': _sifreController.text,
        }),
      );

      if (response.statusCode == 200) {
        // Başarılı giriş, token'ı al
        final veriler = jsonDecode(response.body);
        final accessToken = veriler['access'];

        // Cihaza güvenle kaydet
        await _tokenServisi.tokenKaydet(accessToken);

        // TODO: Ana ekrana yönlendirme işlemi burada yapılacak
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Giriş Başarılı! Ana sayfaya yönlendiriliyorsunuz...')),
        );
      } else {
        // Yanlış şifre veya kullanıcı adı
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Kullanıcı adı veya şifre hatalı!')),
        );
      }
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Sunucuya bağlanılamadı: $e')),
      );
    } finally {
      setState(() { _yukleniyor = false; });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Dua Uygulaması - Giriş')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              TextFormField(
                controller: _kullaniciAdiController,
                decoration: const InputDecoration(labelText: 'Kullanıcı Adı'),
                validator: (value) => value!.isEmpty ? 'Kullanıcı adı boş olamaz' : null,
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: _sifreController,
                decoration: const InputDecoration(labelText: 'Şifre'),
                obscureText: true, // Şifreyi gizle
                validator: (value) => value!.isEmpty ? 'Şifre boş olamaz' : null,
              ),
              const SizedBox(height: 32),
              _yukleniyor
                  ? const CircularProgressIndicator()
                  : ElevatedButton(
                      onPressed: _girisYap,
                      child: const Text('Giriş Yap'),
                    ),
            ],
          ),
        ),
      ),
    );
  }
}

Bu Yapının Eğitim ve Proje Açısından Önemi

  • GlobalKey<FormState> Kullanımı: Boş form gönderimini engelleyerek sunucuyu gereksiz yere yormamış oluyoruz.

  • Hata Yönetimi (try-catch): Sunucu kapalıysa (örneğin Pardus sunucusunda Gunicorn henüz çalışmıyorsa) uygulamanın çökmesi yerine kullanıcıya anlamlı bir hata mesajı dönüyoruz.

  • Güvenlik (flutter_secure_storage): Token'lar SharedPreferences gibi düz metin tutan yerler yerine, Android'de Keystore, iOS'te Keychain kullanılarak kriptolanıp saklanıyor.

O halde akışı hiç bozmadan, giriş yapan kullanıcının token'ını cihazdan okuyup Django API'mizden duaları ve yorumları çekeceğimiz GET isteğini ve ana ekran tasarımını yapalım.

Bu aşama, asenkron programlamayı (Future, async/await) ve yetkilendirilmiş (Authorized) API isteklerini uygulamalı olarak göstermek için harika bir örnektir. 

1. Dart Modellerini Oluşturma

Gelen JSON verisini Dart nesnelerine dönüştürmek, kodumuzu daha güvenli ve yönetilebilir hale getirir. Django'daki iç içe (nested) yapımıza uygun modellerimizi yazalım:

Dart
class Dua {
  final int id;
  final String kullaniciAdi;
  final String icerik;
  final int begeniSayisi;
  final bool kullaniciBegendiMi;

  Dua({
    required this.id,
    required this.kullaniciAdi,
    required this.icerik,
    required this.begeniSayisi,
    required this.kullaniciBegendiMi,
  });

  factory Dua.fromJson(Map<String, dynamic> json) {
    return Dua(
      id: json['id'],
      kullaniciAdi: json['kullanici']['username'], // İç içe JSON'dan okuma
      icerik: json['icerik'],
      begeniSayisi: json['begeni_sayisi'],
      kullaniciBegendiMi: json['kullanici_begendi_mi'],
    );
  }
}

2. Token ile GET İsteği Atma

Şimdi, daha önce yazdığımız TokenServisi'ni kullanarak cihazdaki token'ı alıp, HTTP başlığına (Header) ekleyerek Django'ya istek atalım.

Dart:
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'token_servisi.dart'; // Token servisimiz

Future<List<Dua>> dualariGetir() async {
  final tokenServisi = TokenServisi();
  // Cihaz hafızasından token'ı oku
  final token = await tokenServisi.tokenGetir(); 

  if (token == null) {
    throw Exception('Oturum bulunamadı, lütfen giriş yapın.');
  }

  // Pardus üzerindeki yerel sunucunuz veya emülatör adresi
  final url = Uri.parse('http://10.0.2.2:8000/api/dualar/'); 
  
  final response = await http.get(
    url,
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer $token', // Token'ı Header'a ekliyoruz!
    },
  );

  if (response.statusCode == 200) {
    // Türkçe karakter sorunu yaşamamak için utf8.decode kullanıyoruz
    List jsonResponse = json.decode(utf8.decode(response.bodyBytes));
    return jsonResponse.map((dua) => Dua.fromJson(dua)).toList();
  } else {
    throw Exception('Dualar yüklenemedi: ${response.statusCode}');
  }
}

3. Ana Ekran ve FutureBuilder Kullanımı

Veriler internetten (asenkron olarak) geleceği için, ekran çizilirken verinin gelmesini beklememiz gerekir. Flutter'da bunun en şık yolu FutureBuilder kullanmaktır. Yüklenme sırasında dönen bir çember (spinner), veri geldiğinde ise listeyi gösteririz.

Dart:
import 'package:flutter/material.dart';

class AnaEkran extends StatefulWidget {
  @override
  _AnaEkranState createState() => _AnaEkranState();
}

class _AnaEkranState extends State<AnaEkran> {
  late Future<List<Dua>> _dualarFuture;

  @override
  void initState() {
    super.initState();
    // Ekran açıldığında verileri çekme işlemini başlat
    _dualarFuture = dualariGetir(); 
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Dualar'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () async {
              // Çıkış yap ve Login ekranına dön
              await TokenServisi().tokenSil();
              Navigator.of(context).pushReplacementNamed('/login');
            },
          )
        ],
      ),
      body: FutureBuilder<List<Dua>>(
        future: _dualarFuture,
        builder: (context, snapshot) {
          // 1. Durum: Veri hala yükleniyor
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          } 
          // 2. Durum: Hata oluştu
          else if (snapshot.hasError) {
            return Center(child: Text('Hata: ${snapshot.error}'));
          } 
          // 3. Durum: Veri başarıyla geldi
          else if (snapshot.hasData) {
            final dualar = snapshot.data!;
            return ListView.builder(
              itemCount: dualar.length,
              itemBuilder: (context, index) {
                final dua = dualar[index];
                return Card(
                  margin: const EdgeInsets.all(8.0),
                  child: ListTile(
                    title: Text(dua.icerik),
                    subtitle: Text('Paylaşan: ${dua.kullaniciAdi}'),
                    trailing: Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Text('${dua.begeniSayisi}'),
                        Icon(
                          dua.kullaniciBegendiMi ? Icons.favorite : Icons.favorite_border,
                          color: dua.kullaniciBegendiMi ? Colors.red : Colors.grey,
                        ),
                      ],
                    ),
                  ),
                );
              },
            );
          } 
          // 4. Durum: Veri yok
          else {
            return const Center(child: Text('Henüz dua paylaşılmamış.'));
          }
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // TODO: Yeni Dua Ekleme Ekranına Git
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

Bu kodlarla birlikte projenin belkemiği tamamlanmış oldu: Kullanıcı giriş yaptı $\rightarrow$ Token alındı ve kaydedildi $\rightarrow$ Ana ekrana geçildi $\rightarrow$ Token kullanılarak yetki gerektiren API'den veriler çekildi ve listelendi.

Bu iki işlem, hem mobil uygulamalarda kullanıcı deneyimini (UX) pürüzsüz hale getirmek hem de HTTP POST istekleriyle form yönetimini öğrencilerinize uygulamalı olarak göstermek için çok değerli konulardır. İşte her iki işlemin de adım adım Flutter tarafındaki uygulaması:

1. "Kalp" İkonu ve İyimser Arayüz (Optimistic UI) ile Beğenme İşlemi

Kullanıcı "Beğen" butonuna bastığında, sunucudan cevap gelmesini beklemeden arayüzü anında güncelleyeceğiz (kalbi kırmızı yapacağız ve sayıyı artıracağız). Eğer sunucu isteği başarısız olursa, işlemi geri alacağız. Bu yaklaşım, uygulamanın çok hızlı ve akıcı hissedilmesini sağlar.

Bunun için listedeki her bir duayı kendi durumunu (state) yönetebilen ayrı bir StatefulWidget haline getirmek en temiz yoldur.

DuaKarti Widget'ı:

Dart:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'token_servisi.dart'; // Daha önce yazdığımız servis

class DuaKarti extends StatefulWidget {
  final int duaId;
  final String kullaniciAdi;
  final String icerik;
  final int baslangicBegeniSayisi;
  final bool baslangicKullaniciBegendiMi;

  const DuaKarti({
    Key? key,
    required this.duaId,
    required this.kullaniciAdi,
    required this.icerik,
    required this.baslangicBegeniSayisi,
    required this.baslangicKullaniciBegendiMi,
  }) : super(key: key);

  @override
  _DuaKartiState createState() => _DuaKartiState();
}

class _DuaKartiState extends State<DuaKarti> {
  late int _begeniSayisi;
  late bool _kullaniciBegendiMi;
  bool _islemDevamEdiyor = false; // Çoklu tıklamayı önlemek için

  @override
  void initState() {
    super.initState();
    _begeniSayisi = widget.baslangicBegeniSayisi;
    _kullaniciBegendiMi = widget.baslangicKullaniciBegendiMi;
  }

  Future<void> _begeniGuncelle() async {
    if (_islemDevamEdiyor) return;

    // 1. İyimser Güncelleme (Arayüzü anında değiştir)
    setState(() {
      _islemDevamEdiyor = true;
      if (_kullaniciBegendiMi) {
        _begeniSayisi--;
        _kullaniciBegendiMi = false;
      } else {
        _begeniSayisi++;
        _kullaniciBegendiMi = true;
      }
    });

    // 2. Arka planda sunucuya isteği at
    try {
      final token = await TokenServisi().tokenGetir();
      final url = Uri.parse('http://10.0.2.2:8000/api/dualar/${widget.duaId}/begen/');
      
      final response = await http.post(
        url,
        headers: {
          'Authorization': 'Bearer $token',
        },
      );

      if (response.statusCode != 200 && response.statusCode != 201) {
        // Hata durumunda işlemi geri al (Rollback)
        _islemGeriAl();
      }
    } catch (e) {
      // Ağ hatası durumunda işlemi geri al
      _islemGeriAl();
    } finally {
      _islemDevamEdiyor = false;
    }
  }

  void _islemGeriAl() {
    setState(() {
      if (_kullaniciBegendiMi) {
        _begeniSayisi--;
        _kullaniciBegendiMi = false;
      } else {
        _begeniSayisi++;
        _kullaniciBegendiMi = true;
      }
    });
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Beğeni işlemi başarısız oldu.')),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
      child: ListTile(
        title: Text(widget.icerik),
        subtitle: Text('Paylaşan: ${widget.kullaniciAdi}'),
        trailing: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('$_begeniSayisi'),
            IconButton(
              icon: Icon(
                _kullaniciBegendiMi ? Icons.favorite : Icons.favorite_border,
                color: _kullaniciBegendiMi ? Colors.red : Colors.grey,
              ),
              onPressed: _begeniGuncelle,
            ),
          ],
        ),
      ),
    );
  }
}

Not: Önceki adımda yazdığımız ListView.builder içinde artık direkt DuaKarti(...) widget'ını çağırabilirsiniz.


2. Yeni Dua Ekleme Formu (POST)

Kullanıcının yeni bir içerik oluşturacağı, form doğrulaması (validation) içeren ve yetkili (token kullanan) bir POST isteği yapacağımız ekran.

YeniDuaEkleEkrani:

Dart:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'token_servisi.dart';

class YeniDuaEkleEkrani extends StatefulWidget {
  @override
  _YeniDuaEkleEkraniState createState() => _YeniDuaEkleEkraniState();
}

class _YeniDuaEkleEkraniState extends State<YeniDuaEkleEkrani> {
  final _formKey = GlobalKey<FormState>();
  final TextEditingController _icerikController = TextEditingController();
  bool _yukleniyor = false;

  Future<void> _duaPaylas() async {
    if (!_formKey.currentState!.validate()) return;

    setState(() { _yukleniyor = true; });

    try {
      final token = await TokenServisi().tokenGetir();
      final url = Uri.parse('http://10.0.2.2:8000/api/dualar/');

      final response = await http.post(
        url,
        headers: {
          'Content-Type': 'application/json; charset=UTF-8',
          'Authorization': 'Bearer $token',
        },
        body: jsonEncode({
          'icerik': _icerikController.text,
        }),
      );

      if (response.statusCode == 201) { // 201 Created
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Duanız başarıyla paylaşıldı!')),
        );
        // İşlem başarılıysa önceki ekrana (listeye) dön ve sayfanın yenilenmesi için "true" döndür
        Navigator.pop(context, true); 
      } else {
        throw Exception('Sunucu hatası: ${response.statusCode}');
      }
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Paylaşılamadı: $e')),
      );
    } finally {
      setState(() { _yukleniyor = false; });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Yeni Dua Paylaş')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              TextFormField(
                controller: _icerikController,
                maxLines: 5,
                maxLength: 500, // Karakter sınırı
                decoration: const InputDecoration(
                  labelText: 'Duanızı buraya yazın...',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return 'İçerik boş bırakılamaz.';
                  }
                  if (value.length < 10) {
                    return 'Lütfen biraz daha detaylı yazın.';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 24),
              _yukleniyor
                  ? const Center(child: CircularProgressIndicator())
                  : ElevatedButton(
                      onPressed: _duaPaylas,
                      style: ElevatedButton.styleFrom(
                        padding: const EdgeInsets.symmetric(vertical: 16),
                      ),
                      child: const Text('Paylaş', style: TextStyle(fontSize: 18)),
                    ),
            ],
          ),
        ),
      ),
    );
  }
}

Ana Ekrandan Yönlendirme:

Ana sayfadaki FloatingActionButton içerisini şu şekilde güncelleyerek, yeni dua eklendiğinde listenin otomatik olarak yenilenmesini sağlayabilirsiniz:

Dart:
floatingActionButton: FloatingActionButton(
  onPressed: () async {
    // Ekrana git ve dönen sonucu bekle (yeni dua eklendi mi?)
    final sonuc = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => YeniDuaEkleEkrani()),
    );

    // Eğer yeni dua eklenip geri dönüldüyse listeyi yenile
    if (sonuc == true) {
      setState(() {
        _dualarFuture = dualariGetir(); // Listeyi baştan çek
      });
    }
  },
  child: const Icon(Icons.add),
),

Şimdiye kadar parça parça yazdığımız tüm mimariyi, eksiksiz (daha önce yazmadığımız views.py ve main.dart dahil olmak üzere) ve düzenli bir klasör yapısı içinde toparlayalım.

Simple JWT Kurulumu ve Ayarları

Flutter tarafında güvenli bir giriş sistemi (Login) yapmak için token bazlı bir yapı şarttır. Django'da bu iş için en popüler ve modern paket djangorestframework-simplejwt'dir.

Kurulum:

Bash:
pip install djangorestframework-simplejwt

1. Bölüm: Django (Backend) Yapısı ve Dosyaları

Klasör Hiyerarşisi:

Plaintext:
dua_backend/
├── manage.py
├── dua_backend/           # Ana Proje Klasörü
│   ├── settings.py
│   ├── urls.py
│   └── ...
└── api/                   # Uygulama (App) Klasörü
    ├── models.py
    ├── serializers.py
    ├── views.py
    └── urls.py

1. dua_backend/settings.py (Sadece İlgili Eklemeler)

Python:
INSTALLED_APPS = [
    # ... varsayılanlar ...
    'rest_framework',
    'rest_framework_simplejwt',
    'api', # Uygulamamız
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
}

2. api/models.py

Python:
from django.db import models
from django.contrib.auth.models import User

class Dua(models.Model):
    kullanici = models.ForeignKey(User, on_delete=models.CASCADE)
    icerik = models.TextField()
    olusturulma_tarihi = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.kullanici.username} - {self.icerik[:20]}"

class Begeni(models.Model):
    kullanici = models.ForeignKey(User, on_delete=models.CASCADE)
    dua = models.ForeignKey(Dua, related_name='begeniler', on_delete=models.CASCADE)

    class Meta:
        unique_together = ('kullanici', 'dua') 

class Yorum(models.Model):
    kullanici = models.ForeignKey(User, on_delete=models.CASCADE)
    dua = models.ForeignKey(Dua, related_name='yorumlar', on_delete=models.CASCADE)
    icerik = models.TextField()
    olusturulma_tarihi = models.DateTimeField(auto_now_add=True)

3. api/serializers.py

Python:
from rest_framework import serializers
from .models import Dua, Yorum, Begeni
from django.contrib.auth.models import User

class KullaniciSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username']

class YorumSerializer(serializers.ModelSerializer):
    kullanici = KullaniciSerializer(read_only=True)

    class Meta:
        model = Yorum
        fields = ['id', 'kullanici', 'icerik', 'olusturulma_tarihi']

class DuaSerializer(serializers.ModelSerializer):
    kullanici = KullaniciSerializer(read_only=True)
    yorumlar = YorumSerializer(many=True, read_only=True) 
    begeni_sayisi = serializers.SerializerMethodField() 
    kullanici_begendi_mi = serializers.SerializerMethodField() 

    class Meta:
        model = Dua
        fields = ['id', 'kullanici', 'icerik', 'olusturulma_tarihi', 'yorumlar', 'begeni_sayisi', 'kullanici_begendi_mi']

    def get_begeni_sayisi(self, obj):
        return obj.begeniler.count()

    def get_kullanici_begendi_mi(self, obj):
        request = self.context.get('request')
        if request and request.user.is_authenticated:
            return obj.begeniler.filter(kullanici=request.user).exists()
        return False

4. api/views.py (API Mantığı)

Python:
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from .models import Dua, Begeni
from .serializers import DuaSerializer

class DuaViewSet(viewsets.ModelViewSet):
    queryset = Dua.objects.all().order_by('-olusturulma_tarihi')
    serializer_class = DuaSerializer
    permission_classes = [IsAuthenticated] # Tüm işlemler için giriş zorunlu

    def perform_create(self, serializer):
        # Dua eklenirken kullanıcıyı otomatik olarak isteği atan kişi yap
        serializer.save(kullanici=self.request.user)

    @action(detail=True, methods=['post'])
    def begen(self, request, pk=None):
        dua = self.get_object()
        # Kullanıcı daha önce beğenmiş mi kontrol et
        begeni, created = Begeni.objects.get_or_create(kullanici=request.user, dua=dua)
        
        if not created:
            # Zaten beğenmişse, beğeniyi kaldır (Toggle mantığı)
            begeni.delete()
            return Response({'durum': 'beğeni kaldırıldı'}, status=status.HTTP_200_OK)
            
        return Response({'durum': 'beğenildi'}, status=status.HTTP_201_CREATED)

5. api/urls.py (Uygulama URL'leri)

Python:
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import DuaViewSet

router = DefaultRouter()
router.register(r'dualar', DuaViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

6. dua_backend/urls.py (Ana Proje URL'leri)

Python:
from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')), # api uygulamasının rotaları
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), # Login
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
Kaynak Kod: https://github.com/nuritiras/dua_backend


2. Bölüm: Flutter (Frontend) Yapısı ve Dosyaları

Klasör Hiyerarşisi:

Plaintext:
dua_app/
├── pubspec.yaml
└── lib/
    ├── main.dart
    ├── models/
    │   └── dua.dart
    ├── services/
    │   └── token_servisi.dart
    ├── screens/
    │   ├── login_ekrani.dart
    │   ├── ana_ekran.dart
    │   └── yeni_dua_ekle_ekrani.dart
    └── widgets/
        └── dua_karti.dart

1. pubspec.yaml (Bağımlılıklar)

YAML:
# ...
dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.0
  flutter_secure_storage: ^9.0.0

2. lib/models/dua.dart

Dart:
class Dua {
  final int id;
  final String kullaniciAdi;
  final String icerik;
  final int begeniSayisi;
  final bool kullaniciBegendiMi;

  Dua({
    required this.id,
    required this.kullaniciAdi,
    required this.icerik,
    required this.begeniSayisi,
    required this.kullaniciBegendiMi,
  });

  factory Dua.fromJson(Map<String, dynamic> json) {
    return Dua(
      id: json['id'],
      kullaniciAdi: json['kullanici']['username'],
      icerik: json['icerik'],
      begeniSayisi: json['begeni_sayisi'],
      kullaniciBegendiMi: json['kullanici_begendi_mi'],
    );
  }
}

3. lib/services/token_servisi.dart

Dart:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class TokenServisi {
  final _storage = const FlutterSecureStorage();
  static const _tokenKey = 'jwt_access_token';

  Future<void> tokenKaydet(String token) async {
    await _storage.write(key: _tokenKey, value: token);
  }

  Future<String?> tokenGetir() async {
    return await _storage.read(key: _tokenKey);
  }

  Future<void> tokenSil() async {
    await _storage.delete(key: _tokenKey);
  }
}

4. lib/screens/login_ekrani.dart

Dart:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../services/token_servisi.dart';
import 'ana_ekran.dart';

class LoginEkrani extends StatefulWidget {
  @override
  _LoginEkraniState createState() => _LoginEkraniState();
}

class _LoginEkraniState extends State<LoginEkrani> {
  final _formKey = GlobalKey<FormState>();
  final _kullaniciAdiController = TextEditingController();
  final _sifreController = TextEditingController();
  bool _yukleniyor = false;

  Future<void> _girisYap() async {
    if (!_formKey.currentState!.validate()) return;
    setState(() => _yukleniyor = true);

    final url = Uri.parse('http://10.0.2.2:8000/api/token/');

    try {
      final response = await http.post(
        url,
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode({
          'username': _kullaniciAdiController.text,
          'password': _sifreController.text,
        }),
      );

      if (response.statusCode == 200) {
        final veriler = jsonDecode(response.body);
        await TokenServisi().tokenKaydet(veriler['access']);
        
        Navigator.of(context).pushReplacement(
          MaterialPageRoute(builder: (_) => AnaEkran())
        );
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Hatalı giriş!')),
        );
      }
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Bağlantı hatası: $e')),
      );
    } finally {
      setState(() => _yukleniyor = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Giriş Yap')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              TextFormField(
                controller: _kullaniciAdiController,
                decoration: const InputDecoration(labelText: 'Kullanıcı Adı'),
                validator: (v) => v!.isEmpty ? 'Boş olamaz' : null,
              ),
              TextFormField(
                controller: _sifreController,
                obscureText: true,
                decoration: const InputDecoration(labelText: 'Şifre'),
                validator: (v) => v!.isEmpty ? 'Boş olamaz' : null,
              ),
              const SizedBox(height: 32),
              _yukleniyor
                  ? const CircularProgressIndicator()
                  : ElevatedButton(onPressed: _girisYap, child: const Text('Giriş')),
            ],
          ),
        ),
      ),
    );
  }
}

5. lib/widgets/dua_karti.dart

Dart:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import '../services/token_servisi.dart';

class DuaKarti extends StatefulWidget {
  final int duaId;
  final String kullaniciAdi;
  final String icerik;
  final int baslangicBegeniSayisi;
  final bool baslangicKullaniciBegendiMi;

  const DuaKarti({
    Key? key,
    required this.duaId,
    required this.kullaniciAdi,
    required this.icerik,
    required this.baslangicBegeniSayisi,
    required this.baslangicKullaniciBegendiMi,
  }) : super(key: key);

  @override
  _DuaKartiState createState() => _DuaKartiState();
}

class _DuaKartiState extends State<DuaKarti> {
  late int _begeniSayisi;
  late bool _kullaniciBegendiMi;
  bool _islemDevamEdiyor = false;

  @override
  void initState() {
    super.initState();
    _begeniSayisi = widget.baslangicBegeniSayisi;
    _kullaniciBegendiMi = widget.baslangicKullaniciBegendiMi;
  }

  Future<void> _begeniGuncelle() async {
    if (_islemDevamEdiyor) return;

    setState(() {
      _islemDevamEdiyor = true;
      _kullaniciBegendiMi ? _begeniSayisi-- : _begeniSayisi++;
      _kullaniciBegendiMi = !_kullaniciBegendiMi;
    });

    try {
      final token = await TokenServisi().tokenGetir();
      final url = Uri.parse('http://10.0.2.2:8000/api/dualar/${widget.duaId}/begen/');
      final response = await http.post(url, headers: {'Authorization': 'Bearer $token'});

      if (response.statusCode != 200 && response.statusCode != 201) _islemGeriAl();
    } catch (e) {
      _islemGeriAl();
    } finally {
      _islemDevamEdiyor = false;
    }
  }

  void _islemGeriAl() {
    setState(() {
      _kullaniciBegendiMi ? _begeniSayisi-- : _begeniSayisi++;
      _kullaniciBegendiMi = !_kullaniciBegendiMi;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(8.0),
      child: ListTile(
        title: Text(widget.icerik),
        subtitle: Text('Paylaşan: ${widget.kullaniciAdi}'),
        trailing: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('$_begeniSayisi'),
            IconButton(
              icon: Icon(
                _kullaniciBegendiMi ? Icons.favorite : Icons.favorite_border,
                color: _kullaniciBegendiMi ? Colors.red : Colors.grey,
              ),
              onPressed: _begeniGuncelle,
            ),
          ],
        ),
      ),
    );
  }
}

6. lib/screens/yeni_dua_ekle_ekrani.dart

Dart:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:io'; // Platform kontrolü için eklendi
import '../services/token_servisi.dart';

// Eğer ApiConfig sınıfını ayrı bir dosyaya (örn: constants.dart) koyduysanız,
// o dosyayı import edip bu tanımlamayı silebilirsiniz.
class ApiConfig {
  static String get baseUrl {
    if (Platform.isAndroid) return 'http://10.0.2.2:8000/api/';
    if (Platform.isIOS) return 'http://127.0.0.1:8000/api/';
    return 'http://127.0.0.1:8000/api/';
  }
}

class YeniDuaEkleEkrani extends StatefulWidget {
  // 1. DÜZELTME: Linter uyarısı için (Constructors for public widgets should have a named 'key' parameter)
  const YeniDuaEkleEkrani({super.key});

  @override
  State<YeniDuaEkleEkrani> createState() => _YeniDuaEkleEkraniState();
}

class _YeniDuaEkleEkraniState extends State<YeniDuaEkleEkrani> {
  final _formKey = GlobalKey<FormState>();
  final _icerikController = TextEditingController();
  bool _yukleniyor = false;

  // Bellek yönetimi: Controller ile işimiz bitince hafızadan siliyoruz
  @override
  void dispose() {
    _icerikController.dispose();
    super.dispose();
  }

  Future<void> _duaPaylas() async {
    // Form doğrulamasından geçemezse fonksiyonu durdur
    if (!_formKey.currentState!.validate()) return;
    
    setState(() => _yukleniyor = true);

    try {
      final token = await TokenServisi().tokenGetir();
      // 2. DÜZELTME: Sabit IP yerine iOS/Android uyumlu dinamik URL kullanımı
      final url = Uri.parse('${ApiConfig.baseUrl}dualar/');

      final response = await http.post(
        url,
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer $token',
        },
        body: jsonEncode({'icerik': _icerikController.text}),
      );

      // 3. DÜZELTME: "Don't use BuildContexts across async gaps" uyarısı için.
      // Await sonrasında widget'ın hala ekranda olup olmadığını kontrol ediyoruz.
      if (!mounted) return;

      if (response.statusCode == 201) {
        // İşlem başarılıysa önceki ekrana dön ve sayfayı yenilemesi için 'true' gönder
        Navigator.pop(context, true); 
      } else {
        // Django'dan 401 (Yetkisiz) veya 400 (Kötü İstek) gibi bir hata gelirse kullanıcıya göster
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Kayıt başarısız! Hata Kodu: ${response.statusCode}')),
        );
      }
    } catch (e) {
      // Catch bloğu içindeki ScaffoldMessenger için de mounted kontrolü şart
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Bağlantı Hatası: $e')),
      );
    } finally {
      // Yüklenme animasyonunu durdurmadan önce yine ekranda mıyız diye bakıyoruz
      if (mounted) {
        setState(() => _yukleniyor = false);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Yeni Dua'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _icerikController,
                maxLines: 4,
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: 'İçeriği buraya yazın...',
                ),
                validator: (v) {
                  // Sadece boşluk girilmesini engellemek için trim() ekledik
                  if (v == null || v.trim().isEmpty) {
                    return 'Boş bırakılamaz';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              _yukleniyor
                  ? const CircularProgressIndicator()
                  : ElevatedButton(
                      onPressed: _duaPaylas, 
                      child: const Text('Paylaş'),
                    ),
            ],
          ),
        ),
      ),
    );
  }
}

7. lib/screens/ana_ekran.dart

Dart:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../models/dua.dart';
import '../services/token_servisi.dart';
import '../widgets/dua_karti.dart';
import 'yeni_dua_ekle_ekrani.dart';
import 'login_ekrani.dart';

class AnaEkran extends StatefulWidget {
  @override
  _AnaEkranState createState() => _AnaEkranState();
}

class _AnaEkranState extends State<AnaEkran> {
  late Future<List<Dua>> _dualarFuture;

  @override
  void initState() {
    super.initState();
    _dualarFuture = _dualariGetir();
  }

  Future<List<Dua>> _dualariGetir() async {
    final token = await TokenServisi().tokenGetir();
    final url = Uri.parse('http://10.0.2.2:8000/api/dualar/');
    final response = await http.get(url, headers: {'Authorization': 'Bearer $token'});

    if (response.statusCode == 200) {
      List jsonResponse = json.decode(utf8.decode(response.bodyBytes));
      return jsonResponse.map((d) => Dua.fromJson(d)).toList();
    } else {
      throw Exception('Veri çekilemedi');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Dualar'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () async {
              await TokenServisi().tokenSil();
              Navigator.of(context).pushReplacement(
                MaterialPageRoute(builder: (_) => LoginEkrani())
              );
            },
          )
        ],
      ),
      body: FutureBuilder<List<Dua>>(
        future: _dualarFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Center(child: Text('Hata: ${snapshot.error}'));
          } else if (snapshot.hasData) {
            final dualar = snapshot.data!;
            return ListView.builder(
              itemCount: dualar.length,
              itemBuilder: (context, index) {
                final dua = dualar[index];
                return DuaKarti(
                  duaId: dua.id,
                  kullaniciAdi: dua.kullaniciAdi,
                  icerik: dua.icerik,
                  baslangicBegeniSayisi: dua.begeniSayisi,
                  baslangicKullaniciBegendiMi: dua.kullaniciBegendiMi,
                );
              },
            );
          }
          return const Center(child: Text('Veri yok.'));
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          final sonuc = await Navigator.push(
            context, MaterialPageRoute(builder: (_) => YeniDuaEkleEkrani())
          );
          if (sonuc == true) {
            setState(() { _dualarFuture = _dualariGetir(); });
          }
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

8. lib/main.dart (Uygulamanın Başlangıç Noktası)

Dart:
import 'package:flutter/material.dart';
import 'screens/login_ekrani.dart';

void main() {
  runApp(const DuaUygulamasi());
}

class DuaUygulamasi extends StatelessWidget {
  const DuaUygulamasi({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dua Paylaşım',
      theme: ThemeData(primarySwatch: Colors.teal),
      home: LoginEkrani(), // Başlangıç ekranı
    );
  }
}

Kaynak Kod: https://github.com/nuritiras/dua_app

Android Emülatör:

Androiid emülatörleri, üzerinde çalıştıkları bilgisayarın (sizin durumunuzda Mac'inizin) localhost'una erişmek için 10.0.2.2 IP adresini özel bir köprü olarak kullanır. Django ise bir güvenlik önlemi olarak varsayılan ayarlarda sadece localhost ve 127.0.0.1 üzerinden gelen isteklere yanıt verir, diğerlerini reddeder.

İşte çözümü:

settings.py Dosyasını Güncelleyin

Django projenizdeki dua_backend/settings.py dosyasını açın, ALLOWED_HOSTS ayarını bulun ve Android emülatörünün IP'sini listeye ekleyin:

Python:
ALLOWED_HOSTS = ['localhost', '127.0.0.1', '10.0.2.2']

Eğer geliştirme aşamasında IP adresleriyle hiç uğraşmak istemezseniz ve ileride aynı ağdaki fiziksel bir telefondan veya Pardus makinenizden de Mac'teki bu sunucuya bağlanarak test yapmayı planlıyorsanız, geçici olarak tüm host'lara izin verebilirsiniz (sadece geliştirme ortamında ve DEBUG = True iken tavsiye edilir):

Python:
ALLOWED_HOSTS = ['*']

Dosyayı kaydettikten sonra terminalde CONTROL-C ile sunucuyu durdurup, python manage.py runserver komutuyla tekrar başlatın.

iOS Simülatör:

Android emülatörü kendi izole sanal ağına sahip olduğu için Mac'inize köprü kurmak adına 10.0.2.2 IP'sine ihtiyaç duyuyordu. Ancak iOS Simülatörü, doğrudan Mac'inizin ağ bağlantısını (network stack) paylaşır. Bu nedenle iOS simülatöründe Mac'inizdeki yerel sunucuya ulaşmak için özel bir IP'ye ihtiyacınız yoktur; doğrudan 127.0.0.1 veya localhost kullanabilirsiniz.

İşte yapmanız gereken iki küçük değişiklik:

1. Flutter Kodunuzu Güncelleyin (En Önemlisi)

Flutter uygulamanızda API'ye istek attığınız (muhtemelen http paketi ile yazdığınız) servisteki temel URL'yi (Base URL) iOS'a göre değiştirmelisiniz.

10.0.2.2 yazan yeri 127.0.0.1 olarak güncelleyin:

Dart:

// Eski (Android için olan)
final String baseUrl = 'http://10.0.2.2:8000/api/';

// Yeni (iOS Simülatörü için olan)
final String baseUrl = 'http://127.0.0.1:8000/api/'; 

2. Django settings.py Ayarını Kontrol Edin

Madem artık 127.0.0.1 üzerinden istek atacağız, Django'nun bu adresi kabul ettiğinden emin olmalıyız. dua_backend/settings.py dosyanızdaki ALLOWED_HOSTS ayarını şu şekilde tutmanız iOS simülatörü için yeterli olacaktır:

Python:
ALLOWED_HOSTS = ['localhost', '127.0.0.1']

(Eğer önceki mesajımdaki gibi ['*'] yaptıysanız dokunmanıza gerek yok, o da çalışacaktır.)


Eğer derslerde öğrencilere gösterirken veya kendi geliştirme sürecinizde hem Android hem de iOS simülatörünü aynı anda kullanıyorsanız, sürekli URL değiştirmek yorucu olabilir. 

Özellikle sınıfta öğrencilere Flutter ve Django entegrasyonunu gösterirken, bir simülatörden diğerine geçtiğinizde kodda IP adresi değiştirmekle uğraşmak dersin akışını bozabiliyor.

Bu küçük yardımcı fonksiyon, cihazın işletim sistemini algılayıp doğru yerel adresi otomatik olarak seçecektir.

Dinamik API URL Fonksiyonu

Flutter projenizde API isteklerini yönettiğiniz dosyanın (örneğin api_service.dart veya constants.dart) en üstüne öncelikle dart:io kütüphanesini eklememiz gerekiyor.

Dart:
import 'dart:io'; // Platform sınıfını kullanabilmek için gerekli

class ApiConfig {
  // Bu fonksiyon cihazın işletim sistemine göre doğru yerel IP'yi döndürür
  static String get getBaseUrl {
    if (Platform.isAndroid) {
      // Android Emülatör için köprü IP'si
      return 'http://10.0.2.2:8000/api/'; 
    } else if (Platform.isIOS) {
      // iOS Simülatörü (veya Mac'in kendisi) için doğrudan localhost
      return 'http://127.0.0.1:8000/api/'; 
    }
    
    // Pardus, macOS masaüstü uygulaması veya Web gibi diğer durumlar için varsayılan
    return 'http://127.0.0.1:8000/api/'; 
  }
}

Kodun İçinde Kullanımı

Artık http paketi ile istek atarken uzun uzun IP yazmak yerine, doğrudan bu sınıfı çağırabilirsiniz. Örneğin token almak için yazdığınız fonksiyonu şu şekilde güncelleyebilirsiniz:

Dart:
import 'package:http/http.dart' as http;
// ApiConfig sınıfını yazdığınız dosyayı da import etmeyi unutmayın.

Future<void> loginUser(String username, String password) async {
  // Cihaza göre doğru URL'yi otomatik alır
  final String apiUrl = '${ApiConfig.getBaseUrl}token/'; 

  try {
    final response = await http.post(
      Uri.parse(apiUrl),
      body: {
        'username': username,
        'password': password,
      },
    );

    if (response.statusCode == 200) {
      print('Giriş başarılı! Token alındı.');
      // Token kaydetme işlemleri...
    } else {
      print('Hata kodu: ${response.statusCode}');
    }
  } catch (e) {
    print('İstek atılamadı: $e');
  }
}

Bu yapıyı kurduktan sonra hem Mac'inizdeki iOS simülatöründe hem de Android emülatöründe kodda hiçbir değişiklik yapmadan Django backend'inize sorunsuzca bağlanabileceksiniz.

Yorumlar

Bu blogdaki popüler yayınlar

Pardus Üzerine Django Kurulumu

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