Android: Where to schedule a job? - android

I'm using LiveData+Room+ViewModel to build a simple kotlin app. The main activity (which presents a list) is getting the required data from a ViewModel which is getting the info from a database (the data is transformed before being consumed by the activity). Now, I need to allow the user to refresh the data through a swipe. When that happens, the app should check if the current connection can be used for that and if it can't, then the app should schedule a job.
Currently, I'm delegating this work (check the current connection and the eventual job scheduling) to my ViewModel. It looks like this:
fun tryToRefreshDataFromService(){
//first, check if there's network
//If there is, call web service and then update db
//if no network, schedule a job and try to refresh from the database
if(canGetDataFromNetwork()){
Timber.d("With network access, getting data from web services")
WebServiceAsyncTask(newsManager).execute()
}
else{
//schedule job for refreshing
//no network access, setting up job
Timber.d("No network access, setting up job")
scheduleJob()
}
}
The activity will then be able to call the method from within a helper method (which handles the swiper refresh event):
private fun recoverDataForTabs(swiper: SwipeRefreshLayout? = null){
_swiper = swiper //for clearing
_viewModel.tryToRefreshDataFromService()
}
However, it seems like this is really a bad idea because it seems like ViewModels shouldn't know anything about Android framework classes (and that ends up being required for this case). So, does this mean that I should update my code so that the network checking + job scheduling is done from the activity?
Thanks

You can inject framework-related objects into your ViewModels. For example:
class MyViewModel(val networkChecker: IMyNetworkChecker, val jobSetter: IMyJobSetter, ...) {
fun tryToRefreshDataFromService(){
if(networkChecker.canGetDataFromNetwork()){
Timber.d("With network access, getting data from web services")
WebServiceAsyncTask(newsManager).execute()
}
else{
Timber.d("No network access, setting up job")
jobSetter.scheduleJob()
}
}
}

Related

AuthStateListener callback is not triggering after reloading the firebase user on Android

I'm having a little issue with FirebaseAuth.AuthStateListener while working with email verification on Firebase. I've verified my email by clicking the received verification link, and then I reloaded the current user by the lines of code below:
suspend fun reloadUserInfo() {
firebaseAuth.currentUser?.reload()?.await()
}
But AuthStateListener is not firing up even tho I reloaded the cached user. If I understood correctly AuthStateListener should trigger after reloading the current user. The reload() function's documentation says: Manually refreshes the data of the current user (for example, attached providers, display name, and so on). The isEmailVerified state changed the firebase user. Right?
val isEmailVerified: Flow<Boolean> = callbackFlow {
val authStateListener = AuthStateListener { auth ->
val isEmailVerified = auth.currentUser?.isEmailVerified == true
trySend(isEmailVerified)
}
firebaseAuth.addAuthStateListener(authStateListener)
awaitClose {
firebaseAuth.removeAuthStateListener(authStateListener)
}
}
This flow is not sending anything. But after restarting the application the callback gets fired. I don't want to restart the application to get the job done. It would not be a good user experience.
I did some research but nothing was found. If you take the time to help me, I appreciate it.
The email verification happens out of the band, so when you click the link in the email, there is nothing built-in that triggers Firebase Authentication clients to be updated. So there is no callback for that. Firebase only refreshes the ID token once per hour. This means that it may take up to an hour before the token is refreshed, a case in which the onIdTokenChanged() method fires. So onAuthStateChanged() nor onIdTokenChanged() fires when the link is clicked, which basically means that we have to check that in our application code, on demand. Since you're using Kotlin, the solution is quite simple:
return try {
auth.currentUser?.reload()?.await()
} catch (e: Exception) {
Log.e(TAG, "${e.message}")
}
However, do not attach a complete listener and call await() at the same time. It's one or the other. Do not combine them! Why? Because there is no guarantee that the listener will be called before or after await(). That means that there is a chance that the code in the complete listener won't be called until or after the suspend function returns. Besides that, one of the major reasons to use Kotlin Coroutines in the first place is to avoid using callbacks, which are not life-cycle aware.
If you want to see a concrete example, this resource will help. Here is the corresponding repo.

Android GlobalScope.launch and viewModelScope different behaviour

