DEV Community

Max
Max

Posted on

Improve code readability when taking pictures in android development

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?"
                    )
                )
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

=> 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:

Image description

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)
    }

}
Enter fullscreen mode Exit fullscreen mode

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) }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Register Activity in onCreate

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    takePictureContract.registerForActivity(this)
}
Enter fullscreen mode Exit fullscreen mode

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 kotlin object 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)