Step #3: Caching the Report

To share the report with other apps, we need to write it to a file that can be served by the FileProvider. Based on the FileProvider metadata, that would be in a shared/ subdirectory off of the location supplied by the getCacheDir() method on a Context. The work to save the report to this file should be done on a background thread. When that work is done, then we can actually share the report with other apps.

Add these functions to RosterMotor:

  fun shareReport() {
    viewModelScope.launch {
      saveForSharing()
    }
  }

  private suspend fun saveForSharing() {
    withContext(Dispatchers.IO + appScope.coroutineContext) {
      val shared = File(context.cacheDir, "shared").also { it.mkdirs() }
      val reportFile = File(shared, "report.html")
      val doc = FileProvider.getUriForFile(context, AUTHORITY, reportFile)

      _states.value.let { report.generate(it.items, doc) }
      _navEvents.emit(Nav.ShareReport(doc))
    }
  }

saveReport() is simply a wrapper around saveForSharing(), launching another coroutine. saveForSharing() implements that coroutine, where we:

You will have a few compile errors. One is that AUTHORITY is undefined. This needs to match the value that we have in the android:authorities attribute in the <provider> element in the manifest. That, in turn, is being created based on our application ID. So, add this constant to RosterMotor.kt:

private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".provider"

BuildConfig is a code-generated class that contains constants associated with our build, and BuildConfig.APPLICATION_ID is our application ID. As a result, AUTHORITY is being assembled the same way that the android:authorities value is being assembled.

Another compile error is that there is no Nav.ShareReport class. Fix that by changing Nav to look like:

sealed class Nav {
  data class ViewReport(val doc: Uri) : Nav()
  data class ShareReport(val doc: Uri) : Nav()
}

This adds another subclass to Nav called ShareReport, which also wraps a Uri.

saveForSharing() refers to context in a couple of places. That needs to be a Context, so we can use it for getCacheDir(), and for FileProvider.getUriForFile(). It also refers to appScope, which is the custom CoroutineScope that we are using for write operations. So, add two more constructor parameters to RosterMotor for context and appScope:

class RosterMotor(
  private val repo: ToDoRepository,
  private val report: RosterReport,
  private val context: Application,
  private val appScope: CoroutineScope
) : ViewModel() {

We use Application for the context parameter to avoid an invalid Lint check complaint. Google is worried that a ViewModel with a Context parameter might result in a memory leak, but the Lint check cannot distinguish between valid and invalid uses of Context. To make the Lint error go away, we use Application, which means that our ToDoApp will need to be the Context.

This requires a corresponding change to ToDoApp, adding androidApplication() and get(named("appScope")) to our RosterMotor constructor call:

    viewModel { RosterMotor(get(), get(), androidApplication(), get(named("appScope"))) }

androidApplication() is functionally equivalent to androidContext(), but it returns an Application to satisfy compiler type checking.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.