Я изо всех сил пытался использовать новый WorkManager, потому что я не вижу способа получить детальный статус моей работы. По сути, я хочу использовать WorkManager для загрузки файлов, а затем мне нужен мой пользовательский интерфейс, чтобы отразить ход этих загрузок. WorkInfo, который вы можете получить относительно WorkRequest, определяет только состояния терминала (SUCCESS или FAILURE), а не такие вещи, как пользовательский ход выполнения задания.
Я подумал, что, возможно, смогу использовать Room и LiveData, чтобы обеспечить элегантный способ обновления статуса в моем пользовательском интерфейсе. По сути, я создаю объект базы данных с именем VideoAsset
, а затем предоставляю методы, которые возвращают LiveData<VideoAsset>
в моем DAO. В приведенном ниже примере моего надуманного приложения, когда пользователь нажимает на FAB, в базу данных добавляется новый актив видео, а затем назначается новый работник, а UUID этого видео актива передается работнику. Внутри doWork()
на работнике работник извлекает UUID, извлекает связанный с ним актив видео, а затем обновляет поле progress
внутри базы данных (сейчас я просто сплю и обновляюсь, чтобы имитировать загрузку по сети, чтобы сохранить это просто). Затем внутри моего поля просмотра я извлекаю LiveData<VideoAsset>
и добавляю к нему наблюдателя.
Я хочу, чтобы мой пользовательский интерфейс выглядел следующим образом, где UUID представлен с прогрессом загрузки:
Вот вывод журнала, показывающий, что он никогда не имеет объекта наблюдателя, который не является нулевым. Он правильно обновляет прогресс внутри базы данных через работника.
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?
Во-первых, MainActivity. Довольно просто, он просто устанавливает RecyclerView и ViewModel, подключает обработчик нажатия кнопки FAB. uploadNewVideo
- это место, где видеоустройство создается внутри базы данных (с использованием модели представления, за которой находится репозиторий ...). Затем внутри VideoAssetsAdapter#onBindViewHolder
у меня есть код, который извлекает видео и добавляет наблюдателя. Он никогда не обновляет прогресс, он всегда идет в ветку else и говорит 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"
}
}
Объект видео актива (и DAO, База данных, Репозиторий) выглядит следующим образом:
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 )
}
Тогда рабочий здесь. Опять же, он просто имитирует процесс загрузки, увеличивая переменную на случайное число от 1 до 10, а затем спя на секунду.
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"
}
}
Наконец, вид модели:
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>
А, 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'
}