Наблюдаемое значение LiveData всегда равно нулю внутри RecyclerView.ViewHolder (где данные обновляются внутри Worker) - PullRequest
0 голосов
/ 16 января 2019

Я изо всех сил пытался использовать новый WorkManager, потому что я не вижу способа получить детальный статус моей работы. По сути, я хочу использовать WorkManager для загрузки файлов, а затем мне нужен мой пользовательский интерфейс, чтобы отразить ход этих загрузок. WorkInfo, который вы можете получить относительно WorkRequest, определяет только состояния терминала (SUCCESS или FAILURE), а не такие вещи, как пользовательский ход выполнения задания.

Я подумал, что, возможно, смогу использовать Room и LiveData, чтобы обеспечить элегантный способ обновления статуса в моем пользовательском интерфейсе. По сути, я создаю объект базы данных с именем VideoAsset, а затем предоставляю методы, которые возвращают LiveData<VideoAsset> в моем DAO. В приведенном ниже примере моего надуманного приложения, когда пользователь нажимает на FAB, в базу данных добавляется новый актив видео, а затем назначается новый работник, а UUID этого видео актива передается работнику. Внутри doWork() на работнике работник извлекает UUID, извлекает связанный с ним актив видео, а затем обновляет поле progress внутри базы данных (сейчас я просто сплю и обновляюсь, чтобы имитировать загрузку по сети, чтобы сохранить это просто). Затем внутри моего поля просмотра я извлекаю LiveData<VideoAsset> и добавляю к нему наблюдателя.

Я хочу, чтобы мой пользовательский интерфейс выглядел следующим образом, где UUID представлен с прогрессом загрузки:

enter image description here

Вот вывод журнала, показывающий, что он никогда не имеет объекта наблюдателя, который не является нулевым. Он правильно обновляет прогресс внутри базы данных через работника.

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'
}
...