- Retrofit interfeysi ilə
suspendşəbəkə sorğusu yazmaq və JSON-u data sinfə çevirmək - Sorğunu
viewModelScope-da işə salıb yüklənmə, uğur və xəta vəziyyətlərini göstərmək - Room-da entity, DAO və verilənlər bazası sinfi yaratmaq
- Kiçik parametrləri Preferences DataStore-da saxlamaq
Murad metroda Educora-nı açır, internet isə yoxdur. Yaxşı tətbiq bu anda boş ekran göstərmir: dərslər əvvəlcədən telefonda saxlanıb, tünd tema seçimi yadda qalıb, internet qayıdanda isə yeni dərslər serverdən sakitcə yüklənir. Bunun üçün üç alət lazımdır: şəbəkə üçün Retrofit və ya Ktor, siyahılar üçün Room verilənlər bazası, parametrlər üçün DataStore.
Şəbəkə: Retrofit və Ktor
Əvvəlcə AndroidManifest.xml-ə internet icazəsi əlavə et: <uses-permission android:name="android.permission.INTERNET" />. Retrofit Android-də ən geniş yayılmış HTTP kitabxanasıdır: sən sadəcə interfeys yazırsan, sorğunun kodunu isə kitabxana özü yaradır. JSON-u Kotlin obyektinə kotlinx.serialization çevirir. Aşağıdakı nümunə sınaq üçün açıq olan JSONPlaceholder xidmətindən tapşırıqlar siyahısını yükləyir.
@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-dakı artıq sahələri (məsələn, userId) sakitcə buraxır. baseUrl mütləq / ilə bitməlidir.Ktor JetBrains-in Kotlin üçün yaratdığı alternativdir; o, Kotlin Multiplatform-da da işləyir, yəni eyni şəbəkə kodu Android və iOS arasında bölüşdürülə bilər. Ktor-da sorğu belə görünür: client.get("https://jsonplaceholder.typicode.com/todos").body<List<Todo>>() — burada client ContentNegotiation plagini və json() ilə qurulmuş HttpClient-dir.
@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 sahəsi sinifdə olmadığı üçün buraxılır.Korutinlər ViewModel-də
Şəbəkə sorğusu saniyələr çəkə bilər, ona görə onu ViewModel-in **viewModelScope** sahəsində korutin kimi işə salırıq. İstifadəçi ekranı bağlayanda ViewModel silinir və bu sahədəki bütün korutinlər avtomatik ləğv olunur — lazımsız iş və yaddaş sızması olmur. Nəticəni isə keçən dərslərdən tanış olan sealed vəziyyətlə təsvir edirik.
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) ilə üç halı göstərir: fırlanan indikator, siyahı və ya «Try again» düyməli xəta 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") }
}
}
}Diqqət et: TodosViewModel konstruktorda api parametri alır, ona görə adi viewModel() onu yarada bilmir — ya kiçik fabrik (ViewModelProvider.Factory) yazılır, ya da real layihələrdə olduğu kimi Hilt və ya Koin kimi asılılıq inyeksiyası kitabxanası istifadə olunur. Adətən ViewModel ilə şəbəkə arasında bir qat da olur — repozitoriya (TodoRepository): o, məlumatın haradan gəldiyini (server, baza, keş) gizlədir, ViewModel isə yalnız onunla danışır.
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: cihazdakı verilənlər bazası
Room SQLite üzərində qurulmuş rəsmi kitabxanadır. Üç hissədən ibarətdir: Entity — cədvəlin sətri olan data sinif; DAO — SQL sorğularını funksiyalar kimi təsvir edən interfeys; Database — onları birləşdirən abstrakt sinif. Kod KSP plagini ilə kompilyasiya zamanı yaradılır və SQL səhvləri elə o zaman tapılı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 qaytaran sorğu cədvəl hər dəfə dəyişəndə yeni siyahı göndərir.DataStore: parametrlər üçün
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 faylın yuxarı səviyyəsində bir dəfə elan olunur; eyni fayl üçün ikinci nüsxə yaratmaq xətaya səbəb olur.| Nə saxlanır | Harada |
|---|---|
| tema, dil, bildiriş açarları | DataStore |
| dərslər, nəticələr, axtarış və çeşidləmə lazım olan siyahılar | Room |
| şəkillər və böyük fayllar | tətbiqin fayl qovluğu |
| hesab və bütün cihazlarda eyni olmalı olan irəliləyiş | server |
- 1App Inspection-u aç
Tətbiqi emulyatorda işə sal və
View › Tool Windows › App Inspectionseç. - 2Bazaya bax
Database Inspectortabındaeducora.dbvəlessonscədvəlini aç. Cədvəl canlı yenilənir; SQL sorğusunu da orada yazıb yoxlaya bilərsən. - 3Sorğuları izlə
Network Inspectortabında hər HTTP sorğusunu, onun vaxtını və serverin qaytardığı JSON-u görəcəksən.
Cmd+Shift+F)Ctrl+Shift+FCmd+Shift+Return)Ctrl+Shift+EnterCmd+D)Ctrl+DCmd+/)Ctrl+/Əsas fikirlər
- Şəbəkə üçün manifestdə
INTERNETicazəsi lazımdır; Retrofit interfeysdəkisuspendfunksiyalardan sorğu kodu yaradır. - Sorğular
viewModelScope.launchdaxilində işləyir; ekran bağlananda onlar avtomatik ləğv olunur. - Nəticəni
Loading / Success / Errorsealed vəziyyəti ilə təsvir et və xətalarıtry/catchilə tut. - Room: Entity + DAO + Database;
Flowqaytaran sorğu cədvəl dəyişəndə ekranı özü yeniləyir. - Kiçik parametrlər DataStore-da saxlanır; ekran üçün vahid həqiqət mənbəyi yerli bazadır.
Özünü yoxla
10 sual. Hər düzgün cavab XP qazandırır.