I am new to coroutines, and I am trying to load in several files from internal storage on Dispatchers.IO along with some other things. So I am starting a coroutine and then calling several suspend functions each of which uses withContext to read its file. But in the first function, withContext runs its code and never returns, so the rest of the coroutine never gets run. Is there something special about withContext or Dispatchers.IO which I am missing?
class MainViewModel(
application: Application
) : AndroidViewModel(application) {
private val liveSettings: MutableLiveData<Settings>
suspend fun loadSettings(context: Context): Settings = withContext(Dispatchers.IO) {
var settings: Settings
try {
context.openFileInput(FileManager.settingsDir).use { stream ->
val jsonData = stream.readBytes().decodeToString()
settings = Json.decodeFromString(Settings.serializer(), jsonData)
}
} catch (e: IOException){
settings = Settings(mutableListOf(), mutableListOf())
}
Log.d("MainViewModel", "Loading Settings") // this line is run
settings
}
init{
Log.i("MainViewModel", "ViewModel Constructed")
liveSettings = MutableLiveData()
viewModelScope.launch {
Log.d("MainViewModel", "constructor coroutine started")
liveSettings.value = loadSettings(getApplication())
Log.d("MainViewModel", "Hello") // this line is not run
// other stuff...
}
}
}
I expect an output like
ViewModel Constructed
constructor coroutine started
Loading Settings
Hello
but the output is
ViewModel Constructed
constructor coroutine started
Loading Settings
My guess would be that that the app is crashing and you don't realize it. Double check logcat. You cannot set a live data value outside the main thread. You should use postValue instead:
viewModelScope.launch {
Log.d("MainViewModel", "constructor coroutine started")
// This is launched on an non UI thread - use postValue
liveSettings.postValue(loadSettings(getApplication()))
Log.d("MainViewModel", "Hello") // this line is not run
// other stuff...
}
Related
I've been mulling this over for some time now and I just can't get it to work.
So in brief, I have a Splash Activity from where I call another activity that contains my ViewModel. The ViewModel in simple terms just needs to sequentially run function A(which is getfbdata below; it is a network call.). And only after this function completes, it should run function B (which is dosavefbdata below; save info to DB.). Again, it should wait for function B to complete before running the main thread function, function C(which is confirm first below; it checks whether function B has completed by getting the result from function B (dosavefbdata below). If function C is positive, it closes the Splash activity.
Suffice to say, none of the above works. Println results show all functions were run sequentially without waiting for each to complete. Lastly, SplashActivity().killActivity() call on function C did not work.
Note: withContext does not require to await() on the suspended functions right? I also tried using viewModelScope.async instead of viewModelScope.launch.
I would really appreciate your help here. Thanks in advance.
*Under SplashActivity:
fun killActivity(){
finish()
}
*Under onCreate(SplashActivity):
CoroutingClassViewModel(myc).initialize()
**
class CoroutingClassViewModel(val myc: Context): ViewModel() {
fun initialize() {
viewModelScope.launch(Dispatchers.Main) {
try {
val fbdata = withContext(Dispatchers.IO) { getfbdata() }
val test1 = withContext(Dispatchers.IO) { test1(fbdata) }
val savedfbdata = withContext(Dispatchers.IO) { dosavefbdata(fbdata,myc) }
val confirmfirst = { confirmfunc(savedfbdata,myc) }
println("ran savedfbdata.")
} catch (exception: Exception) {
Log.d(TAG, "$exception handled !")
}
}
}
fun confirmfunc(savedfbdata: Boolean, myc: Context){
if (savedfbdata==true){
SplashActivity().killActivity()
}
}
suspend fun getfbdata(): MutableList<FirebaseClass> {
return withContext(Dispatchers.IO) {
//perform network call
return#withContext fbdata
}
}
suspend fun dosavefbdata(fbdata: MutableList<FirebaseClass>,myc: Context): Boolean{
return withContext(Dispatchers.IO) {
//save to database
return#withContext true
}
}
suspend fun test1(fbdata: MutableList<FirebaseClass>){
return withContext(Dispatchers.IO) {
println("test1: fbdata is: $fbdata")
}
}
}
Use AndroidViewModel if you want to have Context in it:
class CoroutingClassViewModel(myc: Application) : AndroidViewModel(myc) { ... }
In onCreate method of SplashActivity activity instantiate the view model like this:
val vm = ViewModelProvider(this)[CoroutingClassViewModel::class.java]
vm.initialize()
In CoroutingClassViewModel class create LiveData object to notify activity about operations completion:
val completion = MutableLiveData<Boolean>()
fun confirmfunc(savedfbdata: Boolean, myc: Context) {
if (savedfbdata) {
completion.postValue(true)
}
}
In your SplashActivity use this code to observe completion:
vm.completion.observe(this, Observer {
if (it) killActivity()
})
You use withContext(Dispatchers.IO) function two times for the same operation. Don't do that. For example in this code:
val fbdata = withContext(Dispatchers.IO) { getfbdata() }
if we look at getfbdata function we see that function withContext(Dispatchers.IO) is already called there. So get rid of repeated calls:
val fbdata = getfbdata()
I had same issue with withContext(Dispatcher.IO), I thought that switching coroutine context doesn't work, while in fact in splash screen i launched super long operation on Dispatcher.IO, then later when trying to use the same Dispatcher.IO it didn't work or in other words it waited until the first work in splash screen finished then started the new work.
I have a file managing class that can save a big file. The file manager class is an application singleton, so it outlives my UI classes. My Activity/Fragment can call the save suspend function of the file manager from a coroutine and then show success or failure in the UI. For example:
//In MyActivity:
private fun saveTheFile() = lifecycleScope.launch {
try {
myFileManager.saveBigFile()
myTextView.text = "Successfully saved file"
} catch (e: IOException) {
myTextView.text = "Failed to save file"
}
}
//In MyFileManager
suspend fun saveBigFile() {
//Set up the parameters
//...
withContext(Dispatchers.IO) {
//Save the file
//...
}
}
The problem with this approach is that I don't want the save operation to be aborted if the Activity is finished. If the activity is destroyed before the withContext block gets going, or if the withContext block has any suspension points in it, then saving will not be completed because the coroutine will be canceled.
What I want to happen is that the file is always saved. If the Activity is still around, then we can show UI updates on completion.
I thought one way to do it might be to start a new coroutineScope from the suspend function like this, but this scope still seems to get cancelled when its parent job is cancelled.
suspend fun saveBigFile() = coroutineScope {
//...
}
I thought another alternative might be to make this a regular function that updates some LiveData when it's finished. The Activity could observe the live data for the result, and since LiveData automatically removes lifecycle observers when they're destroyed, the Activity is not leaked to the FileManager. I'd like to avoid this pattern if the something less convoluted like the above can be done instead.
//In MyActivity:
private fun saveTheFile() {
val result = myFileManager.saveBigFile()
result.observe(this#MyActivity) {
myTextView.text = when (it) {
true -> "Successfully saved file"
else -> "Failed to save file"
}
}
}
//In MyFileManager
fun saveBigFile(): LiveData<Boolean> {
//Set up the parameters
//...
val liveData = MutableLiveData<Boolean>()
MainScope().launch {
val success = withContext(Dispatchers.IO) {
//Save the file
//...
}
liveData.value = success
}
return liveData
}
You can wrap the bit that you don't want to be cancelled with NonCancellable.
// May cancel here.
withContext(Dispatchers.IO + NonCancellable) {
// Will complete, even if cancelled.
}
// May cancel here.
If you have code whose lifetime is scoped to the lifetime of the whole application, then this is a use case for the GlobalScope. However, just saying GlobalScope.launch is not a good strategy because you could launch several concurrent file operations that may be in conflict (this depends on your app's details). The recommended way is to use a globally-scoped actor, in the role of an executor service.
Basically, you can say
#ObsoleteCoroutinesApi
val executor = GlobalScope.actor<() -> Unit>(Dispatchers.IO) {
for (task in channel) {
task()
}
}
And use it like this:
private fun saveTheFile() = lifecycleScope.launch {
executor.send {
try {
myFileManager.saveBigFile()
withContext(Main) {
myTextView.text = "Successfully saved file"
}
} catch (e: IOException) {
withContext(Main) {
myTextView.text = "Failed to save file"
}
}
}
}
Note that this is still not a great solution, it retains myTextView beyond its lifetime. Decoupling the UI notifications from the view is another topic, though.
actor is labeled as "obsolete coroutines API", but that's just an advance notice that it will be replaced with a more powerful alternative in a future version of Kotlin. It doesn't mean it's broken or unsupported.
I tried this, and it appears to do what I described that I wanted. The FileManager class has its own scope, though I suppose it could also be GlobalScope since it's a singleton class.
We launch a new job in its own scope from the coroutine. This is done from a separate function to remove any ambiguity about the scope of the job. I use async
for this other job so I can bubble up exceptions that the UI should respond to.
Then after launch, we await the async job back in the original scope. await() suspends until the job is completed and passes along any throws (in my case I want IOExceptions to bubble up for the UI to show an error message). So if the original scope is cancelled, its coroutine never waits for the result, but the launched job keeps rolling along until it completes normally. Any exceptions that we want to ensure are always handled should be handled within the async function. Otherwise, they won't bubble up if the original job is cancelled.
//In MyActivity:
private fun saveTheFile() = lifecycleScope.launch {
try {
myFileManager.saveBigFile()
myTextView.text = "Successfully saved file"
} catch (e: IOException) {
myTextView.text = "Failed to save file"
}
}
class MyFileManager private constructor(app: Application):
CoroutineScope by MainScope() {
suspend fun saveBigFile() {
//Set up the parameters
//...
val deferred = saveBigFileAsync()
deferred.await()
}
private fun saveBigFileAsync() = async(Dispatchers.IO) {
//Save the file
//...
}
}
I have some code below. Delay (3000) is just replacement for a long loop (or cycle). I’m expecting that after completion of loop println(res) will print “Some String” and then enable button. But in real life println(res) prints an empty string and button became enabled at same time when I click it.
My question is: how I can wait for end of a coroutine and only after completion of the coroutine run println(res) and button.isEnabled = true.
private var res: String = ""
private suspend fun test(): String {
delay(3000) // delay - just replacement for long loop
return "Some String" // String received after loop
}
fun onClick(view: View) {
res = ""
button.isEnabled = false
GlobalScope.launch {
res = withContext(Dispatchers.Default) {
test()
}
}
println(res) // 1. trying to get string received after loop, but not working
button.isEnabled = true // 2. button must be enabled after loop in cycle, but it's not waiting till end of loop
}
The main thing to understand here is that code within coroutine is by default executed sequentially.
I.e. coroutine is executed asynchronously in relation to "sibling" code, but code within coroutine executes synchronously by default.
For example:
fun DoSometing () {
coroutineA {
doSomethingA1()
doSomethingA2()
}
some additional code
}
Corroutine A will execute async in relation to some additional code but doSometingA2 will be executed after doSomethingA1 is done.
That means, that within a coroutine every next piece of code will be executed after the previous one is done.
So, whatever you want to execute when coroutine is done, you just put at the end of that coroutine and declare context (withContext) in which you want to execute it.
The exception is of course if you start another async piece of code within coroutine (like another coroutine).
EDIT: If you need to update UI from the coroutine, you should execute that on the main context, i.e. you'll have something like this:
GlobalScope.launch (Dispatchers.IO) {
//do some background work
...
withContext (Dispatchers.Main) {
//update the UI
button.isEnabled=true
...
}
}
You can try some thing like this:
suspend fun saveInDb() {
val value = GlobalScope.async {
delay(1000)
println("thread running on [${Thread.currentThread().name}]")
10
}
println("value = ${value.await()} thread running on [${Thread.currentThread().name}]")
}
await will wait for the coroutine to finish and then run code below it
fun onClick(view: View) {
res = ""
button.isEnabled = false
GlobalScope.launch(Dispatchers.Main){ // launches coroutine in main thread
updateUi()
}
}
suspend fun updateUi(){
val value = GlobalScope.async { // creates worker thread
res = withContext(Dispatchers.Default) {
test()
}
}
println(value.await()) //waits for workerthread to finish
button.isEnabled = true //runs on ui thread as calling function is on Dispatchers.main
}
launch is for situations where you don't care about the result outside the coroutine. To retrieve the result of a coroutine use async.
val res = GlobalScope.async(Dispatchers.Default) { test() }.await()
Note: avoid using GlobalScope, provide your own CoroutineScope instead.
why you don't move println and button.isEnabled inside GlobalScope.launch coroutine.
fun onClick(view: View) {
res = ""
button.isEnabled = false
GlobalScope.launch {
val res = withContext(Dispatchers.Default) {
test()
}
println(res)
button.isEnabled = true
}
}
if you whant your code run on main thread add Dispatchers.Main as an argument.
GlobalScope.launch(Dispatchers.Main) {
val res = withContext(Dispatchers.Default) {
test()
}
println(res)
button.isEnabled = true
}
now println and button.isEnabled run on main thread and test() fun runs on Default which in real is a worker thread.
Use the Job.join(): Unit method to wait for a coroutine to finish before continuing with current the thread:
//launch coroutine
var result = ""
val request = launch {
delay(500)
result = "Hello, world!"
}
//join coroutine with current thread
request.join()
//print "Hello, world!"
println(result)
I'm new to coroutines and I'm wondering if it's possible to switch from coroutineScope (GlobalScope) to UI scope for the code below. My problem is that the steps inside the coroutine launch body must be executed in a worker thread, otherwise the listener notification must be executed in the ui thread in order to avoid to call runOnUiThread in my activity code.
override suspend fun startRent(name: String, bikeMode: BikeMode, listener: StartRentListener) {
var bleDevice : RxBleDevice
val scanFilter: ScanFilter = ScanFilter.Builder().setDeviceName(name).build()
val scanSettings: ScanSettings = ScanSettings.Builder().build()
val job = GlobalScope.launch {
try {
bleDevice = rxBleClient.scanBleDevicesExt(rxBleClient, scanSettings, scanFilter)
val bleConnection = bleDevice.establishConnectionExt()
// write handshake
connectionManager.writeHandshake(bleDevice, bleConnection)
// open lock
openLock(bleDevice, bikeMode, bleConnection)
// getting user position
apiHelper.sendLockRequest(bleDevice.name, getPosition())
bleDevice.disconnect()
// this should be called on main thread once all the previous operations are finished
listener.onSuccess()
} catch (e: Exception) {
listener.onError(e)
}
}
job.join()
}
A snippet of my current activity code:
bikeAccessClient.startRent(bikeBLEName, BikeMode.HYBRID, object :
StartRentListener {
override fun onSuccess() {
runOnUiThread {
// UI update here
}
}
You may use withContext(Dispatchers.Main) {..} function to execute a part of your code with the other Coroutine Dispatcher.
kotlinx.coroutines.android contains the definition of the Dispatchers.Main function and it integrates correctly with Android UI.
Using explicit Dispatcher in your code is quite error-prone. Instead, I would recommend designing the code with fewer explicit requirements.
I would wrote something like that:
fun uiActionHandlerToStartTheProcess() {
launch(Dispatchers.Main) {
val result = startRent(...) // no callback here, suspend function
//UI Update Here
}
}
suspend fun CoroutineScope.startRent() : SomeResultOfWork {
//that function offloads the execution to a IO (aka brackground) thread
return withContext(Dispatchers.IO){
//here goes your code from `startRent`
//use `suspendCancellableCoroutine {cont -> .. }` if you need to handle callbacks from it
SomeResultOfWork()
}
The code in the launch(Dispatchers.Main){..} block is executed in the UI thread. The call to startRent suspend function suspends the execution in the UI thread. Once the startRent is ready with the reply (from a background thread) it resumes the execution (which is done by the Dispatchers.Main and equivalent to the runOnUiThread {...}) and executes the UI update from the right thread
I'm trying to update a list inside the adapter using async, I can see there is too much boilerplate.
Is it the right way to use Kotlin Coroutines?
can this be optimized more?
fun loadListOfMediaInAsync() = async(CommonPool) {
try {
//Long running task
adapter.listOfMediaItems.addAll(resources.getAllTracks())
runOnUiThread {
adapter.notifyDataSetChanged()
progress.dismiss()
}
} catch (e: Exception) {
e.printStackTrace()
runOnUiThread {progress.dismiss()}
} catch (o: OutOfMemoryError) {
o.printStackTrace()
runOnUiThread {progress.dismiss()}
}
}
After struggling with this question for days, I think the most simple and clear async-await pattern for Android activities using Kotlin is:
override fun onCreate(savedInstanceState: Bundle?) {
//...
loadDataAsync(); //"Fire-and-forget"
}
fun loadDataAsync() = async(UI) {
try {
//Turn on busy indicator.
val job = async(CommonPool) {
//We're on a background thread here.
//Execute blocking calls, such as retrofit call.execute().body() + caching.
}
job.await();
//We're back on the main thread here.
//Update UI controls such as RecyclerView adapter data.
}
catch (e: Exception) {
}
finally {
//Turn off busy indicator.
}
}
The only Gradle dependencies for coroutines are: kotlin-stdlib-jre7, kotlinx-coroutines-android.
Note: Use job.await() instead of job.join() because await() rethrows exceptions, but join() does not. If you use join() you will need to check job.isCompletedExceptionally after the job completes.
To start concurrent retrofit calls, you can do this:
val jobA = async(CommonPool) { /* Blocking call A */ };
val jobB = async(CommonPool) { /* Blocking call B */ };
jobA.await();
jobB.await();
Or:
val jobs = arrayListOf<Deferred<Unit>>();
jobs += async(CommonPool) { /* Blocking call A */ };
jobs += async(CommonPool) { /* Blocking call B */ };
jobs.forEach { it.await(); };
How to launch a coroutine
In the kotlinx.coroutines library you can start new coroutine using either launch or async function.
Conceptually, async is just like launch. It starts a separate coroutine which is a light-weight thread that works concurrently with all the other coroutines.
The difference is that launch returns a Job and does not carry any resulting value, while async returns a Deferred - a light-weight non-blocking future that represents a promise to provide a result later. You can use .await() on a deferred value to get its eventual result, but Deferred is also a Job, so you can cancel it if needed.
Coroutine context
In Android we usually use two context:
uiContext to dispatch execution onto the Android main UI thread (for the parent coroutine).
bgContext to dispatch execution in background thread (for the child coroutines).
Example
//dispatches execution onto the Android main UI thread
private val uiContext: CoroutineContext = UI
//represents a common pool of shared threads as the coroutine dispatcher
private val bgContext: CoroutineContext = CommonPool
In following example we are going to use CommonPool for bgContext which limit the number of threads running in parallel to the value of Runtime.getRuntime.availableProcessors()-1. So if the coroutine task is scheduled, but all cores are occupied, it will be queued.
You may want to consider using newFixedThreadPoolContext or your own implementation of cached thread pool.
launch + async (execute task)
private fun loadData() = launch(uiContext) {
view.showLoading() // ui thread
val task = async(bgContext) { dataProvider.loadData("Task") }
val result = task.await() // non ui thread, suspend until finished
view.showData(result) // ui thread
}
launch + async + async (execute two tasks sequentially)
Note: task1 and task2 are executed sequentially.
private fun loadData() = launch(uiContext) {
view.showLoading() // ui thread
// non ui thread, suspend until task is finished
val result1 = async(bgContext) { dataProvider.loadData("Task 1") }.await()
// non ui thread, suspend until task is finished
val result2 = async(bgContext) { dataProvider.loadData("Task 2") }.await()
val result = "$result1 $result2" // ui thread
view.showData(result) // ui thread
}
launch + async + async (execute two tasks parallel)
Note: task1 and task2 are executed in parallel.
private fun loadData() = launch(uiContext) {
view.showLoading() // ui thread
val task1 = async(bgContext) { dataProvider.loadData("Task 1") }
val task2 = async(bgContext) { dataProvider.loadData("Task 2") }
val result = "${task1.await()} ${task2.await()}" // non ui thread, suspend until finished
view.showData(result) // ui thread
}
How to cancel a coroutine
The function loadData returns a Job object which may be cancelled. When the parent coroutine is cancelled, all its children are recursively cancelled, too.
If the stopPresenting function was called while dataProvider.loadData was still in progress, the function view.showData will never be called.
var job: Job? = null
fun startPresenting() {
job = loadData()
}
fun stopPresenting() {
job?.cancel()
}
private fun loadData() = launch(uiContext) {
view.showLoading() // ui thread
val task = async(bgContext) { dataProvider.loadData("Task") }
val result = task.await() // non ui thread, suspend until finished
view.showData(result) // ui thread
}
The complete answer is available in my article Android Coroutine Recipes
I think you can get rid of runOnUiThread { ... } by using UI context for Android applications instead of CommonPool.
The UI context is provided by the kotlinx-coroutines-android module.
We also have another option. if we use Anko library , then it looks like this
doAsync {
// Call all operation related to network or other ui blocking operations here.
uiThread {
// perform all ui related operation here
}
}
Add dependency for Anko in your app gradle like this.
implementation "org.jetbrains.anko:anko:0.10.5"
Like sdeff said, if you use the UI context, the code inside that coroutine will run on UI thread by default. And, if you need to run an instruction on another thread you can use run(CommonPool) {}
Furthermore, if you don't need to return nothing from the method, you can use the function launch(UI) instead of async(UI) (the former will return a Job and the latter a Deferred<Unit>).
An example could be:
fun loadListOfMediaInAsync() = launch(UI) {
try {
withContext(CommonPool) { //The coroutine is suspended until run() ends
adapter.listOfMediaItems.addAll(resources.getAllTracks())
}
adapter.notifyDataSetChanged()
} catch(e: Exception) {
e.printStackTrace()
} catch(o: OutOfMemoryError) {
o.printStackTrace()
} finally {
progress.dismiss()
}
}
If you need more help I recommend you to read the main guide of kotlinx.coroutines and, in addition, the guide of coroutines + UI
If you want to return some thing from background thread use async
launch(UI) {
val result = async(CommonPool) {
//do long running operation
}.await()
//do stuff on UI thread
view.setText(result)
}
If background thread is not returning anything
launch(UI) {
launch(CommonPool) {
//do long running operation
}.await()
//do stuff on UI thread
}
All the above answers are right, but I was having a hard time finding the right import for the UI from kotlinx.coroutines, it was conflicting with UI from Anko.
Its
import kotlinx.coroutines.experimental.android.UI
Here's the right way to use Kotlin Coroutines. Coroutine scope simply suspends the current coroutine until all child coroutines have finished their execution. This example explicitly shows us how child coroutine works within parent coroutine.
An example with explanations:
fun main() = blockingMethod { // coroutine scope
launch {
delay(2000L) // suspends the current coroutine for 2 seconds
println("Tasks from some blockingMethod")
}
coroutineScope { // creates a new coroutine scope
launch {
delay(3000L) // suspends this coroutine for 3 seconds
println("Task from nested launch")
}
delay(1000L)
println("Task from coroutine scope") // this line will be printed before nested launch
}
println("Coroutine scope is over") // but this line isn't printed until nested launch completes
}
Hope this helps.
Please find attached the implementation for a remote API call with Kotlin Coroutines & Retrofit library.
import android.view.View
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.test.nyt_most_viewed.NYTApp
import com.test.nyt_most_viewed.data.local.PreferenceHelper
import com.test.nyt_most_viewed.data.model.NytAPI
import com.test.nyt_most_viewed.data.model.response.reviews.ResultsItem
import kotlinx.coroutines.*
import javax.inject.Inject
class MoviesReviewViewModel #Inject constructor(
private val nytAPI: NytAPI,
private val nytApp: NYTApp,
appPreference: PreferenceHelper
) : ViewModel() {
val moviesReviewsResponse: MutableLiveData<List<ResultsItem>> = MutableLiveData()
val message: MutableLiveData<String> = MutableLiveData()
val loaderProgressVisibility: MutableLiveData<Int> = MutableLiveData()
val coroutineJobs = mutableListOf<Job>()
override fun onCleared() {
super.onCleared()
coroutineJobs.forEach {
it.cancel()
}
}
// You will call this method from your activity/Fragment
fun getMoviesReviewWithCoroutine() {
viewModelScope.launch(Dispatchers.Main + handler) {
// Update your UI
showLoadingUI()
val deferredResult = async(Dispatchers.IO) {
return#async nytAPI.getMoviesReviewWithCoroutine("full-time")
}
val moviesReviewsResponse = deferredResult.await()
this#MoviesReviewViewModel.moviesReviewsResponse.value = moviesReviewsResponse.results
// Update your UI
resetLoadingUI()
}
}
val handler = CoroutineExceptionHandler { _, exception ->
onMoviesReviewFailure(exception)
}
/*Handle failure case*/
private fun onMoviesReviewFailure(throwable: Throwable) {
resetLoadingUI()
Log.d("MOVIES-REVIEWS-ERROR", throwable.toString())
}
private fun showLoadingUI() {
setLoaderVisibility(View.VISIBLE)
setMessage(STATES.INITIALIZED)
}
private fun resetLoadingUI() {
setMessage(STATES.DONE)
setLoaderVisibility(View.GONE)
}
private fun setMessage(states: STATES) {
message.value = states.name
}
private fun setLoaderVisibility(visibility: Int) {
loaderProgressVisibility.value = visibility
}
enum class STATES {
INITIALIZED,
DONE
}
}