Андроид-приложение, 45-я неделя
Предупреждение
Предостерегаю: это не пошаговое руководство! Также я не проверял код ниже на ошибки.
Создание слоя данных
- Создаю пакет «дата» (data, «данные») в корневом пакете приложения, то есть в пакете вида «домен первого уровня . домен второго уровня . имя приложения», например «ком . экзампл . май апп» (<com.example.myapp>). (Полное имя пакета будет «ком . экзампл . май апп . дата» (<com.example.myapp.data>).)
- Внутри только что созданного пакета «дата» создаю интерфейс контейнера приложения, «Апп контейнер» (AppContainer, «контейнер приложения»). Ниже создаю класс, который реализует интерфейс. Пример:
- Там же, внутри пакета «дата», создаю интерфейс репозитория, вместе с классом, который реализует этот интерфейс. Пример (вместо ListOfAmphibianInfos должно быть: List<AmphibianInfo>):
- На одном уровне с пакетом «дата» создаю пакет «нетворк» (network, «сеть»).
- Внутри только что созданного пакета «нетворк» создаю класс данных для элементов репозитория. В приложении «Амфибии» это выглядит так:
- Там же создаю абстрактную модель программного интерфейса. Пример (вместо ListOfAmphibianInfos должно быть: List<AmphibianInfo>):
- Непосредственно в корневом пакете создаю класс приложения. Свойство класса «контейнер» (container) реализует созданный выше интерфейс контейнера приложения, «Апп контейнер» (AppContainer, «контейнер приложения»). В «Амфибиях» это может выглядеть так:
package com.example.amphibians.data ... interface AppContainer { val amphibiansRepository: AmphibiansRepository } class DefaultAppContainer : AppContainer { private val baseUrl... private val retrofit... private val retrofitService... override val amphibiansRepository... }
package com.example.amphibians.data import com.example.amphibians.network.AmphibianInfo interface AmphibiansRepository { suspend fun getAmphibiansInfo(): ListOfAmphibianInfos } class NetworkAmphibiansRepository( private val amphibiansApiService: AmphibiansApiService ) : AmphibiansRepository { override suspend fun getAmphibiansInfo(): ListOfAmphibianInfos = amphibiansApiService.getInfo() }
package com.example.amphibians.network import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class AmphibianInfo( val id: String, val name: String, val type: String, val description: String, @SerialName(value = "img_src") val imgSrc: String )
package com.example.amphibians.network import retrofit2.http.GET interface AmphibiansApiService { @GET("info") suspend fun getInfo(): ListOfAmphibianInfos }
package com.example.amphibians import android.app.Application import com.example.amphibians.data.AppContainer import com.example.amphibians.data.DefaultAppContainer class AmphibiansApplication : Application() { lateinit var container: AppContainer override fun onCreate() { super.onCreate() container = DefaultAppContainer() } }
Создание слоя пользовательского интерфейса
- Создаю пакет «скринс» (screens, «экраны») внутри пакета «ю ай» (ui, «пользовательский интерфейс», полное имя <com.example.myapp.ui>). (Кликаю правой кнопкой мыши по пакету <com.example.myapp> на боковой панели, в открывшемся контекстном меню выбираю создать пакет (New («нью») — Package («пе́кидж»)), затем ввожу ui.screens, дополняя имя корневого пакета <com.example.myapp>.)
- Внутри создаю два файла. Первый — для класса вью-модели (ViewModel, «модель представления»): «май апп вью мо́дэл . кей ти» (MyAppViewModel.kt). Второй — для компо́усэблов домашнего экрана: «хо́ум скрин . кей ти» (HomeScreen.kt).
- Рядом с классом вью-модели создаю «запечатанный интерфейс» (sealed interface, «силд интерфейс»), описывающий общее состояние приложения. В приложении «Амфибии» это может выглядеть примерно так (вместо ListOfAmphibianInfos должно быть: List<AmphibianInfo>):
- Создаю наблюдаемую переменную — свойство класса вью-модели. Это свойство будет хранить состояние приложения. Пример:
- Инъектирую зависимость: конструктору класса вью-модели прибавляю параметр. Параметром будет репозиторий; этот репозиторий будет источником данных для методов вью-модели. Соответствующее свойство экземпляра класса будет приватным (private var). Пример:
- Внутри класса вью-модели создаю методы, которые будут изменять состояние приложения (свойство «амфибианс ю-ай стейт» (amphibiansUiState) экземпляра класса вью-модели). Для получения данных из Интернета использую корутину, которая создастся при помощи функции «лонч» (launch) и запустится в области видимости «вью модел скоуп» (viewModelScope). Пример:
- Там же, в определении класса вью-модели, определяю свойство — объект-компаньон. Пример:
package com.example.amphibians.ui.screens import com.example.amphibians.network.AmphibianPhoto sealed interface AmphibiansUiState { data class Success(val photos: ListOfAmphibianInfos) : AmphibiansUiState object Loading : AmphibiansUiState object Error : AmphibiansUiState }
package com.example.amphibians.ui.screens import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue ... class AmphibiansViewModel : ViewModel() { var amphibiansUiState: AmphibiansUiState by mutableStateOf(AmphibiansUiState.Loading) private set }
... import com.example.amphibians.data.AmphibiansRepository ... class AmphibiansViewModel(private val amphibiansRepository: AmphibiansRepository) : ViewModel() { ... }
... import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import java.io.IOException ... class AmphibiansViewModel(private val amphibiansRepository: AmphibiansRepository) : ViewModel() { ... init { getAmphibiansInfo() } fun getAmphibiansInfo() { viewModelScope.launch { amphibiansUiState = try { AmphibianUiState.Success(amphibiansRepository.getAmphibiansInfo()) } catch(error: IOException) { AmphibianUiState.Error } } } }
... import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.example.amphibians.AmphibiansApplication ... class AmphibiansViewModel(private val amphibiansRepository: AmphibiansRepository) : ViewModel() { ... companion object { val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { val application = (this[APPLICATION_KEY] as AmphibiansApplication) val amphibiansRepository = application.container.amphibiansRepository AmphibiansViewModel(amphibiansRepository = amphibiansRepository) } } } }