I have this basic flow emit / collect code but collect is not receiving any emits.
object TodoRepository {
fun getTodos(): Flow<List<Todo>> = flow {
val data = KtorClient.httpClient.use {
it.get("...")
}
val todos = data.body<List<Todo>>()
emit(todos) // << emit is called
}.flowOn(Dispatchers.IO)
}
class TodoViewModel: ViewModel() {
val response = MutableSharedFlow<List<Todo>>()
init {
viewModelScope.launch {
TodoRepository.getTodos().collect {
// NEVER INVOKED !!!
response.emit(it)
}
}
}
}
class MainActivity: AppCompatActivity() {
private val todoViewModel: TodoViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
runBlocking {
todoViewModel.response.collect {
}
}
}
}
Thanks to #Tenfour04 comment. runBlocking was the problem. For example, this will work
CoroutineScope(Dispatchers.Main).launch {
todoViewModel.response.collect {
}
}
Related
I was trying to get response from viewModel But, I am having hard time to use the response data on another activity to display it on a text view.
I have already setup the backend for the repo's and interfaces, data classes etc...
Thank you!!!
// View Model
class HomeActivityViewModel : ViewModel() {
lateinit var createPostLiveData: MutableLiveData<PostResponseData?>
init {
createPostLiveData = MutableLiveData()
}
fun getPostLiveObserver(): MutableLiveData<PostResponseData?> {
return createPostLiveData
}
fun createPostData(postdata: PostData) {
val retroService = RetrofitApiFactory.retroInstance().create(ChefApi::class.java)
val call = retroService.postData(postdata)
call.enqueue(object : Callback<PostResponseData> {
override fun onResponse(
call: Call<PostResponseData>,
response: Response<PostResponseData>
) {
if (response.isSuccessful) {
createPostLiveData.postValue(response.body())
var text = response.body()!!.choices[0].text
Log.d("response", text) // only shows the one in the viewModel
} else {
createPostLiveData.postValue(null)
Log.d("failed", response.errorBody().toString())
}
}
override fun onFailure(call: Call<PostResponseData>, t: Throwable) {
Log.d("failed", t.message.toString())
createPostLiveData.postValue(null)
}
})
}
}
Activity.kt
class HomeActivity : AppCompatActivity() {
lateinit var mAuth: FirebaseAuth
lateinit var viewModel: HomeActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home_actvity)
mAuth = FirebaseAuth.getInstance()
initViewModel()
generate.setOnClickListener {
createPost()
}
logout.setOnClickListener {
logoutUser()
}
}
private fun createPost() {
// creating a post
val prompt = "Some string..."
val postdata = PostData(120, prompt, 0.3, 1.0, 0.0)
viewModel.createPostData(postdata)
}
private fun initViewModel() {
// initialize view model
viewModel = ViewModelProvider(this).get(HomeActivityViewModel::class.java)
viewModel.getPostLiveObserver().observe(this, Observer<PostResponseData?> {
if (it == null) {
Toast.makeText(this#HomeActivity, "Failed to post data", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this#HomeActivity, "Successfully posted data", Toast.LENGTH_SHORT)
.show()
}
})
}
private fun logoutUser() {
mAuth.signOut()
updateUI()
}
private fun updateUI() {
val intent = Intent(this#HomeActivity, MainActivity::class.java)
startActivity(intent)
finish()
}
Try to change your HomeActivityViewModel class and add a LiveData object to it:
class HomeActivityViewModel : ViewModel() {
var _createPostLiveData: MutableLiveData<PostResponseData?>()
// Live data instance
val createPostLiveData
get() = _createPostLiveData
fun createPostData(postdata: PostData) {
val retroService = RetrofitApiFactory.retroInstance().create(ChefApi::class.java)
val call = retroService.postData(postdata)
call.enqueue(object : Callback<PostResponseData> {
override fun onResponse(
call: Call<PostResponseData>,
response: Response<PostResponseData>
) {
if (response.isSuccessful) {
// Update live data value
_createPostLiveData.value = response.body()
var text = response.body()!!.choices[0].text
Log.d("response", text) // only shows the one in the viewModel
} else {
// Update live data value
_createPostLiveData.value = null
Log.d("failed", response.errorBody().toString())
}
}
override fun onFailure(call: Call<PostResponseData>, t: Throwable) {
Log.d("failed", t.message.toString())
// Update live data value
_createPostLiveData.value = null
}
})
}
}
You should then be able to observe the LiveData instance in your Activity:
class HomeActivity : AppCompatActivity() {
lateinit var mAuth: FirebaseAuth
// Initialize view model in declaration
private val viewModel: HomeActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home_actvity)
mAuth = FirebaseAuth.getInstance()
// Observe forĀ `createPostLiveData` changes
viewModel.createPostLiveData.observe(this) {
if (it == null) {
Toast.makeText(this#HomeActivity, "Failed to post data", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this#HomeActivity, "Successfully posted data", Toast.LENGTH_SHORT)
.show()
}
}
generate.setOnClickListener {
createPost()
}
logout.setOnClickListener {
logoutUser()
}
}
private fun createPost() {
// creating a post
val prompt = "Some string..."
val postdata = PostData(120, prompt, 0.3, 1.0, 0.0)
viewModel.createPostData(postdata)
}
...
The code below works only once.
class MainActivity : AppCompatActivity() {
lateinit var userLiveData: LiveData<List<User>>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getUsers() //WORKING <---------
userLiveData.observe(this, Observer {
it.forEach { user ->
Log.i("TAG", user.name)
}
})
lifecycleScope.launch {
delay(5000) //Refresh the result
getUsers() //NOT WORKING <---------
}
}
private fun getUsers() {
val apiHelper = ApiHelper(RetrofitBuilder.apiService)
val mainRepository = MainRepository(apiHelper)
userLiveData = liveData {
val res = mainRepository.getUsers() //Suspend function
emit(res)
}
}
}
The first time I call the getUsers() function, I can see the logs. But Later I call getUsers() , there is no log on the screen.
Which means that the userLiveData.observe( function runs once.
your LiveData can be referencing MutableLiveData, where you post stuff
private val userMLD = MutableLiveData<List<User>>()
val userLiveData: LiveData<List<User>> = userMLD
and
private fun getUsers() {
val apiHelper = ApiHelper(RetrofitBuilder.apiService)
val mainRepository = MainRepository(apiHelper)
userMLD.postValue(mainRepository.getUsers())
}
Instead of creating a LiveData everytime, run your code inside a coroutine and update the LiveData with the new value. You need a MutableLiveData for that:
class MainActivity : AppCompatActivity() {
private val userLiveData = MutableLiveData<List<User>>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getUsers() //WORKING <---------
userLiveData.observe(this, Observer {
it.forEach { user ->
Log.i("TAG", user.name)
}
})
lifecycleScope.launch {
delay(5000) //Refresh the result
getUsers() //NOT WORKING <---------
}
}
private fun getUsers() {
val apiHelper = ApiHelper(RetrofitBuilder.apiService)
val mainRepository = MainRepository(apiHelper)
lifecycleScope.launch {
val users = withContext(Dispatchers.IO) {
mainRepository.getUsers() //Suspend function
}
userLiveData.value = users
}
}
}
I am using the following DAO
#Dao
interface GroceryListDao {
#Insert
fun insert(list: GroceryList)
#Update
fun update(list: GroceryList)
#Query("Delete from grocery_list_table")
fun clear()
#Query ("Select * from grocery_list_table")
fun getAllItems(): LiveData<List<GroceryList>>
#Query ("Select * from grocery_list_table where itemId = :item")
fun getItem(item: Long): GroceryList?
#Query ("Select * from grocery_list_table where item_status = :status")
fun getItemsBasedOnStatus(status: Int): List<GroceryList>
}
And my database has 3 columns groceryId(Long - autogenerated), groceryName(String) and groceryStatus(Int - 1/0).
When I am using getItemsBasedOnStatus(status: Int) without using LiveData I am able to retrieve the data. But when it is wrapped with LiveData I am getting null.
The other issue is when I get a list of items from a database without wrapping with LiveData and assigning to MutableLiveData in ViewModel, then the assigned MutableLiveData is displaying null values. I see lot of questions on this but no answer.
Adding code for my viewModel and Fragment
ViewModel
class GroceryListViewModel(
val database: GroceryListDao,
application: Application
) : AndroidViewModel(application) {
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
var grocerylistItems = database.getAllItems()
var groceryItem = MutableLiveData<GroceryList>()
var groceryitems = MutableLiveData<List<GroceryList>>()
init {
getAllItemsFromDatabase()
}
fun insertIntoDatabase(item: GroceryList) {
uiScope.launch {
insert(item)
}
}
private suspend fun insert(item: GroceryList) {
withContext(Dispatchers.IO) {
database.insert(item)
}
}
fun updateGrocerylist(itemId: Long, status: Int) {
uiScope.launch {
groceryItem.value = getItem(itemId)
groceryItem.value?.itemStatus = status
groceryItem.value?.let { update(it) }
}
}
private suspend fun update(item: GroceryList) {
withContext(Dispatchers.IO) {
database.update(item)
}
}
private suspend fun getItem(itemId: Long): GroceryList? {
return withContext(Dispatchers.IO) {
var item = database.getItem(itemId)
item
}
}
fun getAllItemsFromDatabase() {
uiScope.launch {
getAllItems()
}
}
private suspend fun getAllItems() {
withContext(Dispatchers.IO) {
grocerylistItems = database.getAllItems()
}
}
fun getPendingItemsFromDatabase(status: Int) {
uiScope.launch {
getPendingItems(status)
}
}
private suspend fun getPendingItems(status: Int) {
withContext(Dispatchers.IO) {
val items = database.getItemsBasedOnStatus(status)
groceryitems.postValue(items)
Log.i("Grocery List", "Pending Items:" + items.size)
}
}
fun getDoneItemsFromDatabase(status: Int) {
uiScope.launch {
getDoneItems(status)
}
}
private suspend fun getDoneItems(status: Int) {
withContext(Dispatchers.IO) {
val items = database.getItemsBasedOnStatus(status)
groceryitems.postValue(items)
Log.i("Grocery List", "Done Items:" + items.size)
}
}
fun clearAllItemsFromDatabase() {
uiScope.launch {
clearItems()
}
}
private suspend fun clearItems() {
withContext(Dispatchers.IO) {
database.clear()
getAllItemsFromDatabase()
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
Fragment
class GroceryLIstFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val binding = FragmentGroceryLIstBinding.inflate(inflater,container,false)
val application = requireNotNull(this.activity).application
val dataSource = GroceryDatabase.getInstance(application)?.groceryListDatabaseDao
val viewModelFactory = dataSource?.let { GroceryListViewModelFactory(it, application) }
val viewModel = viewModelFactory?.let {
ViewModelProvider(
this,
it
).get(GroceryListViewModel::class.java)
}
viewModel?.grocerylistItems?.observe(this , Observer {
binding.grocerylistView.removeAllViews() // is it correct ?
for (item in it){
Log.i("Grocery List","Grocery Id=" + item.itemId+" ,Grocery Name=" + item.itemName +", Grocery status="+item.itemStatus)
addItemToScrollbar(item, binding, viewModel)
}
})
binding.addGrocery.setOnClickListener {
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view?.windowToken, 0)
val item = binding.groceryitemField.text.toString()
if (!item.isNullOrBlank()) {
val newItem = GroceryList(itemName = item)
viewModel?.insertIntoDatabase(newItem)
if (viewModel != null) {
addItemToScrollbar(newItem, binding,viewModel)
}
binding.groceryitemField.text.clear()
}
}
binding.doneCheckbox.setOnClickListener {
if (binding.doneCheckbox.isChecked)
viewModel?.getDoneItemsFromDatabase(1)
else
viewModel?.getAllItemsFromDatabase()
}
binding.pendingCheckbox.setOnClickListener {
if (binding.pendingCheckbox.isChecked) {
viewModel?.getPendingItemsFromDatabase(0)
}
else
viewModel?.getAllItemsFromDatabase()
}
binding.clearGrocery.setOnClickListener {
viewModel?.clearAllItemsFromDatabase()
binding.grocerylistView.removeAllViews()
}
return binding.root
}
private fun addItemToScrollbar(
item: GroceryList,
binding: FragmentGroceryLIstBinding,
viewModel: GroceryListViewModel
) {
val itemBox = AppCompatCheckBox(context)
itemBox.text = item.itemName
itemBox.isChecked = item.itemStatus == 1
binding.grocerylistView.addView(itemBox)
itemBox.setOnClickListener {
val itemstatus: Int = if (itemBox.isChecked)
1
else {
0
}
viewModel?.updateGrocerylist(item.itemId,itemstatus)
}
}
}
Any help would be appreciated.
This most likely the same issue as here (read the linked answer). Due to way asynchronous way LiveData is working, it will return null when you call it. LiveData is meant to be used in conjunction with Observers, that will be triggered once changes to observed subject occur.
An Observer can look like this
database.getItemsBasedOnStatus(status).observe(viewLifecycleOwner, Observer { groceryList->
// Do cool grocery stuff here
})
If you want to retrieve your data inside your ViewModel you do not have a viewLifecycleOwner, you can then use "observeForever()", but then you have to remove the Observer explicitly, see this answer.
Same issue and answer also in this post
I want to show ProgressDialog while fetching data from ViewModel and it works fine when I fetch data for the first time, but when I want to refresh the data from API the ProgressDialog starts and does not stops
I create MutableLiveData<Boolean>() and try to manage the visibility but it's not working
This is how i refresh my data from my Activity
private fun loadNorthTram() {
val model =
ViewModelProviders.of(this#MainActivity).get(MyViewModelNorth::class.java)
model.isNorthUpdating.observe(
this#MainActivity,
Observer { b ->
if (b!!)
AppUtil.showProgressSpinner(this#MainActivity)
else
AppUtil.dismissProgressDialog()
})
model.getNorthTrams().observe(this#MainActivity, Observer
{
if (it != null) {
setData(it)
}
})
}
Below is my ViewModel class
class MyViewModelNorth : ViewModel() {
private lateinit var mtoken: String
private val apiService: ApiInterface = ApiClient.client.create(ApiInterface::class.java)
private lateinit var trams: MutableLiveData<TramModel>
val isNorthUpdating = MutableLiveData<Boolean>().default(false)
fun getNorthTrams(): MutableLiveData<TramModel> {
isNorthUpdating.value = true
if (!::trams.isInitialized) {
trams = MutableLiveData()
callTokenAPI()
}
return trams
}
private fun callTokenAPI() {
val tokenObservable: Observable<TokenModel> = apiService.fetchToken()
tokenObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { self ->
mtoken = self.responseObject[0].DeviceToken
callTramAPI()
}
.subscribe(getTokenObserver())
}
private fun callTramAPI() {
val apiService: ApiInterface = ApiClient.client.create(ApiInterface::class.java)
val observable: Observable<TramModel> = apiService.fetchTrams(AppUtil.NORTH_TRAMS, mtoken)
observable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getTramObserver())
}
private fun getTokenObserver(): Observer<TokenModel> {
return object : Observer<TokenModel> {
override fun onComplete() {}
override fun onSubscribe(d: Disposable) {}
override fun onNext(tokenModel: TokenModel) {}
override fun onError(e: Throwable) {
if (e is HttpException) {
val errorBody = e.response().errorBody()
HttpErrorUtil(e.code()).handelError()
}
}
}
}
private fun getTramObserver(): Observer<TramModel> {
return object : Observer<TramModel> {
override fun onComplete() {
isNorthUpdating.value = false
}
override fun onSubscribe(d: Disposable) {}
override fun onNext(t: TramModel) {
if (!t.hasError && t.hasResponse)
trams.value = t
else if (t.errorMessage.isBlank())
applicationContext().showToast(t.errorMessage)
else
applicationContext().showToast(applicationContext().getString(R.string.something_wrong))
}
override fun onError(e: Throwable) {
if (e is HttpException) {
val errorBody = e.response().errorBody()
HttpErrorUtil(e.code()).handelError()
}
}
}
}
public fun getIsNothUpdating(): LiveData<Boolean> {
return isNorthUpdating
}
fun <T : Any?> MutableLiveData<T>.default(initialValue: T) = apply { setValue(initialValue) }
}
I have not tested your code but I think your problem is in function getNorthTrams() in viewmodel.
First time when you fetch data, trams is not initialized, your api call is happening and at only onCompleted, you are setting isNorthUpdating.value = false. This code works.
But when you refresh data, trams is already initialized. So, there is no case for isNorthUpdating.value = false, which is causing progress dialog to not dismiss.
So I think you should handle else case in your viewmodel.
fun getNorthTrams(): MutableLiveData<TramModel> {
isNorthUpdating.value = true
if (!::trams.isInitialized) {
trams = MutableLiveData()
callTokenAPI()
}else{
//do your thing for refresh
isNorthUpdating.value = false
}
return trams
}
Also, in api call, if error occur, you should set isNorthUpdating to false and show some error message. Otherwise, progress dialog will always be showing even if some error occur in api call.
I'm trying to learn how to use RxJava in Android, but have run into a dead end. I have the following DataSource:
object DataSource {
enum class FetchStyle {
FETCH_SUCCESS,
FETCH_EMPTY,
FETCH_ERROR
}
var relay: BehaviorRelay<FetchStyle> = BehaviorRelay.createDefault(FetchStyle.FETCH_ERROR)
fun fetchData(): Observable<DataModel> {
return relay
.map { f -> loadData(f) }
}
private fun loadData(f: FetchStyle): DataModel {
Thread.sleep(5000)
return when (f) {
FetchStyle.FETCH_SUCCESS -> DataModel("Data Loaded")
FetchStyle.FETCH_EMPTY -> DataModel(null)
FetchStyle.FETCH_ERROR -> throw IllegalStateException("Error Fetching")
}
}
}
I want to trigger an update downstream, whenever I change the value of relay, but this doesn't happen. It works when the Activity is initialized, but not when I'm updating the value. Here's my ViewModel, from where I update the value:
class MainViewModel : ViewModel() {
val fetcher: Observable<UiStateModel> = DataSource.fetchData().replay(1).autoConnect()
.map { result -> UiStateModel.from(result) }
.onErrorReturn { exception -> UiStateModel.Error(exception) }
.startWith(UiStateModel.Loading())
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
fun loadSuccess() {
DataSource.relay.accept(DataSource.FetchStyle.FETCH_SUCCESS)
}
fun loadEmpty() {
DataSource.relay.accept(DataSource.FetchStyle.FETCH_EMPTY)
}
fun loadError() {
DataSource.relay.accept(DataSource.FetchStyle.FETCH_ERROR)
}
}
This is the code from the Activity that does the subsciption:
model.fetcher
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
uiState -> mainPresenter.loadView(uiState)
})
Ended up using kotlin coroutines instead, as I was unable to re-subscribe to ConnectableObservable and start a new fetch.
Here's the code for anyone interested.
The presenter:
class MainPresenter(val view: MainView) {
private lateinit var subscription: SubscriptionReceiveChannel<UiStateModel>
fun loadSuccess(model: MainViewModel) {
model.loadStyle(DataSource.FetchStyle.FETCH_SUCCESS)
}
fun loadError(model: MainViewModel) {
model.loadStyle(DataSource.FetchStyle.FETCH_ERROR)
}
fun loadEmpty(model: MainViewModel) {
model.loadStyle(DataSource.FetchStyle.FETCH_EMPTY)
}
suspend fun subscribe(model: MainViewModel) {
subscription = model.connect()
subscription.subscribe { loadView(it) }
}
private fun loadView(uiState: UiStateModel) {
when(uiState) {
is Loading -> view.isLoading()
is Error -> view.isError(uiState.exception.localizedMessage)
is Success -> when {
uiState.result != null -> view.isSuccess(uiState.result)
else -> view.isEmpty()
}
}
}
fun unSubscribe() {
subscription.close()
}
}
inline suspend fun <E> SubscriptionReceiveChannel<E>.subscribe(action: (E) -> Unit) = consumeEach { action(it) }
The view:
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
launch(UI) {
mainPresenter.subscribe(model)
}
btn_load_success.setOnClickListener {
mainPresenter.loadSuccess(model)
}
btn_load_error.setOnClickListener {
mainPresenter.loadError(model)
}
btn_load_empty.setOnClickListener {
mainPresenter.loadEmpty(model)
}
}
override fun onDestroy() {
super.onDestroy()
Log.d("View", "onDestroy()")
mainPresenter.unSubscribe()
}
...
The model:
class MainViewModel : ViewModel() {
val TAG = this.javaClass.simpleName
private val stateChangeChannel = ConflatedBroadcastChannel<UiStateModel>()
init {
/** When the model is initialized we immediately start fetching data */
fetchData()
}
override fun onCleared() {
super.onCleared()
Log.d(TAG, "onCleared() called")
stateChangeChannel.close()
}
fun connect(): SubscriptionReceiveChannel<UiStateModel> {
return stateChangeChannel.openSubscription()
}
fun fetchData() = async {
stateChangeChannel.send(UiStateModel.Loading())
try {
val state = DataSource.loadData().await()
stateChangeChannel.send(UiStateModel.from(state))
} catch (e: Exception) {
Log.e("MainModel", "Exception happened when sending new state to channel: ${e.cause}")
}
}
internal fun loadStyle(style: DataSource.FetchStyle) {
DataSource.style = style
fetchData()
}
}
And here's a link to the project on github.