Jetpack Compose behaves strangely - kotlin

I am using an JSON asset file to load some data into app as a List.
fun onGeneral(context: Context): List<List<General>> {
try {
val json = context.assets.open("general.json").bufferedReader().use { it.readText() }
val list = Gson().fromJson<List<General>>(json, object : TypeToken<List<General>>() {}.type)
return listOf(
list.subList(0, 30),
list.subList(30, 60),
list.subList(60, 90),
list.subList(90, 120),
list.subList(120, 150),
list.subList(150, 180),
list.subList(180, 210),
list.subList(210, 240),
list.subList(240, 270),
list.subList(270, 300)
)
} catch (e: Exception) {
return emptyList()
}
}
Since the list is big, I divide it into sublists and as a result, I get a list of sublists in a screen:
var items = remember { mutableStateListOf<Item>() }
val context = LocalContext.current
val general = onGeneral(context)[0] // The first entry
Then I add that general list into the items:
general.forEach { items.add(Item(it)) }
and somewhere on the screen I show the counter
Text("Item ${position + 1} of ${items.size}")
The problem is when entered the screen, the text should show
Item 1 of 30
but it shows a running counter that does not stop:
It just keeps counting up.
Moreover, I added a Log to see what's there is happening:
Log.wtf("TST_Output", "Say Hello, when screen entered!")
Output:
....
E/TST_Output: Say Hello, when screen entered!
E/TST_Output: Say Hello, when screen entered!
E/TST_Output: Say Hello, when screen entered!
E/TST_Output: Say Hello, when screen entered!
E/TST_Output: Say Hello, when screen entered!
E/TST_Output: Say Hello, when screen entered!
E/TST_Output: Say Hello, when screen entered!
E/TST_Output: Say Hello, when screen entered!
E/TST_Output: Say Hello, when screen entered!
E/TST_Output: Say Hello, when screen entered!
E/TST_Output: Say Hello, when screen entered!
E/TST_Output: Say Hello, when screen entered!
E/TST_Output: Say Hello, when screen entered!
E/TST_Output: Say Hello, when screen entered!
E/TST_Output: Say Hello, when screen entered!
E/TST_Output: Say Hello, when screen entered!
....
It runs endless....
I understand, that log outputs are caused by this line
general.forEach { items.add(Item(it)) }
But, when the forEarch loop is done, it should not be repeated again. Why does it keeps running?
I played around and find a solution that does not keep repeating:
var items: MutableList<Item> = remember { mutableStateListOf() }
val temp = arrayListOf<Item>()
general.forEach { temp.add(Item(it)) }
items = temp
Specifying the items variable as MutableList<Item> seems to work.
Additionally, regardless of the issues above, I added that Log line into another Screen, where I do not retrieve any data. In this case the Log line should be triggered one time, but here the Logs:
......
D/ViewRootImpl#e8d5d7b[MainActivity]: reportDrawFinished (fn: -1)
E/TST_Output: Say Hello, when screen entered! <!!!!!!!!!!----------!!!!!!!!
D/CompatibilityChangeReporter: Compat change id reported: 171228096; UID 11701; state: ENABLED
I/ViewRootImpl#e8d5d7b[MainActivity]: Relayout returned: old=(0,0,1440,3040) new=(0,0,1440,3040) req=(1440,3040)0 dur=6 res=0x1 s={true 500687993696} ch=false fn=2
I/OpenGLRenderer: Davey! duration=793ms; Flags=0, FrameTimelineVsyncId=8404895, IntendedVsync=215478232101981, Vsync=215478448768639, InputEventId=0, HandleInputStart=215478449436685, AnimationStart=215478449438954, PerformTraversalsStart=215478749464762, DrawStart=215478991676915, FrameDeadline=215478265435313, FrameInterval=215478449418954, FrameStartTime=16666666, SyncQueued=215479007135992, SyncStart=215479007221531, IssueDrawCommandsStart=215479007348107, SwapBuffers=215479020229915, FrameCompleted=215479025430838, DequeueBufferDuration=20731, QueueBufferDuration=1477346, GpuCompleted=215479025430838, SwapBuffersCompleted=215479022577992, DisplayPresentTime=1554322967633985549,
E/TST_Output: Say Hello, when screen entered! <!!!!!!!!!!----------!!!!!!!!
I/ViewRootImpl#e8d5d7b[MainActivity]: MSG_WINDOW_FOCUS_CHANGED 1 1
D/InputMethodManager: startInputInner - Id : 0
I/InputMethodManager: startInputInner - mService.startInputOrWindowGainedFocus
D/InputMethodManager: startInputInner - Id : 0
W/System: A resource failed to call close.
E/TST_Output: Say Hello, when screen entered! <!!!!!!!!!!----------!!!!!!!!
......
As you can see, that line (marked <!!!!!!!!!!----------!!!!!!!!) was triggered three times.
What's wong? Why the Jetpack Compose behaves so strangely?
Am I doing something wrong?
Any help would be helpful. Thanks a lot.
EDIT:
Code of Screen:
#Composable
#Preview(showBackground = true)
#OptIn(ExperimentalPagerApi::class)
#Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
fun GeneralScreen(controller: NavController = rememberNavController()) {
val context = LocalContext.current
var items: MutableList<Item> = remember { mutableStateListOf() }
var list by remember { mutableStateOf(1) }
var position by remember { mutableStateOf(0) }
val general = onGeneral(context)[0]
val temp = arrayListOf<Item>()
general.forEach {
temp.add(Item(it))
}
items = temp
Log.wtf("TST_Output", "Say Hello, when screen entered!")
Column(Modifier.fillMaxWidth().padding(10.dp), Arrangement.spacedBy(8.dp), Alignment.CenterHorizontally) {
Text("General list $list", fontSize = 18.sp)
Text("Item ${position + 1} of ${items.size}", fontSize = 16.sp, color = Color.Gray)
}
}

