I am doing baby step on Channel Buffer. I am learning to Poll item through Channel . When I send item, it doesn't receive() all item. I don't understand why?
class QueueViewModel(private val application: Application) : AndroidViewModel(application) {
val basketChannel = Channel<String>(Channel.UNLIMITED)
init {
startPolling()
}
fun addItems() {
addItemInChannel(100L, "Item 1")
addItemInChannel(1000L, "Item 2")
addItemInChannel(400L, "Item 3")
addItemInChannel(500L, "Item 4")
}
fun addItemInChannel(delay: Long, item: String) {
viewModelScope.launch {
delay(delay)
logE("basketChannelItem added -> $item")
basketChannel.send(item)
}
}
fun startPolling() {
viewModelScope.launch {
Log.e(TAG, "Starting Polling")
for (element in basketChannel) {
logE("basketChannel Item poll -> $element")
basketChannel.receive()
}
}
}
}
I called addItems() in activity..
Output
where other items gone?
It's because you get items of your channel in two places: for and receive(). In general, they work in the same way. According to your log, for received items 1 and 4, while receive() method got 2 and 3.
You can remove basketChannel.receive() line, and you will receive all elements in for loop
Related
So I am starting to build a chat app and now I am at the registration screen.
Every time I press the login button,the request is sent only 1 time,like it should do.
The problem starts when I get in return the error message(e.g "Your password is incorrect"),after I get the error,I am pressing the login button again with the same wrong password,and I get Log error that I made but its showing 3 times, at the same time and firebase tells me that I have made too many attempts....
This is what I have done:
ViewModel:
private val _authState by lazy { MutableLiveData<AuthState>(AuthState.Loading) }
val authState: LiveData<AuthState> = _authState
fun loginUser(emailAddress: String, password: String) {
if (!isEmailAddressValid(emailAddress)) {
_authState.value = AuthState.AuthError("Invalid email")
return
} else if (password.isEmpty()) {
_authState.value = AuthState.AuthError("Password field can't be empty")
return
} else if (emailAddress.isEmpty()) {
_authState.value = AuthState.AuthError("Email field can't be empty")
return
}
auth.signInWithEmailAndPassword(emailAddress, password).addOnCompleteListener { task ->
if (task.isSuccessful) {
_authState.value = AuthState.Success
} else {
task.exception?.let {
_authState.value = AuthState.AuthError(it.localizedMessage)
}
}
}
}
This is the Activity:
binding.loginButton.setOnClickListener {
val emailEditText = binding.emailAddressEditText.text.toString()
val passwordEditText = binding.passwordEditText.text.toString()
registerLoginViewModel.loginUser(emailEditText, passwordEditText)
registerLoginViewModel.authState.observe(this#LoginRegisterActivity, object : Observer<AuthState?> {
override fun onChanged(loginState: AuthState?) {
when (loginState) {
is AuthState.Success -> {
hideLoadingScreen()
Toast.makeText(this#LoginRegisterActivity,"Welcome Back!",Toast.LENGTH_SHORT).show()
Intent(this#LoginRegisterActivity, MainActivity::class.java)
finish()
}
is AuthState.AuthError -> {
hideLoadingScreen()
Log.e("Error:","Error Message: ${loginState.message}") // This line returns 3 times after the second attempt
Toast.makeText(this#LoginRegisterActivity,loginState.message,Toast.LENGTH_SHORT).show()
}
else -> {
showLoadingScreen()
}
}
}
})
}
Thank you !
LiveData.observe(...) doesn't need to be in any kind of listener. You can observe in onCreate() of Activity ahead of API call. As it is in your code now, you're adding one new observer every time your click listener is called.
Here's a small example:
class FruitsActivity : AppCompatActivity {
private val binding by lazy {
FruitsActivityBinding.inflate(layoutInflater)
}
private val fruitsViewModel by viewModels<FruitsViewModel>()
#Override
fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
// Observe from fruitsViewModel.fruits
fruitsViewModel.fruits.observe(this) { fruitList ->
// Use `fruitList` in your adapter
}
// Fetch fruits on tap of a button
binding.loadFruitsButton.setOnClickListener {
fruitsViewModel.fetchFruits()
}
}
}
class FruitsViewModel : ViewModel() {
private val _fruits = MutableLiveData<List<Fruit>>()
val fruits: LiveData<List<Fruit>> = _fruits
fun fetchFruits() {
viewModelScope.launch {
// `someRepository` can be anything that calls an API
// or queries a database to get the required data.
// Repository Pattern + Coroutines recommended
val fruitList = someRepository.fetchFruits()
// if needed, perform any filters or modifications to `fruitList` here
// set the result data on LiveData
_fruits.value = fruitList
}
}
}
So, this is what happens:
Activity launches.
Initializes binding and fruitsViewModel.
Adds an Observer on fruits from fruitsViewModel
Sets click listener on a button to load fruits
When you tap the button, fruitsViewModel fetches fruits and sets result data on LiveData (_fruits).
LiveData finds its observers and notifies them about new data.
Let me know if you have any questions or if there's something wrong. I wrote the code directly in this text-field, so there might be a dot, comma or colon misplaced or missing.
I have a RecyclerView that inherits from PagingDataAdapter, inside the adapter I have two viewHolders, they are divided by viewType, the fact is that I wrote a code that combined the body of two requests into one MutableLiveData, which can then be observed in a fragment and set its value via submitData.
private val _searchResult = MutableLiveData<Resource<PagingData<SearchItems>>>()
val searchResult: LiveData<Resource<PagingData<SearchItems>>> = _searchResult
fun getSearchResult(q: String, id: String) = viewModelScope.launch {
_searchResult.postValue(Resource.Loading())
val searchDeferred = async { repository.getSearch(q) }
val channelsDeferred = async { repository.fetchChannels(id) }
val search = searchDeferred.await()
val channels = channelsDeferred.await()
channels.collect {
_searchResult.value = Resource.Success(it)
}
search.collect {
_searchResult.value = Resource.Success(it)
}
}
this is the code in the ViewModel
viewModel.searchResult.observe(viewLifecycleOwner, {
when (it) {
is Resource.Error -> {
it.message?.let { it1 -> Log.d("mecal", it1) }
}
is Resource.Success -> {
lifecycleScope.launch {
it.data?.map { item ->
viewModel.getSearchResult(
args.query,
item.snippet?.channelId.toString()
)
}
it.data?.let { it1 -> searchAdapter.submitData(it1) }
}
}
}
})
and is the code in the Fragment. The point is that this code does not work, i.e. it does not display data in the recyclerview. But I don’t know what exactly I did wrong, if you have any suggestions why it doesn’t work or there is another way, please write, I really need it!
Here I have something very similar that you need. It is a chat app that uses the same RecyclerView to display 2 different viewholders depending on if the message is sent to you or you sent the message
I am having some scoping issues that I don't know how to solve. In the code below the array allRecentItems is not populated after it has been assigned Items inside the forEach loop.
The idea is to query the Room database for the ID of an Item and then use a function getItemById() to return the details for the item with that ID by querying a Firestore collection for a document with that ID.
What method can I use to solve this problem? Thanks.
Approach 1
override fun getAllRecentlyTappedItems(callback: (ArrayList<Item>) -> Unit): LiveData<ArrayList<Item>>
{
val allRecentItems: ArrayList<Item> = arrayListOf()
launch {
val recentlyTappedItems: List<EntityRecentItems> = withContext(Dispatchers.IO){
itemDatabase.entityRecentlyTappedItem().getAll()
}
recentlyTappedItems.forEach { entityRecentItem ->
getItemById(entityRecentItem.itemId){ item: Item ->
allRecentItems.add(item)
//`item` is present here
Log.d(
this.javaClass.simpleName,
"getAllRecentlyTappedItems: {add: ${item.name}}"
)
}
}
// `allRecentItems` is empty at this point where I need it.
Log.d(
this.javaClass.simpleName,
"getAllRecentlyTappedItems: {final allRecentItems: ${allRecentItems}}"
)
mutableLiveDataItemArrayList.postValue(allRecentItems)
}
// mutableLiveDataItemArrayList not updated yet.
return mutableLiveDataItemArrayList
}
getItemById function
override fun getItemById(itemId: String, callback: (item: Item) -> Unit)
{
firestore.collection(Constants.FirebasePaths.DATABASE_ITEMS)
.document(itemId)
.get()
.addOnSuccessListener { documentSnapshot ->
if(documentSnapshot != null)
{
Log.d(this.javaClass.simpleName,
"getItemById: {" +
"itemName: ${documentSnapshot.data!![Constants.FirebaseDocumentSnapshotKeys.DATABASE_ITEMS_ITEM_NAME].toString()}" +
"}")
callback(
Item(name = documentSnapshot.data!![Constants.FirebaseDocumentSnapshotKeys.DATABASE_ITEMS_ITEM_NAME].toString())
)
}
}
}
Approach 2
override fun getAllRecentlyTappedItems(callback: (ArrayList<Item>) -> Unit): LiveData<ArrayList<Item>>
{
launch {
val listOfRecentItems = async(Dispatchers.IO){
itemDatabase.entityRecentlyTappedItem().getAll()
}
val res = async(Dispatchers.Main) {
val allRecentItems: ArrayList<Item> = arrayListOf()
listOfRecentItems.await().forEach { recentItem ->
Log.d(this.javaClass.simpleName, "getAllRecentlyTappedItems: " +
"{recentTappedIdFromDatabase: ${recentItem.itemId}}")
getItemById(recentItem.itemId) {item: Item ->
Log.d(this.javaClass.simpleName, "getAllRecentlyTappedItems: " +
"{(itemId: ${item.itemId}, itemName: ${item.itemName})}")
allRecentItems.add(item)
}
}
// `res` is always empty at this point
allRecentItems
}
// `res.await()` returns an empty array
Log.d(this.javaClass.simpleName, "getAllRecentlyTappedItems: " +
"{allRecentItems: ${res.await()}}")
mutableLiveDataItemArrayList.postValue(res.await())
}
return mutableLiveDataItemArrayList
}
You will want to look into Coroutine.async. Problem is that the method is returning before the launch can execute and complete. You require some asynchronous functionality.
val deferred: Deferred<ArrayList<Item>> = GlobalScope.async(Dispatchers.IO){
val recentlyTappedItems: List<EntityRecentItems> = withContext(Dispatchers.IO){
itemDatabase.entityRecentlyTappedItem().getAll()
}
val recentItems: List<Item> = arrayListOf()
recentlyTappedItems.forEach { entityRecentItem ->
getItemById(entityRecentItem.itemId){ item: Item ->
recentItems.add(item)
}
}
recentItems
}
val allRecentItems = deferred.await()
This would need to be wrapped in a suspend fun I believe.
suspend fun getAllRecentItems(): List<Item> {
// Above snippet
}
Your code looks incomplete with regards to mutableLiveDataItemArrayList and the methods return type, but I am sure this will help get you to where you need.
In my app, I have this flow:
ClickListender in my fragment:
search_button.setOnClickListener {
if(search_input.text.isNullOrEmpty())
Toast.makeText(activity, "Input Error", Toast.LENGTH_LONG).show()
else
viewModel.onSearchButtonClicked(search_input.text.toString())
}
onSearchButtonClicked inside viewModel:
fun onSearchButtonClicked(input: String) {
coroutineScope.launch {
repo.insertToDatabase(input)
}
}
insertToDatabase inside Repository:
suspend fun insertToDatabase(string: String) {
withContext(Dispatchers.IO) {
val dataList =
ExternalApi.retrofitCall.getData(string).await()
if (dataList.intialDataResult < 1) {
//show error
} else {
//all good
database.myDataBase.insertAll(dataList)
}
}
}
I need to show error message if intialDataResult is less then one.
I thought about create MutableLiveData inside my repository with initial value of false and listen from the fragment through the viewModel, but it's not good approach because I have no way to set the LiveData to "false" again after I show error message.
I also tried to return bool from the insertToDatabase function and decide if to show error or not, with no success.
Any ideas how can I solve this?
Why not create a LiveData to manage your work's result state?
Create a class to store result of work why sealed class?
sealed class ResultState{
object Success: ResultState() // this is object because I added no params
data class Failure(val message: String): ResultState()
}
Create a LiveData to report this result
val stateLiveData = MutableLiveData<ResultState>()
Make insertToDatabase() return a result
suspend fun insertToDatabase(input: String): ResultState {
return withContext<ResultState>(Dispatchers.IO) {
val dataList =
ExternalApi.retrofitCall.getData(string).await()
if (dataList.intialDataResult < 1) {
return#withContext ResultState.Failure("Reason of error...")
} else {
database.myDataBase.insertAll(dataList)
return#withContext ResultState.Success
}
}
}
Now, report result to UI
fun onSearchButtonClicked(input: String) {
coroutineScope.launch {
val resultState = repo.insertToDatabase(input)
stateLiveData.value = resultState
}
}
In UI,
viewModel.stateLiveData.observe(viewLifeCycleOwner, Observer { state ->
when (state) {
is ResultState.Success -> { /* show success in UI */ }
is ResultState.Failure -> { /* show error in UI with state.message variable */ }
}
})
Similarly, you can add a ResultState.PROGRESS to show that a task is running in the UI.
If you have any queries, please add a comment.
i recently learning RxJava. I am learning about operator. This is my code :
tvText = findViewById(R.id.tvText)
val observable2 = Observable.just(1, 2, 3, 4, 5, 6)
observable2.filter { i ->
//filter genap
i!!
i % 2 == 0
}
tvText.setOnClickListener {
observable2.subscribe(object: Subscriber<Int>() {
override fun onNext(t: Int?) {
Toast.makeText(this#MainActivity, t.toString(), Toast.LENGTH_SHORT).show()
}
override fun onError(e: Throwable?) {
e!!.printStackTrace()
}
override fun onCompleted() {
Toast.makeText(this#MainActivity, "Complete", Toast.LENGTH_SHORT).show()
}
})
}
The problem is the filter is not working. The toast show all the number. What's wrong with my code?
The problem is that you're using the original observable2, not the filtered one. The filter function does not modify the Observable it is called on but instead returns a new one.
So, to fix your code, save the filter result into a new variable and use it instead of the original observable:
val filteredObservable2 = observable2.filter { i ->
i % 2 == 0
}
tvText.setOnClickListener {
filteredObservable2.subscribe(object: Subscriber<Int>() {
/*...*/
}
}