Jetpack Composable ๐Ÿš€ to Bitmap Image ๐ŸŒ†

jetpackcompose Oct 24, 2021

How to convert a jetpack composable to the bitmap image by not going insane jumping between the interpolation.



Mobile App Circular

Jetpack Composable ๐Ÿš€ to Bitmap Image ๐ŸŒ†
How to convert a jetpack composable to the bitmap image by not going insane jumping between the interpolation. Hacktoberfest 2021 is live and many developers like me who are enthusiastic about thisโ€ฆ

Hacktoberfest 2021 is live and many developers like me who are enthusiastic about this event and love Open Source take some time to contribute to some Open Source Projects.

My 3rd PR is on Shreyas Patilโ€™s NotyKT repository Issue number 119 :

[JetpackCompose] Add support to share note as Image ยท Issue #119 ยท PatilShreyas/NotyKT
In the notes details screen, share as Image option should be provided. Question: How to convert Composables into Bitmap?

That is to add support to share composable into Bitmap, so the app could share Notes in Image format as well.

The solution for this problem is already there on StackOverflow and Medium. Quick google with get you those, I also went over them to understand how to provide this feature, but there was a lot of complexity and Manual hardCoded height, width, Canvas hacks, and Ugly ass API design for Creating View to capture bitmap was there.

Providing this Complexity but a huge no-no for me but the way they solve seems to be the solution for now. Mostly solution required you to convert Composable โ†’ View, View โ†’ Bitmap, and there was a lot of passing and multiple composable calling in the solution.

For which I decided to clean up and use my small brain ๐Ÿง  to reduce those repeated work and clean up the API design.

How to Convert Composable โ†’ View??

Simple we can use ComposeView which would accept composable as its View.

// Native View which accepts Composable as its View Content
ComposeView(context).apply {
    setContent {
        // composable goes here!
    }
}

We can Create Compose View Item at call site programatically. Since our app would be in Compose It would be great if we could use this inside of Composable.

How to Covert Native View โ†’ Composable ??

We can use AndroidView to wrap our Compose View then externally pass it the composable context to apply on ComposeView.

@Composable
fun CaptureBitmap(...){
  
  // we will use compose view as target to get bitmap
  val composeView = ComposeView(...)
  
  // put compose native view to compose ui using AndroidView
  AndroidView(
    factory = {
      composeView.apply {
        setContent {
          .... put compose here 
        }
      }
    }
  )
}


Enjoying the Post?

a clap is much appreciated if you enjoyed. No sign up or cost associated :)

If you want to support my content do consider dropping a tip/coffee/donation๐Ÿ’ฐ


We will get the compose content body from outside so it could be re-used as a scaffold for all the places that need to be screenshot.

How to create Scaffolding Composables for Template Like Reusing?

@Composable
fun CaptureBitmap(
  content: @Composable ()->Unit  // <--- we use callback as parameter
){
  
  val composeView = ComposeView(...)
  
  AndroidView(
    factory = {
      composeView.apply {
        setContent {
          content.invoke() // <--- Content get places in between this code
        }
      }
    }
  )
}

We have created the logic to get our composable content wrapped inside of a native view which we can use to get bitmap, BUT : this capturing canโ€™t be done instantly cause when this code will run it will take some time to inflate this view.

We can have multiple ways to solve this waiting time, from observing view tree to writing a state which callbacks when view is rendered. This will make to capture your view bitmap only once the layout is over.

But what might happen that you composable UI might get updated many time cause of from state changes. This will ask you to recapture bitmap every time the UI changes and cache it to get the latest value. This flow is very complex to deal with and code.

Ideally, we should be able to get bitmap out for some sharing action is getting performed on the UI so we can get the latest value from the state itself.

I solved this by using a callback. Ideally composable function holding UI thus should not return a value but this is an exceptional case I guess where we need action-based returned value.

How to create a callback to get latest bitmap?

@Composable
fun CaptureBitmap(
  content: @Composable ()->Unit
) : () -> Bitmap // <---- this will return a callback which returns a bitmap
{
  
  val composeView = ComposeView(...)
  
  //callback that convert view to bitmap
  fun captureBitmap() = composeView.drawToBitmap() 
  
  AndroidView(
    factory = {
      composeView.apply {
        setContent {
          content.invoke() // <--- Content get places in between this code
        }
      }
    }
  )
  
  return ::captureBitmap // <--- return functional reference of the callback
}

captureBitmap() will be returned from function which would be a callback that would trigger ComposeView to return a bitmap of the View.

Final Composable Would look like?

