android - 在 RecyclerView.ViewHolder 中观察到的 LiveData 值始终为 null(其中数据在 Worker 中更新)
问题描述
我一直在努力使用新的 WorkManager,因为我看不到一种方法来获得我的工作的细粒度状态。基本上,我想使用 WorkManager 上传文件,然后我需要我的 UI 来反映这些上传的进度。您可以检索的有关 WorkRequest 的 WorkInfo 仅指定终端状态(成功或失败),而不是用户定义的作业进度之类的内容。
我想也许我可以使用 Room 和 LiveData 提供一种优雅的方式来更新我的 UI 中的状态。基本上,我创建了一个名为 的数据库实体VideoAsset
,然后提供LiveData<VideoAsset>
在我的 DAO 中返回的方法。在下面我设计的示例应用程序中,当用户单击 FAB 时,会将新的视频资产添加到数据库中,然后安排新的工作人员,并将该视频资产的 UUID 传递给工作人员。在doWork()
工作人员内部,工作人员检索 UUID,检索与之关联的视频资产,然后更新progress
数据库中的字段(现在我只是在睡觉和更新以模拟网络上传以保持简单)。然后,在我的视图中,我正在检索LiveData<VideoAsset>
并添加一个观察者。
我想让我的 UI 看起来像这样,其中 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)
}
}
}
活动主.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>
预览图像.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'
}
解决方案
推荐阅读
- flutter - 无法使用 flutter_screenutil 包设置高度和宽度
- reactjs - RN-Redux VirtualizedList:你有一个更新缓慢的大列表
- reactjs - 反应同一对象内其他键值的Formik setFieldValue
- angular - 如何使用角度拦截器获取每个 API 响应以构建通用功能
- angular - forkJoin 和拦截器一起不起作用
- r - R 中的 tidyverse VS dplyr - 处理能力/性能
- python - 如何在设备 IP 地址上运行 Django 服务器?
- python - UnboundError 来自 dict.get()
- javascript - 有没有办法将 javascript 对象转换为字符串?
- c# - Application.Run 试图读取或写入受保护的内存