Search code examples
androidkotlinandroid-viewandroid-jetpack-compose

How can I make Android Jetpack Compose AndroidView be replaced when the parameters that created it change?


I have an app that shows several different views encapsulated in AndroidView. In the simple example to reproduce below, these are just TextView instances. The problem is that changing the text (in this case cycling through three different values) doesn't seem to update what the app is displaying.

sealed class AppView
data class ShowSomeText(val text: String) : AppView()
data class SomeOtherState(val data: Any?) : AppView()
data class ShowSomeText2(val text: String) : AppView()

class AppViewModel : ViewModel() {

    var currentView = MutableLiveData<AppView>(ShowSomeText("original text"))
    var currentViewWorkaround = MutableLiveData<AppView>(ShowSomeText("original text"))


    private val textRing = arrayOf("one", "two", "three")
    private var textRingPosition = 0

    fun incrementTextState() {
        val nextState = ShowSomeText(textRing[textRingPosition])
        currentView.postValue(nextState)

        val nextStateWorkaround = when(currentViewWorkaround.value) {
            is ShowSomeText -> ShowSomeText2(textRing[textRingPosition])
            else -> ShowSomeText(textRing[textRingPosition])
        }
        currentViewWorkaround.postValue(nextStateWorkaround)
        textRingPosition = (textRingPosition + 1) % textRing.size
    }
}

class MainActivity : AppCompatActivity() {

    private val viewModel = AppViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ViewContainer(viewModel)
        }
    }
}

@Composable
fun ViewContainer(viewModel: AppViewModel) {

    // Add this to gradle.build for the observeAsState function:
    //     implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
    val currentView: AppView by viewModel.currentView.observeAsState(ShowSomeText("starting text"))
    val currentViewWorkaround: AppView by viewModel.currentViewWorkaround.observeAsState(ShowSomeText("starting text"))

    Column {
        Button(onClick = viewModel::incrementTextState) {
            Text(
                text = "tap to change",
                style = TextStyle(fontSize = 12.em)
            )
        }
        Text("Compose Text")
        when (currentView) {
            is ShowSomeText -> createComposeTextView((currentView as ShowSomeText).text)
            is SomeOtherState -> Text("the other state")
        }
        Text("AndroidView wrapping TextView")
        when (currentView) {
            is ShowSomeText -> createAndroidViewForTextView((currentView as ShowSomeText).text)
            is SomeOtherState -> Text("the other state")
        }
        Text("AndroidView wrapping TextView with 2-state workaround")
        when (currentViewWorkaround) {
            is ShowSomeText -> createAndroidViewForTextView((currentViewWorkaround as ShowSomeText).text)
            is ShowSomeText2 -> createAndroidViewForTextView((currentViewWorkaround as ShowSomeText2).text)
            is SomeOtherState -> Text("the other state")
        }
    }

}

@Composable
fun createAndroidViewForTextView(text: String) {
    val context = ContextAmbient.current
    val tv = remember(text, context) {
        val x = TextView(context)
        x.text = text
        x.textSize = 48.0f
        x
    }
    AndroidView({ tv })
}

@Composable
fun createComposeTextView(text: String) {
    Text(text, style = TextStyle(fontSize = 12.em))
}

The first text is displayed via the Compose Text function and works, the second with a TextView wrapped an AndroidView Compose function and does not work, the third also uses the same AndroidView wrapper but triggers the change somehow by using another state variable.

Why doesn't the middle text update?

Full gist of a reproducing kt file with the hack fix: https://gist.github.com/okhobb/ba7791af4562ea672d0c52769a7cd8ba

============

UPDATE: Working code based on the accepted answer:

@Composable
fun TraditionalViewAsComposable(text: String){

    var updatableString by remember{mutableStateOf("")}
    updatableString = text

    AndroidView(
        factory={ TextView(it).apply {
            this.text = text
            this.textSize = 48.0f
        } },
        update={ it.text = updatableString }
    )
}

Solution

  • AndroidView() composables do not recompose by default upon statechange. You have to "opt in" to listen to state by explicitly defining an update parameter.

    So the syntax would be something like:

    @Composable
    fun TraditionalViewAsComposable(initialString:String){
        var updatableString by remember{mutableStateOf("")}
        AndroidView(factory={it:Context->
            TraditionalView(it).apply{this:TraditionalView->
                this.property=initialString
            },
            update={it:TraditionalView->
                it.property=updatableString
                },
            modifier=Modifier
            )
    }