How to maintain Lottie animation progress across different recompositions - android

I'm trying to use Lottie compose for playing animation in compose. But the animation starts from the very beginning for all recompositions. I wish to maintain the current playback and not restart the animation for each recomposition. Here is my current code
#Composable
fun Loader() {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.main))
LottieAnimation(composition)
}

You need to save animation progress outside of the composable functions, that will be recomposed
#Composable
fun ParentComposable() {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.main))
val animationProgress by animateLottieCompositionAsState(composition = composition)
ChildComposable(animationProgress = animationProgress, composition = composition)
}
#Composable
fun ChildComposable(animationProgress: Float, composition: LottieComposition?) {
...
Loader(progress = animationProgress, composition = composition)
...
}
#Composable
fun Loader(animationProgress: Float, composition: LottieComposition?) {
...
LottieAnimation(composition, animationProgress)
...
}

Related

How to scroll a Column to a new position whenever a ViewModel emits a new value?

I have a case where I have a ViewModel that emits scroll values to a StateFlow. Now, my Composable view should scroll to the most recent value, but I can't figure out how to achieve that.
My view model looks something like this:
class MyViewModel : ViewModel() {
private val scrollFlow = MutableStateFlow<Int>(0)
fun getScrollFlow(): StateFlow<Int> = scrollFlow.asStateFlow()
}
And my view is like this:
#Composable
fun MyScrollingView() {
val viewModel = viewModel()
val scroll by viewModel.getScrollFlow().collectAsState()
val scrollState = rememberScrollState(0)
Column(modifier = Modifier.verticalScroll(scrollState)) {
// Content here
}
}
The thing here is, how do I make the scroll state to react to the values coming from the view model?
There are many way to achieve that, for example:
val scrollState = rememberScrollState(0)
rememberCoroutineScope().launch {
viewModel.getScrollFlow()
.flowOn(Dispatchers.Default)
.onEach{scrollVal ->
scrollState.animateScrollTo(scrollVal)
}.collect()
}
Actually I'm afraid that code could be launched on every recomposition and is not ideal so maybe:
remember{
viewModel
.getScrollFlow()
.onEach{scrollVal ->
scrollState.animateScrollTo(scrollVal)
}
}.collectAsState(0, Dispatchers.Default)
or
val scroll by viewModel.getScrollFlow().collectAsState(0, Dispatchers.Default)
LaunchedEffect(scroll){
crollState.animateScrollTo(scroll)
}
You can try doing it inside a LaunchedEffect and use the scrollFlow value as its key, every time the scrollFlow emits new value, the LaunchedEffect will trigger its block.
#Composable
fun MyScrollingView() {
val viewModel = viewModel()
val scroll by viewModel.getScrollFlow().collectAsState()
val scrollState = rememberScrollState(0)
Column(modifier = Modifier.verticalScroll(scrollState)) {
// Content here
}
LaunchedEffect(scroll) {
scrollState.animateScrollTo(0)
//or
// scrollState.scrollTo(0)
}
}
I'm just not sure if this would work in your case especially having this statement inside your composable though.
val viewModel = viewModel()

Jetpack Compose Preview is not showing when having a ViewModel parameter

