Tuesday, January 24, 2023

New Kung Fu

PYDroid 26.2.0 has been released which brings support for fully Compose layouts. No more view binding, no more XML, no more fragments - all Compose. PYDroid is now a Kotlin first, Compose first library.

And since PYDroid is the backbone of all pyamsoft applications, updates to all pyamsoft applications have been released (Home Button still reviewing) that move them to fully Compose layouts too.

TetherFi has received a plethora of fixes and new features as the community has picked up around it a bit this weekend. The network can be shared with a QR code, the password will be hidden by default, and language changes attempt to clarify features in the application. Future updates will continue to bring more features, and one day UDP proxying will work too - one can hope.

These updates have been pushed to the store and will go live for you soon.

Stay tuned!

========================
Follow pyamsoft around the Web for updates and announcements about the newest applications!
Like what I do?

Send me an email at: pyam.soft@gmail.com
Or find me online at: https://pyamsoft.blogspot.com

Follow my Facebook Page
Check out my code on GitHub
=========================

Wednesday, January 18, 2023

The important of re-reading documentation

PYDroid is being updated to 26.2.0 which brings support for a fully Compose application. PYDroid internally is dropping all Fragments and moving to a Single-Activity, multi-Composable architecture.

With this change comes a lot of learning about Compose. One particular problem that had me super confused for almost a day involves some very innocent code and some very particular Compose behaviors.

@Composable
fun Test() {
  val (show, setShow) = remember { mutableStateOf(false) }

  Scaffold {
    Box(
      modifier = Modifier.fillMaxSize(),
      contentAlignment = Alignment.Center,
    ) {
      Button(
        onClick = { setShow(true) },
      ) {
        Text(
          text = "Click Me",
        )
      }
    }
  }

  if (show) {
    Dialog(
      onDismissRequest = { setShow(false) },
    ) {
      Text(
        text = "Hello!",
      )
    }
  }
}

Looks pretty simple right? A composable with a button in the center, that when clicked, shows a Dialog. This works fine.

This doesn't:

@Composable
fun Test() {
  val (show, setShow) = remember { mutableStateOf(false) }
  val breakItAll by rememberUpdatedState { setShow(true) }

  Scaffold {
    Box(
      modifier = Modifier.fillMaxSize(),
      contentAlignment = Alignment.Center,
    ) {
      Button(
        onClick = breakItAll,
      ) {
        Text(
          text = "Click Me",
        )
      }
    }
  }

  if (show) {
    Dialog(
      onDismissRequest = { setShow(false) },
    ) {
      Text(
        text = "Hello!",
      )
    }
  }
}

See what changed? I've added a breakItAll field which remembers a callback passed to it through future renders.

If you've ever coded in React before, this kind of pattern may look very familiar to you. Instead of passing a lambda function to a callback, you memoize the callback with React.useCallback so it won't continue to be allocated each render pass.

In Compose this breaks horribly. Once you click the button and the Dialog launches, your Button will begin infinitely recomposing, and it will never ever stop, even if you close the Dialog. Like a ticking time bomb, once you set it off there is no going back.  Even worse, your app will probably still continue to work. The button still works, the Dialog still shows. But performance dies in a hole. No crashes, just extreme amounts of lag. What the heck is going on here, why does one simple change, which should optimize the code even, make it blow up so spectacularly?

Compose does not like this, because Compose also cares about the idea of Stability. There are plenty of good articles on the web about Compose and Stable such as this one. Now what is the problem with the above by rememberUpdatedState?

Compose treats lambdas as Stable. The first code works because the onClick is a lambda, so when it fires, the re-render sees that the Button component has not changed at all and skips it. It renders the Dialog and is done, life moves on. This is how things should work.

When we change this to by rememberUpdatedState, our resulting variable (despite being the same Type as the lambda) is no longer stable, since the type of the variable is actually the delegated property. Thus, when the button is clicked and the state is switched, the Button is re-rendered and (even though the onClick is not re-allocated, it is re-assigned to a new lambda) a new onClick is attached. It then goes and re-renders the Dialog as it did before. The issue now though, is the Composition snapshot has treated Button as a dirty change, and tells the engine to once again re-render the Button. The Button is rendered, a re-assigned onClick handler is given to it, and Compose once again marks it dirty. This continues to happen infinitely, causing the problem above.

The reason you may want to memoize something like a callback is for all the reasons Compose provides you with that callback function. But you need to be aware that typewise, the function is not the same as a lambda at all. You should generally only be using this rememberUpdatedState when you are attempting a callback from an Effect hook, as it was designed. This second code block is an anti-pattern and should not be used.

That was a fun day. Thankfully the problem has been fixed, and various Stable issues in PYDroid have been addressed, so performance is significantly improved. With the new library changes will come new versions of pyamsoft applications with shiny new features, so get excited for that!

Stay tuned!

========================
Follow pyamsoft around the Web for updates and announcements about the newest applications!
Like what I do?

Send me an email at: pyam.soft@gmail.com
Or find me online at: https://pyamsoft.blogspot.com

Follow my Facebook Page
Check out my code on GitHub
=========================