One can create a EnterTransition in jetpack compose by concatenating various types of transitions like slideIn() + fadeIn() etc. which then constructs the EnterTransition which contains all the transitions in a TransitionData object.
But the problem is that the TransitionData property inside the EnterTransition is marked as internal. I want to animate properties in the graphics layer such as alpha and translationX based on what transition are available.
Is there any other way to get the all the different types of transitions defined in a EnterTransition like this:
fun createAnimation(
enter: EnterTransition = slideInHorizontaly() + fadeIn()
) {
val fade = enter.data.fade ?: defaultFadeIn // not possible: data is internal
val slide = enter.data.slide ?: defaultSlideIn // not possible: data is internal
...
}
Related
I have a Compose screen, with two separate components:
horizontal scroll of items
vertical scroll of card items, which need to be paginated
I also have a ViewModel attached to that screen which provides the state:
val viewState: StateFlow<MyScreenState> = _viewState
...
data class MyScreenState(
val horizontalObjects: List<MyObject>,
val verticalCardsPaged: Flow<PagingData<MyCard>>
)
The cards are paged, the horizontal list doesn't have to be. In the Compose screen, I receive and use the state like so:
val state = viewModel.viewState.collectAsState().value
MyScreen(state)
...
#Composable
fun MyScreen(state: MyScreenState) {
val cards: LazyPagingItems<MyCard> = state.verticalCardsPaged.collectAsLazyPagingItems()
LazyRow {
items(state.horizontalObjects) {
...
}
}
LazyColumn {
items(cards) {
...
}
}
}
So I have a Flow inside Flow, effectively. It all seems to be working ok, but I'm not sure if I should be combining them instead of nesting? What would be the preferred approach here?
I've created simple application to scan products barcode and retrieve information from API by this code. My whole application is routed by this composable:
#Composable
fun AppNavigationHost(navController: NavHostController, modifier: Modifier = Modifier) {
val sharableViewModel: SharableViewModel = hiltViewModel()
NavHost(navController = navController, startDestination = Main.route, modifier = modifier) {
composable(route = Main.route) {
MainScreen(viewModel = sharableViewModel, onSuccessfulProductScan = {
LaunchedEffect(sharableViewModel.product) {
navController.navigate(ProductDetail.route) {
popUpTo(Main.route)
}
}
})
}
composable(route = ProductDetail.route) {
ProductDetailsScreen(viewModel = sharableViewModel)
}
}
}
It works in a simple way:
From MainScreen i call viewModel.findProduct when button is clicked.
When product not exists, it has state NOT_FOUND and simply returns message, that product does not exist. My state has type mutableStateFlow<ProductState>. When It was LiveData, I couldn't navigate between my composables.
When product exists, I update my product in viewModel of type mutableLiveData<Product>. When I change it to StateFlow, my whole app crushes.
ProductDetailsScreen observe product from viewModel. When it's filled with data, composable updates it's view with product information.
Now, I don't understand few things, like:
I must initialize my SharedViewModel in shareable place like AppNavigationHost. When I add hiltViewModel() as a default parameter in those composables, my app instantly crashes. Why I can't inject viewModel with it's state as a separated parameter in composable?
Why I need to use StateFlow when I have to navigate between composables, why LiveData is not enought to handle it? Is there any possibility to use StateFlow instead of LiveData through navigated views without handling LaunchedEffect scope?
Why I need to use LiveData to persist object in ViewModel between composables? StateFlow is bounded to viewModel, not to composable. It should "emit" my product once and next view should retrieve that event.
I wish I could understand well basics of composable concept, but some of them are not clear enough for me, e.g. passing data in other way than shareable view model.
I'm using the following sippet of code to navigate from a composable to another one, but it has a default fade animation. How can I remove it? I tried using an empty anim resource but it doesn't work.
navHostController.navigate(
"destination_route",
navOptions {
popUpTo("this_route") {
inclusive = true
}
anim {
enter = R.anim.empty_animation
exit = R.anim.empty_animation
popEnter = R.anim.empty_animation
popExit = R.anim.empty_animation
}
}
)
R.anim.empty_animation:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!--Empty to disable animation-->
</set>
As of right now, as EpicPandaForce said it is not possible, but that is because it is in the works!
Currently this functionality is served under accompanist, in the accompanist-navigation-animation artifact. You can read more about it here or in a more detailed blogpost here where they talk a bit about the future of it too.
The gist of it is that with that dependency (and without it when it eventually gets merged to the normal navigation-compose library) you will be able to write something like this:
composable(
"profile/{id}",
enterTransition = { _, _ ->
// Whatever EnterTransition object you want, like:
fadeIn(animationSpec = tween(2000))
}
) { // Content }
Currently, there is no way to configure the animations in the NavHost offered by Navigation-Compose's current version (2.4.0-beta02).
#Composable
public fun NavHost(
navController: NavHostController,
graph: NavGraph,
modifier: Modifier = Modifier
) {
val backStackEntry = visibleTransitionsInProgress.lastOrNull() ?: visibleBackStack.lastOrNull()
var initialCrossfade by remember { mutableStateOf(true) }
if (backStackEntry != null) {
// while in the scope of the composable, we provide the navBackStackEntry as the
// ViewModelStoreOwner and LifecycleOwner
Crossfade(backStackEntry.id, modifier) { //// <<----- this
As Crossfade is not configurable, the transition cannot be changed.
To change the animation, you have to abandon using the NavHost provided by Navigation-Compose.
So it seems like the recommended thing in Jetpack Compose is to hoist state out of your composables, to make them stateless, reusable, and testable, and allow using them in previews easily.
So instead of having something like
#Composable
fun MyInputField() {
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it })
}
You'd hoist the state, like this
#Composable
fun MyInputField(text: String, onTextChange: (String) -> Unit) {
TextField(value = text, onValueChange = onTextChange)
}
This is fine, however what of some more complex uses?
Let's pretend I have a screen represented by a composable, with multiple interactions between the View and the ViewModel. This screen is split into multiple inner composable (think for instance one for a header, one for the body, which in turn is split into several smaller composables)
You can't create a ViewModel (with viewModel() at least, you can instantiate one manually) inside a composable and use this composable in a Preview (previews don't support creating viewmodel like this)
Using a ViewModel inside the inner composables would make them stateful, wouldn't it ?
So the "cleanest" solution I see, would be to instantiate my viewmodel only at the highest composable level, and then pass to the children composables only vals representing the state, and callbacks to the ViewModel functions.
But that's wild, I'm not passing down all my ViewModel state and functions through individual parameters to all composables needing them.
Grouping them in a data class for example could be a solution
data class UiState(
val textInput: String,
val numberPicked: Int,
……
and maybe create another one for callbacks ?
But that's still creating a whole new class just to mimic what the viewmodel already has.
I don't actually see what the best way of doing this could be, and I find nothing about that anywhere
A good way to manage complex states is to encapsulate required complex behavior into a class and use remember function while having stateless widgets as most as you can and change any properties of state whenever it's required.
SearchTextField is a component that uses only state hoisting, SearchBar has back arrow and SearchTextField and also itself is a stateless composable. Communication between these two and parent of Searchbar is handled via callback functions only which makes both SearchTextField re-suable and easy to preview with a default state in preview. HomeScreen contains this state and where you manage changes.
Full implementation is posted here.
#Composable
fun <R, S> rememberSearchState(
query: TextFieldValue = TextFieldValue(""),
focused: Boolean = false,
searching: Boolean = false,
suggestions: List<S> = emptyList(),
searchResults: List<R> = emptyList()
): SearchState<R, S> {
return remember {
SearchState(
query = query,
focused = focused,
searching = searching,
suggestions = suggestions,
searchResults = searchResults
)
}
}
remember function to keep state for this only to be evaluated during the composition.
class SearchState<R, S>(
query: TextFieldValue,
focused: Boolean,
searching: Boolean,
suggestions: List<S>,
searchResults: List<R>
) {
var query by mutableStateOf(query)
var focused by mutableStateOf(focused)
var searching by mutableStateOf(searching)
var suggestions by mutableStateOf(suggestions)
var searchResults by mutableStateOf(searchResults)
val searchDisplay: SearchDisplay
get() = when {
!focused && query.text.isEmpty() -> SearchDisplay.InitialResults
focused && query.text.isEmpty() -> SearchDisplay.Suggestions
searchResults.isEmpty() -> SearchDisplay.NoResults
else -> SearchDisplay.Results
}
}
And change state in any part of UI by passing state to other composable or by ViewModel as
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel,
navigateToTutorial: (String) -> Unit,
state: SearchState<TutorialSectionModel, SuggestionModel> = rememberSearchState()
) {
Column(
modifier = modifier.fillMaxSize()
) {
SearchBar(
query = state.query,
onQueryChange = { state.query = it },
onSearchFocusChange = { state.focused = it },
onClearQuery = { state.query = TextFieldValue("") },
onBack = { state.query = TextFieldValue("") },
searching = state.searching,
focused = state.focused,
modifier = modifier
)
LaunchedEffect(state.query.text) {
state.searching = true
delay(100)
state.searchResults = viewModel.getTutorials(state.query.text)
state.searching = false
}
when (state.searchDisplay) {
SearchDisplay.InitialResults -> {
}
SearchDisplay.NoResults -> {
}
SearchDisplay.Suggestions -> {
}
SearchDisplay.Results -> {
}
}
}
}
Jetmagic is an open source framework that deals exactly with this issue while also solving other major issues that Google neglected when developing Compose. Concerning your request, you don't pass in viewmodels at all as parameters. Jetmagic follows the "hoisted state" pattern, but it manages the viewmodels for you and keeps them associated with your composables. It treats composables as resources in a way that is similar to how the older view system treats xml layouts. Instead of directly calling a composable function, you ask Jetmagic's framework to provide you with an "instance" of the composable that best matches the device's configuration. Keep in mind, under the older xml-based system, you could effectively have multiple layouts for the same screen (such as one for portrait mode and another for landscape mode). Jetmagic picks the correct one for you. When it does this, it provides you with an object that it uses to manage the state of the composable and it's related viewmodel.
You can easily access the viewmodel anywhere within your screen's hierarchy without the need to pass the viewmodel down the hierarchy as parameters. This is done in part using CompositionLocalProvider.
Jetmagic is designed to handle the top-level composables that make up your screen. Within your composable hierarchy, you still call composables as you normally do but using state hoisting where it makes sense.
The best thing is to download Jetmagic and try it out. It has a great demo that illustrates the solution you are looking for:
https://github.com/JohannBlake/Jetmagic
My crossfade animations are no longer working since the release of Compose Alpha and I would really appreciate some help getting them working again. I am fairly new to Android/Compose. I understand that Crossfade is looking for a state change in its targetState to trigger the crossfade animation, but I am confused how to incorporate this. I am trying to wrap certain composables in the Crossfade animation.
Here are the official docs and helpful playground example, but I still cannot get it to work since the release of Alpha
https://developer.android.com/reference/kotlin/androidx/compose/animation/package-summary#crossfade
https://foso.github.io/Jetpack-Compose-Playground/animation/crossfade/
Here is my code, in this instance I was hoping to use the String current route itself as the targetState as a mutableStateOf object. I'm willing to use whatever will work though.
#Composable
fun ExampleComposable() {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute: String? = navBackStackEntry?.arguments?.getString(KEY_ROUTE)
val exampleRouteTargetState = remember { mutableStateOf(currentRoute)}
Scaffold(
...
NavHost(navController, startDestination = "Courses") {
composable("Route") {
Crossfade(targetState = exampleRouteTargetState, animationSpec = tween(2000)) {
ExampleComposable1()
}
}
composable("Other Route")
ExampleComposable2()
}
)
...
}
Shouldn't navigation trigger a state change of the "exampleRouteTargetState" variable and then trigger crossfade? I could also wrap the composable elsewhere if you think wrapping it inside the NavHost may create an issue. Thanks so much for the help!!
Lately Google Accompanist has added a library which provides Compose Animation support for Jetpack Navigation Compose.. Do check it out. 👍🏻
https://github.com/google/accompanist/tree/main/navigation-animation
Still haven't gotten Crossfade working again, but I was able to implement some transitions inside NavHost. Hope this helps someone. Here are the docs if you want to fine tune these high level animations:
https://developer.android.com/jetpack/compose/animation#animatedvisibility
#ExperimentalAnimationApi
#Composable
fun ExampleAnimation(content: #Composable () -> Unit) {
AnimatedVisibility(
visible = true,
enter = fadeIn(initialAlpha = 0.3f),
exit = fadeOut(),
content = content,
initiallyVisible = false
)
}
And then simply wrap your NavHost composable declarations with your animation like so
NavHost(navController, startDestination = "A Route") {
composable(Screen.YourObject.Route) {
ExampleAnimation {
YourComposable()
}
}