Reputation: 346
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
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