I've been struggling to use the new WorkManager because I don't see a way to get fine grained status of my job. Basically, I want to use the WorkManager to upload files, and then I need my UI to reflect the progress of those uploads. The WorkInfo which you can retrieve regarding a WorkRequest only specifies terminal states (SUCCESS or FAILURE) not things like user defined progress of the job.
I thought perhaps I could use Room and LiveData to provide an elegant way to update status inside my UI. Basically, I create a database entity called VideoAsset, then provide methods which return LiveData<VideoAsset> inside my DAO. In my contrived example app below, when a user clicks on the FAB, a new video asset is added to the database, and then a new worker is scheduled, and the UUID of that video asset is passed to the worker. Inside doWork() on the worker, the worker retrieves the UUID, retrieves the video asset associated with that, and then updates the progress field inside the database (right now I'm just sleeping and updating to simulate a network upload to keep it simple). Then, inside my viewholder I am retrieving the LiveData<VideoAsset> and adding an observer to it.
I want to have my UI look like this, where the UUID is presented with an upload progress:
Here is the log output, showing it never has an observer object which is non null. It does properly update the progress inside the database via the worker.
D/WSVDB: Updating progress for 84b78a30: 98
D/WSVDB: Video is null, WHY?
I/chatty: uid=10091(com.webiphany.workerstatusviadb) identical 69 lines
D/WSVDB: Video is null, WHY?
D/WSVDB: Updating progress for 84b78a30: 99
D/WSVDB: Video is null, WHY?
I/chatty: uid=10091(com.webiphany.workerstatusviadb) identical 72 lines
D/WSVDB: Video is null, WHY?
D/WSVDB: Updating progress for 84b78a30: 105
I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=3612d7d3-23dd-4c29-9566-d1e15672ded7, tags={ com.webiphany.workerstatusviadb.UploadWorker } ]
D/WSVDB: Video is null, WHY?
I/chatty: uid=10091(com.webiphany.workerstatusviadb) identical 75 lines
D/WSVDB: Video is null, WHY?
First, the MainActivity. Pretty simple, it just sets up the RecyclerView and ViewModel, wires up the FAB button click handler. uploadNewVideo is where the video asset is created inside the database (using the view model which has a repository behind it...). Then, inside the VideoAssetsAdapter#onBindViewHolder I have code which retrieves the video and adds the observer. It never updates the progress, it always goes into the else branch and says Video is null, WHY.
package com.webiphany.workerstatusviadb
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.android.synthetic.main.activity_main.*
import java.util.*
class MainActivity : AppCompatActivity() {
private var videoAssetsViewModel: VideoAssetViewModel? = null
private var adapter: VideoAssetsAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener { _ ->
uploadNewVideo()
}
videoAssetsViewModel = ViewModelProviders.of(this).get(VideoAssetViewModel::class.java)
setupPreviewImages()
}
private fun uploadNewVideo() {
val videoAsset = VideoAsset()
videoAsset.uuid = Integer.toHexString(Random().nextInt() * 10000)
videoAssetsViewModel?.insert(videoAsset)
// Create a new worker, add to the items
val uploadRequestBuilder = OneTimeWorkRequest.Builder(UploadWorker::class.java)
val data = Data.Builder()
data.putString(UploadWorker.UUID, videoAsset.uuid)
uploadRequestBuilder.setInputData(data.build())
val uploadRequest = uploadRequestBuilder.build()
WorkManager.getInstance().enqueue(uploadRequest)
}
private fun setupPreviewImages() {
val mLayoutManager = GridLayoutManager(this, 4)
previewImagesRecyclerView.layoutManager = mLayoutManager
adapter = VideoAssetsAdapter(videoAssetsViewModel?.videos?.value)
previewImagesRecyclerView.adapter = adapter
videoAssetsViewModel?.videos?.observe(this, androidx.lifecycle.Observer { t ->
if( t != null ){
if (t.size > 0 ){
adapter?.setVideos(t)
previewImagesRecyclerView.adapter = adapter
}
}
})
}
inner class VideoAssetViewHolder(videoView: View) : RecyclerView.ViewHolder(videoView) {
var progressText: TextView
var uuidText: TextView
init {
uuidText = videoView.findViewById(R.id.uuid)
progressText = videoView.findViewById(R.id.progress)
}
}
inner class VideoAssetsAdapter(private var videos: List<VideoAsset>?) :
RecyclerView.Adapter<VideoAssetViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup,
viewType: Int): VideoAssetViewHolder {
return VideoAssetViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.preview_image, parent, false))
}
override fun onBindViewHolder(holder: VideoAssetViewHolder, position: Int) {
val video = videos?.get(position)
if (video != null && videoAssetsViewModel != null) {
val uuid = video.uuid
if( uuid != null ) {
holder.uuidText.text = uuid
// Get the livedata to observe and change
val living = videoAssetsViewModel?.getByUuid(uuid)
living?.observe(this@MainActivity, androidx.lifecycle.Observer { v ->
// Got a change, do something with it.
if (v != null) {
holder.progressText.text = "${v.progress}%"
}
else {
Log.d( TAG, "Video is null, WHY?")
}
})
}
}
}
fun setVideos(t: List<VideoAsset>?) {
videos = t
notifyDataSetChanged()
}
override fun getItemCount(): Int {
var size = 0
if (videos != null) {
size = videos?.size!!
}
return size
}
}
companion object {
var TAG: String = "WSVDB"
}
}
The video asset entity (and the DAO, Database, Repository) looks like this:
package com.webiphany.workerstatusviadb
import android.app.Application
import android.content.Context
import android.os.AsyncTask
import android.util.Log
import androidx.annotation.NonNull
import androidx.lifecycle.LiveData
import androidx.room.*
@Entity(tableName = "video_table")
class VideoAsset {
@PrimaryKey(autoGenerate = true)
@NonNull
@ColumnInfo(name = "id")
var id: Int = 0
@ColumnInfo(name = "progress")
var progress: Int = 0
@ColumnInfo(name = "uuid")
@NonNull
var uuid: String? = null
}
class VideoAssetRepository(application: Application) {
private var videoDao: VideoAssetDao? = null
init {
val db = VideoAssetDatabase.getDatabase(application)
if (db != null) {
videoDao = db.videoAssetDao()
}
}
fun findAllVideos(): LiveData<List<VideoAsset>>? {
if (videoDao != null) {
return videoDao?.findAll()
} else {
Log.v(MainActivity.TAG, "DAO is null, fatal error")
return null
}
}
fun insert(video: VideoAsset) {
insertAsyncTask(videoDao).execute(video)
}
fun get(id: String): LiveData<VideoAsset>? = videoDao?.findVideoAssetById(id)
private class insertAsyncTask internal
constructor(private val asyncTaskDao: VideoAssetDao?) :
AsyncTask<VideoAsset, Void, Void>() {
override fun doInBackground(vararg params: VideoAsset): Void? {
asyncTaskDao?.insert(params[0])
return null
}
}
companion object {
var instance: VideoAssetRepository? = null
fun getInstance(application: Application): VideoAssetRepository? {
synchronized(VideoAssetRepository::class) {
if (instance == null) {
instance = VideoAssetRepository(application)
}
}
return instance
}
}
}
@Database(entities = arrayOf(VideoAsset::class), version = 3)
abstract class VideoAssetDatabase : RoomDatabase() {
abstract fun videoAssetDao(): VideoAssetDao
companion object {
@Volatile
private var INSTANCE: VideoAssetDatabase? = null
fun getDatabase(context: Context): VideoAssetDatabase? {
if (INSTANCE == null) {
synchronized(VideoAssetDatabase::class.java) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.applicationContext,
VideoAssetDatabase::class.java, "video_asset_database")
.build()
}
}
}
return INSTANCE
}
}
}
@Dao
interface VideoAssetDao {
@Insert
fun insert(asset: VideoAsset)
@Query("SELECT * from video_table")
fun findAll(): LiveData<List<VideoAsset>>
@Query("select * from video_table where id = :s limit 1")
fun findVideoAssetById(s: String): LiveData<VideoAsset>
@Query("select * from video_table where uuid = :uuid limit 1")
fun findVideoAssetByUuid(uuid: String): LiveData<VideoAsset>
@Query( "update video_table set progress = :p where uuid = :uuid")
fun updateProgressByUuid(uuid: String, p: Int )
}
Then, the worker is here. Again, it merely simulates upload progress by incrementing a variable by a random number between 1 and 10, and then sleeping for a second.
package com.webiphany.workerstatusviadb
import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import java.util.*
class UploadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
// Get out the UUID
var uuid = inputData.getString(UUID)
if (uuid != null) {
doLongOperation(uuid)
return Result.success()
} else {
return Result.failure()
}
}
private fun doLongOperation(uuid: String) {
var progress = 0
var videoDao: VideoAssetDao? = null
val db = VideoAssetDatabase.getDatabase(applicationContext)
if (db != null) {
videoDao = db.videoAssetDao()
}
while (progress < 100) {
progress += (Random().nextFloat() * 10.0).toInt()
try {
Thread.sleep(1000)
} catch (ie: InterruptedException) {
}
Log.d( MainActivity.TAG, "Updating progress for ${uuid}: ${progress}")
videoDao?.updateProgressByUuid(uuid, progress)
}
}
companion object {
val UUID = "UUID"
}
}
Lastly, the view model:
package com.webiphany.workerstatusviadb
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import java.util.concurrent.Executors
class VideoAssetViewModel(application: Application) : AndroidViewModel(application) {
private val videoAssetRepository: VideoAssetRepository?
var videos: LiveData<List<VideoAsset>>? = null
private val executorService = Executors.newSingleThreadExecutor()
init {
videoAssetRepository = VideoAssetRepository.getInstance(application)
videos = videoAssetRepository?.findAllVideos()
}
fun getByUuid(id: String) = videoAssetRepository?.get(id)
fun insert(video: VideoAsset) {
executorService.execute {
videoAssetRepository?.insert(video)
}
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
tools:showIn="@layout/activity_main">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/previewImagesRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:isScrollContainer="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:listitem="@layout/preview_image"
>
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:srcCompat="@android:drawable/ic_dialog_email" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
preview_image.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="92dp"
android:layout_height="92dp"
android:padding="4dp"
android:background="@color/colorPrimary"
android:orientation="vertical">
<TextView
android:id="@+id/uuid"
android:layout_width="0dp"
android:layout_height="20dp"
android:background="@color/colorPrimaryDark"
android:gravity="end|center"
android:padding="2dp"
android:textColor="@android:color/white"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="abcd"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/progress"
android:layout_width="0dp"
android:layout_height="20dp"
android:background="@color/colorAccent"
android:gravity="end|center"
android:padding="2dp"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="0%"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
And, the app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.webiphany.workerstatusviadb"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0-alpha01'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
implementation 'com.google.android.material:material:1.1.0-alpha02'
def work_version = "1.0.0-beta02"
implementation("android.arch.work:work-runtime-ktx:$work_version")
def room_version = "2.1.0-alpha03"
implementation "androidx.room:room-runtime:$room_version"
kapt "android.arch.persistence.room:compiler:$room_version"
testImplementation "androidx.room:room-testing:$room_version"
debugImplementation 'com.amitshekhar.android:debug-db:1.0.4'
}
