Search code examples
androidkotlingoogle-cloud-firestorekotlin-coroutinesandroid-workmanager

Android: Workmanager CoroutineWorker is always failing


My current problem is, that my Workmanager is always failing, but I don't know why. Actually, I don't even want to use a Workmanager but I didn't know a better way to cancel my Coroutine when the network is lost or when there never was a network.

What I am trying to do: Check if the network is available -> Download Collection from Cloud Firestore -> Show Progressbar -> Download Succeed -> Show List. I already managed to do this without workmanager with coroutines (without the check of network availability)

How it is currently: Toast "Enqueue" -> Progress Bar -> Toast "Failed"

DocumentWorker

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)
    }
}

ViewModel

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)
                }
            }
        }
    }
}

Fragment

@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)
                }
            }
        }
    }

App

@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()
}

If there is a better way of doing all this WITHOUT a Workmanager (e.g. manually checking the network state and cancelling a coroutine if the network is lost), then please tell me!

I appreciate every help, thank you

EDIT

Okay, I found the error, here is the stacktrace:

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$1.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)

And this might be the error: val data = workDataOf("documents" to documents)


Solution

  • Okay, I have managed to solve my problem! As stated out above, it is (unfortunately) not possible to put a List<Document> inside a Workmanager inputData. So I came up with this solution:

    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()
        }
    }
    

    ViewModel

    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"))
                            }
                        }
                    }
                }
            }
    

    If there is a better way, then tell me. But this should work for now! The only problem I currently have is, that I want to return Result.failed when the Constraints are not fullfied within 5 seconds.

    Edit

    I've made a function that converts the Result of the Workinfo to my own Status result.

    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"))
            }
        }
    }