Skip to content

Commit

Permalink
Add LiveData and ViewModels and implement detail screen with it
Browse files Browse the repository at this point in the history
Migrate edit super hero screen

Migrate main screen to view model

WIP
  • Loading branch information
Serchinastico committed Feb 5, 2019
1 parent 7caf147 commit 6ccc843
Show file tree
Hide file tree
Showing 31 changed files with 368 additions and 423 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.karumi.jetpack.superheroes.data

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData

fun <T> singleValueLiveData(value: T): LiveData<T> =
MutableLiveData<T>().apply { postValue(value) }
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -37,7 +38,7 @@ class EditSuperHeroActivityTest :
true,
""
)
whenever(repository.get(ANY_ID)).thenReturn(superHero)
whenever(repository.get(ANY_ID)).thenReturn(singleValueLiveData(superHero))
return superHero
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -83,12 +84,12 @@ class MainActivityTest : AcceptanceTest<MainActivity>(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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -40,7 +41,7 @@ class SuperHeroDetailActivityTest : AcceptanceTest<SuperHeroDetailActivity>(
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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@ package com.karumi.jetpack.superheroes.ui.view

import android.view.LayoutInflater
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.MediatorLiveData
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.karumi.jetpack.superheroes.ui.viewmodel.SuperHeroesViewModel
import com.nhaarman.mockitokotlin2.mock
import org.junit.Test
import org.mockito.Mockito.mock

class SuperHeroViewHolderTest : ScreenshotTest {

@Test
fun showsAnySuperHero() {
val superHero = givenASuperHero()
val holder = givenASuperHeroViewHolder()
val viewModel = givenASuperHeroViewModel(superHero)

holder.render(superHero)
holder.render(viewModel, 0)

compareScreenshot(holder, R.dimen.super_hero_row_height)
}
Expand All @@ -27,8 +29,9 @@ class SuperHeroViewHolderTest : ScreenshotTest {
fun showsSuperHeroesWithLongNames() {
val superHero = givenASuperHeroWithALongName()
val holder = givenASuperHeroViewHolder()
val viewModel = givenASuperHeroViewModel(superHero)

holder.render(superHero)
holder.render(viewModel, 0)

compareScreenshot(holder, R.dimen.super_hero_row_height)
}
Expand All @@ -37,8 +40,9 @@ class SuperHeroViewHolderTest : ScreenshotTest {
fun showsSuperHeroesWithLongDescriptions() {
val superHero = givenASuperHeroWithALongDescription()
val holder = givenASuperHeroViewHolder()
val viewModel = givenASuperHeroViewModel(superHero)

holder.render(superHero)
holder.render(viewModel, 0)

compareScreenshot(holder, R.dimen.super_hero_row_height)
}
Expand All @@ -47,22 +51,29 @@ class SuperHeroViewHolderTest : ScreenshotTest {
fun showsAvengersBadge() {
val superHero = givenASuperHero(isAvenger = true)
val holder = givenASuperHeroViewHolder()
val viewModel = givenASuperHeroViewModel(superHero)

holder.render(superHero)
holder.render(viewModel, 0)

compareScreenshot(holder, R.dimen.super_hero_row_height)
}

private fun givenASuperHeroViewModel(superHero: SuperHero): SuperHeroesViewModel {
val superHeroes: MediatorLiveData<List<SuperHero>> =
mock { on(it.value).thenReturn(listOf(superHero)) }

return mock { vm ->
on(vm.superHeroes).thenReturn(superHeroes)
}
}

private fun givenASuperHeroViewHolder(): SuperHeroViewHolder =
runOnUi {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val inflater = LayoutInflater.from(context)
val binding: SuperHeroRowBinding =
DataBindingUtil.inflate(inflater, R.layout.super_hero_row, null, false)
SuperHeroViewHolder(
binding,
mock<SuperHeroesPresenter>(SuperHeroesPresenter::class.java)
)
SuperHeroViewHolder(binding)
}

private fun givenASuperHeroWithALongDescription(): SuperHero {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -43,13 +44,14 @@ class SuperHeroesApplication : Application(), KodeinAware {
SuperHeroRepository(instance(), instance())
}
bind<LocalSuperHeroDataSource>() with singleton {
LocalSuperHeroDataSource(instance())
LocalSuperHeroDataSource(instance(), instance())
}
bind<RemoteSuperHeroDataSource>() with provider {
RemoteSuperHeroDataSource()
RemoteSuperHeroDataSource(instance())
}
bind<ExecutorService>() with provider {
Executors.newCachedThreadPool()
}
bind<DKodein>() with provider { this }
}
}
Original file line number Diff line number Diff line change
@@ -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 <T : ViewModel> create(modelClass: Class<T>): T {
return injector.instanceOrNull<ViewModel>(tag = modelClass.simpleName) as T?
?: modelClass.getConstructor(Application::class.java).newInstance(application)
}
}

inline fun <reified VM : ViewModel, T> T.viewModel(): Lazy<VM>
where T : KodeinAware,
T : BaseActivity<*> {
return lazy { ViewModelProviders.of(this, direct.instance()).get(VM::class.java) }
}

inline fun <reified T : ViewModel> Kodein.Builder.bindViewModel(
overrides: Boolean? = null
): Kodein.Builder.TypeBinder<T> {
return bind<T>(T::class.java.simpleName, overrides)
}
Original file line number Diff line number Diff line change
@@ -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<SuperHero> =
dao.getAll()
.map { it.toSuperHero() }
fun getAllSuperHeroes(): LiveData<List<SuperHero>> =
Transformations.map(dao.getAll()) { it.toSuperHeroes() }

fun get(id: String): SuperHero? =
dao.getById(id)?.toSuperHero()
fun get(id: String): LiveData<SuperHero?> =
Transformations.map(dao.getById(id)) { it?.toSuperHero() }

fun saveAll(all: List<SuperHero>) {
fun saveAll(all: List<SuperHero>) = 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<SuperHeroEntity>.toSuperHeroes(): List<SuperHero> = map { it.toSuperHero() }
private fun SuperHeroEntity.toSuperHero(): SuperHero = superHero

private fun SuperHero.toEntity(): SuperHeroEntity = SuperHeroEntity(this)
}
Original file line number Diff line number Diff line change
@@ -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<String, SuperHero>
private val superHeroes: MutableMap<String, SuperHero> =
fakeData().associateBy { it.id }.toMutableMap()

init {
superHeroes = fakeData().associateBy { it.id }.toMutableMap()
fun getAllSuperHeroes(): LiveData<List<SuperHero>> {
val allSuperHeroes = MutableLiveData<List<SuperHero>>()
executor.execute {
waitABit()
allSuperHeroes.postValue(superHeroes.values.toList())
}
return allSuperHeroes
}

fun getAllSuperHeroes(): List<SuperHero> {
waitABit()
return superHeroes.values.toList()
}

fun get(id: String): SuperHero? {
waitABit()
return superHeroes[id]
fun get(id: String): LiveData<SuperHero?> {
val superHero = MutableLiveData<SuperHero?>()
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
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<SuperHero> =
local.getAllSuperHeroes().ifEmpty {
remote.getAllSuperHeroes()
.also { local.saveAll(it) }
fun getAllSuperHeroes(): LiveData<List<SuperHero>> = MediatorLiveData<List<SuperHero>>().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<SuperHero?> = MediatorLiveData<SuperHero?>().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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -9,10 +10,10 @@ import androidx.room.Update
@Dao
interface SuperHeroDao {
@Query("SELECT * FROM superheroes")
fun getAll(): List<SuperHeroEntity>
fun getAll(): LiveData<List<SuperHeroEntity>>

@Query("SELECT * FROM superheroes WHERE superhero_id = :id")
fun getById(id: String): SuperHeroEntity?
fun getById(id: String): LiveData<SuperHeroEntity?>

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(superHeroes: List<SuperHeroEntity>)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SuperHero?> = superHeroesRepository.get(id)
}
Loading

0 comments on commit 6ccc843

Please sign in to comment.