I'm doing some tests and I cannot understand the different behaviour when I use the GlobalScope.launch and viewModelScope CoroutineScope
I have the following code in a viewModel:
Log.d("init")
GlobalScope.launch((Dispatchers.Main)) {
repository
.storedItemsListener()
.onStart {
Log.d("onStart")
}
.onEach {
Log.d("onEach")
}
.onEmpty {
Log.d("onEmpty")
}
.onCompletion {
Log.d("onCompletion")
}
.launchIn(this)
}
Whenever the database is updated with items, storedItemsListener logs Storage Updated
At app launch, I perform a network request that updates storage with items. I then do a pull to refresh that performs a new request that also stores items on the database. With the above code, I have these logs:
// After app launch
init
onStart
Storage Updated //when the listener on the database is triggered
onEach
//After pull to refresh, although I know I store items on the database, no logs are produced. It seems that is the coroutine scope is dead and stops responding.
I then change the above code to use viewModel (.launchIn(viewModelScope)). I then obtain the logs that I expect.
// After app launch
init
onStart
Storage Updated //when the listener on the database is triggered
onEach
Storage Updated //when storage is updated with network request result
onEach
//After pull to refresh
Storage Updated //when storage is updated with network request result
onEach
My question is this. Shouldn't GlobalScope.launch be kept “alive” and notify me of all storage updates?
Please note that I want to keep this mechanism alive always and not only bound to a viewModel scope, and this is why I've chosen Global scope. The above description is a simplified version of what I need.

Android Coroutines in SyncAdapter being Interrupted

I'm having a tough time figuring out why do I observe InterruptedExceptions in the SyncAdapter#onPerformSync method.
Brief intro
My project originally contained only Java code and had a working SyncAdapter pattern implemented.
In the onPerfromSync method I did the following operations for each resource that I needed to sync:
query local sqlite database
send data to web server over HTTP
save data to local sqlite database
As the project progressed I introduced Kotlin and Coroutines and decided to use them in the SyncAdapter.
Most of the resources kept the existing logic, and for a few I decided to use Coroutines.
I had to bridge the onPerformSync method with a CoroutineScope so that I can launch coroutines. After doing so I started observing InterruptedExceptions occurring.
Code before (no exceptions)
class MySyncAdapter extends AbstractThreadedSyncAdapter {
...
#Override
public void onPerformSync(...) {
syncResourceX();
syncResourceY();
}
private void syncResourceX() {
// query db
// send to server
// store locally
}
}
Code After (with exceptions)
class MySyncAdapter(...): AbstractThreadedSyncAdapter(...) {
...
override fun onPerformSync(...) {
runBlocking {
syncResourceX();
syncResourceY();
}
}
private suspend fun syncResourceX() {
// query db
// send to server
// store locally
}
}
After this refactoring I started receiving InterruptedExceptions where runBlocking is being invoked.
Initially I thought this might be due to not performing network operations on the current thread as the docs of the sync adapter state that the system monitors network traffic and might interrupt the sync process if no traffic is generated.
But runBlocking should cause any network requests to be executed on the current thread right?
After which I started thinking that this process has existed in the Java code as well. It's just that there is nothing to report the interruption of the sync process. It is up until I started using coroutines and runBlocking that this problem has revealed it self. Or so I think now.
Question:
Any thoughts or explanations why I previously did not observe InterruptedException with the Java code and do observe InterruptedException with the Kotlin code is more than welcome.
Notes
I haven't overriden onSyncCanceled
The sync process usually takes less then 20 seconds to complete

How to get WorkManager Status Synchronously

I am working with WorkManager Alpha 05.
I'm developing a Service that enqueues task on demand of other applications.
It has two methods:
createTask (Create a new task, given a name and a set of data, it returns and ID)
checkTaskStatus (The application asks the services given a ID, the status of the task)
The communication is done via bound services using messages. That means both client and services has the correct implementations to communicate information.
Method 1 is working fine.
I have problems with method 2.
WorkManager.getInstance().getStatusById(taskID)
.observe(LifecycleOwner, Observer {
status -> if (status !=null){
val myResult = status.state.toString()
statusString = myResult
Log.d("Task Status",myResult)
}
})
The observer is logging the status correctly, but I can't send back that message to the client. Is there a way to check the status in a sync way?
I don't really need to have the task attached to a LiveData.
Seems like SynchronousWorkManager was removed on October 11:
Removed WorkManager.synchronous() and WorkContinuation.synchronous() and all related methods. Added ListenableFuture as the return type of many methods in the API. This is a breaking API change.
How to use ListenableFuture:
You can now synchronously get and observe by using ListenableFutures. For example, WorkManager.enqueue() used to return void; it now returns a ListenableFuture. You can call ListenableFuture.addListener(Runnable, Executor) or ListenableFuture.get() to run code once the operation is complete.
More info can be found here.
The WorkManager instance has a synchronous method which returns the SynchronousWorkManager, This will give you a set of methods to perform synchronous operations. Take into account that this is meant to be used in a background thread.

