Android: Workmanager CoroutineWorker 总是失败

Android: Workmanager CoroutineWorker is always failing

我目前的问题是,我的 Workmanager 总是失败,但我不知道为什么。实际上,我什至不想使用 Workmanager,但我不知道在网络丢失或根本没有网络时取消协程的更好方法。

我正在尝试做的事情:检查网络是否可用 -> 从 Cloud Firestore 下载集合 -> 显示进度条 -> 下载成功 -> 显示列表。我已经在没有 workmanager 和协程的情况下做到了这一点(没有检查网络可用性)

当前情况:Toast "Enqueue" -> Progress Bar -> Toast "Failed"

文档工作者

class DocumentWorker @WorkerInject constructor(
    @Assisted context: Context,
    @Assisted params: WorkerParameters,
    private val firebaseEntity: DocumentFirebaseRepository,
    private val documentDao: DocumentDao,
    private val networkMapper: DocumentNetworkMapper,
    private val cacheMapper: DocumentCacheMapper
): CoroutineWorker(context, params) {
    override suspend fun doWork(): Result {

        // Get Data from Cloud Firestore and map it to a DocumentCacheEntity to insert it to the database
        val documentEntityList: List<DocumentFirebaseEntity> = firebaseEntity.getAllDocuments()
        val documentCacheList: List<DocumentCacheEntity> = networkMapper.mapFromEntityList(documentEntityList)

        documentDao.insert(documentCacheList)

        //  Get the inserted list from the DAO and map it to Documents
        val cachedDocumentEntities: List<DocumentCacheEntity> = documentDao.getList()
        val documents: List<Document> = cacheMapper.mapFromEntityList(cachedDocumentEntities)

        // Convert List<Documents> to WorkData Object. Is this the correct way? I don't know...
        val data = workDataOf("documents" to documents)
        return Result.success(data)
    }
}

视图模型

class DocumentViewModel @ViewModelInject constructor(
    @ApplicationContext private val context: Context,
    @Assisted private val savedStateHandle: SavedStateHandle,
) : ViewModel() {

    private val work = OneTimeWorkRequestBuilder<DocumentWorker>()
        .setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
        .build()

    val workInfo: LiveData<WorkInfo> = WorkManager.getInstance(context).getWorkInfoByIdLiveData(work.id)


    fun setStateEvent(documentStateEvent: DocumentStateEvent) {
        viewModelScope.launch {
            when (documentStateEvent) {
                is DocumentStateEvent.GetDocumentEvent -> {
                    WorkManager.getInstance(context).enqueue(work)
                }
            }
        }
    }
}

片段

@AndroidEntryPoint
class DocumentsFragment(private val documentListAdapter: DocumentListAdapter) : Fragment() {
    private val documentViewModel: DocumentViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        subscribeToWorker()
        documentViewModel.setStateEvent(DocumentStateEvent.GetDocumentEvent)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return DataBindingUtil.inflate<FragmentDocumentsBinding>(inflater, R.layout.fragment_documents, container, false).apply {
            adapter = documentListAdapter
        }.root
    }

    private fun subscribeToWorker() {
        documentViewModel.workInfo.observe(viewLifecycleOwner) {
            when(it.state) {
                WorkInfo.State.ENQUEUED -> requireContext().toast("ENQUEUED")
                WorkInfo.State.RUNNING -> displayProgressBar(true)
                WorkInfo.State.SUCCEEDED -> {
                    displayProgressBar(false)
                    // Here I want to get my List<Document> and submit it to my ListAdapter...
                    documentListAdapter.submitList(it.outputData.getString("documents") as MutableList<Document>)
                }
                WorkInfo.State.BLOCKED -> {
                    requireContext().toast("BLOCKED")
                    displayProgressBar(false)
                }
                WorkInfo.State.FAILED -> {
                    requireContext().toast("FAILED")
                    displayProgressBar(false)
                }
                WorkInfo.State.CANCELLED -> {
                    requireContext().toast("CANCELLED")
                    displayProgressBar(false)
                }
            }
        }
    }

应用

@HiltAndroidApp
class App : Application(), Configuration.Provider {
    @Inject lateinit var workerFactory: HiltWorkerFactory
    override fun onCreate() {
        super.onCreate()
        Timber.plant(Timber.DebugTree())
    }

    override fun getWorkManagerConfiguration(): Configuration =
        Configuration.Builder().setWorkerFactory(workerFactory).build()
}

如果有更好的方法在没有 Workmanager 的情况下完成所有这些工作(例如,手动检查网络状态并在网络丢失时取消协程),请告诉我!

非常感谢您的帮助,谢谢

编辑

好的,我发现了错误,这里是堆栈跟踪:

