I am wondering about some compose internals and how compose could be "taught" that one composable is the same composable as before.
Following example:
enum class State { S1, S2 }
#Composable
fun MyUi(state : State){
when (state) {
S1 -> FancyUi("coming from s1")
S2 -> FancyUi("coming from s2")
}
}
#Composable
fun FancyUi(text : String) {
// some fancy UI stuff is happening here :)
}
I don't know what the right terminology is in compose world but what is happening is that the first FancyUi("coming from s1") is stopped (I think correct terminology is that composition is left) and the second FancyUi("coming from s1") is started (I think correct terminology is composition is entered).
What I would like to achieve though is "recomposition", so basically teach compose runtime somehow that both FancyUi() are actually the same object, just with different parameters.
I know you could achieve this with something like this (instead of the example above):
#Composable
fun MyUi(state : State){
val str = when (state) {
S1 -> "coming from s1"
S2 -> "coming from s2"
}
FancyUi(str)
}
Now FancyUi() doesn't get stopped when the state changes but instead does "recomposition" as expected.
But I am really just curious: getting back to the first example, can I teach somehow compose that both FancyUiare actually the same? I thought key() does the trick, but apparently, it doesn't:
#Composable
fun MyUi(state : State){
when (state) {
S1 -> key("samekey") { FancyUi("coming from s1") }
S2 -> key("samekey") { FancyUi("coming from s2") }
}
}
Is it possible to "teach" compose that these elements are the same?
Related
One astonishing thing about my research into compose interop was that there is absolutely no coverage anywhere on the subject of Activity/Fragment to compose communication .
Isn't this a very commonly occurring situation in which a fragment/activity might want to refresh its content ? .
This is how I use my compose in fragment (standard way)
class Fragment
{
onCreateView() {
showCompose()
}
private fun showCompose() {
binding.composeView.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MotivoTheme() {
Text(text="this is compose")
}
}
}
}
}
The only way i see at the moment to send arguments to compose view is via a state variable in the Fragment , something like this
class Fragment {
var somethingHappened = false
private fun showCompose() {
binding.composeView.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MotivoTheme() {
if (somethingHappened) {
Text(text = "Something happenede")
} else {
Text(text = "this is compose")
}
}
}
}
}
}
This approach obviously is sinful .
Any guesses how to do this elegantly ?
It is my understanding that the whole Fragment UI idea is basically deprecated. Composables aren't supposed to run inside Fragments -- they're meant to replace them. Here's a very basic write-up about it, but I'm sure that you can find others.
You can technically have Compose interact with the old View architecture using the Interoperability API, but your life will be much simpler if you just embrace Compose.
Say I have 2 custom Snackbar implementations, for example:
#Composable
fun RedSnackbar() {
...
}
#Composable
fun GreenSnackbar() {
...
}
I also have a Scaffold implementation on a screen where I'd like to invoke the red snackbar in some cases, and the green one in others.
The Google documentation says you can use your own SnackbarHost implementation, but you can only set one snackbarHost on the Scaffold, and this only allows for one kind of a snackbar - how can I toggle between RedSnackbar and GreenSnackbar?
The only way I can think of, is to check the SnackbarData.message like so, but that doesn't feel like a great solution:
SnackbarHost(it) { data ->
if (data.message == "red") {
RedSnackbar()
} else {
GreenSnackbar()
}
}
Is there a better way to achieve this?
I need to check when a certain LazyColumn item comes into view, and once it does, make a callback to onItemWithKeyViewed() only once to notify that this item has been viewed.
My attempt:
#Composable
fun SpecialList(
someItems: List<Things>,
onItemWithKeyViewed: () -> Unit
) {
val lazyListState = rememberLazyListState()
if (lazyListState.isScrollInProgress) {
val isItemWithKeyInView = lazyListState.layoutInfo
.visibleItemsInfo
.any { it.key == "specialKey" }
if (isItemWithKeyInView) {
onItemWithKeyViewed()
}
}
LazyColumn(
state = lazyListState
) {
items(items = someItems) { itemData ->
ComposableOfItem(itemData)
}
item(key = "specialKey") {
SomeOtherComposable()
}
}
}
Issue with my method is I notice the list scrolling performance degrades badly and loses frames. I realize this may be because it's checking all visible item keys on every frame?
Also, onItemWithKeyViewed() is currently being called multiple times instead of just the first time it's viewed.
Is there a more efficient way to make a single callback to onItemWithKeyViewed() only the first time "specialKey" item is viewed?
In such cases, when you have a state that is updated often, you should use derivedStateOf: this will cause recomposition only when the result of the calculation actually changes.
You should not call side effects (which is calling onItemWithKeyViewed) directly in the composable builder. You should use one of the special side-effect functions instead, usually LaunchedEffect - this ensures that the action is not repeated. You can find more information on this topic in Thinking in Compose and in side-effects documentation.
val isItemWithKeyInView by remember {
derivedStateOf {
lazyListState.isScrollInProgress &&
lazyListState.layoutInfo
.visibleItemsInfo
.any { it.key == "specialKey" }
}
}
if (isItemWithKeyInView) {
LaunchedEffect(Unit) {
onItemWithKeyViewed()
}
}
I am working on a compose screen, where on application open, i redirect user to profile page. And if profile is complete, then redirect to user list page.
my code is like below
#Composable
fun UserProfile(navigateToProviderList: () -> Unit) {
val viewModel: MainActivityViewModel = viewModel()
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
return
}
else {
//compose elements here
}
}
but the app is blinking and when logged, i can see its calling the above redirect condition again and again. when going through doc, its mentioned that we should navigate only through callbacks. How do i handle this condition here? i don't have onCLick condition here.
Content of composable function can be called many times.
If you need to do some action inside composable, you need to use side effects
In this case LaunchedEffect should work:
LaunchedEffect(viewModel.userProfileComplete == true) {
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
}
}
In the key(first argument of LaunchedEffect) you need to specify some key. Each time this key changes since the last recomposition, the inner code will be called. You may put Unit there, in this case it'll only be called once, when the view appears at the first place
The LaunchedEffect did not work for me since I wanted to use it in UI thread but it wasn't for some reason :/
However, I made this for my self:
#Composable
fun <T> SelfDestructEvent(liveData: LiveData<T>, onEvent: (argument: T) -> Unit) {
val previousState = remember { mutableStateOf(false) }
val state by liveData.observeAsState(null)
if (state != null && !previousState.value) {
previousState.value = true
onEvent.invoke(state!!)
}
}
and you use it like this in any other composables:
SingleEvent(viewModel.someLiveData) {
//your action with that data, whenever it was triggered, but only once
}
I am trying to create a splash screen using Jetpack Compose. I created my navigation and I have all my IDs to go to different screens, but I cannot make a screen navigate to another inside of a Hadler. How do you guys go about that?
#Composable
fun GoToMainScreen(navController: NavHostController){
Handler(Looper.getMainLooper()).postDelayed(object : Thread() {
override fun run() {
navController.navigate("main_screen")
Log.i("LOOPER", "It got here!")
}
}, 4000L)
}
I've tried this, and in my case it navigates fine with your composable:
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "profile") {
composable("profile") {
GoToMainScreen(navController)
}
composable("main_screen") { Text("main_screen") }
}
Not sure what's different in your case, but in compose we wouldn't usually need Handler
First of all, you need to wrap creation of the handler LaunchedEffect, otherwise your handler may be created many times in case of screen recomposition.
And inside LaunchedEffect we can use coroutine, so same with much less code looks like:
#Composable
fun GoToMainScreen(navController: NavHostController) {
LaunchedEffect(Unit) {
delay(2000L)
navController.navigate("main_screen")
}
}
If this still doesn't help make sure providing a minimal-reproducible-example, something like my first block of code.