Jetpack Compose Crossfade broken in Alpha - android

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()
}
}

Related

Animate the Content Changes for a particular Composable,

I have a simple Composable that shows some data to the user, and that data needs to be updated periodically. Now, I wish to add a smooth transition between data changes instead of just snapping the new data in, so that is the prime focus of this question.
Now, for a mere example, we could take a simple Image Composable. I have this
#Composable
fun ImageFrame(imagePainter: Painter){
Box{
Image(
imagePainter,
... //Modifications, etcetra
)
}
}
If I want the painter to change every three seconds, how should I go about animating this change in reference to the Box with a slide-in and slide-out motion?
Found a great experimental API, built into Compose for the exact same thing, it's called AnimatedContent. Here's an implementation for my use-case
#Composable
fun AnimatedImageFrame(image: Painter){
AnimatedContent(
targetState = image,
transitionSpec = {
(slideInHorizontally { -it } with slideOutHorizontally { it })
.using(
// Disable clipping since the faded slide-in/out should
// be displayed out of bounds.
SizeTransform(clip = false)
)
}
) {
Image(
painter = it,
contentDescription = ""
)
}
}
Every time you update the parameter passed to the AnimatedImageFrame, the image displayed would animate by doing a sliding motion.
To read more, refer to this doc.
I had a similar issue, and as far as I know, the only out-of-the-box animation in Compose for content changes is Crossfade.
I ended up implementing a custom animation based on the Crossfade implementation, as it effectively will take care of managing the state for you, you'll just need to swap out the alpha value animation for slide in/out animation. Inspect the Crossfade implementation, and you'll see how it works.
The bit where Crossfade composes the new content with the fading animation is this:
CrossfadeAnimationItem(key) {
val alpha by transition.animateFloat(
transitionSpec = { animationSpec }
) { if (it == key) 1f else 0f }
Box(Modifier.graphicsLayer { this.alpha = alpha }) {
content(key)
}
}
So for sliding in/out instead, you may end up with something like:
SlideContentInAnimationItem(key) {
transition.AnimatedVisibility(
visible = { it == key },
enter = slideInHorizontally( /* your animation config*/ ),
exit = slideOutHorizontally( /* your animation config*/ )
) {
content(key)
}
}
The targetState property and the key value are one and the same - in your example it would represent your ImagePainter - if that value changes due to a state change in the composable, the Crossfade or your new custom sliding animation will be triggered.

How to remove default transitions in Jetpack Compose Navigation

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.

Jetpack Compose State Hoisting, Previews, and ViewModels best practices

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

Why need the author to add the keyword remember in this #Composable?

The Code A is from the project ThemingCodelab, you can see full code here.
I think that the keyword remember is not necessary in Code A.
I have tested the Code B, it seems that I can get the same result just like Code A.
Why need the author to add the keyword remember in this #Composable ?
Code A
#Composable
fun Home() {
val featured = remember { PostRepo.getFeaturedPost() }
val posts = remember { PostRepo.getPosts() }
MaterialTheme {
Scaffold(
topBar = { AppBar() }
) { innerPadding ->
LazyColumn(contentPadding = innerPadding) {
item {
Header(stringResource(R.string.top))
}
item {
FeaturedPost(
post = featured,
modifier = Modifier.padding(16.dp)
)
}
item {
Header(stringResource(R.string.popular))
}
items(posts) { post ->
PostItem(post = post)
Divider(startIndent = 72.dp)
}
}
}
}
}
Code B
#Composable
fun Home() {
val featured =PostRepo.getFeaturedPost()
val posts = PostRepo.getPosts()
...//It's the same with the above code
}
You need to use remember to prevent recomputation during recomposition.
Your example works without remember because this view will not recompose while you scroll through it.
But if you use animations, add state variables or use a view model, your view can be recomposed many times(when animating up to once a frame), in which case getting data from the repository will be repeated many times, so you need to use remember to save the result of the computation between recompositions.
So always use remember inside a view builder if the calculations are at least a little heavy, even if right now it looks like the view is not gonna be recomposed.
You can read more about the state in compose in documentation, including this youtube video, which explains the basic principles.

Overscroll handling in Jetpack Compose

I'm trying to create a Pull-to-Refresh logic in my app.
I know it starts with handling Overscrolling, but I can't seem to find anything in compose that has to do with Overscrolling.
Is it not implemented in Compose yet? Or is it hidden somewhere?
I'm using a LazyColumn right now, I didn't find anything in the LazyListState.
You can use the Swipe Refresh feature included in Google's Accompanist library.
Example usage:
val viewModel: MyViewModel = viewModel()
val isRefreshing by viewModel.isRefreshing.collectAsState()
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = { viewModel.refresh() },
) {
LazyColumn {
items(30) { index ->
// TODO: list items
}
}
}
See the docs for more details.

Categories

Resources