Here is an extension function that wraps the code of taking pictures into shorter and better readable code:
fun ComponentActivity.registerForTakePicture(
callback: (Result<Unit>) -> Unit
): ActivityResultLauncher<Uri> {
return registerForActivityResult(
ActivityResultContracts.TakePicture()
) { success ->
if (success) {
callback(Result.success(Unit))
} else {
callback(
Result.failure(
Throwable(
"Didn't save a picture. " +
"ACTION_IMAGE_CAPTURE-Activity cancel?"
)
)
)
}
}
}
=> Watch out: A failure will be also returned if the user presses the abort-button in the Take Image Screen intentionally. Unfortunately we can't differ if the user aborts intentionally or if there was an error recording the image.
But there is another bad code design in the default android api. The trigger point of launching the take-photo activity and the result handler are very separate although they logically belong to each other in my opinion:
See this article: https://medium.com/codex/how-to-use-the-android-activity-result-api-for-selecting-and-taking-images-5dbcc3e6324b
This forces the developer to create a global variable (either as an activity member field or in the ViewModel/State) that has to be nullable or lateinit.
What if we can create a better api?
Singleton that solves the problem
class TakePictureContract {
private val logger by provideLogger(this::class.java.simpleName)
private var activityResultLauncher: ActivityResultLauncher<Uri>? = null
private var targetUri: Uri? = null
private var continuation: Continuation<Boolean>? = null
fun registerForActivity(activity: ComponentActivity) {
activityResultLauncher = activity.registerForTakePicture { result ->
result
.onSuccess {
val uri = targetUri
?: throw IllegalStateException(
"targetUri is null"
)
continuation?.resume(true)
this.continuation = null
this.targetUri = null
}
.onFailure {
logger.d(it.message.toString())
continuation?.resume(false)
this.continuation = null
this.targetUri = null
}
}
}
suspend fun run(targetUri: Uri) = suspendCoroutine {
if (this.continuation != null) {
throw IllegalStateException(
"TakePictureContract is already running"
)
}
this.targetUri = targetUri
this.continuation = it
activityResultLauncher?.launch(targetUri)
}
}
Usage from ViewModel
fun newImage(fileUri: Uri) {
viewModelScope.launchIO {
try {
_state.update { it.copy(isLoading = true) }
val result = takePictureContract.run(fileUri)
if (result) {
addTakenImage(fileUri)
}
} finally {
_state.update { it.copy(isLoading = false) }
}
}
}
Register Activity in onCreate
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
takePictureContract.registerForActivity(this)
}
Some additional notes
- The Instance of
TakePictureContract
has to be the same in Activity and ViewModel. I solved this via Koin Dependency Injection. An easy alternative to DI would be to use kotlinobject TakePictureContract
instead of class. - DI for
TakePictureContract
also allows mocking the wanted behaviour for unit tests - It is okay to use a singleton in my opinion because there can only be one camera activity active at a single time.
Let me know that you think about the class in the comments!
Regards
Max
Top comments (0)