İçeriğe geç
Educora
İleri25 dk13 / 14

Veri: ağ, Room ve DataStore

Retrofit ya da Ktor ile sunucudan JSON yükle, istekleri ViewModel'de coroutine'lerle ekranı dondurmadan çalıştır, listeleri Room veritabanında, ayarları ise DataStore'da sakla.

Kendini test et
Bu derste öğreneceklerin
  • Bir Retrofit arayüzüyle suspend ağ isteği yazmak ve JSON'u bir veri sınıfına çevirmek
  • Bir isteği viewModelScope'ta başlatıp yükleniyor, başarılı ve hata durumlarını göstermek
  • Room'da bir varlık (entity), DAO ve veritabanı sınıfı oluşturmak
  • Küçük ayarları Preferences DataStore'da saklamak

Murad metroda Educora'yı açıyor ve internet yok. İyi bir uygulama bu anda boş bir ekran göstermez: dersler önceden telefona kaydedilmiştir, koyu tema seçimi hatırlanır, internet geri geldiğinde de yeni dersler sunucudan sessizce yüklenir. Bunun için üç araç gerekir: ağ için Retrofit ya da Ktor, listeler için Room veritabanı ve ayarlar için DataStore.

Ağ: Retrofit ve Ktor

Önce AndroidManifest.xml'e internet iznini ekle: <uses-permission android:name="android.permission.INTERNET" />. Retrofit, Android'de en yaygın HTTP kütüphanesidir: sen yalnızca bir arayüz yazarsın, istek kodunu kütüphane kendisi üretir. JSON'u Kotlin nesnelerine kotlinx.serialization çevirir. Aşağıdaki örnek, test için herkese açık bir servis olan JSONPlaceholder'dan yapılacaklar listesini yükler.

Kotlin
@Serializable
data class Todo(val id: Int, val title: String, val completed: Boolean)

interface TodoApi {
    @GET("todos")
    suspend fun getTodos(): List<Todo>

    @GET("todos/{id}")
    suspend fun getTodo(@Path("id") id: Int): Todo
}

private val json = Json { ignoreUnknownKeys = true }

val todoApi: TodoApi = Retrofit.Builder()
    .baseUrl("https://jsonplaceholder.typicode.com/")
    .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
    .build()
    .create(TodoApi::class.java)
ignoreUnknownKeys = true, JSON'daki fazla alanları (örneğin userId) sessizce atlar. baseUrl mutlaka / ile bitmelidir.

Ktor, JetBrains'in Kotlin için geliştirdiği alternatiftir; Kotlin Multiplatform'da da çalışır, yani aynı ağ kodu Android ile iOS arasında paylaşılabilir. Ktor'da bir istek şöyle görünür: client.get("https://jsonplaceholder.typicode.com/todos").body<List<Todo>>(); burada client, ContentNegotiation eklentisi ve json() ile kurulmuş bir HttpClient'tır.

Kotlin
@Serializable
data class Lesson(val id: Int, val title: String, val minutes: Int)

fun main() {
    val lesson = Lesson(1, "Variables", 10)
    println(Json.encodeToString(lesson))

    val json = Json { ignoreUnknownKeys = true }
    val text = """{"id":2,"title":"Loops","minutes":15,"level":"beginner"}"""
    val loaded = json.decodeFromString<Lesson>(text)
    println(loaded)
    println("Total: ${lesson.minutes + loaded.minutes} min")
}
Beklenen çıktı
{"id":1,"title":"Variables","minutes":10}
Lesson(id=2, title=Loops, minutes=15)
Total: 25 min
Ağsız bir deneme: kotlinx.serialization bir nesneyi JSON'a ve geri çevirir. level alanı sınıfta olmadığı için atlanır.

ViewModel'de coroutine'ler

Bir ağ isteği saniyeler sürebilir; bu yüzden onu ViewModel'in **viewModelScope** kapsamında bir coroutine olarak başlatırız. Kullanıcı ekranı kapatınca ViewModel temizlenir ve bu kapsamdaki tüm coroutine'ler kendiliğinden iptal edilir; boşa iş ya da bellek sızıntısı olmaz. Sonucu önceki derslerden tanıdığın sealed bir durumla tanımlarız.

Kotlin
sealed interface TodosUiState {
    data object Loading : TodosUiState
    data class Success(val todos: List<Todo>) : TodosUiState
    data class Error(val message: String) : TodosUiState
}

class TodosViewModel(private val api: TodoApi) : ViewModel() {
    private val _uiState = MutableStateFlow<TodosUiState>(TodosUiState.Loading)
    val uiState: StateFlow<TodosUiState> = _uiState.asStateFlow()

    init { load() }

    fun load() {
        viewModelScope.launch {
            _uiState.value = TodosUiState.Loading
            _uiState.value = try {
                TodosUiState.Success(api.getTodos())
            } catch (e: IOException) {
                TodosUiState.Error("No connection")
            } catch (e: HttpException) {
                TodosUiState.Error("Server error ${e.code()}")
            }
        }
    }
}
Ekran üç durumu when (state) ile gösterir: dönen bir gösterge, liste ya da “Try again” düğmeli bir hata mesajı.
Kotlin
@Composable
fun TodosScreen(viewModel: TodosViewModel) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()

    when (val current = state) {
        TodosUiState.Loading -> CircularProgressIndicator()
        is TodosUiState.Success -> LazyColumn {
            items(current.todos, key = { it.id }) { todo -> Text(todo.title) }
        }
        is TodosUiState.Error -> Column {
            Text(current.message)
            Button(onClick = viewModel::load) { Text("Try again") }
        }
    }
}
Ekranda önce dönen bir daire, sonra yapılacaklar listesi; internet yoksa “No connection” metni ve “Try again” düğmesi.

