MVVM на Android с компонентами архитектуры + библиотека Koin



Книга MVVM на Android с компонентами архитектуры + библиотека Koin

Введение


С MVVM (ModelView-ViewModel) процесс разработки графического интерфейса для пользователей делится на две части. Первая — это работа с языком разметки или кодом GUI. Вторая — разработка бизнес-логики или логики бэкенда (модель данных). Часть View model в MVVM — это конвертер значений. Это значит, что view model отвечает за конвертирование объектов данных из модели в такой вид, чтобы с объектами было легко работать. Если смотреть с этой стороны, то view model — это скорее модель, чем представление. Она контролирует большую часть логики отображения. Модель представления может реализовывать паттерн медиатор. Для этого организуется доступ к логике бэкенда вокруг набора юз-кейсов, поддерживаемых представлением.


В этом туториале мы попробуем определить каждый компонент паттерна MVVM, чтобы создать небольшое приложение на Android в соответствии с ним.


На следующей картинке — разные элементы, которые мы собираемся создать при помощи компонента Architecture и библиотеки Koin для внедрения зависимостей.



Архитектуру ниже можно разделить на три различные части.


Представление


Содержит структурное определение того, что пользователи получат на экранах. Вы можете поместить сюда статическое и динамическое содержимое (анимацию и смену состояний). Тут может не быть никакой логики приложения. Для нашего случая в представлении может быть активность или фрагмент.


Модель представления


Этот компонент связывает модель и представление. Отвечает за управление ссылками данных и возможных конверсий. Здесь появляется биндинг. В Android мы не беспокоимся об этом, потому что можно напрямую использовать класс AndroidViewModel или ViewModel.


Модель


Это уровень бизнес-данных и он не связан ни с каким особенным графическим представлением. В Android, согласно “чистой” архитектуре, модель может содержать базу данных, репозиторий и класс бизнес-логики. Картинка ниже описывает взаимодействие между разными компонентами.



Как реализовать паттерн MVVM


Чтобы реализовать паттерн MVVM, важно начать с компонентов, которым для работы нужен другой компонент. Это и есть зависимость.


А с момента появления компонента архитектуры, логичное общее решение  — реализовать Android-приложения при помощи модели с изображения ниже. Там вы увидите стрелки, которые ведут от представления (активности/фрагмента) к модели


А это значит, что View знает о View-Model, а не наоборот, и View Model знает о Model, и не наоборот. То есть у представления будет связь с моделью представления, а у модели представления будет связь с моделью. Строго в таком порядке, никак иначе. Благодаря такой архитектуре приложение легко поддерживать и тестировать.



Чтобы программировать быстро и эффективно, вам нужно начать с моделирования, так как модели не нужны другие компоненты для работы.


Сценарий приложения и реализация модели


Чтобы понять, как функционирует паттерн MVVM, мы напишем небольшое приложение, в котором будут все компоненты с предыдущей картинки. Мы создадим программу, которая покажет данные. Мы их взяли по этой ссылке. Приложение будет сохранять данные локально для того, чтобы потом оно работало в режиме оффлайн.


Пользовательская модель


@Entity(tableName = "users")
data class GithubUser(
@PrimaryKey val id: Long,
val login: String,
val avatar_url: String
)

Приложение будет обрабатывать данные такой структуры. А для простоты я выберу всего лишь некоторые параметры. У класса GithubUser есть room-аннотация и у данных в локальной БД будет такая же структура, как и у данных в API. 


У пространства DAO есть только два метода. Один — добавление информации в БД. Второй — ее извлечение. 


@Dao
interface UserDao {

@Query("SELECT * FROM users")
fun findAll(): LiveData<List<GithubUser>>

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun add(users: List<GithubUser>)
}
//UserDao.kt

Пространство базы данных выглядит так:


