My app contains a Room db of cocktail recipes that it downloads via a Retrofit api call, all of that is working well. To focus in on where my problem lies, my use case is a user adding a cocktail to a list. This is done via a DialogFragment and here the DialogFragment displays, the transaction executes, the DialogFragment goes away and the Room db is updated. The cocktail fragment does not get the update though - if you navigate away and back, the update is visible so I know the transaction worked as expected. One thing I just noticed is if I rotate the device, the update gets picked up as well.
Here is the relevant section of my fragment:
class CocktailDetailFragment : BaseFragment<CocktailDetailViewModel, FragmentCocktailDetailBinding, CocktailDetailRepository>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.cocktail.observe(viewLifecycleOwner, Observer {
when(it){
is Resource.Success -> {
updateCocktail(it.value)
}
}
})
}
private fun updateCocktail(cocktail: Cocktail) {
with(binding){
detailCocktailName.text = cocktail.cocktailName
//...
//this is the piece of functionality i'm expecting the LiveData observer to execute and change the drawable
if(cocktail.numLists > 0) {
detailCocktailListImageView.setImageResource(R.drawable.list_filled)
} else {
detailCocktailListImageView.setImageResource(R.drawable.list_empty)
}
}
}
override fun getViewModel() = CocktailDetailViewModel::class.java
override fun getFragmentBinding(
inflater: LayoutInflater,
container: ViewGroup?
) = FragmentCocktailDetailBinding.inflate(inflater,container,false)
override fun getFragmentRepository(): CocktailDetailRepository {
val dao = GoodCallDatabase(requireContext()).goodCallDao()
return CocktailDetailRepository(dao)
}
}
BaseFragment:
abstract class BaseFragment<VM: BaseViewModel, B: ViewBinding, R: BaseRepository>: Fragment() {
protected lateinit var userPreferences: UserPreferences
protected lateinit var binding: B
protected lateinit var viewModel: VM
protected val remoteDataSource = RemoteDataSource()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
userPreferences = UserPreferences(requireContext())
binding = getFragmentBinding(inflater, container)
val factory = ViewModelFactory(getFragmentRepository())
viewModel = ViewModelProvider(this, factory).get(getViewModel())
return binding.root
}
abstract fun getViewModel() : Class<VM>
abstract fun getFragmentBinding(inflater: LayoutInflater, container: ViewGroup?): B
abstract fun getFragmentRepository(): R
}
ViewModelFactory:
class ViewModelFactory(
private val repository: BaseRepository
): ViewModelProvider.NewInstanceFactory() {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return when{
modelClass.isAssignableFrom(CocktailDetailViewModel::class.java) -> CocktailDetailViewModel(repository as CocktailDetailRepository) as T
else -> throw IllegalArgumentException("ViewModel class not found")
}
}
}
ViewModel:
class CocktailDetailViewModel(
private val repository: CocktailDetailRepository
): BaseViewModel(repository) {
private val _cocktail: MutableLiveData<Resource<Cocktail>> = MutableLiveData()
val cocktail: LiveData<Resource<Cocktail>>
get() = _cocktail
fun getCocktailByCocktailId(cocktailId: Int) = viewModelScope.launch {
_cocktail.value = Resource.Loading
_cocktail.value = repository.getCocktail(cocktailId)
}
}
Repository:
class CocktailDetailRepository(
private val dao: GoodCallDao
):BaseRepository(dao) {
suspend fun getCocktail(cocktailId: Int) = safeApiCall {
dao.getCocktail(cocktailId)
}
}
Safe Api Call (I use this so db/api calls run on IO):
interface SafeApiCall {
suspend fun <T> safeApiCall(
apiCall: suspend () -> T
): Resource<T> {
return withContext(Dispatchers.IO) {
try {
Resource.Success(apiCall.invoke())
} catch (throwable: Throwable) {
when (throwable) {
is HttpException -> {
Resource.Failure(false, throwable.code(), throwable.response()?.errorBody())
}
else -> {
Resource.Failure(true, null, null)
}
}
}
}
}
}
Resource:
sealed class Resource<out T> {
data class Success<out T>(val value: T) : Resource<T>()
data class Failure(
val isNetworkError: Boolean,
val errorCode: Int?,
val errorBody: ResponseBody?
): Resource<Nothing>()
object Loading: Resource<Nothing>()
}
Dao:
#Query("SELECT * FROM cocktail c WHERE c.cocktail_id = :cocktailId")
suspend fun getCocktail(cocktailId: Int): Cocktail
Thank you in advance for any help! Given the issue and how the app is working, I believe I have provided all the relevant parts but please advise if more of my code is required to figure this out.
Related
I want to display the sum of a column from my room database in a textview.
After implementing my Query and setting the text of the Textview, it displays the following:
androidx.lifecycle.CoroutineLiveData#76f6edd
Any idea why this happens? Is it because my Query does not work or my way of implementign it is wrong?
It is my first time ever of actually programming. I know probably most of my code is not 100% correct, but I am working on it.
The Query from my Dao:
#Query("SELECT SUM(total)AS sum_total FROM receipt_table")
fun getSum(): Flow<Float>
The Fragment:
#AndroidEntryPoint
class HistoryFragment : Fragment(R.layout.fragment_history) {
private val viewModel: PurchaseViewmodel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_history, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentHistoryBinding.bind(view)
val exampleAdapter = ExampleAdapter()
binding.apply{
recyclerView.apply{
layoutManager = LinearLayoutManager(requireContext())
adapter = exampleAdapter
}
totalSumTextView.apply {
val totalSum = viewModel.totalSum
text = totalSum.toString()
}
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val receipt = exampleAdapter.currentList[viewHolder.adapterPosition]
viewModel.onSwipe(receipt)
}
}).attachToRecyclerView(recyclerView)
}
setFragmentResultListener("add_receipt_request"){_,bundle ->
val result = bundle.getInt("add_receipt_request")
viewModel.onAddResult(result)
}
viewModel.receipts.observe(viewLifecycleOwner){
exampleAdapter.submitList(it)
}
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.addTaskEvent.collect { event->
when(event){
is PurchaseViewmodel.TasksEvent.ShowUndoDelete -> {
Snackbar.make(requireView(),"Tasks deleted", Snackbar.LENGTH_LONG)
.setAction("UNDO"){
viewModel.unDoDeleteClick(event.receipts)
}.show()
}
}
}
}
}
}
And the Viewmodel:
#HiltViewModel
class PurchaseViewmodel #Inject constructor(
private val receiptDao: ReceiptDao
): ViewModel() {
private val tasksEventChannel = Channel<TasksEvent>()
val addTaskEvent = tasksEventChannel.receiveAsFlow()
val receipts = receiptDao.getAllReceipts().asLiveData()
val totalSum = receiptDao.getSum().asLiveData()
fun onAddResult(result: Int){
when (result){
ADD_RECEIPT_RESULT_OK ->showReceiptSavedConfirmation("Receipt has been saved")
}
}
private fun showReceiptSavedConfirmation (text: String) = viewModelScope.launch {
tasksEventChannel.send(TasksEvent.ShowReceiptSavedConfirmation(text))
}
fun onSwipe (receipts: Receipts) = viewModelScope.launch {
receiptDao.delete(receipts)
tasksEventChannel.send(TasksEvent.ShowUndoDelete(receipts))
}
fun unDoDeleteClick (receipts: Receipts) = viewModelScope.launch {
receiptDao.insert(receipts)
}
sealed class TasksEvent {
data class ShowReceiptSavedConfirmation(val msg: String) : TasksEvent()
data class ShowUndoDelete(val receipts: Receipts) : TasksEvent()
}
}
You are getting a LiveData object here, not just a value:
val totalSum = receiptDao.getSum().asLiveData()
When you call:
totalSum.toString()
It calls toString method on the LiveData object that the reason, why you have "androidx.lifecycle.CoroutineLiveData#76f6edd" inside your TextView.
To fix the issue, just replace:
totalSumTextView.apply {
val totalSum = viewModel.totalSum
text = totalSum.toString()
}
with:
viewModel.totalSum.observe(this) { totalSumTextView.text = it}
Detailed review, how to work with a LiveData you can find here:
https://developer.android.com/topic/libraries/architecture/livedata
Hey I want to create BaseFragment class that gets viewModel by generic type:
abstract class BaseFragment<B : ViewDataBinding, VM : ViewModel> : DaggerFragment() {
val viewModel by viewModels<VM> { viewModelFactory }
...
}
// Native function
#MainThread
inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)
but getting error Cannot use 'VM' as reified type parameter. Use a class instead.
is it at all possible to achieve what I am trying to do? Maybe with other approach?
Found working way, but is it clean enough?
abstract class BaseModelFragment<VM : ViewModel>(viewModelClass: KClass<VM>) : DaggerFragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
val viewModel by viewModel(viewModelClass) { viewModelFactory }
private fun Fragment.viewModel(
clazz: KClass<VM>,
ownerProducer: () -> ViewModelStoreOwner = { this },
factoryProducer: (() -> ViewModelProvider.Factory)? = null,
) = createViewModelLazy(clazz, { ownerProducer().viewModelStore }, factoryProducer)
}
And usage:
open class SomeFragment : BaseModelFragment<CustomerSupportViewModel>(CustomerSupportViewModel::class) {
...
}
It is tested and working. Any ideas how to improve it? :)
There is clearer solution:
abstract class BaseActivity<VM : BaseViewModel> : AppCompatActivity {
protected val viewModel: VM by viewModel(clazz = getViewModelClass())
private fun getViewModelClass(): KClass<VM> = (
(javaClass.genericSuperclass as ParameterizedType)
.actualTypeArguments[0] as Class<VM>
).kotlin
}
And usage:
class MainActivity : BaseActivity<MainViewModel>(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.onViewCreated()
}
}
There dirty way to get ViewModel and ViewBinding only from generics:
abstract class BaseFragment<BINDING : ViewDataBinding, VM : ViewModel> : Fragment() {
val viewModel by viewModels(getGenericClassAt<VM>(1))
var binding: BINDING? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = inflater.inflateBindingByType<BINDING>(container, getGenericClassAt(0)).apply {
lifecycleOwner = this#BaseFragment
}.also {
it.onBindingCreated()
}
return binding?.root
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = false
internal open fun BINDING.onBindingCreated() {}
fun <T> withBinding(action: BINDING.() -> T): T? = binding?.let { action(it) }
}
#Suppress("UNCHECKED_CAST")
fun <CLASS : Any> Any.getGenericClassAt(position: Int): KClass<CLASS> =
((javaClass.genericSuperclass as? ParameterizedType)
?.actualTypeArguments?.getOrNull(position) as? Class<CLASS>)
?.kotlin
?: throw IllegalStateException("Can not find class from generic argument")
fun <BINDING : ViewBinding> LayoutInflater.inflateBindingByType(
container: ViewGroup?,
genericClassAt: KClass<BINDING>
): BINDING = try {
#Suppress("UNCHECKED_CAST")
genericClassAt.java.methods.first { inflateFun ->
inflateFun.parameterTypes.size == 3
&& inflateFun.parameterTypes.getOrNull(0) == LayoutInflater::class.java
&& inflateFun.parameterTypes.getOrNull(1) == ViewGroup::class.java
&& inflateFun.parameterTypes.getOrNull(2) == Boolean::class.java
}.invoke(null, this, container, false) as BINDING
} catch (exception: Exception) {
throw IllegalStateException("Can not inflate binding from generic")
}
And usage:
class BoardFragment : BaseFragment<FragmentBoardBinding, BoardViewModel>() {
override fun FragmentBoardBinding.onBindingCreated() {
viewModel = this#BoardFragment.viewModel
}
}
Dirty, but saves tones of coding
I am trying to figure out how to pass an integer from a fragment to a viewmodel while using hilt. I have ready that viewmodel factories can be used for this, I am not sure how this would be done using DI.
In the code below, I am trying to figure out how I can pass albumId to the viewModel. The albumId will be used when fetching data from an API endpoint.
Fragment
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_album_details, container, false)
val albumId = arguments?.getInt("album_id")
viewModel.songs.observe(viewLifecycleOwner) {
view.song_recyclerview.apply {
layoutManager = LinearLayoutManager(this.context)
adapter = SongAdapter(viewModel.songs)
}
}
return view
}
ViewModel
class SongViewModel #ViewModelInject constructor(
songRepo: SongRepository,
#Assisted savedStateHandle: SavedStateHandle
) : ViewModel(), LifecycleObserver {
val songs: LiveData<List<Song>> = songRepo.getSongs(1)
}
Repository
class SongRepository constructor(
private val musicService: MusicService
)
{
fun getSongs(album_id: Int): LiveData<List<Song>> {
val data = MutableLiveData<List<Song>>()
musicService.getAlbumTracks(album_id).enqueue(object : Callback<List<Song>> {
override fun onResponse(call: Call<List<Song>>, response: Response<List<Song>>) {
data.value = response.body()
}
override fun onFailure(call: Call<List<Song>>, t: Throwable) {
}
})
return data
}
}
I was finally able to figure out a solution to the problem. I added a field to the viewmodel, and a method to set a value for that field. Basically, I call viewModel.start(int) then call viewModel.songs.
ViewModel
class SongViewModel #ViewModelInject constructor(
songRepo: SongRepository,
#Assisted savedStateHandle: SavedStateHandle
) : ViewModel(), LifecycleObserver {
private val _id = MutableLiveData<Int>() // int field
private val _songs = _id.switchMap { id ->
songRepo.getSongs(id)
}
val songs: LiveData<List<Song>> = _songs
fun start(id: Int) { // method to update field
_id.value = id
}
}
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// get data from bundle and pass to start()
arguments?.getInt(("album_id"))?.let { viewModel.start(it)}
viewModel.songs.observe(viewLifecycleOwner) {
view.song_recyclerview.apply {
layoutManager = LinearLayoutManager(this.context)
adapter = SongAdapter(viewModel.songs)
}
}
}
I am using live data from a shared ViewModel across multiple fragments. I have a sign-in fragment which takes user's phone number and password and then the user presses sign in button I am calling the API for that, now if the sign-in fails I am showing a toast "Sign In failed", now if the user goes to "ForgotPassword" screen which also uses the same view model as "SignInFragment" and presses back from the forgot password screen, it comes to sign-in fragment, but it again shows the toast "Sign In failed" but the API is not called, it gets data from the previously registered observer, so is there any way to fix this?
SignInFragment.kt
class SignInFragment : Fragment() {
private lateinit var binding: FragmentSignInBinding
//Shared view model across two fragments
private val onBoardViewModel by activityViewModels<OnBoardViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_sign_in,
container,
false
)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
//This is calling again after coming back from new fragment it.
showToast("Sign In Failed")
}
}
override fun onClick(v: View?) {
when (v?.id!!) {
R.id.forgotPasswordTV -> {
findNavController().navigate(SignInFragmentDirections.actionSignInFragmentToForgotPasswordFragment())
}
R.id.signInTV -> {
val phoneNumber = binding.phoneNumberET.text
val password = binding.passwordET.text
val signInRequestModel = SignInRequestModel(
phoneNumber.toString(),
password.toString(),
""
)
//Calling API for the sign-in
onBoardViewModel.callSignInAPI(signInRequestModel)
}
}
}
}
ForgotPasswordFragment
class ForgotPasswordFragment : Fragment() {
private lateinit var binding: FragmentForgotPasswordBinding
//Shared view model across two fragments
private val onBoardViewModel by activityViewModels<OnBoardViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_forgot_password,
container,
false
)
return binding.root
}
}
OnBoardViewModel
class OnBoardViewModel : ViewModel() {
private var repository: OnBoardRepository = OnBoardRepository.getInstance()
private val signInRequestLiveData = MutableLiveData<SignInRequestModel>()
//Observing this data in sign in fragment
val signInResponse: LiveData<APIResource<SignInResponse>> =
signInRequestLiveData.switchMap {
repository.callSignInAPI(it)
}
//Calling this function from sign in fragment
fun callSignInAPI(signInRequestModel: SignInRequestModel) {
signInRequestLiveData.value = signInRequestModel
}
override fun onCleared() {
super.onCleared()
repository.clearRepo()
}
}
I have tried to move this code inside onActivityCreated but it's still getting called after coming back from new fragment.
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
showToast("Sign In Failed")
}
Using SingleLiveEvent class instead of LiveData in OnBoardViewModel class will solve your problem:
val signInResponse: SingleLiveEvent <APIResource<SignInResponse>>.
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer<T> { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
fun call() {
postValue(null)
}
}
This is a lifecycle-aware observable that sends only new updates after subscription. This LiveData only calls the observable if there's an explicit call to setValue() or call().
I would provide a way to reset your live data. Give it a nullable type. Your observers can ignore it when they get a null value. Call this function when you receive login data, so you also won't be repeating messages on a screen rotation.
class OnBoardViewModel : ViewModel() {
// ...
fun consumeSignInResponse() {
signInRequestLiveData.value = null
}
}
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
if (response != null) {
showToast("Sign In Failed")
onBoardViewModel.consumeSignInResponse()
}
}
For Kotlin users #Sergey answer can also be implemented using delegates like below
class SingleLiveEvent<T> : MutableLiveData<T>() {
var curUser: Boolean by Delegates.vetoable(false) { property, oldValue, newValue ->
newValue != oldValue
}
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer<T> { t ->
if (curUser) {
observer.onChanged(t)
curUser = false
}
})
}
override fun setValue(t: T?) {
curUser = true
super.setValue(t)
}
fun call() {
postValue(null)
}
}
FIXED
SEE BELOW
If a picture is worth 1,000 words, a video is worth, like, a lot of words. Here's a video explanation of the issue.
I've included a video as it makes things much clearer. The problem: When I first load up the fragment containing the list of items with state I need to toggle, I can toggle that state just fine. I send the update to the Room database and the changes are emitted back to my ViewModel, who then dispatches them to the Fragment.
However, when I leave the fragment and come back, the changes are no longer dispatched. I don't know if I'm doing something incredibly stupid or if this is a bug.
I'm also using the Jetpack Navigation components if that's relevant. I'll include code below.
Please let me know if you need to see any other code referenced below and I'll add it to the question.
Thank you very much for your time and consideration.
ShowsFragment
class ShowsFragment : Fragment(), ShowClickListener, Observer<Resource<List<ShowDomainModel>>> {
#Inject
lateinit var factory: ViewModelFactory
#Inject
lateinit var adapter: ShowsAdapter
private lateinit var showsViewModel: ShowsViewModel
override fun onAttach(context: Context) {
super.onAttach(context)
AndroidSupportInjection.inject(this)
showsViewModel = ViewModelProviders.of(this, factory).get(ShowsViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
) = inflater.inflate(R.layout.fragment_shows, container, false)!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter.clickListener = this
shows.adapter = adapter
val shows = showsViewModel.getShows()
shows.observe(this, this)
}
override fun onChanged(resource: Resource<List<ShowDomainModel>>) {
Timber.d("onChanged")
when (resource.state) {
State.SUCCESS -> {
adapter.shows = resource.data!!
adapter.notifyDataSetChanged()
}
State.LOADING -> Unit
State.ERROR -> TODO("Handle error state in ShowsFragment")
}
}
override fun onShowFavoriteClicked(show: ShowDomainModel) {
if (show.favorite) {
showsViewModel.unfavoriteShow(show.playlistId)
} else {
showsViewModel.favoriteShow(show.playlistId)
}
}
override fun onShowClicked(show: ShowDomainModel) {
findNavController().navigate(
ShowsFragmentDirections.showEpisodes(show.name, show.playlistId)
)
}
}
ShowsDao
#Dao
abstract class ShowsDao {
#Query("SELECT * FROM $TABLE_NAME")
abstract fun getShows(): Observable<List<ShowCacheModel>>
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insertShows(shows: List<ShowCacheModel>)
#Query("SELECT * from $TABLE_NAME WHERE favorite = 1")
abstract fun getFavoriteShows(): Observable<List<ShowCacheModel>>
#Query("UPDATE $TABLE_NAME SET favorite = :favorite WHERE $COLUMN_SHOW_ID = :showId")
abstract fun setFavorite(showId: String, favorite: Boolean)
}
ShowsViewModel
#Singleton
class ShowsViewModel #Inject constructor(
private val getShows: GetShows,
private val addShowToFavorites: AddShowToFavorites,
private val removeShowFromFavorites: RemoveShowFromFavorites
) : ViewModel() {
private val shows: MutableLiveData<Resource<List<ShowDomainModel>>> = MutableLiveData()
init {
shows.postValue(Resource.loading())
getShows.execute(GetShowsObserver())
}
override fun onCleared() {
getShows.dispose()
super.onCleared()
}
fun getShows(): LiveData<Resource<List<ShowDomainModel>>> = shows
fun favoriteShow(id: String) = addShowToFavorites.execute(
AddShowToFavoritesObserver(),
AddShowToFavorites.Params.forShow(id)
)
fun unfavoriteShow(id: String) = removeShowFromFavorites.execute(
RemoveShowFromFavoritesObserver(),
RemoveShowFromFavorites.Params.forShow(id)
)
inner class GetShowsObserver : DisposableObserver<List<ShowDomainModel>>() {
override fun onComplete() {
Log.d("ShowsViewModel","onComplete")
throw RuntimeException("GetShows should not complete, should be observing changes to data.")
}
override fun onNext(showList: List<ShowDomainModel>) {
shows.postValue(Resource.success(showList))
}
override fun onError(e: Throwable) {
shows.postValue(Resource.error(e.localizedMessage))
}
}
inner class AddShowToFavoritesObserver : DisposableCompletableObserver() {
override fun onComplete() = Unit
override fun onError(e: Throwable) =
shows.postValue(Resource.error(e.localizedMessage))
}
inner class RemoveShowFromFavoritesObserver : DisposableCompletableObserver() {
override fun onComplete() = Unit
override fun onError(e: Throwable) =
shows.postValue(Resource.error(e.localizedMessage))
}
}
Turns out the fix is very simple. I just needed to use the Activity as the lifecycle that was passed to the ViewModel.
ShowsFragment
// ...
override fun onAttach(context: Context) {
super.onAttach(context)
AndroidSupportInjection.inject(this)
// CHANGED ONE LINE AND IT WORKS
// (changed this to activity!!)
// CHANGED ONE LINE AND IT WORKS
showsViewModel = ViewModelProviders.of(activity!!, factory).get(ShowsViewModel::class.java)
}
// ...
}