@Composable
fun CaptureBitmap(
    content: @Composable () -> Unit,
): () -> Bitmap {

    val context = LocalContext.current
    
    /** 
     * ComposeView that would take composable as its content
     * Kept in remember so recomposition doesn't re-initialize it
     **/
    val composeView = remember { ComposeView(context) }
    
    /**
     * Callback function which could get latest image bitmap
     **/
    fun captureBitmap(): Bitmap {
        return composeView.drawToBitmap()
    }
    
    /** Use Native View inside Composable **/
    AndroidView(
        factory = {
            composeView.apply {
                setContent {
                    content.invoke()
                }
            }
        }
    )

    /** returning callback to bitmap **/
    return ::captureBitmap
}


How to use composable at the call-site?

val snapShot = CaptureBitmap {
    //.. wite composable you want to capture
    // it would be visible on view as well
}

// Caution : needs to be done on click action
// ui must be visible/laid out before calling this
val bitmap = snapShot.invoke()

when snapShot.invoke() it called, it will trigger the callback and would get the latest bitmap value. Check out examples to understand the use case.

Example :

Coloumn {
    val snapShot = CaptureBitmap {
        Button(
        onClick = {},
        modifier = Modifier
            .fillMaxWidth()
            .padding(24.dp)
            .height(55.dp),
        ) {
            Text(text = "Capture my image")
        }
    }

    Button(
        onClick = {
            MainScope().launch {
                val bitmap = snapShot.invoke()
                val uri = saveImage(bitmap, context)
                if (uri != null) {
                    shareImageUri(context, uri)
                }
            }
        },
        modifier = Modifier
            .fillMaxWidth()
            .padding(24.dp)
            .height(55.dp),
        ) {
            Text(text = "Share")
        }
}

Checkout full Implementation at :

๐Ÿ–ผ๏ธ ISSUE-119 Share Note as image by ch8n ยท Pull Request #269 ยท PatilShreyas/NotyKT
Summary Added share Note as Image feature #119 Description for the changelog added Share Drop Down Modified Share Action behavior Added CaptureBitmap Composable CaptureBitmap returns a callback to...

If you guys have suggestions that could help me implement a better API Iโ€™m all ears reach me out on my social media.

Update Dec 2021 :

Thanks @Shreyas Patil to come up with way to convert the method to covert it to callback/event out style api

@Composable
fun CaptureBitmap(
	captureRequestKey: String,
    content: @Composable () -> Unit,
    onBitmapCaptured : (Bitmap) -> Unit
) {

    val context = LocalContext.current
    
    /** 
     * ComposeView that would take composable as its content
     * Kept in remember so recomposition doesn't re-initialize it
     **/
    val composeView = remember { ComposeView(context) }
    
    // If key is changed it means it's requested to capture a Bitmap
    LaunchedEffect(captureRequestKey) {
        view.post {
           onBitmapCaptured.invoke(view.drawToBitmap())
        }
    }
   
    /** Use Native View inside Composable **/
    AndroidView(
        factory = {
            composeView.apply {
                setContent {
                    content.invoke()
                }
            }
        }
    )
}

check out more on :

NotyKT/Capturable.kt at master ยท PatilShreyas/NotyKT
๐Ÿ“’ NotyKT is a complete ๐Ÿ’ŽKotlin-stack (Backend + Android) ๐Ÿ“ฑ application built to demonstrate the use of Modern development tools with best practices implementation๐Ÿฆธ. - NotyKT/Capturable.kt at master...

If you like my content and want to see more, click right here to see more ๐Ÿ‘‡

About ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป
@ch8nLinktree. Make your link do more.Linktree [https://linktr.ee/ch8n]Hey bud ๐Ÿ™Œ, Welcome to my personal blogging site! This is a place where I keep all of my tips and tricks regarding Android Development , Kotlin and Java, which I have gathered from my experience and various sources. So this is โ€ฆ

Until next time, Happy Hacking! ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป


Enjoying the Post?

a clap is much appreciated if you enjoyed. No sign up or cost associated :)

If you want to support my content do consider dropping a tip/coffee/donation๐Ÿ’ฐ


Reach me out :

Chetan Gupta
Software engineer ๐Ÿ‘จโ€๐Ÿ’ป, Kotlin ๐Ÿš€ Android ๐Ÿ“ฑ.



Chetan gupta

Hi there! call me Ch8n, I'm a mobile technology enthusiast! love Android #kotlinAlltheWay, CodingMantra: #cleanCoder #TDD #SOLID #designpatterns

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.