Caused by: java.lang.IllegalArgumentException: Key documents has invalid type class java.util.ArrayList
        at androidx.work.Data$Builder.put(Data.java:830)
        at com.example.app.data.models.validator.DocumentWorker.doWork(DocumentWorker.kt:42)
        at com.example.app.data.models.validator.DocumentWorker$doWork.invokeSuspend(Unknown Source:11)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

这可能是错误:val data = workDataOf("documents" to documents)

好的,我的问题已经解决了!如上所述,(不幸的是)不可能将 List<Document> 放入 Workmanager 输入数据中。所以我想出了这个解决方案:

工作经理

class DocumentWorker @WorkerInject @Singleton constructor(
    @Assisted context: Context,
    @Assisted params: WorkerParameters,
    private val firebaseEntity: DocumentFirebaseRepository,
    private val documentDao: DocumentDao,
    private val networkMapper: DocumentNetworkMapper,
): CoroutineWorker(context, params) {
    override suspend fun doWork(): Result {
        // Get Data from Cloud Firestore and map it to a DocumentCacheEntity to insert it to the database
        val documentEntityList: List<DocumentFirebaseEntity> = firebaseEntity.getAllDocuments()
        val documentCacheList: List<DocumentCacheEntity> = networkMapper.mapFromEntityList(documentEntityList)
        documentDao.insert(documentCacheList)

        return Result.success()
    }
}

视图模型

class DocumentViewModel @ViewModelInject constructor(
    @ApplicationContext private val context: Context,
    @Assisted private val savedStateHandle: SavedStateHandle,
    private val documentDB: DocumentDao,
    private val cacheMapper: DocumentCacheMapper
    //private val documentRepository: DocumentRepository,
) : ViewModel() {
    // Save State and Document List in a Livedata Object that can be observed from the fragment
    private val _documentDataState: MutableLiveData<Status<List<Document>>> = MutableLiveData()
    val documentState: LiveData<Status<List<Document>>> get() = _documentDataState

    // Build the OnetimeWorkRequest
    private val work = OneTimeWorkRequestBuilder<DocumentWorker>()
        .setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
        .build()

    // Get the workInfo asFlow to observer it in the viewModel
    private val workInfo: Flow<WorkInfo> = WorkManager.getInstance(context).getWorkInfoByIdLiveData(work.id).asFlow()

    fun setStateEvent(documentStateEvent: DocumentStateEvent) {
        when (documentStateEvent) {
            is DocumentStateEvent.GetDocumentEvent -> {
                // Here we Enqueue the Workmanager
                WorkManager.getInstance(context).enqueue(work)

                // Now we will observe (collect) the workInfo
                viewModelScope.launch {
                    workInfo.collect {
                        when(it.state) {
                            WorkInfo.State.ENQUEUED -> _documentDataState.postValue(Status.loading())
                            WorkInfo.State.RUNNING -> _documentDataState.postValue(Status.loading())
                            WorkInfo.State.SUCCEEDED -> {
                                // Document loaded successfully into db, so get it from there and post it to the livedata
                                val documentCacheEntityList = documentDB.getList()
                                val documentList = cacheMapper.mapFromEntityList(documentCacheEntityList)
                                _documentDataState.postValue(Status.success(documentList))
                            }
                            WorkInfo.State.BLOCKED -> _documentDataState.postValue(Status.failed("No Internet Connection"))
                            WorkInfo.State.FAILED -> _documentDataState.postValue(Status.failed("No Internet Connection"))
                            WorkInfo.State.CANCELLED -> _documentDataState.postValue(Status.failed("Loading cancelled"))
                        }
                    }
                }
            }
        }

如果有更好的方法,请告诉我。但这现在应该有效!我目前遇到的唯一问题是,当约束未在 5 秒内完成时,我想 return Result.failed

编辑

我做了一个函数,converts Workinfo 的结果到我自己的 Status 结果。

suspend inline fun observerWorkerState(workInfFlow: Flow<WorkInfo>): Flow<Status<Unit>> = flow {
    workInfFlow.collect {
        when (it.state) {
            WorkInfo.State.ENQUEUED -> emit(Status.loading<Unit>())

            WorkInfo.State.RUNNING -> emit(Status.loading<Unit>())

            WorkInfo.State.SUCCEEDED -> emit(Status.success(Unit))

            WorkInfo.State.BLOCKED -> emit(Status.failed<Unit>("Workmanager blocked"))

            WorkInfo.State.FAILED -> emit(Status.failed<Unit>("Workmanager failed"))

            WorkInfo.State.CANCELLED -> emit(Status.failed<Unit>("Workmanager cancelled"))
        }
    }
}