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 Mar 15, 2019
1 parent 6e0488d commit 6100eb2
Show file tree
Hide file tree
Showing 30 changed files with 360 additions and 414 deletions.
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 @@ -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 {

Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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>(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().sortedBy { it.id })
}
return allSuperHeroes
}

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

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,24 +1,44 @@
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)) {
if (it == null) {
syncSuperHeroFromRemote(id)
} else {
postValue(it)
}
}
}

fun save(superHero: SuperHero): SuperHero {
local.save(superHero)
remote.save(superHero)
return superHero
}

private fun MediatorLiveData<SuperHero?>.syncSuperHeroFromRemote(id: String) {
addSource(remote.get(id)) { superHero ->
superHero ?: return@addSource
local.save(superHero)
}
}
}
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 ORDER BY superhero_id ASC")
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)
}
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 GetSuperHeroes(private val superHeroesRepository: SuperHeroRepository) {
@WorkerThread
operator fun invoke(): List<SuperHero> = superHeroesRepository.getAllSuperHeroes()
operator fun invoke(): LiveData<List<SuperHero>> = superHeroesRepository.getAllSuperHeroes()
}
Loading

0 comments on commit 6100eb2

Please sign in to comment.