Dikkat et: TodosViewModel yapıcısında bir api parametresi alır; bu yüzden sıradan viewModel() onu oluşturamaz. Ya küçük bir fabrika (ViewModelProvider.Factory) yazılır ya da gerçek projelerde olduğu gibi Hilt veya Koin gibi bir bağımlılık enjeksiyonu kütüphanesi kullanılır. ViewModel ile ağ arasında genellikle bir katman daha bulunur: depo (TodoRepository). Verinin nereden geldiğini (sunucu, veritabanı, önbellek) gizler; ViewModel yalnızca onunla konuşur.

Yanlış: ana iş parçacığı bloke edilir
fun load() {
    // blocks the main thread until the server answers
    val todos = runBlocking { api.getTodos() }
    _uiState.value = TodosUiState.Success(todos)
}
Doğru: viewModelScope'ta bir coroutine
fun load() {
    viewModelScope.launch {
        _uiState.value = try {
            TodosUiState.Success(api.getTodos())
        } catch (e: IOException) {
            TodosUiState.Error("No connection")
        }
    }
}
Solda ekran cevap gelene kadar donar ve birkaç saniye sonra sistem “Application Not Responding” (ANR) penceresi gösterebilir. Soldaki sürüm hataları da yakalamaz.

Room: cihazdaki veritabanı

Room, SQLite üzerine kurulmuş resmî kütüphanedir. Üç parçadan oluşur: Entity — bir tablonun satırı olan veri sınıfı; DAO — SQL sorgularını fonksiyonlar olarak tanımlayan arayüz; Database — bunları birleştiren soyut sınıf. Kod derleme sırasında KSP eklentisiyle üretilir ve SQL hataları daha o anda yakalanır.

Kotlin
@Entity(tableName = "lessons")
data class LessonEntity(
    @PrimaryKey val id: Int,
    val title: String,
    val done: Boolean = false
)

@Dao
interface LessonDao {
    @Query("SELECT * FROM lessons ORDER BY id")
    fun observeAll(): Flow<List<LessonEntity>>

    @Upsert
    suspend fun upsertAll(lessons: List<LessonEntity>)

    @Query("UPDATE lessons SET done = 1 WHERE id = :id")
    suspend fun markDone(id: Int)
}

@Database(entities = [LessonEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun lessonDao(): LessonDao
}
Veritabanı bir kez oluşturulur: Room.databaseBuilder(context, AppDatabase::class.java, "educora.db").build(). Flow döndüren bir sorgu, tablo her değiştiğinde yeni bir liste gönderir.

DataStore: ayarlar için

Kotlin
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

private val DARK_THEME = booleanPreferencesKey("dark_theme")

class SettingsRepository(private val context: Context) {
    val darkTheme: Flow<Boolean> =
        context.dataStore.data.map { prefs -> prefs[DARK_THEME] ?: false }

    suspend fun setDarkTheme(enabled: Boolean) {
        context.dataStore.edit { prefs -> prefs[DARK_THEME] = enabled }
    }
}
preferencesDataStore, bir dosyanın en üst düzeyinde bir kez tanımlanır; aynı dosya için ikinci bir örnek oluşturmak hataya yol açar.
Ne saklanırNerede
tema, dil, bildirim anahtarlarıDataStore
dersler, sonuçlar, arama ve sıralama gereken listelerRoom
görseller ve büyük dosyalaruygulamanın dosya klasörü
hesap ve tüm cihazlarda aynı olması gereken ilerlemesunucu
  1. 1
    App Inspection'ı aç

    Uygulamayı emülatörde çalıştır ve View › Tool Windows › App Inspection'ı seç.

  2. 2
    Veritabanına bak

    Database Inspector sekmesinde educora.db'yi ve lessons tablosunu aç. Tablo canlı güncellenir; orada bir SQL sorgusu yazıp deneyebilirsin de.

  3. 3
    İstekleri izle

    Network Inspector sekmesinde her HTTP isteğini, ne kadar sürdüğünü ve sunucunun döndürdüğü JSON'u görürsün.

tüm projede metin aramak (Mac: Cmd+Shift+F)Ctrl+Shift+F
ifadeyi tamamlamak: parantezleri kendisi kapatır (Mac: Cmd+Shift+Return)Ctrl+Shift+Enter
satırı çoğaltmak (Mac: Cmd+D)Ctrl+D
satırı yorum yapmak ya da geri almak (Mac: Cmd+/)Ctrl+/

Önemli noktalar

  • Ağ için manifestte INTERNET izni gerekir; Retrofit, bir arayüzdeki suspend fonksiyonlardan istek kodu üretir.
  • İstekler viewModelScope.launch içinde çalışır; ekran kapanınca kendiliğinden iptal edilir.
  • Sonucu Loading / Success / Error sealed durumuyla tanımla ve hataları try/catch ile yakala.
  • Room: Entity + DAO + Database; Flow döndüren sorgu, tablo değişince ekranı kendiliğinden günceller.
  • Küçük ayarlar DataStore'da tutulur; ekranın tek doğruluk kaynağı yerel veritabanıdır.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
Uygulamanın internete çıkabilmesi için manifestte hangi izin olmalıdır?