I am using Jetpack Compose and noticed that the preview is not shown. I read articles like this, but it seems my problem has a different root cause. Even I added defaults to all parameters in the compose function like this:
#OptIn(ExperimentalLifecycleComposeApi::class)
#Composable
#ExperimentalFoundationApi
#Preview
fun VolumeSettingsScreen(
speech: SpeechHelper = SpeechHelper(), // my class that converts text to speech
viewModel: VolumeSettingsViewModel = hiltViewModel(), // using Hilt to inject ViewModels
navController: NavHostController = rememberNavController() // Compose Navigation component
) {
MyAppheme {
Box(
...
)
}
}
When I rollbacked some changes I realized that the #Preview does not support the viewModels regardless of whether they are injected with Hilt or not.
Any Idea how this could be fixed?
Have you considered having a structure where you have a Screen and the actual Content separated like this?
// data class
data class AccountData(val accountInfo: Any?)
// composable "Screen", where you define contexts, viewModels, hoisted states, etc
#Composable
fun AccountScreen(viewModel: AccountViewModel = hiltViewModel()) {
val accountData = viewModel.accountDataState.collectAsState()
AccountContent(accountData = accountData) {
// click callback
}
}
//your actual composable that hosts your child composable widget/components
#Composable
fun AccountContent(
accountData: AccountData,
clickCallback: () ->
) {
...
}
where you can have a preview for the Content like this?
#Preview
#Composable
fun AccountContentPreview() {
// create some mock AccountData
val mockData = AccountData(…)
AccountContent(accountData = mockData) {
// I'm not expecting some actual ViewModel calls here, instead I'll just manipulate the mock data
}
}
this way, all components that aren't needed to be configured by the actual content composable are separated, taking you off from headaches configuring a preview.
Just an added note and could be off-topic, I just noticed you have a parameter like this,
speech: SpeechHelper = SpeechHelper()
you might consider utilizing compositionLocalProvider (if needed), that could clean up your parameters.
I managed to visualize the preview of the screen, by wrapping the ViewModels's functions into data classes, like this:
#OptIn(ExperimentalLifecycleComposeApi::class)
#Composable
#ExperimentalFoundationApi
#Preview
fun VolumeSettingsScreen(
modifier: Modifier = Modifier,
speechCallbacks: SpeechCallbacks = SpeechCallbacks(),
navigationCallbacks: NavigationCallbacks = NavigationCallbacks(),
viewModelCallbacks: VolumeSettingsScreenCallbacks = VolumeSettingsScreenCallbacks()
) {
MyAppheme {
Box(
...
)
}
}
I passed not the ViewModel directly in the compose but needed functions in a Data class for example, like this:
data class VolumeSettingsScreenCallbacks(
val uiState: Flow<BaseUiState?> = flowOf(null),
val onValueUpSelected: () -> Boolean = { false },
val onValueDownSelected: () -> Boolean = { false },
val doOnBoarding: (String) -> Unit = {},
val onScreenCloseRequest: (String) -> Unit = {}
)
I made a method that generates those callbacks in the ViewModel, like this:
#HiltViewModel
class VolumeSettingsViewModel #Inject constructor() : BaseViewModel() {
fun createViewModelCallbacks(): VolumeSettingsScreenCallbacks =
VolumeSettingsScreenCallbacks(
uiState = uiState,
onValueUpSelected = ::onValueUpSelected,
onValueDownSelected = ::onValueDownSelected,
doOnBoarding = ::doOnBoarding,
onScreenCloseRequest = ::onScreenCloseRequest
)
....
}
In the NavHost I hoisted the creation of the ViewModel like this:
#Composable
#ExperimentalFoundationApi
fun MyAppNavHost(
speech: SpeechHelper,
navController: NavHostController,
startDestination: String = HOME.route,
): Unit = NavHost(
navController = navController,
startDestination = startDestination,
) {
...
composable(route = Destination.VOLUME_SETTINGS.route) {
hiltViewModel<VolumeSettingsViewModel>().run {
VolumeSettingsScreen(
modifier = keyEventModifier,
speechCallbacks = speech.createCallback() // my function,
navigation callbacks = navController.createCallbacks(), //it is mine extension function
viewModelCallbacks = createViewModelCallbacks()
)
}
}
...
}
It is a bit complicated, but it works :D. I will be glad if there are some comets for improvements.

View Model with Jetpack compose view

I am using ViewModelFactory to obtain view model instance which is to be used by my jetpack compose view.
class AchievementsScreenViewModelFactory() :
ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T = AchievementsScreenViewModel() as T
}
As soon as instantiate my viewmodel, i want to perform some operations. I am currently storing those operations in the viewmodel constructor(Like some firebase operation to check if the user instance is found or not).Is that a wrong practice? if so, what should i do?
constructor(context:Context) : this() {
this.context=context
mAuth= FirebaseAuth.getInstance()
if(mAuth.currentUser!=null){
triggerNavigateEvent(Screen.DashboardScreen)
}
}
So, the issue that I am facing is that, whenever I use my View Model Factory to instantiate an instance of my view and then when i pop the view from the NavController and return to it, the View Model Factory returns me the same instance of the View Model and the tasks that are present in my constructor are not being performed.
Is there a way to kill the instance of my View Model at the time of popping the screen from the NavController? or is there an other way?
I am calling the viewmodel from the composable screen like this
#SuppressLint("CoroutineCreationDuringComposition")
#Composable
fun LoginScreen(navController: NavHostController
){
var viewModel:LoginScreenViewModel= viewModel(
factory = LoginScreenViewModelFactory(LocalContext.current)
)
.
.
.
}
I am navigating to the screens using google accompanist navigation library.
AnimatedNavHost(
navController = navController,
startDestination = Screen.SplashScreen.route,
enterTransition = { fadeIn(animationSpec = tween(1000), initialAlpha = 0f) },
exitTransition ={ fadeOut(animationSpec = tween(1000), targetAlpha = 0f) }
){
composable(
route = Screen.LoginScreen.route
){
LoginScreen(navController = navController)
}
}
The navigation-compose NavHost (in your case AnimatedNavHost) will call its composablefunction for the target destination every time the destination changes, i.e. when you navigate to a destination and also when you navigate back. That means that you can put the code that you want to run into a method/function in your ViewModel (instead of its constructor) and use a LaunchedEffect composable to call it. If you use a constant key when invoking the LaunchedEffect composable, for example LaunchedEffect(Unit), it will only run once when it enters the composition, in your case once each time the destination changes.
Move the code from VM constructor to a new function in your VM
suspend fun callSomeApi() {
// your code here
}
And add a LaunchedEffect(Unit) to the composable you want to call this new function from
#Composable
fun LoginScreen(navController: NavHostController){
var viewModel: LoginScreenViewModel = viewModel(
factory = LoginScreenViewModelFactory(LocalContext.current)
)
// called once every time this composable enters the composition
LaunchedEffect(Unit) {
viewModel.callSomeApi()
}
}
Here is an example I use
val viewModel = hiltViewModel<PokemonListVm>()
Usage:
#Composable
fun PokemonListScreen(
navController: NavController
) {
val viewModel = hiltViewModel<PokemonListVm>()
val lazyPokemonItems: LazyPagingItems<PokedexListEntry> = viewModel.pokemonList.collectAsLazyPagingItems()
Surface(
color = MaterialTheme.colors.background,
modifier = Modifier.fillMaxSize()
) {
Column {
Spacer(modifier = Modifier.height(20.dp))
PokemonBanner()
PokemonSearch()
PokemonLazyList(
pokemonList = lazyPokemonItems,
onItemClick = { entry ->
navController.navigate(
"pokemon_detail_screen/${entry.dominentColor.toArgb()}/${entry.pokemonName}"
)
}
)
}
}
}

