I'm using AnimatedContent to animate between two different views but even though I'm specifying I want to use vertical transitions, it's still switching between the view but not applying the actual transition animation. Here's the code:
val isEditState = remember { mutableStateOf(false) }
AnimatedContent(
targetState = isEditState,
transitionSpec = {
(slideInVertically() with slideOutVertically()).using(SizeTransform(clip = false))
}
) { targetState ->
if (targetState.value) {
EditView(...)
} else {
NonEditView(...)
}
}
How can I fix this so the animation works?
If I use AnimatedVisibility it does work but I have to apply it to each view specifically.
AnimatedVisibility(
visible = isEditState.value,
enter = slideInVertically(),
exit = slideOutVertically(),
content = EditView(...)
)
Try using isEditable.value as the target state:
AnimatedContent(
targetState = isEditState.value,
transitionSpec = {
(slideInVertically() with slideOutVertically()).using(SizeTransform(clip = false))
}
) { targetState ->
if (targetState) {
EditView(...)
} else {
NonEditView(...)
}
}
The variable isEditable (which is a State) doesn't change, that's why it doesn't animate. What changes is its value.
I think your error is actually in using
val isEditState = remember { mutableStateOf(false) }
rather than
val isEditState by remember { mutableStateOf(false) }
If you update that to match the standard usage of remember, I think the rest of the solution will fall into place:
val isEditState by remember { mutableStateOf(false) }
AnimatedContent(
targetState = isEditState,
transitionSpec = {
(slideInVertically() with slideOutVertically()).using(SizeTransform(clip = false))
}
) { targetState ->
if (targetState) {
EditView(...)
} else {
NonEditView(...)
}
}
Related
I'm trying to send a variable value from my ViewModel to my composable screen. I tried using the debugger to find out where it gets stuck. It seems like it sends the value but never actually receives it.
This is the code I'm using:
NewEvent.kt
#Composable
fun NewEvent(
viewModel: NewEventViewModel = viewModel(),
navController: NavController
){
val context = LocalContext.current
LaunchedEffect(context){
viewModel.newEventType.collect { eventType ->
Toast.makeText(context, eventType.toString(), Toast.LENGTH_SHORT).show()
}
}
}
changeEventType() gets called here
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false },
) {
eventTypeList.forEach {
if(it != viewModel.event.eventType && it != EventType.UNKNOWN) {
DropdownMenuItem(
onClick = { viewModel.changeEventType(it); menuExpanded = false },
text = { Text(stringResource(context.resources.getIdentifier(it.toString().lowercase(), "string", context.packageName))) }
)
}
}
}
NewEventViewModel.kt
private val newEventTypeChannel = Channel<EventType>()
val newEventType = newEventTypeChannel.receiveAsFlow()
fun changeEventType(newEventType: EventType){
viewModelScope.launch {
newEventTypeChannel.send(newEventType)
}
}
I downloaded a sample project from GitHub using this exact implementation and it worked, I'm not sure what I'm missing here.
If you want to display toast message, you don't have to create separate composable function for it. Since Toast is dynamic and does not need to be recomposed, you don't need composable function for it.
It would be more clear and better to implement it like this (in one composable function)
val context = LocalContext.current
LaunchedEffect(context){
viewModel.newEventType.collect { eventType ->
Toast.makeText(context, eventType.toString(), Toast.LENGTH_SHORT).show()
}
}
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false },
) {
eventTypeList.forEach {
if(it != viewModel.event.eventType && it != EventType.UNKNOWN) {
DropdownMenuItem(
onClick = { viewModel.changeEventType(it); menuExpanded = false },
text = { Text(stringResource(context.resources.getIdentifier(it.toString().lowercase(), "string", context.packageName))) }
)
}
}
}
The composable host an AndroidView that is a FragmentContainerView which has multiple child Fragments on back press of the FragmentContainerView we want to close the #Game composable.
#Composable
fun Game(data: Bundle? = null) {
val user = GamingHubAuthManager.getUser().observeAsState()
AndroidViewBinding(EntryPointBinding::inflate) {
// val myFragment = fragmentGameContainerView.getFragment<FeatureCardFragment>
}
}
You can control if your Game composable is part of the composition from its parent composable with some state and a simple if statement.
To change the state on back press you can use the BackHandler composable.
A working example:
import androidx.compose.runtime.*
#Composable
fun GameParent() {
var gameIsActive by remember { mutableStateOf(true) } // or false for the starting state
BackHandler(enabled = gameIsActive) {
gameIsActive = false
}
if (gameIsActive) {
Game()
} else {
Button(
onClick = { gameIsActive = true }
) {
Text("Start game")
}
}
}
#Composable
fun Game(data: Bundle? = null) {
val user = GamingHubAuthManager.getUser().observeAsState()
AndroidViewBinding(EntryPointBinding::inflate) {
// val myFragment = fragmentGameContainerView.getFragment<FeatureCardFragment>
}
}
If you will have to close the game from some other handler(s) from inside the Game composable then taking this approach might be better
import androidx.compose.runtime.*
#Composable
fun GameParent() {
var gameIsActive by remember { mutableStateOf(true) } // or false for the starting state
if (gameIsActive) {
Game(onClose = { gameIsActive = false })
} else {
Button(
onClick = { gameIsActive = true }
) {
Text("Start game")
}
}
}
#Composable
fun Game(data: Bundle? = null, onClose: () -> Unit) {
BackHandler(enabled = true) {
// this way you can even pass some result back if you parametrize
// this callback, for example won/lost/draw/quit.
onClose()
}
val user = GamingHubAuthManager.getUser().observeAsState()
AndroidViewBinding(EntryPointBinding::inflate) {
// val myFragment = fragmentGameContainerView.getFragment<FeatureCardFragment>
// call onClose() from some other handler
}
}
How to create swipe to refresh in Jetpack compose using kotlin? Please Share proper reference link
SwipeRefresh is not available
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = { },
) {
LazyColumn {
}
}
Now you can use the built-in pullRefresh modifier.
Something like:
val refreshScope = rememberCoroutineScope()
var refreshing by remember { mutableStateOf(false) }
fun refresh() = refreshScope.launch {
refreshing = true
//...do something
refreshing = false
}
val state = rememberPullRefreshState(refreshing, ::refresh)
Box(Modifier.pullRefresh(state)) {
LazyColumn(Modifier.fillMaxSize()) {
if (!refreshing) {
items(itemCount) {
//...
}
}
}
PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
}
To create a swipe-to-refresh layout, we need to add dependency in buld.gradle which will provide swipe to refresh layout just like SwipeRefreshLayout in traditional android.
implementation 'com.google.accompanist:accompanist-swiperefresh:0.24.13-rc'
..
To create this kind of layout we require two APIs one SwipeRefresh for layout and another rememberSwipeRefreshState which will remember the state.
#Composable
fun SwipeRefreshCompose() {
var refreshing by remember { mutableStateOf(false) }
LaunchedEffect(refreshing) {
if (refreshing) {
delay(3000)
refreshing = false
}
}
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing = refreshing),
onRefresh = { refreshing = true },
) {
// list view
}
}
In the following viewModel code I am generating a list of items from graphQl server
private val _balloonsStatus =
MutableStateFlow<Status<List<BalloonsQuery.Edge>?>>(Status.Loading())
val balloonsStatus get() = _balloonsStatus
private val _endCursor = MutableStateFlow<String?>(null)
val endCursor get() = _endCursor
init {
loadBalloons(null)
}
fun loadBalloons(cursor: String?) {
viewModelScope.launch {
val data = repo.getBalloonsFromServer(cursor)
if (data.errors == null) {
_balloonsStatus.value = Status.Success(data.data?.balloons?.edges)
_endCursor.value = data.data?.balloons?.pageInfo?.endCursor
} else {
_balloonsStatus.value = Status.Error(data.errors!![0].message)
_endCursor.value = null
}
}
}
and in the composable function I am getting this data by following this code:
#Composable
fun BalloonsScreen(
navHostController: NavHostController? = null,
viewModel: SharedBalloonViewModel
) {
val endCursor by viewModel.endCursor.collectAsState()
val balloons by viewModel.balloonsStatus.collectAsState()
AssignmentTheme {
Column(modifier = Modifier.fillMaxSize()) {
when (balloons) {
is Status.Error -> {
Log.i("Reyjohn", balloons.message!!)
}
is Status.Loading -> {
Log.i("Reyjohn", "loading..")
}
is Status.Success -> {
BalloonList(edgeList = balloons.data!!, navHostController = navHostController)
}
}
Spacer(modifier = Modifier.weight(1f))
Button(onClick = { viewModel.loadBalloons(endCursor) }) {
Text(text = "Load More")
}
}
}
}
#Composable
fun BalloonList(
edgeList: List<BalloonsQuery.Edge>,
navHostController: NavHostController? = null,
) {
LazyColumn {
items(items = edgeList) { edge ->
UserRow(edge.node, navHostController)
}
}
}
but the problem is every time I click on Load More button it regenerates the view and displays a new set of list, but I want to append the list at the end of the previous list. As far I understand that the list is regenerated as the flow I am listening to is doing the work behind this, but I am stuck here to get a workaround about how to achieve my target here, a kind hearted help would be much appreciated!
You can create a private list in ViewModel that adds List<BalloonsQuery.Edge>?>
and instead of
_balloonsStatus.value = Status.Success(data.data?.balloons?.edges)
you can do something like
_balloonsStatus.value = Status.Success(myLiast.addAll(
data.data?.balloons?.edges))
should update Compose with the latest data appended to existing one
I am currently trying to write an App for my thesis and currently, I am looking into different approaches. Since I really like Flutter and the Thesis requires me to use Java/Kotlin I would like to use Jetpack compose.
Currently, I am stuck trying to update ListElements.
I want to have a List that shows Experiments and their state/result. Once I hit the Button I want the experiments to run and after they are done update their state. Currently, the run Method does nothing besides setting the state to success.
The problem is I don't know how to trigger a recompose from the viewModel of the ExperimentRow once an experiment updates its state.
ExperimentsActivity:
class ExperimentsActivity : AppCompatActivity() {
val exViewModel by viewModels<ExperimentViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//For now this is just Dummy Data and will be replaced
exViewModel.experiments += listOf(
Experiment("Test1", exViewModel::experimentStateChanged),
Experiment("Strongbox", exViewModel::experimentStateChanged)
)
setContent {
TpmTheme {
// A surface container using the 'background' color from the theme
Surface {
ExperimentScreen(
exViewModel.experiments,
exViewModel::startTests
)
}
}
}
}
}
ExperimentViewModel:
class ExperimentViewModel : ViewModel() {
var experiments by mutableStateOf(listOf<Experiment>())
fun startTests() {
for (exp in experiments) {
exp.run()
}
}
fun experimentStateChanged(experiment: Experiment) {
Log.i("ViewModel", "Changed expState of ${experiment.name} to ${experiment.state}")
// HOW DO I TRIGGER A RECOMPOSE OF THE EXPERIMENTROW FOR THE experiment????
//experiments = experiments.toMutableList().also { it.plus(experiment) }
Log.i("Vi", "Size of Expirments: ${experiments.size}")
}
}
ExperimentScreen:
#Composable
fun ExperimentScreen(
experiments: List<Experiment>,
onStartExperiments: () -> Unit
) {
Column {
LazyColumnFor(
items = experiments,
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(top = 8.dp),
) { ep ->
ExperimentRow(
experiment = ep,
modifier = Modifier.fillParentMaxWidth(),
)
}
Button(
onClick = { onStartExperiments() },
modifier = Modifier.padding(16.dp).fillMaxWidth(),
) {
Text("Run Tests")
}
}
}
#Composable
fun ExperimentRow(experiment: Experiment, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(experiment.name)
Icon(
asset = experiment.state.vAsset,
)
}
Experiment:
class Experiment(val name: String, val onStateChanged: (Experiment) -> Unit) {
var state: ExperimentState = ExperimentState.DEFAULT
set(value) {
field = value
onStateChanged(this)
}
fun run() {
state = ExperimentState.SUCCESS;
}
}
enum class ExperimentState(val vAsset: VectorAsset) {
DEFAULT(Icons.Default.Info),
RUNNING(Icons.Default.Refresh),
SUCCESS(Icons.Default.Done),
FAILED(Icons.Default.Warning),
}
There's a few ways to address this but key thing is that you need to add a copy of element (with state changed) to experiments to trigger the recomposition.
One possible example would be
data class Experiment(val name: String, val state: ExperimentState, val onStateChanged: (Experiment) -> Unit) {
fun run() {
onStateChanged(this.copy(state = ExperimentState.SUCCESS))
}
}
and then
fun experimentStateChanged(experiment: Experiment) {
val index = experiments.toMutableList().indexOfFirst { it.name == experiment.name }
experiments = experiments.toMutableList().also {
it[index] = experiment
}
}
though I suspect there's probably cleaner way of doing this.