From e5b3c729dad3d7ced53e028afadc5838050dd345 Mon Sep 17 00:00:00 2001 From: Serchinastico <54cymru@gmail.com> Date: Thu, 31 Jan 2019 14:37:40 +0100 Subject: [PATCH] Add LiveData and ViewModels and implement detail screen with it Migrate edit super hero screen Migrate main screen to view model WIP --- app/build.gradle | 1 + .../superheroes/data/singleValueLiveData.kt | 7 ++ .../ui/view/EditSuperHeroActivityTest.kt | 3 +- .../superheroes/ui/view/MainActivityTest.kt | 5 +- .../ui/view/SuperHeroDetailActivityTest.kt | 3 +- .../ui/view/SuperHeroViewHolderTest.kt | 16 ++-- .../superheroes/SuperHeroesApplication.kt | 6 +- .../superheroes/common/ViewModelFactory.kt | 37 ++++++++ .../repository/LocalSuperHeroDataSource.kt | 21 +++-- .../repository/RemoteSuperHeroDataSource.kt | 40 ++++++--- .../data/repository/SuperHeroRepository.kt | 25 ++++-- .../data/repository/room/SuperHeroDao.kt | 5 +- .../domain/usecase/GetSuperHeroById.kt | 5 +- .../domain/usecase/GetSuperHeroes.kt | 5 +- .../domain/usecase/SaveSuperHero.kt | 2 - .../ui/presenter/EditSuperHeroPresenter.kt | 87 ------------------- .../ui/presenter/SuperHeroDetailPresenter.kt | 62 ------------- .../ui/presenter/SuperHeroesPresenter.kt | 55 ------------ .../superheroes/ui/view/BaseActivity.kt | 23 +++-- .../ui/view/EditSuperHeroActivity.kt | 49 ++++------- .../superheroes/ui/view/MainActivity.kt | 37 ++++---- .../superheroes/ui/view/SingleLiveEvent.kt | 75 ++++++++++++++++ .../ui/view/SuperHeroDetailActivity.kt | 44 +++------- .../ui/view/adapter/SuperHeroViewHolder.kt | 8 +- .../ui/view/adapter/SuperHeroesAdapter.kt | 9 +- .../ui/viewmodel/EditSuperHeroViewModel.kt | 60 +++++++++++++ .../ui/viewmodel/SuperHeroDetailViewModel.kt | 35 ++++++++ .../ui/viewmodel/SuperHeroesViewModel.kt | 37 ++++++++ .../res/layout/edit_super_hero_activity.xml | 34 +++----- app/src/main/res/layout/main_activity.xml | 12 +-- .../res/layout/super_hero_detail_activity.xml | 28 +++--- app/src/main/res/layout/super_hero_row.xml | 5 +- 32 files changed, 427 insertions(+), 414 deletions(-) create mode 100644 app/src/androidTest/java/com/karumi/jetpack/superheroes/data/singleValueLiveData.kt create mode 100644 app/src/main/java/com/karumi/jetpack/superheroes/common/ViewModelFactory.kt delete mode 100644 app/src/main/java/com/karumi/jetpack/superheroes/ui/presenter/EditSuperHeroPresenter.kt delete mode 100644 app/src/main/java/com/karumi/jetpack/superheroes/ui/presenter/SuperHeroDetailPresenter.kt delete mode 100644 app/src/main/java/com/karumi/jetpack/superheroes/ui/presenter/SuperHeroesPresenter.kt create mode 100644 app/src/main/java/com/karumi/jetpack/superheroes/ui/view/SingleLiveEvent.kt create mode 100644 app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/EditSuperHeroViewModel.kt create mode 100644 app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/SuperHeroDetailViewModel.kt create mode 100644 app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/SuperHeroesViewModel.kt diff --git a/app/build.gradle b/app/build.gradle index dc9af91..4d40fc1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -52,6 +52,7 @@ dependencies { implementation "androidx.constraintlayout:constraintlayout:2.0.0-alpha3" implementation "androidx.recyclerview:recyclerview:1.0.0" implementation "androidx.lifecycle:lifecycle-runtime:2.0.0" + implementation "androidx.lifecycle:lifecycle-extensions:2.0.0" implementation "androidx.room:room-runtime:2.1.0-alpha04" kapt "androidx.room:room-compiler:2.1.0-alpha04" diff --git a/app/src/androidTest/java/com/karumi/jetpack/superheroes/data/singleValueLiveData.kt b/app/src/androidTest/java/com/karumi/jetpack/superheroes/data/singleValueLiveData.kt new file mode 100644 index 0000000..340b1b9 --- /dev/null +++ b/app/src/androidTest/java/com/karumi/jetpack/superheroes/data/singleValueLiveData.kt @@ -0,0 +1,7 @@ +package com.karumi.jetpack.superheroes.data + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData + +fun singleValueLiveData(value: T): LiveData = + MutableLiveData().apply { postValue(value) } \ No newline at end of file diff --git a/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/EditSuperHeroActivityTest.kt b/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/EditSuperHeroActivityTest.kt index a0bf0b8..9fc82c1 100644 --- a/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/EditSuperHeroActivityTest.kt +++ b/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/EditSuperHeroActivityTest.kt @@ -2,6 +2,7 @@ package com.karumi.jetpack.superheroes.ui.view import android.os.Bundle import com.karumi.jetpack.superheroes.data.repository.SuperHeroRepository +import com.karumi.jetpack.superheroes.data.singleValueLiveData import com.karumi.jetpack.superheroes.domain.model.SuperHero import com.nhaarman.mockitokotlin2.whenever import org.junit.Test @@ -37,7 +38,7 @@ class EditSuperHeroActivityTest : true, "" ) - whenever(repository.get(ANY_ID)).thenReturn(superHero) + whenever(repository.get(ANY_ID)).thenReturn(singleValueLiveData(superHero)) return superHero } diff --git a/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/MainActivityTest.kt b/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/MainActivityTest.kt index 8e5f195..414c938 100644 --- a/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/MainActivityTest.kt +++ b/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/MainActivityTest.kt @@ -1,6 +1,7 @@ package com.karumi.jetpack.superheroes.ui.view import com.karumi.jetpack.superheroes.data.repository.SuperHeroRepository +import com.karumi.jetpack.superheroes.data.singleValueLiveData import com.karumi.jetpack.superheroes.domain.model.SuperHero import com.nhaarman.mockitokotlin2.whenever import org.junit.Test @@ -83,12 +84,12 @@ class MainActivityTest : AcceptanceTest(MainActivity::class.java) ) } - whenever(repository.getAllSuperHeroes()).thenReturn(superHeroes) + whenever(repository.getAllSuperHeroes()).thenReturn(singleValueLiveData(superHeroes)) return superHeroes } private fun givenThereAreNoSuperHeroes() { - whenever(repository.getAllSuperHeroes()).thenReturn(emptyList()) + whenever(repository.getAllSuperHeroes()).thenReturn(singleValueLiveData(emptyList())) } override val testDependencies = Kodein.Module("Test dependencies", allowSilentOverride = true) { diff --git a/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/SuperHeroDetailActivityTest.kt b/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/SuperHeroDetailActivityTest.kt index f596dc4..e960984 100644 --- a/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/SuperHeroDetailActivityTest.kt +++ b/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/SuperHeroDetailActivityTest.kt @@ -2,6 +2,7 @@ package com.karumi.jetpack.superheroes.ui.view import android.os.Bundle import com.karumi.jetpack.superheroes.data.repository.SuperHeroRepository +import com.karumi.jetpack.superheroes.data.singleValueLiveData import com.karumi.jetpack.superheroes.domain.model.SuperHero import com.nhaarman.mockitokotlin2.whenever import org.junit.Test @@ -40,7 +41,7 @@ class SuperHeroDetailActivityTest : AcceptanceTest( val superHeroName = "SuperHero" val superHeroDescription = "Super Hero Description" val superHero = SuperHero(superHeroId, superHeroName, null, isAvenger, superHeroDescription) - whenever(repository.get(superHeroId)).thenReturn(superHero) + whenever(repository.get(superHeroId)).thenReturn(singleValueLiveData(superHero)) return superHero } diff --git a/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/SuperHeroViewHolderTest.kt b/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/SuperHeroViewHolderTest.kt index 3f70a31..7a13aa7 100644 --- a/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/SuperHeroViewHolderTest.kt +++ b/app/src/androidTest/java/com/karumi/jetpack/superheroes/ui/view/SuperHeroViewHolderTest.kt @@ -6,10 +6,9 @@ import androidx.test.platform.app.InstrumentationRegistry import com.karumi.jetpack.superheroes.R import com.karumi.jetpack.superheroes.databinding.SuperHeroRowBinding import com.karumi.jetpack.superheroes.domain.model.SuperHero -import com.karumi.jetpack.superheroes.ui.presenter.SuperHeroesPresenter import com.karumi.jetpack.superheroes.ui.view.adapter.SuperHeroViewHolder +import com.nhaarman.mockitokotlin2.mock import org.junit.Test -import org.mockito.Mockito.mock class SuperHeroViewHolderTest : ScreenshotTest { @@ -18,7 +17,7 @@ class SuperHeroViewHolderTest : ScreenshotTest { val superHero = givenASuperHero() val holder = givenASuperHeroViewHolder() - holder.render(superHero) + holder.render(superHero, mock()) compareScreenshot(holder, R.dimen.super_hero_row_height) } @@ -28,7 +27,7 @@ class SuperHeroViewHolderTest : ScreenshotTest { val superHero = givenASuperHeroWithALongName() val holder = givenASuperHeroViewHolder() - holder.render(superHero) + holder.render(superHero, mock()) compareScreenshot(holder, R.dimen.super_hero_row_height) } @@ -38,7 +37,7 @@ class SuperHeroViewHolderTest : ScreenshotTest { val superHero = givenASuperHeroWithALongDescription() val holder = givenASuperHeroViewHolder() - holder.render(superHero) + holder.render(superHero, mock()) compareScreenshot(holder, R.dimen.super_hero_row_height) } @@ -48,7 +47,7 @@ class SuperHeroViewHolderTest : ScreenshotTest { val superHero = givenASuperHero(isAvenger = true) val holder = givenASuperHeroViewHolder() - holder.render(superHero) + holder.render(superHero, mock()) compareScreenshot(holder, R.dimen.super_hero_row_height) } @@ -59,10 +58,7 @@ class SuperHeroViewHolderTest : ScreenshotTest { val inflater = LayoutInflater.from(context) val binding: SuperHeroRowBinding = DataBindingUtil.inflate(inflater, R.layout.super_hero_row, null, false) - SuperHeroViewHolder( - binding, - mock(SuperHeroesPresenter::class.java) - ) + SuperHeroViewHolder(binding) } private fun givenASuperHeroWithALongDescription(): SuperHero { diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/SuperHeroesApplication.kt b/app/src/main/java/com/karumi/jetpack/superheroes/SuperHeroesApplication.kt index 04f70d0..12b36ee 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/SuperHeroesApplication.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/SuperHeroesApplication.kt @@ -7,6 +7,7 @@ import com.karumi.jetpack.superheroes.data.repository.LocalSuperHeroDataSource import com.karumi.jetpack.superheroes.data.repository.RemoteSuperHeroDataSource import com.karumi.jetpack.superheroes.data.repository.SuperHeroRepository import com.karumi.jetpack.superheroes.data.repository.room.SuperHeroDao +import org.kodein.di.DKodein import org.kodein.di.Kodein import org.kodein.di.KodeinAware import org.kodein.di.android.androidModule @@ -43,13 +44,14 @@ class SuperHeroesApplication : Application(), KodeinAware { SuperHeroRepository(instance(), instance()) } bind() with singleton { - LocalSuperHeroDataSource(instance()) + LocalSuperHeroDataSource(instance(), instance()) } bind() with provider { - RemoteSuperHeroDataSource() + RemoteSuperHeroDataSource(instance()) } bind() with provider { Executors.newCachedThreadPool() } + bind() with provider { this } } } \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/common/ViewModelFactory.kt b/app/src/main/java/com/karumi/jetpack/superheroes/common/ViewModelFactory.kt new file mode 100644 index 0000000..175acd4 --- /dev/null +++ b/app/src/main/java/com/karumi/jetpack/superheroes/common/ViewModelFactory.kt @@ -0,0 +1,37 @@ +package com.karumi.jetpack.superheroes.common + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import com.karumi.jetpack.superheroes.ui.view.BaseActivity +import org.kodein.di.DKodein +import org.kodein.di.Kodein +import org.kodein.di.KodeinAware +import org.kodein.di.direct +import org.kodein.di.erased.bind +import org.kodein.di.erased.instance +import org.kodein.di.erased.instanceOrNull + +class ViewModelFactory( + private val injector: DKodein, + private val application: Application +) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return injector.instanceOrNull(tag = modelClass.simpleName) as T? + ?: modelClass.getConstructor(Application::class.java).newInstance(application) + } +} + +inline fun T.viewModel(): Lazy + where T : KodeinAware, + T : BaseActivity<*> { + return lazy { ViewModelProviders.of(this, direct.instance()).get(VM::class.java) } +} + +inline fun Kodein.Builder.bindViewModel( + overrides: Boolean? = null +): Kodein.Builder.TypeBinder { + return bind(T::class.java.simpleName, overrides) +} \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/LocalSuperHeroDataSource.kt b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/LocalSuperHeroDataSource.kt index c25c87c..44ca70c 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/LocalSuperHeroDataSource.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/LocalSuperHeroDataSource.kt @@ -1,30 +1,33 @@ package com.karumi.jetpack.superheroes.data.repository +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations import com.karumi.jetpack.superheroes.data.repository.room.SuperHeroDao import com.karumi.jetpack.superheroes.data.repository.room.SuperHeroEntity import com.karumi.jetpack.superheroes.domain.model.SuperHero +import java.util.concurrent.ExecutorService class LocalSuperHeroDataSource( - private val dao: SuperHeroDao + private val dao: SuperHeroDao, + private val executor: ExecutorService ) { - fun getAllSuperHeroes(): List = - dao.getAll() - .map { it.toSuperHero() } + fun getAllSuperHeroes(): LiveData> = + Transformations.map(dao.getAll()) { it.toSuperHeroes() } - fun get(id: String): SuperHero? = - dao.getById(id)?.toSuperHero() + fun get(id: String): LiveData = + Transformations.map(dao.getById(id)) { it?.toSuperHero() } - fun saveAll(all: List) { + fun saveAll(all: List) = executor.execute { dao.deleteAll() dao.insertAll(all.map { it.toEntity() }) } fun save(superHero: SuperHero): SuperHero { - dao.update(superHero.toEntity()) + executor.execute { dao.update(superHero.toEntity()) } return superHero } + private fun List.toSuperHeroes(): List = map { it.toSuperHero() } private fun SuperHeroEntity.toSuperHero(): SuperHero = superHero - private fun SuperHero.toEntity(): SuperHeroEntity = SuperHeroEntity(this) } \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/RemoteSuperHeroDataSource.kt b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/RemoteSuperHeroDataSource.kt index ed01dfa..3e30cdd 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/RemoteSuperHeroDataSource.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/RemoteSuperHeroDataSource.kt @@ -1,31 +1,43 @@ package com.karumi.jetpack.superheroes.data.repository +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import com.karumi.jetpack.superheroes.domain.model.SuperHero +import java.util.concurrent.ExecutorService -class RemoteSuperHeroDataSource { +class RemoteSuperHeroDataSource( + private val executor: ExecutorService +) { companion object { private const val BIT_TIME = 1500L } - private val superHeroes: MutableMap + private val superHeroes: MutableMap = + fakeData().associateBy { it.id }.toMutableMap() - init { - superHeroes = fakeData().associateBy { it.id }.toMutableMap() + fun getAllSuperHeroes(): LiveData> { + val allSuperHeroes = MutableLiveData>() + executor.execute { + waitABit() + allSuperHeroes.postValue(superHeroes.values.toList().sortedBy { it.id }) + } + return allSuperHeroes } - fun getAllSuperHeroes(): List { - waitABit() - return superHeroes.values.toList().sortedBy { it.id } - } - - fun get(id: String): SuperHero? { - waitABit() - return superHeroes[id] + fun get(id: String): LiveData { + val superHero = MutableLiveData() + executor.execute { + waitABit() + superHero.postValue(superHeroes[id]) + } + return superHero } fun save(superHero: SuperHero): SuperHero { - waitABit() - superHeroes[superHero.id] = superHero + executor.execute { + waitABit() + superHeroes[superHero.id] = superHero + } return superHero } diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/SuperHeroRepository.kt b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/SuperHeroRepository.kt index a821e65..1276753 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/SuperHeroRepository.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/SuperHeroRepository.kt @@ -1,20 +1,31 @@ package com.karumi.jetpack.superheroes.data.repository +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import com.karumi.jetpack.superheroes.domain.model.SuperHero class SuperHeroRepository( private val local: LocalSuperHeroDataSource, private val remote: RemoteSuperHeroDataSource ) { - fun getAllSuperHeroes(): List = - local.getAllSuperHeroes().ifEmpty { - remote.getAllSuperHeroes() - .also { local.saveAll(it) } + fun getAllSuperHeroes(): LiveData> = MediatorLiveData>().apply { + val localSource = local.getAllSuperHeroes() + val remoteSource = remote.getAllSuperHeroes() + + addSource(remoteSource) { superHeroes -> + removeSource(remoteSource) + addSource(localSource) { postValue(it) } + local.saveAll(superHeroes) } + } - fun get(id: String): SuperHero? = - local.get(id) - ?: remote.get(id)?.also { local.save(it) } + fun get(id: String): LiveData = MediatorLiveData().apply { + addSource(local.get(id)) { postValue(it) } + addSource(remote.get(id)) { superHero -> + superHero ?: return@addSource + local.save(superHero) + } + } fun save(superHero: SuperHero): SuperHero { local.save(superHero) diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroDao.kt b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroDao.kt index 03dc2c1..b9e87aa 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroDao.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroDao.kt @@ -1,5 +1,6 @@ package com.karumi.jetpack.superheroes.data.repository.room +import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy @@ -9,10 +10,10 @@ import androidx.room.Update @Dao interface SuperHeroDao { @Query("SELECT * FROM superheroes ORDER BY superhero_id ASC") - fun getAll(): List + fun getAll(): LiveData> @Query("SELECT * FROM superheroes WHERE superhero_id = :id") - fun getById(id: String): SuperHeroEntity? + fun getById(id: String): LiveData @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(superHeroes: List) diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/GetSuperHeroById.kt b/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/GetSuperHeroById.kt index b07d510..ce48baf 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/GetSuperHeroById.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/GetSuperHeroById.kt @@ -1,10 +1,9 @@ package com.karumi.jetpack.superheroes.domain.usecase -import androidx.annotation.WorkerThread +import androidx.lifecycle.LiveData import com.karumi.jetpack.superheroes.data.repository.SuperHeroRepository import com.karumi.jetpack.superheroes.domain.model.SuperHero class GetSuperHeroById(private val superHeroesRepository: SuperHeroRepository) { - @WorkerThread - operator fun invoke(id: String): SuperHero? = superHeroesRepository.get(id) + operator fun invoke(id: String): LiveData = superHeroesRepository.get(id) } \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/GetSuperHeroes.kt b/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/GetSuperHeroes.kt index e66069a..fa97ecb 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/GetSuperHeroes.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/GetSuperHeroes.kt @@ -1,10 +1,9 @@ package com.karumi.jetpack.superheroes.domain.usecase -import androidx.annotation.WorkerThread +import androidx.lifecycle.LiveData import com.karumi.jetpack.superheroes.data.repository.SuperHeroRepository import com.karumi.jetpack.superheroes.domain.model.SuperHero class GetSuperHeroes(private val superHeroesRepository: SuperHeroRepository) { - @WorkerThread - operator fun invoke(): List = superHeroesRepository.getAllSuperHeroes() + operator fun invoke(): LiveData> = superHeroesRepository.getAllSuperHeroes() } \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/SaveSuperHero.kt b/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/SaveSuperHero.kt index a4468dc..b03554f 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/SaveSuperHero.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/domain/usecase/SaveSuperHero.kt @@ -1,11 +1,9 @@ package com.karumi.jetpack.superheroes.domain.usecase -import androidx.annotation.WorkerThread import com.karumi.jetpack.superheroes.data.repository.SuperHeroRepository import com.karumi.jetpack.superheroes.domain.model.SuperHero class SaveSuperHero(private val superHeroesRepository: SuperHeroRepository) { - @WorkerThread operator fun invoke(superHero: SuperHero): SuperHero? = superHeroesRepository.save(superHero) } \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/presenter/EditSuperHeroPresenter.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/presenter/EditSuperHeroPresenter.kt deleted file mode 100644 index b61a396..0000000 --- a/app/src/main/java/com/karumi/jetpack/superheroes/ui/presenter/EditSuperHeroPresenter.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.karumi.jetpack.superheroes.ui.presenter - -import androidx.lifecycle.Lifecycle.Event.ON_DESTROY -import androidx.lifecycle.Lifecycle.Event.ON_RESUME -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.OnLifecycleEvent -import com.karumi.jetpack.superheroes.common.weak -import com.karumi.jetpack.superheroes.domain.model.SuperHero -import com.karumi.jetpack.superheroes.domain.usecase.GetSuperHeroById -import com.karumi.jetpack.superheroes.domain.usecase.SaveSuperHero -import java.util.concurrent.ExecutorService - -class EditSuperHeroPresenter( - view: View, - private val getSuperHeroById: GetSuperHeroById, - private val saveSuperHero: SaveSuperHero, - private val executor: ExecutorService -) : EditSuperHeroListener, LifecycleObserver { - - private val view: View? by weak(view) - private lateinit var id: String - private var superHero: SuperHero? = null - - fun preparePresenter(id: String?) { - if (id != null) { - this.id = id - } else { - view?.close() - } - } - - @OnLifecycleEvent(ON_RESUME) - fun onResume() { - view?.showLoading() - refreshSuperHero() - } - - @OnLifecycleEvent(ON_DESTROY) - fun onDestroy() { - executor.shutdownNow() - } - - override fun onSaveSuperHeroSelected( - editableSuperHero: EditSuperHeroPresenter.EditableSuperHero - ) { - saveSuperHero(editableSuperHero) - } - - private fun saveSuperHero( - editableSuperHero: EditSuperHeroPresenter.EditableSuperHero - ) = executor.submit { - view?.showLoading() - val superHero = superHero ?: return@submit - saveSuperHero( - superHero.copy( - name = editableSuperHero.name, - description = editableSuperHero.description, - isAvenger = editableSuperHero.isAvenger - ) - ) - view?.close() - } - - private fun refreshSuperHero() = executor.submit { - val superHero = getSuperHeroById(id) ?: return@submit - view?.hideLoading() - view?.showSuperHero(superHero) - this@EditSuperHeroPresenter.superHero = superHero - } - - data class EditableSuperHero( - var isAvenger: Boolean, - var name: String, - var description: String - ) - - interface View { - fun close() - fun hideLoading() - fun showLoading() - fun showSuperHero(superHero: SuperHero) - } -} - -interface EditSuperHeroListener { - fun onSaveSuperHeroSelected(editableSuperHero: EditSuperHeroPresenter.EditableSuperHero) -} \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/presenter/SuperHeroDetailPresenter.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/presenter/SuperHeroDetailPresenter.kt deleted file mode 100644 index 8df0af2..0000000 --- a/app/src/main/java/com/karumi/jetpack/superheroes/ui/presenter/SuperHeroDetailPresenter.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.karumi.jetpack.superheroes.ui.presenter - -import androidx.lifecycle.Lifecycle.Event.ON_DESTROY -import androidx.lifecycle.Lifecycle.Event.ON_RESUME -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.OnLifecycleEvent -import com.karumi.jetpack.superheroes.common.weak -import com.karumi.jetpack.superheroes.domain.model.SuperHero -import com.karumi.jetpack.superheroes.domain.usecase.GetSuperHeroById -import java.util.concurrent.ExecutorService - -class SuperHeroDetailPresenter( - view: View, - private val getSuperHeroById: GetSuperHeroById, - private val executor: ExecutorService -) : SuperHeroDetailListener, LifecycleObserver { - - private val view: View? by weak(view) - - private lateinit var id: String - - fun preparePresenter(id: String?) { - if (id != null) { - this.id = id - } else { - view?.close() - } - } - - @OnLifecycleEvent(ON_RESUME) - fun onResume() { - view?.showLoading() - refreshSuperHero() - } - - @OnLifecycleEvent(ON_DESTROY) - fun onDestroy() { - executor.shutdownNow() - } - - override fun onEditSelected() { - view?.openEditSuperHero(id) - } - - private fun refreshSuperHero() = executor.submit { - val superHero = getSuperHeroById(id) ?: return@submit - view?.hideLoading() - view?.showSuperHero(superHero) - } - - interface View { - fun close() - fun hideLoading() - fun showLoading() - fun showSuperHero(superHero: SuperHero) - fun openEditSuperHero(superHeroId: String) - } -} - -interface SuperHeroDetailListener { - fun onEditSelected() -} \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/presenter/SuperHeroesPresenter.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/presenter/SuperHeroesPresenter.kt deleted file mode 100644 index 534ad7a..0000000 --- a/app/src/main/java/com/karumi/jetpack/superheroes/ui/presenter/SuperHeroesPresenter.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.karumi.jetpack.superheroes.ui.presenter - -import androidx.lifecycle.Lifecycle.Event.ON_DESTROY -import androidx.lifecycle.Lifecycle.Event.ON_RESUME -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.OnLifecycleEvent -import com.karumi.jetpack.superheroes.common.weak -import com.karumi.jetpack.superheroes.domain.model.SuperHero -import com.karumi.jetpack.superheroes.domain.usecase.GetSuperHeroes -import java.util.concurrent.ExecutorService - -class SuperHeroesPresenter( - view: View, - private val getSuperHeroes: GetSuperHeroes, - private val executor: ExecutorService -) : SuperHeroesListener, LifecycleObserver { - - private val view: View? by weak(view) - - @OnLifecycleEvent(ON_RESUME) - fun onResume() { - view?.showLoading() - refreshSuperHeroes() - } - - @OnLifecycleEvent(ON_DESTROY) - fun onDestroy() { - executor.shutdownNow() - } - - private fun refreshSuperHeroes() = executor.submit { - val result = getSuperHeroes() - view?.hideLoading() - when { - result.isEmpty() -> view?.showEmptyCase() - else -> view?.showSuperHeroes(result) - } - } - - override fun onSuperHeroClicked(superHero: SuperHero) { - view?.openDetail(superHero.id) - } - - interface View { - fun hideLoading() - fun showLoading() - fun showEmptyCase() - fun showSuperHeroes(superHeroes: List) - fun openDetail(id: String) - } -} - -interface SuperHeroesListener { - fun onSuperHeroClicked(superHero: SuperHero) -} \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/BaseActivity.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/BaseActivity.kt index a692ec3..160fecb 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/BaseActivity.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/BaseActivity.kt @@ -1,43 +1,48 @@ package com.karumi.jetpack.superheroes.ui.view -import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.databinding.DataBindingUtil import androidx.databinding.ViewDataBinding -import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModelProvider +import com.karumi.jetpack.superheroes.common.ViewModelFactory import org.kodein.di.Kodein import org.kodein.di.KodeinAware import org.kodein.di.android.closestKodein +import org.kodein.di.erased.bind +import org.kodein.di.erased.instance +import org.kodein.di.erased.singleton abstract class BaseActivity : AppCompatActivity(), KodeinAware { private val appKodein by closestKodein() override val kodein: Kodein = Kodein.lazy { extend(appKodein) + includeViewModelFactory() import(activityModules) } - abstract val presenter: LifecycleObserver + abstract val layoutId: Int abstract val toolbarView: Toolbar abstract val activityModules: Kodein.Module + abstract val viewModel: AndroidViewModel protected lateinit var binding: T override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - lifecycle.addObserver(presenter) binding = DataBindingUtil.setContentView(this, layoutId) + binding.setLifecycleOwner(this) configureBinding(binding) setSupportActionBar(toolbarView) - prepare(intent) } - override fun onDestroy() { - super.onDestroy() - lifecycle.removeObserver(presenter) + private fun Kodein.MainBuilder.includeViewModelFactory() { + bind() with singleton { + ViewModelFactory(instance(), instance()) + } } abstract fun configureBinding(binding: T) - open fun prepare(intent: Intent?) {} } \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/EditSuperHeroActivity.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/EditSuperHeroActivity.kt index 96c9726..2a55183 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/EditSuperHeroActivity.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/EditSuperHeroActivity.kt @@ -2,22 +2,23 @@ package com.karumi.jetpack.superheroes.ui.view import android.app.Activity import android.content.Intent +import android.os.Bundle import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.Observer import com.karumi.jetpack.superheroes.R +import com.karumi.jetpack.superheroes.common.bindViewModel import com.karumi.jetpack.superheroes.common.module +import com.karumi.jetpack.superheroes.common.viewModel import com.karumi.jetpack.superheroes.databinding.EditSuperHeroActivityBinding -import com.karumi.jetpack.superheroes.domain.model.SuperHero import com.karumi.jetpack.superheroes.domain.usecase.GetSuperHeroById import com.karumi.jetpack.superheroes.domain.usecase.SaveSuperHero -import com.karumi.jetpack.superheroes.ui.presenter.EditSuperHeroPresenter +import com.karumi.jetpack.superheroes.ui.viewmodel.EditSuperHeroViewModel import kotlinx.android.synthetic.main.edit_super_hero_activity.* import org.kodein.di.erased.bind import org.kodein.di.erased.instance import org.kodein.di.erased.provider -class EditSuperHeroActivity : - BaseActivity(), - EditSuperHeroPresenter.View { +class EditSuperHeroActivity : BaseActivity() { companion object { private const val SUPER_HERO_ID_KEY = "super_hero_id_key" @@ -29,48 +30,28 @@ class EditSuperHeroActivity : } } - override val presenter: EditSuperHeroPresenter by instance() + override val viewModel: EditSuperHeroViewModel by viewModel() override val layoutId = R.layout.edit_super_hero_activity override val toolbarView: Toolbar get() = toolbar private val superHeroId: String by lazy { intent?.extras?.getString(SUPER_HERO_ID_KEY) ?: "" } - override fun configureBinding(binding: EditSuperHeroActivityBinding) { - binding.listener = presenter - binding.isLoading = false - } - - override fun prepare(intent: Intent?) { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) title = superHeroId - presenter.preparePresenter(superHeroId) } - override fun close() = runOnUiThread { - finish() - } - - override fun showLoading() = runOnUiThread { - binding.isLoading = true - } - - override fun hideLoading() = runOnUiThread { - binding.isLoading = false - } - - override fun showSuperHero(superHero: SuperHero) = runOnUiThread { - binding.listener = presenter - binding.superHero = superHero - binding.editableSuperHero = superHero.toEditable() + override fun configureBinding(binding: EditSuperHeroActivityBinding) { + binding.viewModel = viewModel + viewModel.isClosing.observe(this, Observer { finish() }) + viewModel.prepare(superHeroId) } override val activityModules = module { - bind() with provider { - EditSuperHeroPresenter(this@EditSuperHeroActivity, instance(), instance(), instance()) + bindViewModel() with provider { + EditSuperHeroViewModel(instance(), instance(), instance()) } bind() with provider { GetSuperHeroById(instance()) } bind() with provider { SaveSuperHero(instance()) } } - - private fun SuperHero.toEditable() = - EditSuperHeroPresenter.EditableSuperHero(isAvenger, name, description) } \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/MainActivity.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/MainActivity.kt index 4521ade..07986e2 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/MainActivity.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/MainActivity.kt @@ -2,22 +2,25 @@ package com.karumi.jetpack.superheroes.ui.view import android.os.Bundle import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import com.karumi.jetpack.superheroes.R +import com.karumi.jetpack.superheroes.common.bindViewModel import com.karumi.jetpack.superheroes.common.module +import com.karumi.jetpack.superheroes.common.viewModel import com.karumi.jetpack.superheroes.databinding.MainActivityBinding import com.karumi.jetpack.superheroes.domain.model.SuperHero import com.karumi.jetpack.superheroes.domain.usecase.GetSuperHeroes -import com.karumi.jetpack.superheroes.ui.presenter.SuperHeroesPresenter import com.karumi.jetpack.superheroes.ui.view.adapter.SuperHeroesAdapter +import com.karumi.jetpack.superheroes.ui.viewmodel.SuperHeroesViewModel import kotlinx.android.synthetic.main.main_activity.* import org.kodein.di.erased.bind import org.kodein.di.erased.instance import org.kodein.di.erased.provider -class MainActivity : BaseActivity(), SuperHeroesPresenter.View { +class MainActivity : BaseActivity() { - override val presenter: SuperHeroesPresenter by instance() + override val viewModel: SuperHeroesViewModel by viewModel() private lateinit var adapter: SuperHeroesAdapter override val layoutId: Int = R.layout.main_activity override val toolbarView: Toolbar @@ -27,15 +30,17 @@ class MainActivity : BaseActivity(), SuperHeroesPresenter.V super.onCreate(savedInstanceState) initializeAdapter() initializeRecyclerView() + viewModel.prepare() + viewModel.idOfSuperHeroToOpen.observe(this, Observer { openDetail(it) }) + viewModel.superHeroes.observe(this, Observer> { showSuperHeroes(it) }) } override fun configureBinding(binding: MainActivityBinding) { - binding.isLoading = false - binding.isShowingEmptyCase = false + binding.viewModel = viewModel } private fun initializeAdapter() { - adapter = SuperHeroesAdapter(presenter) + adapter = SuperHeroesAdapter(viewModel) } private fun initializeRecyclerView() { @@ -44,25 +49,13 @@ class MainActivity : BaseActivity(), SuperHeroesPresenter.V recycler_view.adapter = adapter } - override fun showLoading() = runOnUiThread { - binding.isLoading = true - } - - override fun hideLoading() = runOnUiThread { - binding.isLoading = false - } - - override fun showEmptyCase() = runOnUiThread { - binding.isShowingEmptyCase = true - } - - override fun showSuperHeroes(superHeroes: List) = runOnUiThread { + private fun showSuperHeroes(superHeroes: List) { adapter.clear() adapter.addAll(superHeroes) adapter.notifyDataSetChanged() } - override fun openDetail(id: String) = runOnUiThread { + private fun openDetail(id: String) { SuperHeroDetailActivity.open( activity = this, superHeroId = id @@ -70,8 +63,8 @@ class MainActivity : BaseActivity(), SuperHeroesPresenter.V } override val activityModules = module { - bind() with provider { - SuperHeroesPresenter(this@MainActivity, instance(), instance()) + bindViewModel() with provider { + SuperHeroesViewModel(instance(), instance()) } bind() with provider { GetSuperHeroes(instance()) } } diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/SingleLiveEvent.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/SingleLiveEvent.kt new file mode 100644 index 0000000..f7d219c --- /dev/null +++ b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/SingleLiveEvent.kt @@ -0,0 +1,75 @@ +package com.karumi.jetpack.superheroes.ui.view + +/** + * See [https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java] + */ + +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.util.Log +import androidx.annotation.MainThread +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import java.util.concurrent.atomic.AtomicBoolean + + +/** + * A lifecycle-aware observable that sends only new updates after subscription, used for events like + * navigation and Snackbar messages. + *

+ * This avoids a common problem with events: on configuration change (like rotation) an update + * can be emitted if the observer is active. This LiveData only calls the observable if there's an + * explicit call to setValue() or call(). + *

+ * Note that only one observer is going to be notified of changes. + */ +class SingleLiveEvent : MutableLiveData() { + + private val mPending = AtomicBoolean(false) + + @MainThread + override fun observe(owner: LifecycleOwner, observer: Observer) { + if (hasActiveObservers()) { + Log.w(TAG, "Multiple observers registered but only one will be notified of changes.") + } + + super.observe(owner, Observer { t -> + if (mPending.compareAndSet(true, false)) { + observer.onChanged(t) + } + }) + } + + @MainThread + override fun setValue(t: T?) { + mPending.set(true) + super.setValue(t) + } + + /** + * Used for cases where T is Void, to make calls cleaner. + */ + @MainThread + fun call() { + value = null + } + + companion object { + private const val TAG = "SingleLiveEvent" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/SuperHeroDetailActivity.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/SuperHeroDetailActivity.kt index ad3e141..d42d041 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/SuperHeroDetailActivity.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/SuperHeroDetailActivity.kt @@ -3,20 +3,21 @@ package com.karumi.jetpack.superheroes.ui.view import android.app.Activity import android.content.Intent import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.Observer import com.karumi.jetpack.superheroes.R +import com.karumi.jetpack.superheroes.common.bindViewModel import com.karumi.jetpack.superheroes.common.module +import com.karumi.jetpack.superheroes.common.viewModel import com.karumi.jetpack.superheroes.databinding.SuperHeroDetailActivityBinding -import com.karumi.jetpack.superheroes.domain.model.SuperHero import com.karumi.jetpack.superheroes.domain.usecase.GetSuperHeroById -import com.karumi.jetpack.superheroes.ui.presenter.SuperHeroDetailPresenter +import com.karumi.jetpack.superheroes.ui.viewmodel.SuperHeroDetailViewModel import kotlinx.android.synthetic.main.super_hero_detail_activity.* import org.kodein.di.erased.bind import org.kodein.di.erased.instance import org.kodein.di.erased.provider class SuperHeroDetailActivity : - BaseActivity(), - SuperHeroDetailPresenter.View { + BaseActivity() { companion object { private const val SUPER_HERO_ID_KEY = "super_hero_id_key" @@ -27,44 +28,27 @@ class SuperHeroDetailActivity : } } - override val presenter: SuperHeroDetailPresenter by instance() + override val viewModel: SuperHeroDetailViewModel by viewModel() override val layoutId: Int = R.layout.super_hero_detail_activity override val toolbarView: Toolbar get() = toolbar private val superHeroId: String by lazy { intent?.extras?.getString(SUPER_HERO_ID_KEY) ?: "" } override fun configureBinding(binding: SuperHeroDetailActivityBinding) { - binding.listener = presenter - binding.isLoading = false + binding.viewModel = viewModel + viewModel.idOfSuperHeroToEdit.observe(this, Observer { openEditSuperHero(it) }) + viewModel.superHero.observe(this, Observer { title = it?.name }) + viewModel.prepare(superHeroId) } - override fun prepare(intent: Intent?) { - title = superHeroId - presenter.preparePresenter(superHeroId) - } - - override fun close() = runOnUiThread { finish() } - - override fun showLoading() = runOnUiThread { - binding.isLoading = true - } - - override fun hideLoading() = runOnUiThread { - binding.isLoading = false - } - - override fun showSuperHero(superHero: SuperHero) = runOnUiThread { - title = superHero.name - binding.superHero = superHero - } - - override fun openEditSuperHero(superHeroId: String) = runOnUiThread { + private fun openEditSuperHero(superHeroId: String?) { + superHeroId ?: return EditSuperHeroActivity.open(this, superHeroId) } override val activityModules = module { - bind() with provider { - SuperHeroDetailPresenter(this@SuperHeroDetailActivity, instance(), instance()) + bindViewModel() with provider { + SuperHeroDetailViewModel(instance(), instance()) } bind() with provider { GetSuperHeroById(instance()) } } diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/adapter/SuperHeroViewHolder.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/adapter/SuperHeroViewHolder.kt index 13e329a..f769f87 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/adapter/SuperHeroViewHolder.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/adapter/SuperHeroViewHolder.kt @@ -3,14 +3,12 @@ package com.karumi.jetpack.superheroes.ui.view.adapter import androidx.recyclerview.widget.RecyclerView import com.karumi.jetpack.superheroes.databinding.SuperHeroRowBinding import com.karumi.jetpack.superheroes.domain.model.SuperHero -import com.karumi.jetpack.superheroes.ui.presenter.SuperHeroesListener +import com.karumi.jetpack.superheroes.ui.viewmodel.SuperHeroesListener class SuperHeroViewHolder( - private val binding: SuperHeroRowBinding, - private val listener: SuperHeroesListener + private val binding: SuperHeroRowBinding ) : RecyclerView.ViewHolder(binding.root) { - - fun render(superHero: SuperHero) { + fun render(superHero: SuperHero, listener: SuperHeroesListener) { binding.superHero = superHero binding.listener = listener binding.executePendingBindings() diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/adapter/SuperHeroesAdapter.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/adapter/SuperHeroesAdapter.kt index fabd7f7..af8ad63 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/adapter/SuperHeroesAdapter.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/ui/view/adapter/SuperHeroesAdapter.kt @@ -7,10 +7,10 @@ import androidx.recyclerview.widget.RecyclerView import com.karumi.jetpack.superheroes.R import com.karumi.jetpack.superheroes.databinding.SuperHeroRowBinding import com.karumi.jetpack.superheroes.domain.model.SuperHero -import com.karumi.jetpack.superheroes.ui.presenter.SuperHeroesListener +import com.karumi.jetpack.superheroes.ui.viewmodel.SuperHeroesViewModel internal class SuperHeroesAdapter( - private val listener: SuperHeroesListener + private val viewModel: SuperHeroesViewModel ) : RecyclerView.Adapter() { private val superHeroes: MutableList = ArrayList() @@ -26,12 +26,11 @@ internal class SuperHeroesAdapter( false ) - return SuperHeroViewHolder(binding, listener) + return SuperHeroViewHolder(binding) } override fun onBindViewHolder(holder: SuperHeroViewHolder, position: Int) { - val superHero = superHeroes[position] - holder.render(superHero) + holder.render(superHeroes[position], viewModel) } override fun getItemCount(): Int { diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/EditSuperHeroViewModel.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/EditSuperHeroViewModel.kt new file mode 100644 index 0000000..829bdf4 --- /dev/null +++ b/app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/EditSuperHeroViewModel.kt @@ -0,0 +1,60 @@ +package com.karumi.jetpack.superheroes.ui.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import com.karumi.jetpack.superheroes.domain.model.SuperHero +import com.karumi.jetpack.superheroes.domain.usecase.GetSuperHeroById +import com.karumi.jetpack.superheroes.domain.usecase.SaveSuperHero + +class EditSuperHeroViewModel( + application: Application, + private val getSuperHeroById: GetSuperHeroById, + private val saveSuperHero: SaveSuperHero +) : AndroidViewModel(application) { + + val isLoading = MutableLiveData() + val superHero = MediatorLiveData() + val editableSuperHero = MutableLiveData() + val isClosing = MutableLiveData() + + fun prepare(id: String) { + loadSuperHero(id) + } + + private fun loadSuperHero(id: String) { + isLoading.value = true + val superHeroSource = getSuperHeroById(id) + superHero.addSource(superHeroSource) { + superHero.removeSource(superHeroSource) + + superHero.postValue(it) + editableSuperHero.postValue(it?.toEditable()) + isLoading.postValue(false) + } + } + + fun onSaveSuperHeroSelected() { + val updatedSuperHero = getUpdatedSuperHero() ?: return + + isLoading.value = true + saveSuperHero(updatedSuperHero) + isClosing.value = true + } + + private fun getUpdatedSuperHero(): SuperHero? { + val superHero = superHero.value ?: return null + return editableSuperHero.value?.applyTo(superHero) + } + + data class EditableSuperHero( + var isAvenger: Boolean, + var name: String, + var description: String + ) + + private fun SuperHero.toEditable() = EditableSuperHero(isAvenger, name, description) + private fun EditableSuperHero.applyTo(superHero: SuperHero) = + superHero.copy(isAvenger = isAvenger, name = name, description = description) +} \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/SuperHeroDetailViewModel.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/SuperHeroDetailViewModel.kt new file mode 100644 index 0000000..cf0369d --- /dev/null +++ b/app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/SuperHeroDetailViewModel.kt @@ -0,0 +1,35 @@ +package com.karumi.jetpack.superheroes.ui.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import com.karumi.jetpack.superheroes.domain.model.SuperHero +import com.karumi.jetpack.superheroes.domain.usecase.GetSuperHeroById +import com.karumi.jetpack.superheroes.ui.view.SingleLiveEvent + +class SuperHeroDetailViewModel( + application: Application, + private val getSuperHeroById: GetSuperHeroById +) : AndroidViewModel(application) { + + val isLoading = MutableLiveData() + val superHero = MediatorLiveData() + val idOfSuperHeroToEdit = SingleLiveEvent() + + fun prepare(id: String) { + loadSuperHero(id) + } + + fun onEditSelected() { + idOfSuperHeroToEdit.value = superHero.value?.id + } + + private fun loadSuperHero(id: String) { + isLoading.value = true + superHero.addSource(getSuperHeroById(id)) { + superHero.postValue(it) + isLoading.postValue(false) + } + } +} diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/SuperHeroesViewModel.kt b/app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/SuperHeroesViewModel.kt new file mode 100644 index 0000000..d467cfb --- /dev/null +++ b/app/src/main/java/com/karumi/jetpack/superheroes/ui/viewmodel/SuperHeroesViewModel.kt @@ -0,0 +1,37 @@ +package com.karumi.jetpack.superheroes.ui.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import com.karumi.jetpack.superheroes.domain.model.SuperHero +import com.karumi.jetpack.superheroes.domain.usecase.GetSuperHeroes +import com.karumi.jetpack.superheroes.ui.view.SingleLiveEvent + +class SuperHeroesViewModel( + application: Application, + private val getSuperHeroes: GetSuperHeroes +) : SuperHeroesListener, AndroidViewModel(application) { + + val isLoading = MutableLiveData() + val isShowingEmptyCase = MutableLiveData() + val superHeroes = MediatorLiveData>() + val idOfSuperHeroToOpen = SingleLiveEvent() + + fun prepare() { + isLoading.value = true + superHeroes.addSource(getSuperHeroes()) { + isShowingEmptyCase.postValue(it.isEmpty()) + superHeroes.postValue(it) + isLoading.postValue(false) + } + } + + override fun onSuperHeroClicked(id: String) { + idOfSuperHeroToOpen.value = id + } +} + +interface SuperHeroesListener { + fun onSuperHeroClicked(id: String) +} \ No newline at end of file diff --git a/app/src/main/res/layout/edit_super_hero_activity.xml b/app/src/main/res/layout/edit_super_hero_activity.xml index 8b033f4..7a3aa82 100644 --- a/app/src/main/res/layout/edit_super_hero_activity.xml +++ b/app/src/main/res/layout/edit_super_hero_activity.xml @@ -8,16 +8,8 @@ - - - - + name="viewModel" + type="com.karumi.jetpack.superheroes.ui.viewmodel.EditSuperHeroViewModel" /> @@ -60,7 +52,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:background="@drawable/super_hero_gradient" - android:visibility="@{ superHero == null ? View.GONE : View.VISIBLE }" + android:visibility="@{ viewModel.superHero == null ? View.GONE : View.VISIBLE }" app:layout_constraintBottom_toBottomOf="@id/iv_super_hero_photo" app:layout_constraintEnd_toEndOf="@id/iv_super_hero_photo" app:layout_constraintStart_toStartOf="@id/iv_super_hero_photo" @@ -73,8 +65,8 @@ android:layout_height="wrap_content" android:layout_margin="8dp" android:buttonTint="@color/white" - android:checked="@={ editableSuperHero.avenger, default = false }" - android:enabled="@{ superHero != null }" + android:checked="@={ viewModel.editableSuperHero.avenger, default = false }" + android:enabled="@{ viewModel.superHero != null }" android:text="@string/is_super_hero_an_avenger_checkbox" android:textColor="@color/white" app:layout_constraintTop_toBottomOf="@id/iv_super_hero_photo" @@ -93,11 +85,11 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:backgroundTint="@color/white" - android:enabled="@{ superHero != null }" + android:enabled="@{ viewModel.superHero != null }" android:hint="@string/edit_super_hero_name_hint" android:importantForAutofill="no" android:inputType="text" - android:text="@={ editableSuperHero.name }" + android:text="@={ viewModel.editableSuperHero.name }" android:textColor="@color/white" tools:ignore="UnusedAttribute" /> @@ -116,12 +108,12 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:backgroundTint="@color/white" - android:enabled="@{ superHero != null }" + android:enabled="@{ viewModel.superHero != null }" android:hint="@string/edit_super_hero_description_hint" android:importantForAutofill="no" android:inputType="textMultiLine" android:scrollbars="vertical" - android:text="@={ editableSuperHero.description }" + android:text="@={ viewModel.editableSuperHero.description }" android:textColor="@color/white" tools:ignore="UnusedAttribute" /> @@ -143,7 +135,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" - android:visibility="@{ isLoading ? View.VISIBLE : View.GONE }" + android:visibility="@{ viewModel.isLoading ? View.VISIBLE : View.GONE }" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -154,8 +146,8 @@ android:layout_width="0dp" android:layout_height="56dp" android:background="@color/royal_blue" - android:enabled="@{ superHero != null }" - android:onClick="@{ () -> listener.onSaveSuperHeroSelected(editableSuperHero) }" + android:enabled="@{ viewModel.superHero != null }" + android:onClick="@{ () -> viewModel.onSaveSuperHeroSelected() }" android:text="@string/save_edited_super_hero_button" android:textColor="@color/white" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index c75c59c..298539f 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -8,12 +8,8 @@ - - + name="viewModel" + type="com.karumi.jetpack.superheroes.ui.viewmodel.SuperHeroesViewModel" /> - - - - + name="viewModel" + type="com.karumi.jetpack.superheroes.ui.viewmodel.SuperHeroDetailViewModel" /> @@ -43,7 +35,7 @@ android:id="@+id/iv_super_hero_photo" android:layout_width="match_parent" android:layout_height="@dimen/super_hero_detail_header_height" - app:imageUrl="@{ superHero.photo }" + app:imageUrl="@{ viewModel.superHero.photo }" app:layout_constraintTop_toBottomOf="@id/toolbar" tools:background="@color/color_primary_dark" tools:ignore="ContentDescription" /> @@ -53,7 +45,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:background="@drawable/super_hero_gradient" - android:visibility="@{ superHero == null ? View.GONE : View.VISIBLE }" + android:visibility="@{ viewModel.superHero == null ? View.GONE : View.VISIBLE }" app:layout_constraintBottom_toBottomOf="@id/iv_super_hero_photo" app:layout_constraintEnd_toEndOf="@id/iv_super_hero_photo" app:layout_constraintStart_toStartOf="@id/iv_super_hero_photo" @@ -66,7 +58,7 @@ android:layout_height="wrap_content" android:layout_margin="@dimen/default_margin" android:src="@mipmap/ic_avengers" - android:visibility="@{ superHero.avenger ? View.VISIBLE : View.GONE }" + android:visibility="@{ viewModel.superHero.avenger ? View.VISIBLE : View.GONE }" app:layout_constraintBottom_toBottomOf="@id/iv_super_hero_photo" app:layout_constraintStart_toStartOf="@id/iv_super_hero_photo" tools:ignore="ContentDescription" @@ -84,9 +76,9 @@ android:elevation="2dp" android:focusable="true" android:foreground="?attr/selectableItemBackgroundBorderless" - android:onClick="@{ () -> listener.onEditSelected() }" + android:onClick="@{ () -> viewModel.onEditSelected() }" android:src="@drawable/ic_edit" - android:visibility="@{ superHero == null ? View.GONE : View.VISIBLE }" + android:visibility="@{ viewModel.superHero == null ? View.GONE : View.VISIBLE }" app:layout_constraintEnd_toEndOf="@id/iv_super_hero_photo" app:layout_constraintTop_toTopOf="@id/iv_super_hero_photo" tools:ignore="UnusedAttribute" @@ -99,7 +91,7 @@ android:layout_marginStart="@dimen/default_margin" android:layout_marginLeft="@dimen/default_margin" android:layout_marginTop="@dimen/default_margin" - android:text="@{ superHero.name }" + android:text="@{ viewModel.superHero.name }" android:textColor="@android:color/white" android:textSize="@dimen/title_text_size" app:layout_constraintEnd_toEndOf="parent" @@ -112,7 +104,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="@dimen/default_margin" - android:text="@{ superHero.description }" + android:text="@{ viewModel.superHero.description }" android:textColor="@android:color/white" android:textSize="@dimen/body_text_size" app:layout_constraintEnd_toEndOf="parent" @@ -126,7 +118,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" - android:visibility="@{ isLoading ? View.VISIBLE : View.GONE }" + android:visibility="@{ viewModel.isLoading ? View.VISIBLE : View.GONE }" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/super_hero_row.xml b/app/src/main/res/layout/super_hero_row.xml index 3bec5ff..f2f266a 100644 --- a/app/src/main/res/layout/super_hero_row.xml +++ b/app/src/main/res/layout/super_hero_row.xml @@ -13,13 +13,14 @@ + type="com.karumi.jetpack.superheroes.ui.viewmodel.SuperHeroesListener" /> + + android:onClick="@{ () -> listener.onSuperHeroClicked(superHero.id) }">