The reason why your items list is growing is because you are adding the items to that list inside the scope of the composable. So every time the screen recomposes your items will be re-added to the list.
This same reason applies to the Log entry you added earlier. This will also log every time the screen gets recomposed.
You can prevent this by enclosing the logic inside a LaunchedEffect block. This block will only run after recomposition when the key of that block changes. Here is an example:
var items: MutableList<Item> = remember { mutableStateListOf() }
val context = LocalContext.current
val general = onGeneral(context)[0]
LaunchedEffect(Unit) {
Log.wtf("TST_Output", "Say Hello, when screen entered!")
general.forEach { items.add(Item(it)) }
}
Since we are passing Unit as the key to the LaunchedEffect this will only run once since Unit won't change. If you would want to run the code inside the LaunchedEffect each time general changes. You could use general as key, like this:
var items: MutableList<Item> = remember { mutableStateListOf() }
val context = LocalContext.current
val general = onGeneral(context)[0]
LaunchedEffect(general) {
Log.wtf("TST_Output", "Say Hello, when screen entered!")
// clearing the list since we are re-adding the items
items.clear()
general.forEach { items.add(Item(it)) }
}
I hope this is helpful, if you want to learn more about Side-Effects you could read this.

Related

JetPack Compose onClick function

Please try to tolerate my inexperience.
I'm practicing Jetpack Compose navigation. I making an app that displays a set of card in a LazyHorizontalGrid format, and when clicked it navigates to a screen (destination) containing details of the card, depending on which card is clicked.
Here's what I mean:
I want to navigate to a screen (destination) that describe a particular "Favorite Collections" when clicked, depending on which is clicked.
I was following a tutorial on youtube, but I got lost when he was using Companion Object on a new activity (i.e having multiple activity in a single project and navigating by intent), since that's not recommended I'm not doing that, so I don't know where to put my Companion Object.
RecyclerView in Jetpack Compose - Jetpack Compose For Beginners #7
As I get errors when I try to put the Companion Object in the file of the Screen I'm trying to navigate to, error; Modifier 'companion' is not applicable inside 'file', or In a Composable in the file, then I get this error; Unresolved reference: companion.
This is the screen I'm trying to navigate to (let's call it; DetailScreen);
#Composable
fun DisplayHomeInfo(){
WelcomeText()
HomeInfoDetails(homeInfo = )
}
#Composable
fun WelcomeText() {
Text(
text = "Welcome, to Home Information",
style = MaterialTheme.typography.h3,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 18.dp)
)
}
#Composable
fun HomeInfoDetails(homeInfo: HomeInfo) {
Card(
modifier = Modifier
.padding(10.dp)
.clip(CircleShape),
elevation = 10.dp,
) {
....
This is the screen I'm trying to navigate from;
#Composable
fun HomeScreen(onHomeCardClick: () -> Unit) {
HomeContentScreen(onHomeCardClick = onHomeCardClick)
}
....
Data class I want to load on the detail screen;
data class HomeInfo(
val id: Int,
val title: String,
val sex: String,
val age: Int,
val description: String,
val homeInfoImageId: Int = 0
)
And this is the Object I want to match with the data;
object HomeInfoModel {
val homeInfoModelList = listOf(
HomeInfo(
id = 1,
title = "Monty",
sex = "Male",
age = 14,
description = "Monty enjoys chicken treats and cuddling while watching Seinfeld.",
homeInfoImageId = R.drawable.ab1_inversions
),
...
)
}
To summarize everything, MY QUESTION is how do I pass the data to the detailScreen, and show each card details, depending on the card clicked.
Please I understand my explanation is not very clear, and I'll be more than happy to clarify anything.
No information is too small, I will appreciate any help. Thanks a lot in advance.

Composable Focus

I have a project where we display a graph, this graph is in an Box which is scrollable.
By opening the view, we need to center the root node which causes the problem currently.
Determining the position and setting the values of the states is currently done the following way:
.onGloballyPositioned { coordinates -> scrollBy = coordinates.positionInParent().y - dpstate!!.scrollState.firstVisibleItemScrollOffset
}
.onFocusChanged {
if(it.isFocused ){
print("is focused")
scope.launch { dpstate!!.scrollState.animateScrollBy(scrollBy)
dpstate!!.offsetState.value = Offset(leftX.toFloat(),dpstate!!.offsetState.value.y)
}
}
}
in the modifier of the box.
The state dpstate is an instance of the following:
data class DisplayState(
val scrollState: LazyListState,
val scaleState: MutableState<Float>,
val offsetState: MutableState<Offset>,
val editState: MutableState<Boolean>,
val showInfo:Map<Int, MutableState<Boolean>>,)
Important is, that I need to center by opening it, not by clicking a button.
My try was in the calling code of all of this the following code:
DisposableEffect(Unit){
com.github.tukcps.appel.ui.rendering.focusRequester!!.requestFocus()
onDispose { }
}
There is no exception, it just don't do anything, thanks for your help.

How to retrigger focusRequester.requestFocus() when coming back to a composable?

I have a MyBasicTextField in a composable to request user input:
#Composable
fun MyBasicTextField() {
val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = remember{ FocusRequester() }
BasicTextField(
modifier = Modifier
.focusRequester(focusRequester),
keyboardActions = keyboardActions ?: KeyboardActions(onAny = { keyboardController?.hide() }),
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
The keyboard automatically slides in when showing this composable, always.
But wherever MyBasicTextField is used:
I tap on a LinkifiedText to leave and open a browser to show link
I tap BACK
and come back to previous MyBasicTextField screen, the keyboard is not shown
also the focusRequester.requestFocus() is not triggered again when coming back
How can I solve my issue?
Create a top-level variable in your activity, then modify it from within the onStart overridden method. Use that variable as the key for LaunchedEffect in place of Unit. That variable basically keeps track of when the user enters the app.
var userIn by mutableStateOf (true)
In your Composable,
Launched effect(userIn){
if(userIn && isKeyboardShown){
...
}
}
Boring,
You can use my answer here as well, but instead of incrmenting the launchKey every time it's called, only increment the launchKey once user clicks on the browser link, that way it will not pop up during other re-compositions.

Drag gestures inside scrollable control

I've made a custom control based around a Canvas. It uses two .pointerInput modifiers, one to detect click and one to detect drag so the user can toggle a column of 50 buttons, either by clicking on one at a time or dragging across a number of them to set them all at once. It works well, and now I'd like to have a horizontally scrollable Row containing a number of these. The immediate problem is that the Row, when the .horizontalScroll modifier is applied, swallows vertical movement as well, and even taps, so although I can scroll through a number of controls I'm no longer able to interact with them.
The only example I can find that's similar is the nested scrolling in the Gestures documentation, but that's between two controls using scrolling, and although the outer control is clearly not preventing the inner control receiving events it's not clear how to apply it in my case.
Without pasting huge quantities of code, I'm defining the Row by
#Composable
fun ScrollBoxes() {
Row(
modifier = Modifier
.background(Color.LightGray)
.fillMaxSize()
.horizontalScroll(rememberScrollState())
) {
repeat(20) {
Column {
Text(
"Item $it", modifier = Modifier
.padding(2.dp)
.width(500.dp)
)
JetwashSlide(
model = JetwashSlideViewModel()
)
}
}
}
}
and the modifier of the Canvas is my custom control is set up as
modifier
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { ... }
},
onDragEnd = { ... },
onDrag = { change, dragAmount ->
change.consumeAllChanges();
...
}
}
)
}
.pointerInput(Unit) {
detectTapGestures(
onPress = { it ->
...
}
)
}
A crude approach would be to have a scrollable row of labels and use the presently selected label to determine which full width custom control is presently visible. This wouldn't be as aesthetically pleasing as having the controls scrolling horizontally. Does anyone have an idea how this could be achieved?
Ok, my starting point was this tutorial on how to make a scrolling pager from scratch;
https://fvilarino.medium.com/creating-a-viewpager-in-jetpack-compose-332d6a9181a5
Using that, you can get a horizontally scrolling row of instances of a control. The problem is that I wanted horizontal swipes to be handled by the pager, and taps and vertical swipes to be handled by the custom control, and although the custom control gets the gestures if made over the control, it couldn't pass them up to the pager if they were horizontal swipes, so you could only scroll using the gaps between the custom controls. I expected that the control could use .detectVerticalDragGestures and .detectTapGestures and the pager could use .detectHorizontalDragGestures, but it wasn't that simple.
I ended up pulling code from the Jetpack source into my own code so I could modify it to produce my own gesture detector which captures vertical scroll and tap events but does not capture horizontal scroll;
suspend fun PointerInputScope.detectVerticalDragOrTapGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onVerticalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit,
onClick: ((Offset) -> Unit) = { },
model: JetwashSlideViewModel
) {
forEachGesture {
awaitPointerEventScope {
model.inhibitGesture = false
val down = awaitFirstDown(requireUnconsumed = false)
//val drag = awaitVerticalTouchSlopOrCancellation(down.id, onVerticalDrag)
val drag = awaitTouchSlopOrCancellation(
down.id
) { change: PointerInputChange, dragAmount: Offset ->
if (abs(dragAmount.y) > abs(dragAmount.x)) {
//This is a real swipe down the slide
change.consumeAllChanges()
onVerticalDrag(change, dragAmount.y)
}
}
if (model.inhibitGesture) {
onDragCancel()
} else {
if (drag != null) {
onDragStart.invoke(drag.position)
if (
verticalDrag(drag.id) {
onVerticalDrag(it, it.positionChange().y)
}
) {
onDragEnd()
} else {
onDragCancel()
}
} else {
//click.
if (!model.inhibitGesture)
onClick(down.position)
}
}
}
}
}
Now in the pager, it was using .horizontalDrag. This is fine if you can actually drag purely horizontally or purely vertically, but you can't and swipes that were intended to be vertical on the inner control often had a tiny bit of horizontal motion which caused the pager to intercept it. So in the pager, I also had to copy and modify some code to make my own .horizontalDrag;
suspend fun AwaitPointerEventScope.horizontalDrag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit
): Boolean = drag(
pointerId = pointerId,
onDrag = onDrag,
motionFromChange = { if (abs(it.positionChangeIgnoreConsumed().x) > 10) it.positionChangeIgnoreConsumed().x else 0f },
motionConsumed = { it.positionChangeConsumed() }
)
This only triggers if the horizontal component of the movement is larger than 10px.
Finally, since a horizontal scroll may also have some element of vertical which could affect the inner controls, in my onPagerScrollStart and onPagerScrollFinished callbacks I set and clear a flag in the model, inhibitGesture which causes the inner control to disregard gestures that it happens to get while the pager is in the process of being scrolled.

