Skip to content

Application ShoppingApp

Devrath edited this page Jul 11, 2021 · 21 revisions

Project-Source-Code: Check here

Contents-and-observations
Using the Resource util class to relay data from the repository to view-model
Wrapping the live data in the Util Event class
Declaring live data observable in view model
Creating a Fake repository
Live Data Extension to test the live data
Unit tests on RoomDB

Using the Resource util class to relay data from the repository to view-model

  • This util class is used to wrap the data object in case of success and failure object in case of failure and return a resource object.
  • This is very handy when we want to test the response and also handling the state.

Resource.kt

data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
    companion object {
        fun <T> success(data: T?): Resource<T> {
            return Resource(Status.SUCCESS, data, null)
        }

        fun <T> error(msg: String, data: T?): Resource<T> {
            return Resource(Status.ERROR, data, msg)
        }

        fun <T> loading(data: T?): Resource<T> {
            return Resource(Status.LOADING, data, null)
        }
    }
}

enum class Status {
    SUCCESS,
    ERROR,
    LOADING
}

Wrapping the live data in the Util Event class

  • This is very useful in case of handling the live data.
  • We make the network request and get either the data in the success state of the resource object we defined earlier of the error state.
  • Problem with this approach is that, say there is an error state and we use snack bar to display the error and now we rotate the device and during this, the snack bar is displayed again.
  • Now Using this generic event class we can make sure the event is emitted only once.

code snippet

private val _images = MutableLiveData<Event<Resource<ImageResponse>>>()
val images: LiveData<Event<Resource<ImageResponse>>> = _images

Event.kt

open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

Declaring live data observable in the view model

  • When we are creating an observable source like live data or flow we need to give the ability to modify the live data by the components inside the ViewModel. Then expose a variable for outside so it can be observed.
  • Thus mutable state is for the view model since only the view model should modify it, thus it's private.
  • We just expose the Live data since it just can be observed and not be modified and has no mutable state, thus it's public.

Declare in viewmodel

private val _images = MutableLiveData<Event<Resource<ImageResponse>>>()
val images: LiveData<Event<Resource<ImageResponse>>> = _images

Set the data in viewmodel

images.value = // set the state

Observe the live data from outside the view model, like activity or fragment

viewModel.images.observe(viewLifecycleOwner, Observer {

Creating a Fake repository

  1. We create a fake repository when we can swap between a real repository and a fake repository depending on our usage.
  2. We use a real repository when we are running our production code.
  3. We use a fake repository when we are testing our test cases.
  4. We inject the interface of the repository in the view model constructor.
  5. Now we need to create classes of the repositories (real repository, fake repository) that implements the interface which we inject into the view model.
  6. We. create the instance of the class that implements the interface using the hilt provides a method using a dagger

Define the interface

ShoppingRepository.kt -> This is useful in swapping the implementation for both the production and test cases of repository implementation

interface ShoppingRepository {
    suspend fun insertShoppingItem(shoppingItem: ShoppingItem)
    suspend fun deleteShoppingItem(shoppingItem: ShoppingItem)
    fun observeAllShoppingItems(): LiveData<List<ShoppingItem>>
    fun observeTotalPrice(): LiveData<Float>
    suspend fun searchForImage(imageQuery: String): Resource<ImageResponse>
}

DefaultShoppingRepository.kt -> This can be used in production code

class DefaultShoppingRepository @Inject constructor(
    private val shoppingDao: ShoppingDao,
    private val pixabayAPI: PixabayAPI
) : ShoppingRepository {
    override suspend fun insertShoppingItem(shoppingItem: ShoppingItem) {}
    override suspend fun deleteShoppingItem(shoppingItem: ShoppingItem) {}
    override fun observeAllShoppingItems(): LiveData<List<ShoppingItem>> {}
    override fun observeTotalPrice(): LiveData<Float> {}
    override suspend fun searchForImage(imageQuery: String): Resource<ImageResponse> {}
}

FakeShoppingRepository.kt -> This can be used in testing unit tests

class FakeShoppingRepository @Inject constructor(
    private val shoppingDao: ShoppingDao,
    private val pixabayAPI: PixabayAPI
) : ShoppingRepository {
    override suspend fun insertShoppingItem(shoppingItem: ShoppingItem) {}
    override suspend fun deleteShoppingItem(shoppingItem: ShoppingItem) {}
    override fun observeAllShoppingItems(): LiveData<List<ShoppingItem>> {}
    override fun observeTotalPrice(): LiveData<Float> {}
    override suspend fun searchForImage(imageQuery: String): Resource<ImageResponse> {}
}

AppModule.kt -> Here we will cast the interface to the repository making it return the repository, by doing so we can inject the interface and use the overridden methods used in repository

@Module
@InstallIn(ApplicationComponent::class)
object AppModule {
    @Singleton
    @Provides
    fun provideDefaultShoppingRepository(
        dao: ShoppingDao,
        api: PixabayAPI
    ) = DefaultShoppingRepository(dao, api) as ShoppingRepository
}

Live Data Extension to test the live data

LiveData.kt

/**
 * Gets the value of a [LiveData] or waits for it to have one, with a timeout.
 *
 * Use this extension from host-side (JVM) tests. It's recommended to use it alongside
 * `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
 */
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValueTest(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValueTest.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

        // Don't wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }

    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

Clone this wiki locally