NOTE: This is now a part of AndroidX via the Activity Results API, which is under alpha at this time. This is another solution to achieve mostly the same result, but allows additional flexibility and a similar type-based implementation.
Foreword
As some of you may know, the Android Activity
has the ability to "request" data from another Activity; taking a picture requires launching the camera and fielding the URI
result that is returned, selecting a file from the File Manager requires requesting and fielding it in a similar fashion. This is normally done via a round trip flow using startActivityForResult
to request the data (e.g for a result) and to handle it in onActivityResult
.
The downsides of this are that is tedious:
- Monotonous callbacks and request flows
- Checking request code equality
- Checking resultCode is
Acitvity.RESULT_OK
- Extracting your data out of the returned
Intent
- Handling it or fielding the error that is a possibility
This also becomes increasingly difficult when ViewPager
and inner fragments come into play, forwarding the result to the sender, as you have to pass it along.
A (possible) solution to the monotony
signInButton.setOnClickListener {
val signIn = SignInWithInstagram(context)
signIn.start(object : SignInWithSocialAccountDispatcher {
override fun onSuccess(result: String?) {
// handle auth token from successful sign in
}
override fun onFailure(error: Throwable) = Unit
override fun onCancel() = Unit
})
}
So what exactly is happening here? One scenario for an activity result feedback-cycle is authentication via a dedicated login activity (or in this case a webview for Instagram authentication). An instance of SignInWithInstagram
, a subclass of our activity result dispatcher, is created with a Context
and optional other arguments. start()
triggers the startActivityForResult
internally tracking the REQUEST_CODE
automatically, and sending the result back to us - INLINE.
This grants us the awesome ability to handle it in a simple interface approach with explicit success, failure, and cancel contracts - that are strongly typed based on the interface used for the activity result dispatcher.
The ActivityResultContainer and dispatcher interface
To strongly type this we are taking advantage of generics and an explicit result interface.
interface ActivityResultDispatcher<S> {
fun onSuccess(result: S? = null)
fun onFailure(error: Throwable)
fun onCancel()
}
This is designed so onSuccess
returns a nullable S
, removing the boilerplate post processing, onFailure
emits any exception that your activity might hit (expected or unexpected), and onCancel
will signal an explicit Activity.RESULT_CANCEL
result sent from the Activity.
abstract class ActivityResultContainer<D: ActivityResultDispatcher<*>> : CoroutineScope by CoroutineScope(Dispatchers.Main) {
fun start() {
start(null)
}
abstract fun start(cb: D? = null)
fun sendIntent(
dispatcher: ReactiveActivityResult,
intent: Intent,
callback: D?
) {
launch {
dispatcher.start(intent).collect { handleResult(it, callback) }
}
}
internal abstract fun handleResult(result: ActivityResult, callback: D?)
}
The strongly typed interface, coupled with a typed abstract class allows seamless extensibility from our subclasses -
create Intent → start() → handleResult → send back to requester (in the data format they want).
The nuts and bolts
This is all pulled off under the hood using what we will refer to as ReactiveActivityResult
. A Fragment (now referred to as ReactiveActivityResultFragment
) is created and added to the FragmentManager
based on the context provided:
- If an Activity context is provided, the SupportFragmentManager will be used
- If a Fragment context is provided, the ChildFragmentManager will be used.
The fragment will then be used facilitate the startActivityForResult
song-and-dance that you normally would have to do, all behind the scenes, using a BroadcastChannel
and Flowable
.
The handoff in ReactiveActivityResultFragment
:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
launch {
channel.send(Tuple2(requestCode, ActivityResult(resultCode, data)))
}
}
fun start(intent: Intent): Flow<ActivityResult> {
val requestCode = RequestCodeGenerator.generate()
startActivityForResult(intent, requestCode)
return flow {
coroutineScope {
channel.consumeEach {
if (it._1 == requestCode) {
emit(it._2)
}
}
}
}
}
The results are handled like we normally would, request code is matched to the one created via our request code generator, and forwarded to our subclass for consumption via our Flowable<ActivityResult>
.
ActivityResult
Activity Results are able to be boiled down to a strict object based entity that make this syntactically easier to handle in our subclasses, as well as follow logically, complete with extension methods to remove the primitive equality checks.
@Parcelize
data class ActivityResult(val resultCode: Int, val data: Intent?) : Parcelable
fun ActivityResult.isOk() = resultCode == Activity.RESULT_OK
fun ActivityResult.isCanceled() = resultCode == Activity.RESULT_CANCELED
fun ActivityResult.isFirstUser() = resultCode == Activity.RESULT_FIRST_USER
Our channel conforms the requestCode: Int, resultCode: Int, data: Intent?
contract of onActivityResult
to our ActivityResult
for the Flowable
to forward along if the request code is a match.
ActivityResult(resultCode, data)
Handling the ActivityResult in our Dispatcher
For our example we are using a dedicated login activity for an IG authentication process, with our interface for our dispatcher resembling something like this:
interface SignInWithSocialAccountDispatcher: ActivityResultDispatcher<String>
where the result (a String) is the authenticate success URI from the backend, containing our access token.
Our subclass then becomes this:
class SignInWithInstagram @JvmOverloads constructor(
private val ctx: Context?
) : ActivityResultContainer<SignInWithSocialAccountDispatcher>() {
override fun start(cb: SignInWithSocialAccountDispatcher?) {
ifNotNull(ctx, { "context instance is null." }) {
// create reactive dispatcher
val reactiveDispatcher = ReactiveActivityResult(it)
// create the Intent
val requestIntent = Intent()
// send it
sendIntent(
dispatcher = reactiveDispatcher,
intent = requestIntent,
callback = cb
)
}
}
override fun handleResult(result: ActivityResult, callback: SignInWithSocialAccountDispatcher?) {
when {
result.isOk() -> {
val uriString = result.data?.getStringExtra("uri")
if (uriString == null) {
callback?.onFailure(Throwable("uri wasn't returned properly. Try again."))
} else {
val uri = uriString.toHttpUrlOrNull()
val tokens = uri?.queryParameterValues("access_token")
callback?.onSuccess(tokens?.firstOrNull())
}
}
result.isCanceled() -> callback?.onCancel()
}
}
As you can see, this forwards the pertinent data for the Activity
is actually after (the auth token), abstracting ALL of the boilerplate request/result and data handling away, and does so inline in a normal callback fashion.
signIn.start(object : SignInWithSocialAccountDispatcher {
override fun onSuccess(result: String?) {
// handle auth token from successful sign in
}
override fun onFailure(error: Throwable) = Unit
override fun onCancel() = Unit
})
Last thoughts
This abstraction and ability to handle Activity results inline at the point of request has been extremely helpful at Planoly. This solution, one which I was faced with solving prior to the Activity Result API being made available as a part of Jetpack, is just one of many possibilities. As I refine this experience, such as making it more lifecycle conscientious via lifecycleObserver, I will continue to update this blog post.
Let me know your thoughts on Social Media, and as always thanks for reading!