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”:
- If we do catch an exception, we log a message to Logcat with the exception itself (so the stack trace shows up)
- And, if we catch an exception, we show a
Toast
to the user, which presents a message in little temporary popup window
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.