@Database(entities = [GithubUser::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract val userDao: UserDao
}
//AppDatabase.kt

Во второй части мы реализуем Webservice, который отвечает за получение данных онлайн. Для того будем пользоваться retrofit+coroutines.


interface UserApi {

@GET("users")
fun getAllAsync(): Deferred<List<GithubUser>>
}
//UserApi.kt

Если вы хотите узнать, как пользоваться Retrofit вместе с сопрограммами, загляните сюда.


В третьей части мы реализуем репозиторий. Этот класс будет отвечать за определение источника данных. Для нашего случая их два, так что репозиторий будет только получать данные онлайн, чтобы потом сохранить их в локальной базе данных. 


class UserRepository(private val userApi: UserApi, private val userDao: UserDao) {

val data = userDao.findAll()

suspend fun refresh() {
withContext(Dispatchers.IO) {
val users = userApi.getAllAsync().await()
userDao.add(users)
}
}
}

Как сами видите, у репозитория есть конструктор с двумя параметрами. Первый — это класс, который представляет онлайн-данные, а второй — представляет данные оффлайн.




View-Model


После того, как мы описали модель и все ее части, пора ее реализовать. Для этого возьмем класс, родителем которого является класс ViewModel Android Jetpack. 


class UserViewModel(private val userRepository: UserRepository) : ViewModel() {

private val _loadingState = MutableLiveData<LoadingState>()
val loadingState: LiveData<LoadingState>
get() = _loadingState

val data = userRepository.data

init {
fetchData()
}

private fun fetchData() {
viewModelScope.launch {
try {
_loadingState.value = LoadingState.LOADING
userRepository.refresh()
_loadingState.value = LoadingState.LOADED
} catch (e: Exception) {
_loadingState.value = LoadingState.error(e.message)
}
}
}
}

Класс ViewModel создан для того, чтобы хранить и управлять данными, связанными с UI относительно жизненного цикла. Он позволяет данным пережить изменения конфигурации, например, повороты экрана.


View-model берет репозиторий в качестве параметра. Этот класс “знает” все источники данных для нашего приложения. В начальном блоке view-model мы обновляем данные БД. Это делается вызовом метода обновления репозитория. А еще у view-model есть свойство data. Оно получает данные локально напрямую. Это гарантия, что у пользователя всегда будет что-то в интерфейсе, даже если устройство не в сети.


Подсказка: я пользовался вспомогательным классом, который помогал мне управлять состоянием загрузки


data class LoadingState private constructor(val status: Status, val msg: String? = null) {
companion object {
val LOADED = LoadingState(Status.SUCCESS)
val LOADING = LoadingState(Status.RUNNING)
fun error(msg: String?) = LoadingState(Status.FAILED, msg)
}

enum class Status {
RUNNING,
SUCCESS,
FAILED
}
}

Представление


Это последний компонент архитектуры. Он напрямую общается с представлением-моделью, получает данные и, например, передает их в recycler-view. В нашем случае представление — это простая активность.


class MainActivity : AppCompatActivity() {

private val userViewModel by viewModel<UserViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

userViewModel.data.observe(this, Observer {
// Todo: Populate the recyclerView here
it.forEach { githubUser ->
Toast.makeText(baseContext, githubUser.login, Toast.LENGTH_SHORT).show()
}
})

userViewModel.loadingState.observe(this, Observer {
when (it.status) {
LoadingState.Status.FAILED -> Toast.makeText(baseContext, it.msg, Toast.LENGTH_SHORT).show()
LoadingState.Status.RUNNING -> Toast.makeText(baseContext, "Loading", Toast.LENGTH_SHORT).show()
LoadingState.Status.SUCCESS -> Toast.makeText(baseContext, "Success", Toast.LENGTH_SHORT).show()
}
})
}
}

В представлении происходит отслеживание того, как изменяются данные, как они автоматически обновляются на уровне интерфейса. Для нашего случая в представлении также отслеживается состояние операций загрузки в фоновом режиме. В процесс включено свойство loadingState, которое мы определили выше. 


Вот вы и увидели, как я получил экземпляр view-model, используя для этого внедрение. А как это сработает, мы увидим дальше.


Конкретизация объектов и внедрение зависимостей


Наблюдательные заметят, что пока я еще не создал репозиторий и его параметры. Мы будет это делать точно при помощи внедрения зависимостей. А для этого в свою очередь мы берем библиотеку, Koin подходит идеально. 


Так мы создадим важные объекты. Нашему приложению они нужны там же и нам останется только вызвать их в разные точки программы. Для этого и нужна магия библиотеки Koin. 


val viewModelModule = module {
viewModel { UserViewModel(get()) }
}


val apiModule = module {
fun provideUserApi(retrofit: Retrofit): UserApi {
return retrofit.create(UserApi::class.java)
}

single { provideUserApi(get()) }
}

val netModule = module {
fun provideCache(application: Application): Cache {
val cacheSize = 10 * 1024 * 1024
return Cache(application.cacheDir, cacheSize.toLong())
}

fun provideHttpClient(cache: Cache): OkHttpClient {
val okHttpClientBuilder = OkHttpClient.Builder()
.cache(cache)

return okHttpClientBuilder.build()
}

fun provideGson(): Gson {
return GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.IDENTITY).create()
}


fun provideRetrofit(factory: Gson, client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create(factory))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.client(client)
.build()
}

single { provideCache(androidApplication()) }
single { provideHttpClient(get()) }
single { provideGson() }
single { provideRetrofit(get(), get()) }

}

val databaseModule = module {

fun provideDatabase(application: Application): AppDatabase {
return Room.databaseBuilder(application, AppDatabase::class.java, "eds.database")
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
}


fun provideDao(database: AppDatabase): UserDao {
return database.userDao
}

single { provideDatabase(androidApplication()) }
single { provideDao(get()) }
}

val repositoryModule = module {
fun provideUserRepository(api: UserApi, dao: UserDao): UserRepository {
return UserRepository(api, dao)
}

single { provideUserRepository(get(), get()) }
}
//Module.kt

ВModule.kt есть объявление объекта, который нужен приложению. А в представлении мы берем inject, который говорит Koin, что нужен объект view-model. Библиотека в свою очередь старается найти этот объект в модуле, который мы определили ранее. Когда найдёт, назначит ему свойство userViewModel. А если не найдёт, то выдаст исключение. В нашем случае, код скомпилируется правильно, у нас есть экземпляр view-model в модуле с соответствующим параметром. 


Похожий сценарий применится к репозиторию внутри view-model. Экземпляр будет получен из модуля Koin, потому что мы уже создали репозиторий с нужным параметром внутри модуля Koin. 


Заключение


Самая сложная работа инженера ПО — это не разработка, а поддержка. Чем больше кода имеет под собой хорошую архитектуру, тем проще поддерживать и тестировать приложение. Вот почему важно пользоваться паттернами. С ними проще создать стабильно работающие программы, а не бомбу.


Вы можете найти полный код приложения у меня на GitHub по этой ссылке.


582   0  

Comments

    Ничего не найдено.