Some basic questions about Kotlin

I am trying to learn Kotlin and TornadoFX, and I am working from this repository
The idea currently is that the app will show three rows of 7 buttons. Each button represents a letter which is either known or unknown. If the letter is known, it is displayed on the button. If the letter is unknown, the button displays the index (0-20).
I did this by creating (using Java thinking I'm sure) a global array with 21 elements. Each element starts out null. I defined this in the main app, using the companion construct:
class InteractiveClientApp : App(MainView::class) {
companion object {
var knownLetters = arrayOfNulls<String>(21)
}
Then in the App constructor, I initialized a few random elements with values, just to see if this would work (it doesn't).
override fun init() {
knownLetters[4] = "T"
knownLetters[9] = "G"
knownLetters[17] = "Z"
}
Then in the LettersGridView I use forEachIndexed on the knownLetters array, so I would have access to the element from the array and the index for it.
import jcn.deduce.client.InteractiveClientApp.Companion.knownLetters
class LettersGridView : View() {
override val root = gridpane {
knownLetters.forEachIndexed { index, element ->
if (index == 0 || index % 7 == 0) {
row {
button(element?.toString() ?: index.toString()) {
useMaxWidth = true
gridpaneConstraints {
marginBottom = 10.0
}
}
}
} else {
button(element?.toString() ?: index.toString()) {
useMaxWidth = true
gridpaneConstraints {
marginBottom = 10.0
}
}
}
}
}
}
Whats actually happening is three buttons appear instead of the expected 21, and the value on the button is always the index, never a letter value. Also the indexes shown are 20, 7 and 14. Not the three I used when setting elements in the array. So I am missing something there.
Also I think I am not understanding this bit correctly:
button(element?.toString() ?: index.toString()) {
useMaxWidth = true
gridpaneConstraints {
marginBottom = 10.0
}
}
What I am trying to say there is "If the element value is not null, use the element value, otherwise use the string value of index. This isn't working, because the buttons only ever have indexes on them, never letters.
I notice if I leave off the .toString() on element, I get an error that button expects String, not string?. Which seems somewhat similar to Java and String vs Optional < String >. However, when I add the toString(), I get an IDE warning that the toString() is redundant.
If I take off the trailing ? altogether, I get a clean compile, but still only three buttons render, and their labels are index, not element.
So I am pretty sure I got lost somewhere along the way, can anyone explain why my program isn't working?
Also, when I am debugging the app, I always wind up with two processes. I don't understand why, but this is what IntelliJ looks like:
Is this normal?
Your init function is working, you can confirm this by changing the initialisation of entry 20, 7 or 14 to a letter and you should see a letter appear when you next run it.
As for your main issue, the reason you are seeing 20, 7 and then 14 is because in this section:
if (index == 0 || index % 7 == 0) {
row {
button(element?.toString() ?: index.toString()) {
useMaxWidth = true
gridpaneConstraints {
marginBottom = 10.0
}
}
}
}
You are adding a row with a single button, these buttons will be 0, 7 and 14 (since they are all == 0 % 7). This means you'll only ever add three rows, each with one button on. You might be confused as to why it says 20 instead of 0... Well this is because the next section:
else {
button(element?.toString() ?: index.toString()) {
useMaxWidth = true
gridpaneConstraints {
marginBottom = 10.0
}
}
}
Is also adding buttons, but not onto any row (notice how these buttons aren't inside a row { } lambda). This means all these buttons will get added to the gridpane on top of eachother, including that first 0 button. The last button to be added is 20, hence why you see 20, it is covering the 0!
Here's an example of how to approach this problem:
val rowSize = 7
val rows = knownLetters.toList().chunked(rowSize)
rows.forEachIndexed { rowIndex, elements ->
row {
elements.forEachIndexed { buttonIndex, element ->
val displayIndex = rowSize * rowIndex + buttonIndex
button("${element ?: displayIndex}") {
useMaxWidth = true
gridpaneConstraints {
marginBottom = 10.0
}
}
}
}
}
This takes the Kotlin libraries' "chunked" method to divide your 21 size array into three lists of size 7. You can then loop through (you do have to piece back together the display index with this approach) creating a new row for each list (3 lists makes 3 rows), whilst creating your buttons in a nested loop inside the row's lambda.
The key thing to take away here is that not all your buttons are being added within a row { } lambda.
As for the double process issue, I do not have this issue if I run the app using a main method like so:
fun main(args: Array<String>) {
launch<InteractiveClientApp>(args)
}
Hope this helps!