Trying to stick to the MVVM pattern rules. The final data should come to the ViewModel (I'm making an exception for Glide, but that's not the point).
Created several subcollections in the document (likes, comments, images, and so on).
After creating them, I ran into the problem of requesting the Firestore inside the main request. The data just doesn't have time to arrive.
How do I wait for data before "data.add" in such a situation?
ViewModel
init {
data = loadData()
}
fun getData(loadNextData: Boolean): LiveData<Response<List<Model>>> {
if (loadNextData)
data = loadData()
return data
}
private fun loadData(): LiveData<Response<List<Model>>> {
return liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
emit(Response.Loading)
val result = repository.loadData()
if (result is Response.Success || result is Response.Error)
emit(result)
}
}
Repository
suspend fun loadData(): Response<List<Model>> {
return suspendCoroutine { continuation ->
if (LAST_DOCUMENT == null) {
firebaseRepository.getReference(DEFAULT_COLLECTION_NAME, DEFAULT_ORDER_FIELD, DEFAULT_ORDER_DIRECTION)
} else {
firebaseRepository.getReference(DEFAULT_COLLECTION_NAME, DEFAULT_ORDER_FIELD, DEFAULT_ORDER_DIRECTION, LAST_DOCUMENT!!)
}
.get()
.addOnSuccessListener { query ->
try {
LAST_DOCUMENT = query.documents.lastOrNull()
query.documents.forEach { document ->
document.toObject(ModelDTO::class.java)?.let {
it.id = document.id
it.likes_count = // TODO
it.comments_count = // TODO
it.images_uri.add(//TODO)
data.add(it.mapToEntity())
}
}
continuation.resume(Response.Success(data))
} catch (exception: Exception) {
continuation.resume(Response.Error(exception))
}
}
.addOnFailureListener { exception ->
continuation.resume(Response.Error(exception))
}
}
}
Related
This question already has answers here:
How to return value from coroutine scope
(5 answers)
Closed 8 months ago.
private fun getUserIdByEmail(email: String): String {
var userId = ""
CoroutineScope(IO).launch {
try {
val querySnapshot = user3CollectionRef
.whereEqualTo("userEmail", email)
.get()
.await()
if (querySnapshot.documents.isNotEmpty()) {
for (document in querySnapshot.documents) {
userId = document.id
}
} else {
withContext(Dispatchers.Main) {
toast("No Match")
}
}
} catch(e: Exception) {
withContext(Dispatchers.Main) {
toast(e.message.toString())
}
}
Log.e("a7a", userId)
withContext(Dispatchers.Default) {
return userId
}
}
}
That's the function I'm using to query through firebase documents, the thing is I want to return the userId which is a variable that I defined in the first line of the function, when I place the return block outside the coroutine scope, it returns an empty string, how do I return it inside the scope so it can run when the coroutine is finished, or any other way to return after the coroutine is finished
In order to make a function returns a pending value; it should be a suspend function to return the value asynchronously.
Then, instead of the launch builder, the async/await builder can be used to return a Deferredvalue.
await() waits until it's assigned a value by the Coroutine
Applying that in code:
private suspend fun getUserIdByEmail(email: String): String {
val userId = CoroutineScope(IO).async {
var tempId = ""
try {
val querySnapshot = user3CollectionRef
.whereEqualTo("userEmail", email)
.get()
.await()
if (querySnapshot.documents.isNotEmpty()) {
for (document in querySnapshot.documents) {
tempId = document.id
}
} else {
withContext(Dispatchers.Main) {
toast("No Match")
}
}
} catch(e: Exception) {
withContext(Dispatchers.Main) {
toast(e.message.toString())
}
}
Log.e("a7a", tempId)
withContext(Dispatchers.Default) {
return#async tempId
}
}
return userId.await() // waits until it's assigned a value from the Coroutine
}
I am debugging an application that communicates with an IoT device via http.
In response to commands, the device sends information in xml format.
An application can also receive binary data on a GET request.
In the functionality of the application, filling the RecyclerView from the list and loading images to fill the RecyclerView and executing individual commands to change modes.
The problem is that the device does not have the most powerful processor, and when a large number of http commands are received, the service cannot cope and hangs for a long time until the WiFi channel fails.
I can’t figure out how to organize interaction so that each next command waits for the previous one to complete. The solution is complicated by the fact that populating the RecyclerView, loading images, and executing commands are in different parts of the code, and each is executed asynchronously.
Populating RecyclerView:
private fun initViewModel(filter: String) {
val st = Storage(requireContext())
val cache = "${st.externalCacheDir}/$filter/"
val viewModel = ViewModelProvider(this).get(DeviceListViewModel::class.java)
viewModel.getRecycerListObserver().observe(requireActivity(), Observer<ResponseData> {
if (it != null) {
val media = it.mediaData?.filter { it.mediaData?.fPath!!.contains(filter, false) }
mediaList = arrayListOf()
if (media != null) {
for (i in media.sortedByDescending { it.mediaData?.fTimeCode }) {
i.mediaData?.let { it1 -> mediaList.add(it1) }
}
}
viewModel.recyclerListLiveData = MutableLiveData()
ThumbDownloader(dataAdapter, mediaList, cache, swipeLayout).execute()
} else {
Toast.makeText(activity, "Error in getting data", Toast.LENGTH_SHORT).show()
}
})
viewLifecycleOwner.lifecycleScope.launch {
viewModel.makeApiCall()
}
}
ViewModel:
class DeviceListViewModel : ViewModel() {
var recyclerListLiveData: MutableLiveData<ResponseData>
init {
recyclerListLiveData = MutableLiveData()
}
fun getRecycerListObserver(): MutableLiveData<ResponseData> {
return recyclerListLiveData
}
fun makeApiCall() {
viewModelScope.launch(Dispatchers.IO) {
try {
val retroInstance =
RetroInstance.getRetroInstance(MainActivity.BaseUrl).create(RetroService::class.java)
val response = retroInstance.getDataFromApi(1, Cmd.WIFIAPP_CMD_FILELIST)
recyclerListLiveData.postValue(response)
} catch (e: Exception) {
var response: ResponseData? = null
when (e) {
is ConnectException -> {
recyclerListLiveData.postValue(response)
}
is SocketTimeoutException -> {
recyclerListLiveData.postValue(response)
}
}
}
}
}
}
Service to make a command (processing results in the Handler):
class DeviceService {
private val handler: Handler
private var mJob: Job? = null
constructor(handler: Handler) {
this.handler = handler
}
fun sendCommand(cmd: Int) {
val service = RetroInstance.buildService(MainActivity.BaseUrl, RetroService::class.java)
mJob = CoroutineScope(Dispatchers.IO).launch {
val response = when (cmd) {
Cmd.WIFIAPP_CMD_MOVIE_GET_LIVEVIEW_FMT -> {
try {
service.getLinkFromApi(1, cmd)
} catch (e: Exception) {
handler.obtainMessage(Msg.MESSAGE_TOAST, "Error in getting data").sendToTarget()
mJob?.cancel()
}
}
else -> {
try {
service.makeCommand(1, cmd)
} catch (e: Exception) {
handler.obtainMessage(Msg.MESSAGE_TOAST, "Error in getting data").sendToTarget()
mJob?.cancel()
}
}
}
withContext(Dispatchers.Main) {
try {
when (cmd) {
Cmd.WIFIAPP_CMD_MOVIE_GET_LIVEVIEW_FMT -> {
handler.obtainMessage(Msg.MESSAGE_LINK_FORMAT, response).sendToTarget()
}
else -> {
handler.obtainMessage(Msg.MESSAGE_PAR_FUNCTION, response).sendToTarget()
}
}
} catch (e: Exception) {
when (e) {
is ConnectException -> {
handler.obtainMessage(Msg.MESSAGE_TOAST, "Connection lost").sendToTarget()
}
is SocketTimeoutException -> {
handler.obtainMessage(Msg.MESSAGE_TOAST, "Connection lost").sendToTarget()
}
}
}
mJob?.cancelAndJoin()
}
}
}
}
Downloading a images:
class ThumbDownloader(dataAdapter: DeviceAdapter, data: ArrayList<MediaData>, file_path: String, swipe: SwipeRefreshLayout) : CoroutineScope {
private var job: Job = Job()
private var file_path: String
private var dataAdapter: DeviceAdapter
private var data: ArrayList<MediaData>
private var swipe: SwipeRefreshLayout
init {
this.data = data
this.file_path = file_path
this.dataAdapter = dataAdapter
this.swipe = swipe
}
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
fun cancel() {
job.cancel()
}
fun execute() = async {
var item: File? = null
for (i in data) {
val task = async(Dispatchers.IO) {
val url = i.fPath!!
val real_url = "${MainActivity.BaseUrl}$url"
item = NetworkUtil.downloadFile(real_url, file_path, false)
}
task.await()
if (item != null) {
dataAdapter.insertItem(i)
}
}
cancel()
swipe.isRefreshing = false
}
}
Any ideas how to come up with their synchronization while waiting for the previous commands to complete?
Hello I tried to make Coroutines Flow (using callbackFlow) and tried to convert it to live data, but it seems it's not updating.
You can see my code below:
#ExperimentalCoroutinesApi
suspend fun checkInDanger(): Flow<NetworkStatus<List<UserFire>>> = callbackFlow {
val check = fs.collection(UserFire.COLLECTION).whereEqualTo(UserFire.DANGER, true)
.addSnapshotListener { value, e ->
if (e != null) {
trySend(NetworkStatus.Failed("Error occurred\n${e.code}"))
return#addSnapshotListener
}
if (value == null || value.isEmpty) trySend(NetworkStatus.Empty)
else {
val users = value.map { it.toObject(UserFire::class.java) }
trySend(NetworkStatus.Success(users))
}
}
awaitClose { }
}.flowOn(Dispatchers.IO)
On my repositories:
#ExperimentalCoroutinesApi
override suspend fun checkInDanger(): Flow<Status<List<User>>> = flow {
when (val result = network.checkInDanger().first()) {
is NetworkStatus.Success -> emit(Status.Success(result.data.map {
MapVal.userFireToDom(it)
}))
is NetworkStatus.Empty -> emit(Status.Success(listOf<User>()))
is NetworkStatus.Failed -> emit(Status.Error(null, result.error))
}
}
In my ViewModel:
val checkInDanger = liveData(Dispatchers.IO) {
try {
useCase.checkInDanger().collectLatest {
emit(it)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
But when I changed the value in my Firebase, it's not fetching new data...
Anyone know why it's not fixed
I kind of find the way, but it's with callback, if we used callback, it can fetch data update even without live-data, but for my case I tried to push the callback to live data again,
so the code will be like this:
fun checkInDanger(networkStatus: (NetworkStatus<List<UserFire>>) -> Unit) {
fs.collection(UserFire.COLLECTION).whereEqualTo(UserFire.DANGER, true)
.addSnapshotListener { value, e ->
if (e != null) {
networkStatus(NetworkStatus.Failed("Error occurred\n${e.code}"))
return#addSnapshotListener
}
if (value == null || value.isEmpty) networkStatus(NetworkStatus.Empty)
else {
val users = value.map { it.toObject(UserFire::class.java) }
networkStatus(NetworkStatus.Success(users))
}
}
}
In my repositories:
override fun checkInDanger(callback: (Status<List<User>>) -> Unit) {
network.checkInDanger { result ->
when (result) {
is NetworkStatus.Success -> callback(Status.Success(result.data.map {
MapVal.userFireToDom(it)
}))
is NetworkStatus.Empty -> callback(Status.Success(listOf<User>()))
is NetworkStatus.Failed -> callback(Status.Error(null, result.error))
}
}
}
In my ViewModel:
fun checkInDanger(callback: (Status<List<User>>) -> Unit) = useCase.checkInDanger { callback(it) }
val setUsers = MutableLiveData<List<User>>().apply { this.value = listOf() }
val users: LiveData<List<User>> = setUsers
In my UI Class (Fragment Main):
val inDangerCallback: (Status<List<User>>) -> Unit = {
if (relative != null) {
when (it) {
is Status.Success ->
viewModel.setUsers.value =
it.data?.filter { user -> user.username in relative!!.pure }
else -> requireView().createSnackBar(it.error!!, 1000)
}
}
}
viewModel.checkInDanger(inDangerCallback)
viewModel.users.observe(viewLifecycleOwner) { users ->
println(users.size})
users?.forEach { user -> println(user.username) }
}
And the code can run perfectly and update automatically...
This question already has answers here:
Collect from several stateflows
(4 answers)
Closed 1 year ago.
This code invokes two methods on the same viewmodel and listens for updates. But only the first method completes, the second does not event trigger.
private fun initData() {
lifecycleScope.launchWhenStarted {
viewModel.incrementCount().collect {
info { "Count: $it" }
}
viewModel.getAllTeams().collect {
when (it) {
is State.Success -> {
info { "Got teams with size: ${it.result}.size" }
}
is State.Error -> {
info { "Error getting teams: ${it.message}" }
}
State.Loading -> {
info { "Loading all teams" }
}
}
}
}
}
ViewModel
class DashboardViewModel : ViewModel(), com.droid.common.Logger {
fun incrementCount(): MutableStateFlow<Int> {
val countState = MutableStateFlow(0)
viewModelScope.launch {
repeat(5) {
countState.value = (it)
delay(1000)
}
}
return countState
}
fun getAllTeams(): MutableStateFlow<State> {
val state = MutableStateFlow<State>(State.None)
state.value = State.Loading
viewModelScope.launch {
try {
val allTeams = FootballAPIClient.apiService.getAllTeams()
state.value = State.Success(allTeams)
} catch (exception: Exception) {
error { "Error getting all teams: ${exception.message}" }
state.value = State.Error(exception.message.toString())
}
}
return state
}
However, calling them with separate lifecycleScope works
private fun initData() {
lifecycleScope.launchWhenStarted {
viewModel.incrementCount().collect {
info { "Count: $it" }
}
}
lifecycleScope.launchWhenStarted {
viewModel.getAllTeams().collect {
when (it) {
is State.Success -> {
info { "Got teams with size: ${it.result}.size" }
}
is State.Error -> {
info { "Error getting teams: ${it.message}" }
}
State.Loading -> {
info { "Loading all teams" }
}
}
}
}
}
I can't seem to understand this behavior, anybody knows why?
You will need different coroutines, since collect() is a suspending function that suspends until your Flow terminates.
The problem with launchWhenStarted is that while your newly emitted items will not be processed your producer will still run in the background.
For collecting multiple flows the currently recommended way is:
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.incrementCount().collect { ... }
}
launch {
viewModel.getAllTeams().collect { ... }
}
}
}
I'm having trouble with Kotlin coroutines, I'm trying to query a database and return the results to the main thread, but I can't figure out how to do so without "freezing" my main thread. In this case runBlocking would be the culprit, but I'm unsure what to substitute it for. Any and all help is greatly appreciated!
fun getResults() : List<String> {
val results = ArrayList<String>()
runBlocking {
viewModelScope.launch(Dispatchers.IO) {
openConnection()
try {
statement = connection!!.createStatement()
resultSet = statement!!.executeQuery("blah blah blah")
while (resultSet != null && resultSet!!.next()) {
results.add(resultSet!!.getString(1))
}
} catch (e: Exception) {
Log.d("getList Catch", e.toString())
}
closeConnection()
}.join()
}
return results
}
runBlocking is unneeded when scoping the blocking task appropriately in tandem with a suspendable coroutine or a callback.
You have a couple options as mentioned:
Suspendable coroutine
suspend fun getResults() : List<String> = suspendCancellableCoroutine { cont ->
openConnection()
val results = ArrayList<String>()
try {
statement = connection!!.createStatement()
resultSet = statement!!.executeQuery("blah blah blah")
while (resultSet != null && resultSet!!.next()) {
results.add(resultSet!!.getString(1))
}
closeConnection()
cont.resumeWith(Result.success(results))
} catch (e: Exception) {
Log.d("getList Catch", e.toString())
closeConnection()
cont.resumeWith(Result.failure(e))
}
}
anonymous callback
fun getResults(cb: (List<String>) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
openConnection()
val results = ArrayList<String>()
try {
statement = connection!!.createStatement()
resultSet = statement!!.executeQuery("blah blah blah")
while (resultSet != null && resultSet!!.next()) {
results.add(resultSet!!.getString(1))
}
} catch (e: Exception) {
Log.d("getList Catch", e.toString())
}
cb.invoke(results)
closeConnection()
}
}
My personal preference is the 2nd option as it removes the need for the consumer of the method to have to be aware and implement suspendable.
launch(Dispatchers.IO) {
val results = getResults()
// handle results
}
vs.
getResults {
// handle results
}