Step #7: Viewing the Report

One limitation of what we have now is that we do not do anything once the report is saved. We should have some sort of acknowledgment, so the user knows the report is ready for use.

One possibility is to simply show the user the report. We can use an ACTION_VIEW Intent to display the report, using the Uri pointing to where we saved it.

First, though, we need our fragment to find out when the report is ready to be viewed and that we should navigate to the Web browser app to display it.

However, we need to be careful about how we do that. We could just tuck the Uri into a new RosterViewState and have our fragment see the Uri and launch the browser. However, we only want to launch the browser once, not on every future updated viewstate.

For Kotlin, the current recommended pattern for handling this is to use a SharedFlow. Whereas StateFlow is for states, a SharedFlow is better for events.

Right now, we have a single thing that we want to treat as an event: the report is ready to be viewed. However, in the next tutorial, we will add another. A typical way of representing this in Kotlin is to use a sealed class, which is basically “an enum with superpowers”. So, add this Nav sealed class to RosterMotor.kt for representing all of our navigation requests:

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

This has a ViewReport subclass for representing the “hey! let’s view the report!” navigation request. ViewReport wraps the Uri that identifies where the report is stored.

Then, add these properties to RosterMotor:

  private val _navEvents = MutableSharedFlow<Nav>()
  val navEvents = _navEvents.asSharedFlow()

This sets up a MutableSharedFlow, this time for a Nav object. Like MutableStateFlow, MutableSharedFlow is a SharedFlow that we manage ourselves, calling emit() when we want to publish an event. This is private; we use asSharedFlow() to make a SharedFlow available for the fragment to use to consume the events off of the channel (navEvents).

Next, modify saveReport() in RosterMotor to be:

  fun saveReport(doc: Uri) {
    viewModelScope.launch {
      report.generate(_states.value.items, doc)
      _navEvents.emit(Nav.ViewReport(doc))
    }
  }

Here, once the report has been saved, we emit() a ViewReport request to our MutableSharedFlow.

In order to view the report, we are going to want to use an ACTION_VIEW Intent and startActivity(). It is very likely that the user will have an app that supports ACTION_VIEW for HTML, such as a Web browser. But, it is not guaranteed. The problem is that startActivity() will throw an ActivityNotFoundException if the user does not have anything that supports ACTION_VIEW for HTML, which will lead to a crash if we do not take some steps.

To that end, add this safeStartActivity() function to RosterListFragment:

  private fun safeStartActivity(intent: Intent) {
    try {
      startActivity(intent)
    } catch (t: Throwable) {
      Log.e(TAG, "Exception starting $intent", t)
      Toast.makeText(requireActivity(), R.string.oops, Toast.LENGTH_LONG).show()
    }
  }

The big thing is that we use a try/catch block to handle any exception that might get raised by trying to start an activity. The most likely exception would be ActivityNotFoundException, meaning that no activity was found that matched the Intent that we used to try to start the activity. safeStartActivity() has a few other “bells and whistles”:

This code will have a couple of errors due to some missing symbols. The first missing symbol is TAG. This is a label that is included in our Logcat output. Since this string is not visible to users, we do not need to worry about translating it, so a plain string is fine. So, add this TAG constant towards the top of the RosterListFragment.kt source file:

private const val TAG = "ToDo"

Also, the Toast.makeText() call references an oops string resource that we have not defined. So, in res/values/strings.xml, add:

  <string name="oops">Sorry! Something went wrong!</string>

Then, in RosterListFragment, add this viewReport() function:

  private fun viewReport(uri: Uri) {
    safeStartActivity(
      Intent(Intent.ACTION_VIEW, uri)
        .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    )
  }

This sets up an ACTION_VIEW Intent, where ACTION_VIEW is the standard Intent action for “I want to view… something…”. Here, the “something…” is the report, identified by the supplied Uri. We need to add FLAG_GRANT_READ_URI_PERMISSION to the Intent to ensure that the Web browser (or other app responding to our Intent) is given read access to our content. Then, we call safeStartActivity() to bring up the Web browser (or whatever).

Finally, in RosterListFragment, towards the bottom of onViewCreated(), add the following:

    viewLifecycleOwner.lifecycleScope.launchWhenStarted {
      motor.navEvents.collect { nav ->
        when (nav) {
          is Nav.ViewReport -> viewReport(nav.doc)
        }
      }
    }

This is the same basic structure that we use for the states StateFlow, this time for the navEvents SharedFlow

Now, if you choose “Save” from the toolbar and pick a spot to write the report, you will either be taken to the saved report or, possibly, see the Toast popup indicating that the report was saved. You may or may not have a Web browser that supports the particular sort of Uri that we get back from the Storage Access Framework.


Prev Table of Contents Next

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