- Bir Retrofit arayüzüyle
suspendağ 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.
@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.
@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")
}{"id":1,"title":"Variables","minutes":10}
Lesson(id=2, title=Loops, minutes=15)
Total: 25 minlevel 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.
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()}")
}
}
}
}when (state) ile gösterir: dönen bir gösterge, liste ya da “Try again” düğmeli bir hata mesajı.@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") }
}
}
}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.
fun load() {
// blocks the main thread until the server answers
val todos = runBlocking { api.getTodos() }
_uiState.value = TodosUiState.Success(todos)
}fun load() {
viewModelScope.launch {
_uiState.value = try {
TodosUiState.Success(api.getTodos())
} catch (e: IOException) {
TodosUiState.Error("No connection")
}
}
}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.
@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
}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
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ır | Nerede |
|---|---|
| tema, dil, bildirim anahtarları | DataStore |
| dersler, sonuçlar, arama ve sıralama gereken listeler | Room |
| görseller ve büyük dosyalar | uygulamanın dosya klasörü |
| hesap ve tüm cihazlarda aynı olması gereken ilerleme | sunucu |
- 1App Inspection'ı aç
Uygulamayı emülatörde çalıştır ve
View › Tool Windows › App Inspection'ı seç. - 2Veritabanına bak
Database Inspectorsekmesindeeducora.db'yi velessonstablosunu aç. Tablo canlı güncellenir; orada bir SQL sorgusu yazıp deneyebilirsin de. - 3İstekleri izle
Network Inspectorsekmesinde her HTTP isteğini, ne kadar sürdüğünü ve sunucunun döndürdüğü JSON'u görürsün.
Cmd+Shift+F)Ctrl+Shift+FCmd+Shift+Return)Ctrl+Shift+EnterCmd+D)Ctrl+DCmd+/)Ctrl+/Önemli noktalar
- Ağ için manifestte
INTERNETizni gerekir; Retrofit, bir arayüzdekisuspendfonksiyonlardan istek kodu üretir. - İstekler
viewModelScope.launchiçinde çalışır; ekran kapanınca kendiliğinden iptal edilir. - Sonucu
Loading / Success / Errorsealed durumuyla tanımla ve hatalarıtry/catchile yakala. - Room: Entity + DAO + Database;
Flowdö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.