A composable function does not work in timer.ontick - android

I'm new to jetpack compose and I have created a composable function with a simple text.
I would like to update it every time a timer reaches timer.ontick function
but it does not work. Any help?
fun LKIDView(text : String, onLKIDViewChange: (String) -> Unit) {
var lkidState by remember { mutableStateOf("Default") }
val onlkidChange={text : String -> lkidState = text}
Column(
horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier
.fillMaxWidth()
.background(Color(0xFF7DCEA0))
) {
Text(
text = lkidState,
// modifier = Modifier.fillMaxWidth(),
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.SansSerif,
//
)
}
}`
`
My ontimer.tick looks like this
`val timer = object : CountDownTimer(Gtime, Ttime) {
var lkidviewState = remember { mutableStateOf("Landkreis/Kreeisfreie Stadt") }
val onTextChange={text : String -> lkidviewState.value = text}
override fun onTick(millisUntilFinished: Long) {
Log.e("TimerTick - ", "Tick ")
LKIDView(text =lkidviewState.value , onLKIDViewChange = onTextChange)
// lkidviewState.value = "dsfdsfdsf"`}`
Android Studio says composable invocation can only happen from the context of a composable function
timer runs - the code did not update the ui

Compose doesn't work in this way.
You can't call a composable inside the CountDownTimer to display the updated value.
Instead you have to use a state (lkidState), and then CountDownTimer has to update this value.
val lkidState = remember {
mutableStateOf(0)
}
val timer = object : CountDownTimer(0, 1000) {
override fun onTick(millisUntilFinished: Long) {
lkidState.value = millisUntilFinished
}
//...
}
Text(text = lkidState.value.toString())
Final note, I would use other option instead of a CountDownTimer, like a side effect.

Related

How to trigger JetPack recomposition when state is re-assigned it's current value

The below code works as desired: the canvas gets recomposed each time the user either clicks the canvas itself or clicks the topBar icon, no matter how many times or in what order. In addition, the state variable value reveals something I want to know: where the user clicked. (Values 0 and 1 mean the icon was clicked and values 2 and 3 mean the canvas).
However, if the canvasState and iconState variables are set to their respective V1 functions instead of the V2 functions, then clicking the canvas or icon multiple times in a row is not detected. Apparently this is because the V1 functions can re-assign the same value to the state variable, unlike the V2 functions.
Since I'm using the neverEqualPolicy(), I thought I didn't have to assign a different value to the state variable to trigger a recompose. As a noob to Kotlin and Compose, what am I misunderstanding?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp()
}
}
}
#Composable
fun MyApp() {
var state by remember { mutableStateOf(value = 0, policy = neverEqualPolicy()) }
val canvasStateV1 = { state = 0 }
val iconStateV1 = { state = 2 }
val canvasStateV2 = { state = if (state == 0) { 1 } else { 0 } }
val iconStateV2 = { state = if (state == 2) { 3 } else { 2 } }
val iconState = iconStateV2
val canvasState = canvasStateV2
Scaffold(
topBar = { TopBar(canvasState) },
content = { padding ->
Column(Modifier.padding(padding)) {
Screen(state, iconState)
}
}
)
}
#Composable
fun TopBar(iconState: () -> Unit) {
TopAppBar(
title = { Text("This is a test") },
actions = {
IconButton(onClick = { iconState() }) {
Icon(Icons.Filled.AddCircle, null)
}
}
)
}
#Composable
fun Screen(state: Int, canvasState: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.aspectRatio(ratio = 1f)
.background(color = MaterialTheme.colors.onSurface)
.pointerInput(Unit) {
detectTapGestures(
onTap = { canvasState() },
)
}
) {
Canvas(
modifier = Modifier.fillMaxSize().clipToBounds()
) {
Log.d("Debug", "Canvas: state = $state")
}
}
}
}
I didn't know other things to try to get the neverEqualPolicy() to work as expected.
I think the main reason for this is because the function Screen() is skippable. If you add the state as a MutableState instead of the Int itself, you will see that the Log.d gets called each time the state value gets updated. Same goes for merging the Screen() function into Column in MyApp
Compose analyses each function during build time. The screen functions receives an integer value, this is an immutable value, so the function itself becomes skippable.
To analyse which function is skippable/stable (and which is not), you can run a report during the build phase
This repo shows how
EDIT:
In this example you have two buttons, one changes the value, one just sets the same value. When setting the same value, you only see the Log.d of the local recomposition. When changing the state value, you see two log lines. the local and external both go through the recomposition.
#Composable
fun StackOverflowApp() {
var state by remember { mutableStateOf(value = 0, policy = neverEqualPolicy()) }
Column() {
Button(onClick = { state = state }) {
Text(text = "State same value")
}
Button(onClick = { state += 1 }) {
Text(text = "State up")
}
Text(text = "[local] current State = $state")
Log.d("TAG","Recomposition local")
ExternalText(state)
}
}
/**
* A skippable function
*
* restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ExternalText(
stable state: Int
)
*/
#Composable
fun ExternalText(state: Int){
Text(text = "[external] current State = $state")
Log.d("TAG","Recomposition external")
}
You can also pass the MutableState instead of the int value itself, when you pass the mutableState, the neverEqualPolicy is still in play. Each interaction fires both log lines
#Composable
fun StackOverflowApp() {
var state = remember { mutableStateOf(value = 0, policy = neverEqualPolicy()) }
Column() {
Button(onClick = { state.value = state.value }) {
Text(text = "State same value")
}
Button(onClick = { state.value += 1 }) {
Text(text = "State up")
}
Text(text = "[local] current State = ${state.value}")
Log.d("TAG","Recomposition internal")
ExternalText(state)
}
}
#Composable
fun ExternalText(state: MutableState<Int>){
Text(text = "[external] current State = ${state.value}")
Log.d("TAG","Recomposition external")
}

Can I wrap Modifier with remember in Jetpack Compose?

In order to share settings among of compose functions, I create a class AboutState() and a compose fun rememberAboutState() to persist settings.
I don't know if I can wrap Modifier with remember in the solution.
The Code A can work well, but I don't know if it maybe cause problem when I wrap Modifier with remember, I think Modifier is special class and it's polymorphic based invoked.
Code A
#Composable
fun ScreenAbout(
aboutState: AboutState = rememberAboutState()
) {
Column() {
Hello(aboutState)
World(aboutState)
}
}
#Composable
fun Hello(
aboutState: AboutState
) {
Text("Hello",aboutState.modifier)
}
#Composable
fun World(
aboutState: AboutState
) {
Text("World",aboutState.modifier)
}
class AboutState(
val textStyle: TextStyle,
val modifier: Modifier=Modifier
) {
val rowSpace: Dp = 20.dp
}
#Composable
fun rememberAboutState(): AboutState {
val aboutState = AboutState(
textStyle = MaterialTheme.typography.body1.copy(
color=Color.Red
),
modifier=Modifier.padding(start = 80.dp)
)
return remember {
aboutState
}
}
There wouldn't be a problem passing a Modifier to a class. What you actually defined above, even if named State, is not class that acts as a State, it would me more appropriate name it as HelloStyle, HelloDefaults.style(), etc.
It would be more appropriate to name a class XState when it should have internal or public MutableState that can trigger recomposition or you can get current State of Composable or Modifier due to changes. It shouldn't contain only styling but state mechanism either to change or observe state of the Composble such as ScrollState or PagerState.
When you have a State wrapper object common way of having a stateful Modifier or Modifier with memory or Modifiers with Compose scope is using Modifier.composed{} and passing State to Modifier, not the other way around.
When do you need Modifier.composed { ... }?
fun Modifier.composedModifier(aboutState: AboutState) = composed(
factory = {
val color = remember { getRandomColor() }
aboutState.color = color
Modifier.background(aboutState.color)
}
)
In this example even if it's not practical getRandomColor is created once in recomposition and same color is used.
A zoom modifier i use for zooming in this library is as
fun Modifier.zoom(
key: Any? = Unit,
consume: Boolean = true,
clip: Boolean = true,
zoomState: ZoomState,
onGestureStart: ((ZoomData) -> Unit)? = null,
onGesture: ((ZoomData) -> Unit)? = null,
onGestureEnd: ((ZoomData) -> Unit)? = null
) = composed(
factory = {
val coroutineScope = rememberCoroutineScope()
// Current Zoom level
var zoomLevel by remember { mutableStateOf(ZoomLevel.Min) }
// Rest of the code
},
inspectorInfo = {
name = "zoom"
properties["key"] = key
properties["clip"] = clip
properties["consume"] = consume
properties["zoomState"] = zoomState
properties["onGestureStart"] = onGestureStart
properties["onGesture"] = onGesture
properties["onGestureEnd"] = onGestureEnd
}
)
Another practical example for this is Modifier.scroll that uses rememberCoroutineScope(), you can also remember object too to not intantiate another object in recomposition
#OptIn(ExperimentalFoundationApi::class)
private fun Modifier.scroll(
state: ScrollState,
reverseScrolling: Boolean,
flingBehavior: FlingBehavior?,
isScrollable: Boolean,
isVertical: Boolean
) = composed(
factory = {
val overscrollEffect = ScrollableDefaults.overscrollEffect()
val coroutineScope = rememberCoroutineScope()
// Rest of the code
},
inspectorInfo = debugInspectorInfo {
name = "scroll"
properties["state"] = state
properties["reverseScrolling"] = reverseScrolling
properties["flingBehavior"] = flingBehavior
properties["isScrollable"] = isScrollable
properties["isVertical"] = isVertical
}
)

Dynamic Text Input does not trigger recomposition

I'm trying to build a custom form using jetpack compose.
What I did so far in the Screen :
#Composable
fun FormContent(
viewModel: FormViewModel,
customFieldList: List<String>,
valuesCustomFieldsList: List<String>
) {
Column(Modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.weight(1f)
.padding(top = 8.dp, bottom = 8.dp)
) {
if (customFieldList.isNotEmpty()) {
customFieldList.forEachIndexed { index, item ->
TextRow(
title = item,
placeholder = "Insert $item",
value = valuesCustomFieldsList[index],
onValueChange = {
viewModel.onCustomFieldChange(index, it)
},
isError = false
)
}
}
}
}
Where customFieldList is the list of textFields i want and valuesCustomFieldsList is
val valuesCustomFields by viewModel.values.collectAsState()
In my viewModel the code is as it follows:
private val _values = MutableStateFlow(emptyList<String>())
val values : StateFlow<List<String>>
get() = _values
private var customFieldValues = mutableListOf<String>()
init {
viewModelScope.launch {
//get all custom fields
customFields = gmeRepository.getCustomField()
if (customFields.isNotEmpty()) {
//init the customFieldsValues
for (i in 0..customFields.size) {
customFieldValues.add("")
}
_values.value = customFieldValues
}
}
}
fun onCustomFieldChange(index : Int, value: String) {
customFieldValues[index] = value
_values.value = customFieldValues
}
What happens here is that if I try to change the value inside the inputText, onCustomFieldChange is correctly triggered with the first letter I wrote inside, but then no change is visible in the UI.
If I try to change another static field inside the form, then only the last char i wrote inside my custom fields are shown cause a recomposition is triggered.
Is there something I can do to achieve my goal?

How to wait for the end of the animation correctly?

I know that I can track the moment when lottie animation is completed using progress.
But the problem is that I want to start a new screen at the moment when the animation is completely finished.
Here is the code of my animation
#Composable
fun AnimatedScreen(
modifier: Modifier = Modifier,
rawId: Int
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier.fillMaxSize()
) {
val compositionResult: LottieCompositionResult = rememberLottieComposition(
spec = LottieCompositionSpec.RawRes(rawId)
)
AnimatedScreenAnimation(compositionResult = compositionResult)
}
}
#Composable
fun AnimatedScreenAnimation(compositionResult: LottieCompositionResult) {
val progress by animateLottieCompositionAsState(composition = compositionResult.value)
Column {
if (progress < 1) {
Text(text = "Progress: $progress")
} else {
Text(
modifier = Modifier.clickable { },
text = "Animation is done"
)
}
LottieAnimation(
composition = compositionResult.value,
progress = progress,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.FillBounds
)
}
}
And here is code of my screen where i want to wait for the end of the animation and then go to a new screen:
#Composable
fun SplashScreen(
navController: NavController,
modifier: Modifier = Modifier,
viewModel: SplashScreenViewModel = getViewModel()
) {
val resIdState = viewModel.splashScreenResId.collectAsState()
val resId = resIdState.value
if (resId != null) {
AnimatedScreen(modifier = modifier, rawId = resId)
}
LaunchedEffect(true) {
navigate("onboarding_route") {
popUpTo(0)
}
}
}
I used the progress & listened to it's updates & as soon as it reaches 1f I'll call my function.
Example:
#Composable
fun Splash() {
LottieTest {
// Do something here
}
}
#Composable
fun LottieTest(onComplete: () -> Unit) {
val composition: LottieCompositionResult =
rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.camera))
val progress by animateLottieCompositionAsState(
composition.value,
iterations = 1,
)
LaunchedEffect(progress) {
Log.d("MG-progress", "$progress")
if (progress >= 1f) {
onComplete()
}
}
LottieAnimation(
composition.value,
progress,
)
}
Note: This is just the way I did it. The best way is still unknown(to me atleast). I feel it lacks the samples for that.
Also, You can modify a lot from this & just concentrate on the core flow.

Jetpack compose button action onClick leak

we've encountered a problem with Jetpack Compose button actions leaking. We provide a class with resources (e.g strings, colors, and BUTTON ACTIONS):
data class CameraOnBoardingComposeViewResources(val title: String, val buttonTitle: String, val buttonAction: () -> Unit)
This data class is created in the fragment like so:
val resources = CameraOnBoardingComposeViewResources(
"Title", "ButtonTitle"
) { navigate() }
And in our composable we use those resources like so:
#Composable
fun composeOnBoardingView(resources: CameraOnBoardingComposeViewResources) {
Surface(
shape = RoundedCornerShape(4.dp),
color = colorResource(R.color.idenfyMainColorV2),
modifier = Modifier
.height(42.dp)
.padding(start = 16.dp, end = 16.dp)
.clickable(onClick = resources.buttonAction)
.fillMaxWidth()
)
}
The problem is that buttonAction leaks. How can we avoid this leak, should we somehow dispose of it, or do we need to change this architecture a bit?
EDIT After CommonsWare
Ok, I’ve located the problem, and it is indeed inside the composeOnBoardingView composable. It’s this code snippet:
val currentInstructionDescription: MutableState<String> = remember {
mutableStateOf("")
}
val progress = remember { mutableStateOf(0.0f) }
val handler = Handler(Looper.getMainLooper())
exoPlayer?.apply {
val runnable = object : Runnable {
override fun run() {
progress.value =
exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat()
currentInstructionDescription.value = InstructionsDescriptionBuilder.build(
context,
(resources.documentCenterImageUiViewModel as OnBoardingCenterImageUIViewModel.DocumentResource).step,
exoPlayer.currentPosition.toInt() / 1000
)
handler.postDelayed(this, 1000)
}
}
handler.post(runnable)
}
I tried disposing the handler like this:
val lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
onDispose {
handler.removeCallbacksAndMessages(null)
}
}
But that did not solve the problem. Any ideas?
EDIT
The issue is solved thanks CommonsWare! We switched to the LaunchedEffect and coroutines for an every-second in-composition timer, rather than Handler

Categories

Resources