How does WorkManager schedule GET requests to REST API?

I've had a look at the codelab for WorkManager plus some examples on here, but everything in code I have seen is either related to doing work locally on the device or work uploading to the server, not downloading data and responding to the data received. In the developer guidelines it even says, "For example, an app might need to download new resources from the network from time to time," so I thought it would be perfect for this task. My question is if WorkManager can handle the following scenario and if not, what is the proper tool for handling it:
Schedule a job that runs once a day in background
The job is to do a data fetch from the REST API (and post it to a LiveData object if possible).
When the data returns, check that it is newer than local data.
Notify the user that new data is available.
My worker class looks something like this:
public class MyWorker extends Worker {
#NonNull
#Override
public WorkerResult doWork() {
lookForNewData();
return WorkerResult.SUCCESS;
}
public void lookForNewData() {
MutableLiveData<MyObject> liveData = new MutableLiveData<>();
liveData.observe(lifeCycleOwner, results -> {
notifyOnNewData(results);
})
APILayer.getInstance().fetchData(searchParams, liveData)
}
My issue is of course that the LiveData object can't observe because there is no activity or fragment that can be its LifecycleOwner. But even if I used a callback from the API to respond to the data arriving, my worker would already have posted that it was successful and it probably would not proceed with the callback, right? So I kind of know this approach is totally wrong, but I can't see any code for getting data with WorkManager
Please help with a proper solution and some example code or some links, either with WorkManager if it can handle this kind of work or something else if it is more appropriate.
Schedule a job that runs once a day in background
You can schedule a PeriodicWorkRequest for that, which should be queued with enqueueUniquePeriodicWork. This makes sure only one PeriodicWorkRequest of a particular name can be active at a time.
Constraints constraint = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(MyWorker.class, 1, TimeUnit.DAYS)
.setConstraints(constraint)
.build();
WorkManager workManager = WorkManager.getInstance();
workManager.enqueueUniquePeriodicWork("my_unique_worker", ExistingPeriodicWorkPolicy.KEEP, workRequest);
The job is to do a data fetch from the REST API (and post it to a LiveData object if possible).
This can by done by sending your request synchronously within doWork() of your worker. I wouldn't use LiveData within your Worker class. We come to that later. The API call would look with Retrofit for example like that:
#Override
public WorkerResult doWork() {
Call<MyData> call = APILayer.getInstance().fetchData();
Response<MyData> response = call.execute();
if (response.code() == 200) {
MyData data = response.body();
// ...
} else {
return Result.RETRY;
}
// ...
return Result.SUCCESS;
}
When the data returns, check that it is newer than local data.
You fetched your API data in a synchronous way. Fetch your local data also synchronously and do whatever you need to do to compare them.
Notify the user that new data is available.
If you schedule a task with WorkManager it is guaranteed to run, even if your app is force-quit or the device is rebooted. So your task might complete while your app is not running. If you want to notify the user in any case you can send a notification. If you want to notify the user within a certain screen you can subscribe on your tasks status. For example like this (taken from the official guide):
WorkManager.getInstance().getStatusById(compressionWork.getId())
.observe(lifecycleOwner, workStatus -> {
// Do something with the status
if (workStatus != null && workStatus.getState().isFinished()) {
// ...
}
});
There's also getStatusesForUniqueWork(String uniqueWorkName) for our example.
The official guide is also explaining how to return data from you Task with which you can call setValue() on your MutableLiveData for example.
I would propose to update your local data within your Worker, subscribe on your workers status and once it succeeds update your UI with the local data (if you are not subscribed on your local data anyways, i.e. with Room and LiveData).
Edit: In reference to point 4, reading status of periodic work requests works a little different. They are only switching between ENQUEUED and RUNNING until CANCELLED. But will never have the state SUCCEEDED or FAILED. So listening for isFinished() might not be what you are expecting.
This is initial thought. Somebody please correct me if i'm wrong.
my worker would already have posted that it was successful and it probably would not proceed with the callback, right?
we can use the callback from API reponse, to construct output Data of the worker and set it using worker.setOutputData()
Then listen to the LiveData<WorkStatus> from workManager. From this workstatus we can get outputData using, workStatus.getOutputdata(). This data can give us the API response we want.
We can pass this response to next worker in the worker chain to carry out tasks like updating local DB.

Categories

Resources