user1851366
user1851366

Reputation: 346

Android MVVM with servicelocator

Im trying to make mvvm pattern with repository and servicelocator to use mock's or remote calls, it depends on flavour. What happening now,is that my liveData is not updating after i receive response from server. So for now i always have a empty list.

I use this google sample to trying make it. sample

My code below, using remote serviceLocator Appreciate your help.

class TestActivity : AppCompatActivity(){

private val viewModel = TestViewModel(ServiceLocator.provideTasksRepository())
private lateinit var  binding : TestBinding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = DataBindingUtil.setContentView(this, R.layout.test)
    binding.viewmodel = viewModel

    setupRecyclerView()
}

private fun setupRecyclerView() {
    binding.viewmodel?.run {
        binding.recyclerViewTest.adapter = TestAdapter(this)
    }
}

}

class TestAdapter(private val viewModel : TestViewModel) : ListAdapter<ResponseEntity, TestAdapter.TestViewHolder>(TestDiffCallback()) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TestViewHolder.from(parent)

override fun onBindViewHolder(holder: TestViewHolder, position: Int) {
    val item = getItem(position)
    holder.bind(viewModel, item)
}

class TestViewHolder private constructor(val binding: ItemTestBinding) :
    RecyclerView.ViewHolder(binding.root) {

    fun bind(viewModel: TestViewModel, item: ResponseEntity) {

        binding.viewmodel = viewModel
        binding.game = item
        binding.executePendingBindings()
    }

    companion object {
        fun from(parent: ViewGroup): TestViewHolder {
            val layoutInflater = LayoutInflater.from(parent.context)
            val binding = ItemTestBinding.inflate(layoutInflater, parent, false)

            return TestViewHolder(binding)
        }
    }
}

}

<?xml version="1.0" encoding="utf-8"?>

<data>

    <import type="android.view.View" />

    <import type="androidx.core.content.ContextCompat" />

    <variable
        name="game"
        type="com.test.ResponseEntity" />

    <variable
        name="viewmodel"
        type="com.test.TestViewModel" />

</data>

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view_test"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:items="@{viewmodel.items}"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
class MyRepository(private val testRemoteDataSource: IDataSource) :
ITestRepository {

override suspend fun getList() = testRemoteDataSource.getList()

}

class TestViewModel(private val testRepository: MyRepository) : ViewModel() {

private var _items = MutableLiveData<List<ResponseEntity>>()
val items: LiveData<List<ResponseEntity>> = _items

init {
    refreshList()
}

private fun refreshList() {
    viewModelScope.launch {
        _items = testRepository.getList()
    }
}

}

object ServiceLocator {

var testRepository: MyRepository? = null

fun provideTasksRepository(): MyRepository {
    synchronized(this) {
        return testRepository ?: createTestRepository()
    }
}

private fun createTestRepository(): MyRepository {
    val newRepo = MyRepository(
        MyRemoteDataSource(RetrofitClient.apiInterface)
    )
    testRepository = newRepo
    return newRepo
}

}

class MyRemoteDataSource(private val retroService: IService) :
IDataSource {

private var responseEntityLiveData: MutableLiveData<List<ResponseEntity>> =
    MutableLiveData<List<ResponseEntity>>()

override suspend fun getGames(): MutableLiveData<List<ResponseEntity>> {

    retroService.getList()
        .enqueue(object : Callback<List<ResponseEntity>> {
            override fun onFailure(
                call: Call<List<ResponseEntity>>,
                t: Throwable
            ) {
                responseEntityLiveData.value = emptyList()
            }

            override fun onResponse(
                call: Call<List<ResponseEntity>>,
                response: Response<List<ResponseEntity>>
            ) {
                responseEntityLiveData.value = response.body()
            }
        })

    return responseEntityLiveData
}

}

Upvotes: 2

Views: 356

Answers (1)

ChristianB
ChristianB

Reputation: 2690

My guess is that mixing Coroutines suspending function with Retrofit and LiveData leads to some side effect here.

I do not have a single solution, but some points they can help you.

In general I would avoid mixing LiveData with suspending functions. LiveData is concept of caching data for the UI/ViewModel layer. Lower layers do not need to know anything like Android concrete stuff like LiveData. More information here

In your repository or dataSource can either use a suspending function that returns a single value or Coroutines Flow that can emit more than one value. In your ViewModel you can then map those results to your LiveData.

DataSource

In your DataSource you can use suspendCoroutine or suspendCancellableCoroutine to connect Retrofit (or any other callback interface) with Coroutines:

class DataSource(privat val retrofitService: RetrofitService) {

    /**
     * Consider [Value] as a type placeholder for this example
     */
    fun suspend fun getValue(): Result<Value> = suspendCoroutine { continuation ->
      retrofitService.getValue().enqueue(object : Callback<Value> {
        override fun onFailure(call: Call<List<ResponseEntity>>,
                               throwable: Throwable) {
          continuation.resume(Result.Failure(throwable)))
        }

        override fun onResponse(call: Call<List<ResponseEntity>>,
                                response: Response<List<ResponseEntity>>) {
          continuation.resume(Result.Success(response.body()))
        }
      }
   }
}

Result Wrapper

You can wrap the response to your own Result type like:

sealed class Result<out T> {

    /**
     * Indicates a success state.
     */
    data class Success<T>(val data: T) : Result<T>()

    /**
     * Indicates an error state.
     */
    data class Failure(val throwable: Throwable): Result<Nothing>
}

I leave the Repository out from this example and call directly the DataSource.

ViewModel

Now in your ViewModel you can launch the coroutine, getting the Result and map it to the LiveData.

class TestViewModel(private val dataSource: DataSource) : ViewModel() {
    private val _value = MutableLiveData<Value>>()
    val value: LiveData<Value> = _value

    private val _error = MutableLiveData<String>()
    val error: LiveData = _error

    init {
        getValue()
    }

    private fun getValue() {
      viewModelScope.launch {
        val result: Result<Value> = dataSource.getValue()
        
        // check wether the result is of type Success or Failure
        when(result) {
          is Result.Success -> _value.postValue(result.data)
          is Result.Failure -> _error.value = throwable.message
        }
      }
    }
}

I hope that helps you a bit.

Upvotes: 1

Related Questions