Thread
#compose
    Christopher Elías

    Christopher Elías

    1 year ago
    Hello guys, I'm using jetpack compose on a project and it's awesome. I'm displaying a Linechart correctly but I believe the performance of the drawing can be improved with coroutines... I'm doing something like this 👇 but it crash with an
    UnsupportedException
    , does anybody has trying to do the same and actually succeed?
    @Composable
    fun LinearTransactionsChart(
        modifier: Modifier = Modifier,
        transactionsPerSecond: TransactionsPerSecond,
        defaultDispatcher: CoroutineDispatcher
    ) {
        if (transactionsPerSecond.transactions.isEmpty()) return
    
        val composableScope = rememberCoroutineScope()
    
        Canvas(modifier = modifier) {
            composableScope.launch(Dispatchers.Main) {
                emitTransactions(
                    defaultDispatcher = defaultDispatcher,
                    maxTransaction = transactionsPerSecond.maxTransaction,
                    transactions = transactionsPerSecond.transactions,
                    canvasWidth = size.width,
                    canvasHeight = size.height
                ) { start, end ->
                    drawLine(
                        start = start,
                        end = end,
                        color = Color(0xFFFFFFFF)
                    )
                }
            }
        }
    }
    
    // The list can have like 1K ~ 2K items...
    suspend fun emitTransactions(
        defaultDispatcher: CoroutineDispatcher,
        maxTransaction: Double,
        transactions: List<TransactionRate>,
        canvasWidth: Float,
        canvasHeight: Float,
        dots: (start: Offset, end: Offset) -> Unit
    ) {
    	// ... Do some heavy computation for creating the start and end Offset...
        withContext(defaultDispatcher) {       
            transactions.forEachIndexed { index, transactionRate ->           
                dots(Offset(x, y), Offset(x, y)) 
            }
        }
    }
    Nader Jawad

    Nader Jawad

    1 year ago
    Drawing in compose (and within Android) does not work in a multithreaded manner so even using coroutines like this would not work. More specifically when a View/Composeable is invalidated on screen, all of it's drawing instructions are re-recorded. An alternative approach would be to allocate an ImageBitmap that has the lines drawn into it incrementally as they are added and that bitmap is drawn on screen on each frame. This would be much more performant than trying to draw all of the lines at once each time.
    Christopher Elías

    Christopher Elías

    1 year ago
    Uhm... I think I don't understand the ImageBitmap approach, do you have some sample maybe?
    Nader Jawad

    Nader Jawad

    1 year ago
    @Composable
    fun DrawLines(modifier: Modifier = Modifier, pt1: Offset, pt2: Offset) {
        class DrawParams(val imageBitmap: ImageBitmap, val canvas: Canvas, val drawScope: CanvasDrawScope)
        val drawParamsRef = remember { Ref<DrawParams>() }
        Box(modifier = modifier.drawWithCache {
            val canvas: Canvas
            val drawScope: CanvasDrawScope
            val imageBitmap: ImageBitmap
            var drawParams = drawParamsRef.value
            // Create offscreen bitmap if we don't have one. Note this should also be updated
            // if the size changes. This bitmap is persisted across composition calls and retains
            // the previously drawn lines across each composition
            if (drawParams == null) {
                imageBitmap = ImageBitmap(size.width.roundToInt(), size.height.roundToInt())
                canvas = Canvas(imageBitmap)
                drawScope = CanvasDrawScope()
                drawParams = DrawParams(imageBitmap, canvas, drawScope)
                drawParamsRef.value = drawParams
            }
            drawParams.drawScope.draw(this, layoutDirection, drawParams.canvas, size) {
                drawLine(color = Color.Blue, start = pt1, end = pt2)
            }
            onDrawBehind {
                // After the bitmap is updated, draw the bitmap once here
                drawImage(drawParams.imageBitmap)
            }
        })
    }
    
    @Composable
    fun RandomLinesDemo() {
        var pt1 by remember{ mutableStateOf(Offset.Zero) }
        var pt2 by remember{ mutableStateOf(Offset.Zero) }
        val size = 300
        val density = LocalDensity.current
        val sizeDp = with(density) {
            size.toDp()
        }
        val random = Random.Default
        DrawLines(modifier = Modifier.size(sizeDp).clickable {
            pt1 = Offset(random.nextInt(0, size).toFloat(), random.nextInt(0, size).toFloat())
            pt2 = Offset(random.nextInt(0, size).toFloat(), random.nextInt(0, size).toFloat())
    
        }, pt1, pt2)
    }
    With the sample above, we are allocating a bitmap once with the desired dimensions and each time we re-compose we draw an additional line with the points specified into this scratch bitmap. Then on the actual draw call, we just draw the bitmap itself. So on each composition we're only drawing the deltas of the newly added lines instead of drawing every line in the data set on each frame. If you tap the box repeatedly, we generate new random lines but still see the previous ones each time. Note an actual solution would be sure to update the bitmap if the size changes and probably include a clear method etc. etc. but just wanted to keep the bare minimum for the sake of the example
    Christopher Elías

    Christopher Elías

    1 year ago
    Thanks for the explanation.