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?
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.
So I've been exploring MVI with Kotlin Flows, basically MutableSharedFlow for Events and MutableStateFlow for States. but I have a problem adding logic to control the text changes event emissions, writing this in a human way would be something like that.
Observe text changes and only search for the latest word written
within half of a second.
So if the user removes or adds letters I only take the search for the latest word after half-second passed.
My attempt to achieve this was by writing the following, here I subscribe to events
events.flatMapConcat { it.eventToUsecase() }
.onEach { _states.value = it }
.launchIn(viewModelScope)
And I map each event to a use case using this function:
private fun SearchScreenEvent.eventToUsecase(): Flow<SearchState> {
return when (this) {
is SearchClicked -> searchUsecase(this.query)
is SearchQueryChanged ->
flowOf(this.query)
.debounce(500)
.flatMapConcat { searchUsecase(it) }
}
}
I know that I have to control the event itself, but how to control only the SearchQueryChanged event independently. with RxJava I was using Publish and switchMap operators is there something like this with Flow.
debounce() can take a lambda parameter that determines the latency per item, so I think this will work:
events.debounce {
when (it) {
is SearchClicked -> 0L
is SearchQueryChanged -> 500L
}
}
.flatMapConcat { searchUsecase(it.query) }
.onEach { _states.value = it }
.launchIn(viewModelScope)
#Tenfour04 Solution will work, but it shows a design problem, so I have to take care of the flow logic in two places now, may be more, so I've posted the same question in slack and someone guides me to redesign my flow as follow:
merge( searchCurrentQuery(),
searchWhileTyping(),
updateUiSearchText())
.flatMapMerge { it.toUsecase() }
.onEach { _states.value = it }
.launchIn(viewModelScope)
By seperating events and using merge to add them into the stream, now I could control each Flow of them as I want.
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.
video with bug
My app architecture looks like: viewModel + Compose layout per screen. In my viewModels I have Channel() where I handle actions from screen:
init {
viewModelScope.launch {
try {
actions.receiveAsFlow().collect { action ->
when (action) {
//handle actions here
}
}
}
}
In Home screen I have list with post. The problem is when I select post and navigate user to PostDetails screen I can choose another post though list with post is underhood. I think it's related to Home viewModel and that action flow works in the background. Any solution what should I change?
#Composable
fun AppNavigationHost(appNavController: NavController) {
NavHost(
navController = appNavController as NavHostController,
startDestination = PostDetails.route
) {
composable(Default.route) {}
composable(PostDetails.route) { it ->
it.arguments?.getString("post").let {
val post = Gson().fromJson(it, Post::class.java)
PostDetailsScreen(post, appNavController)
}
}
}
}
I've had similar problems before, albeit not using compose. If you don't set a dialog to be clickable then you can click through it. I would suggest adding:
android:clickable="true"
To the UI you're using for the post details, or the compose equivalent