본문 바로가기
Android/Compose

[안드로이드 Compose] 애니메이션 생성 시 context by implicit receiver 에러

by 굿햄 2022. 11. 22.

Subject

AnimatedVisibility 을 통해 애니메이션 구현 시, 아래와 같은 에러가 출력되는 경우
can't be called in this context by implicit receiver. Use the explicit one if necessary 

The Challenge

왜 이런 문제가 발생했을까?

코드를 먼저 보자 

Card(
        modifier = Modifier
            .width(300.dp)
            .wrapContentHeight(),
        elevation = CardDefaults.cardElevation(0.dp),
        shape = RoundedCornerShape(10.dp),
    ) {
    Box() {
            AnimatedVisibility(  // Error!
                visible = true,
                enter = fadeIn(),
            ) { 
            ...

Card는 ColumnScope를 갖는다.그리고 AnimatedVisibility는 Card를 통해 ColumnScope를 Context로 전달받는다.

 

문제는 @DslMarker이다. 이 아이는 어떤 역할을 하는 것일까?

우측 주소는 예시를 확인할 수 있는 사이트이다. https://pl.kotl.in/zoTq1_efU

 

Kotlin Playground: Edit, Run, Share Kotlin Code Online

 

play.kotlinlang.org

@DslMarker		// try commenting this line
annotation class FooDsl

@FooDsl
interface Root {
    fun nested(init: Nested.() -> Unit)
}

@FooDsl
interface Nested {
    var stuff: String?
}

fun makeRoot(init: Root.() -> Unit): Unit = TODO()

fun main() {
    makeRoot {
        nested {
            nested {  // 'fun nested(init: Nested.() -> Unit): Unit' can't be called in this context by implicit receiver. Use the explicit one if necessary
                stuff = "stuff"
            }
        }
    }
}

nested{} 안에 있는 nested{}에서 stuff를 접근하려 했기 때문에 에러가 났다. stuff는 Root를 리시버 형태로 컨텍스트를 전달받는데 내부 스코프에서 접근하려하니 @DslMarker 어노테이션 때문에 막혔다.

 

위와 같이 어노테이션을 통해 내부에서 외부 Context에 접근을 제한하는데 ColumnScope@LayoutScopeMarker라는 어노테이션이 달려있어 마찬가지로 외부 접근을 제한한다.

@LayoutScopeMarker
@Immutable
@JvmDefaultWithCompatibility
interface ColumnScope {
    /**
     * Size the element's height proportional to its [weight] relative to other weighted sibling
     * elements in the [Column]. The parent will divide the vertical space remaining after measuring
     * unweighted child elements and distribute it according to this weight.
     * When [fill] is true, the element will be forced to occupy the whole height allocated to it.
     * Otherwise, the element is allowed to be smaller - this will result in [Column] being smaller,
     * as the unused allocated height will not be redistributed to other siblings.
     *
     * @param weight The proportional height to give to this element, as related to the total of
     * all weighted siblings. Must be positive.
     * @param fill When `true`, the element will occupy the whole height allocated.
     *
     * @sample androidx.compose.foundation.layout.samples.SimpleColumn
     */
    @Stable
    fun Modifier.weight(
        /*@FloatRange(from = 0.0, fromInclusive = false)*/
        weight: Float,
        fill: Boolean = true
    ): Modifier

나와 같은 문제에 대해 stackoverflow에 질문이 올라왔고 누군가 Report도 올렸으나 개선의 의지는 아직 보이지 않는다.

 

Why can't I use `AnimatedVisibility` in a `BoxScope`?

I have a layout which looks like this: Row { ... Box( modifier = Modifier .fillMaxHeight() .width(50.dp) ) {

stackoverflow.com

 

다행히 우회책이 존재하는데 Composable 함수를 하나 더 만들어 Context 접근을 분리하는 방법이다.

이에 나는 울며겨자먹기로 AnimatedVisibility를 사용하는 부분만 아래와 같이 분리하였다.

@Composable
fun LoadedImage(
    painter: AsyncImagePainter,
    onClick: () -> Unit
) {
    AnimatedVisibility(  // 함수가 호출되면 fadeIn()을 실행한다.
        visible = true,
        enter = fadeIn(),
        exit = fadeOut()
    ) {
        Image(
            painter = painter,
            contentDescription = null,
            modifier = Modifier
                .clickable { onClick() }
                .fillMaxWidth()
                .wrapContentHeight()
                .animateContentSize(),
            contentScale = ContentScale.FillWidth,
        )
    }
}

 

ⓒ 굿햄 2022. daryeou@gmail.com all rights reserved.

댓글