首页 > 解决方案 > 在 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'
}

标签: androidandroid-roomandroid-livedataandroid-workmanager

解决方案


推荐阅读