I am trying to convert a view based screen to Compose and while what I need should be very basic, somehow I can’t get this to work. The use case at hand is a serial task where one step follows the other and the UI should reflect progress. But I seem to miss something fundamental because none of my Text() will update. Below is a simplified example of what I got:

    override fun onCreate(savedInstanceState: Bundle?) {
        …
        
        setContent {
            Import()
        }
    }
    
    
    @Composable
    fun Import() {        
        var step1 by remember { mutableStateOf("") }
        var step2 by remember { mutableStateOf("") }
          
        Column() {
                Text(text = step1)
                Text(text = step2)
            }
        }

        step1 = "Open ZIP file"
        val zipIn: ZipInputStream = openZIPFile()
        step1 = "✓ $step1"
    
        step2 = "Extract files"
        val count = extractFiles()
        step2 = "✓ $step2"
        …
    }

If I set the initial text in the remember line, like this

var step1 by remember { mutableStateOf("Open ZIP file") }

the text will show, but also never gets updated.

I also tried to move the logic part into a separate function which gets executed right after setContent() but then the step1/step2 aren’t available for me to update.

#######

Edit:

Well, as expected this turned out to be really easy. I have to break this one

var step1 by remember { mutableStateOf("Open ZIP file") }

into 2 statements:

var step1String =  mutableStateOf("Open ZIP file")

With step1String as a class wide variable so I can change it from other functions. In the Import() composable function al I need is this:

var step1 by remember { step1String }

Have to say Compose is growing on me… :-)

  • perryOnCrack
    link
    fedilink
    arrow-up
    1
    ·
    11 months ago

    You shouldn’t write your Composable like this, Composable shouldn’t have business logic in it.

    A Composable only describes the UI it represents, and it’s possible to be re-runned at any point in time. In the best case scenario, it should only display the states it received from an entity like a ViewModel and emit actions to the entity as well.

    So in your case, the zip opening things should be outside of the import() Composable depends on you application structure and updated string through a different means.

    • perryOnCrack
      link
      fedilink
      arrow-up
      1
      ·
      11 months ago

      The Android Developer channel on YouTube has a lot of helpful tutorial on developing in Compose.

      I recommend watching this playlist to understand the basics concepts.

      • Thomas@lemmy.zell-mbc.comOP
        link
        fedilink
        arrow-up
        1
        ·
        11 months ago

        Very helpful, thank you. I will absolutely watch these videos. And I am really glad that I seem to have found a forum where I can get some good input whenever I am stuck on something. It’s been painful in the past :-) I know I am lacking the basics but still managed to get an app off the ground which appears to be useful for quite a few users globally. B

    • Thomas@lemmy.zell-mbc.comOP
      link
      fedilink
      arrow-up
      1
      ·
      11 months ago

      I do agree, just couldn’t figure out how to do it properly. Opening the ZIP and all subsequent actions are now outside of the composable import(). But I realized the UI didn’t get updated until the “outside” function completed, so I ended up pushing the business logic to a coroutine:

      Like this:

              setContent {
                  ImportUI()
              }
              CoroutineScope(Dispatchers.Default).launch {
                  importLogic()
              }
      
        • Thomas@lemmy.zell-mbc.comOP
          link
          fedilink
          arrow-up
          1
          ·
          11 months ago

          Ha, thank you. I didn’t even realize that there is such granularity in dispatchers. Changed accordingly 👍 I assume the IO dispatcher is somehow more efficient when it comes to IO tasks?

          Would you care to elaborate about the lifecycle scope? I somehow don’t seem to be able to add the dependency and am not sure how this is going to improve things? Is this about making sure that the coroutine does or doesn’t get canceled in case the user quits the activity before the import is complete?

          //        LifecycleCoroutineScope(Dispatchers.IO).launch {
          //        LifecycleScope(Dispatchers.IO).launch {
                  CoroutineScope(Dispatchers.IO).launch {
                      importLogic()
                  }
          
          • dan0@programming.dev
            link
            fedilink
            arrow-up
            2
            ·
            11 months ago

            For the dispatchers, the docs do a better job explaining then I should try to give, but in short IO is optimized for long running operations, whereas Default is optimized for running more intensive computations.

            For the lifecycle scope that’s basically it. Fragments, Activities, and ViewModels all have their own variants of this. It’s almost always bad practice to have a coroutine not scoped to some form of lifecycle or you could easily end up with a bunch of memory leaks and unreported issues (https://kotlinlang.org/docs/coroutines-basics.html#structured-concurrency).

      • perryOnCrack
        link
        fedilink
        arrow-up
        1
        ·
        11 months ago

        I’ve done something very similar recently actually.

        I don’t know if it’s a good practice but I ended using DataStore and StateFlow as a signalling mechanism between ViewModel and the zip operation.