Андроид-приложение, 45-я неделя

Предупреждение

Предостерегаю: это не пошаговое руководство! Также я не проверял код ниже на ошибки.

Создание слоя данных

  1. Создаю пакет «дата» (data, «данные») в корневом пакете приложения, то есть в пакете вида «домен первого уровня . домен второго уровня . имя приложения», например «ком . экзампл . май апп» (<com.example.myapp>). (Полное имя пакета будет «ком . экзампл . май апп . дата» (<com.example.myapp.data>).)
  2. Внутри только что созданного пакета «дата» создаю интерфейс контейнера приложения, «Апп контейнер» (AppContainer, «контейнер приложения»). Ниже создаю класс, который реализует интерфейс. Пример:
  3. 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...
    }
    
  4. Там же, внутри пакета «дата», создаю интерфейс репозитория, вместе с классом, который реализует этот интерфейс. Пример (вместо ListOfAmphibianInfos должно быть: List<AmphibianInfo>):
  5. 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()
    }
    
  6. На одном уровне с пакетом «дата» создаю пакет «нетворк» (network, «сеть»).
  7. Внутри только что созданного пакета «нетворк» создаю класс данных для элементов репозитория. В приложении «Амфибии» это выглядит так:
  8. 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
    )
    
  9. Там же создаю абстрактную модель программного интерфейса. Пример (вместо ListOfAmphibianInfos должно быть: List<AmphibianInfo>):
  10. package com.example.amphibians.network
    
    import retrofit2.http.GET
    
    interface AmphibiansApiService {
        @GET("info")
        suspend fun getInfo(): ListOfAmphibianInfos
    }
    
  11. Непосредственно в корневом пакете создаю класс приложения. Свойство класса «контейнер» (container) реализует созданный выше интерфейс контейнера приложения, «Апп контейнер» (AppContainer, «контейнер приложения»). В «Амфибиях» это может выглядеть так:
  12. 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()
        }
    }
    

Создание слоя пользовательского интерфейса

  1. Создаю пакет «скринс» (screens, «экраны») внутри пакета «ю ай» (ui, «пользовательский интерфейс», полное имя <com.example.myapp.ui>). (Кликаю правой кнопкой мыши по пакету <com.example.myapp> на боковой панели, в открывшемся контекстном меню выбираю создать пакет (New («нью») — Package («пе́кидж»)), затем ввожу ui.screens, дополняя имя корневого пакета <com.example.myapp>.)
  2. Внутри создаю два файла. Первый — для класса вью-модели (ViewModel, «модель представления»): «май апп вью мо́дэл . кей ти» (MyAppViewModel.kt). Второй — для компо́усэблов домашнего экрана: «хо́ум скрин . кей ти» (HomeScreen.kt).
  3. Рядом с классом вью-модели создаю «запечатанный интерфейс» (sealed interface, «силд интерфейс»), описывающий общее состояние приложения. В приложении «Амфибии» это может выглядеть примерно так (вместо ListOfAmphibianInfos должно быть: List<AmphibianInfo>):
  4. 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
    }
    
  5. Создаю наблюдаемую переменную — свойство класса вью-модели. Это свойство будет хранить состояние приложения. Пример:
  6. 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
    }
    
  7. Инъектирую зависимость: конструктору класса вью-модели прибавляю параметр. Параметром будет репозиторий; этот репозиторий будет источником данных для методов вью-модели. Соответствующее свойство экземпляра класса будет приватным (private var). Пример:
  8. ...
    import com.example.amphibians.data.AmphibiansRepository
    ...
    class AmphibiansViewModel(private val amphibiansRepository: AmphibiansRepository) : ViewModel() {
        ...
    }
    
  9. Внутри класса вью-модели создаю методы, которые будут изменять состояние приложения (свойство «амфибианс ю-ай стейт» (amphibiansUiState) экземпляра класса вью-модели). Для получения данных из Интернета использую корутину, которая создастся при помощи функции «лонч» (launch) и запустится в области видимости «вью модел скоуп» (viewModelScope). Пример:
  10. ...
    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
                }
            }
        }
    }
    
  11. Там же, в определении класса вью-модели, определяю свойство — объект-компаньон. Пример:
  12. ...
    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)
                }
            }
        }
    }