When do I need to add #Composable with Android Studio Compose?

The following code is from the project.
I find that fun MainScreen() add #Composable, and fun launchDetailsActivity doesn't add #Composable.
It make me confused. I think all function which apply to Compose should to add #Composable, why doesn't fun launchDetailsActivity add #Composable?
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
ProvideWindowInsets {
ProvideImageLoader {
CraneTheme {
MainScreen(
onExploreItemClicked = { launchDetailsActivity(context = this, item = it) }
)
}
}
}
}
}
}
#Composable
fun MainScreen(onExploreItemClicked: OnExploreItemClicked) {
...
}
fun launchDetailsActivity(context: Context, item: ExploreModel) {
context.startActivity(createDetailsActivityIntent(context, item))
}
Function with #Composable is not just a function it tells the compose compiler that this is a UI element. Take this data and build a Widget with it.
So you have to determine when you will add #Composable based on whether this function draws something in the UI or not. In the non compose world you can think of this function like a View.
For example, this function takes a parameter name and builds a Text widget with the text "Hello $name" which you can see in the UI.
#Composable
fun Greeting(name: String) {
Text("Hello $name")
}
But
fun getName(firstName: String, lastName: String): String {
return "$firstName $lastName"
}
This function is not a composable function. It is not annotated with #Composable because it is not a widget, it shouldn't render anything in the UI. It just takes two parameters, Concatenates them, and returns the String.
In your case, MainScreen is the function that is rendering the Main screen of your app so it is a UI element. But function launchDetailsActivity doesn't draw anything in the UI. It just navigates from one activity to another activity.
Few things to remember:
Function with #Composable doesn't return anything.
You can't call a composable function from a non-composable function.
Unlike non-composable function composable function start with an Uppercase letter.
You can read this doc for details https://developer.android.com/jetpack/compose/mental-model
You need to mark view builder functions with #Composable, to be directly called from an other #Composable.
If you have a side effect function, it shouldn't be called directly from composable. It can be called from touch handlers, like click in your example, or using a side effect, like LaunchedEffect. Check out more about what's side effect in documentation.
#Composable
fun SomeView() {
// this is directly called from view builder and should be marked with #Composable
OtherView()
LaunchedEffect(Unit) {
// LaunchedEffect a side effect function, and as it's called
// from LaunchedEffect it can be a suspend fun (optionally)
handleLaunchedEffect()
}
Button(onClick = { handleButtonClick() }) { // or onClick = ::handleButtonClick
}
}
#Composable
fun OtherView() {
}
suspend fun handleLaunchedEffect() {
}
fun handleButtonClick() {
}
You should use #Composable if you are using calling another function annotated with #Composable that's it pretty simple.
Generally all #Composable functions starts with uppercase letter but some also start with lowercase like everything that starts with remember
So when you are using these functions inside another function you need to use #Composable else even android studio will yell at you because composable function can be invoked from another composable.

Jetpack Compose Slider performance

Using LiveData to store the value of the slider makes it lag and move jerky (I suppose because of postValue ())
#Composable
fun MyComposable(
viewModel: MyViewModel
) {
val someValue = viewModel.someValue.observeAsState()
Slider(someValue) {
viewModel.setValue(it)
}
}
class MyViewModel() : ViewModel() {
val someValue: LiveData<Float> = dataStore.someValue // MutableLiveData
fun setValue(value: Float) {
dataStore.setValue(value)
}
}
class MyDataStore() {
val someValue = MutableLiveData<Float>()
fun setValue(value: Float) {
// Some heavy logic
someValue.postValue(value)
}
}
As I understand it, postValue() takes a while, and because of this, the slider seems to be trying to resist changing the value.
In order to somehow get around this, I had to create additional State variables so that the slider would directly update its value
#Composable
fun MyComposable(
viewModel: MyViewModel
) {
val someValue = viewModel.someValue.observeAsState()
var someValue2 by remember { mutableStateOf(someValue) }
Slider(someValue2) {
someValue2 = it
viewModel.setValue(it) // I also had to remove postValue ()
}
}
As I understand it, if the data from the DataStore comes with a delay, then the value in someValue will not have time to initialize and it will be null by the time the view appears (this has not happened yet, but is it theoretically possible?), And thus the value of the slider will not be relevant. Are there any solutions to this problem?

Categories

Resources