英文:
Jetpack Compose behaves strangely
问题
我使用一个JSON资产文件将一些数据加载到应用程序中,作为一个列表。
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()
}
}
由于列表很大,我将其分成子列表,结果是在屏幕上得到一个子列表的列表:
var items = remember { mutableStateListOf<Item>() }
val context = LocalContext.current
val general = onGeneral(context)[0] // 第一个条目
general.forEach { items.add(Item(it)) }
在屏幕上的某个地方,我显示计数器:
Text("Item ${position + 1} of ${items.size}")
问题是,当进入屏幕时,文本应该显示:
Item 1 of 30
但它显示一个不停止的计数器。
此外,我添加了一个Log来查看发生了什么:
Log.wtf("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!
E/TST_Output: Say Hello, when screen entered!
....
它无休止地运行。
我理解,Log输出是由这一行引起的:
general.forEach { items.add(Item(it)) }
但是,当forEarch
循环完成时,它不应该再次重复。为什么它保持运行?
我玩弄了一下,找到了一个不会一直重复的解决方案:
var items: MutableList<Item> = remember { mutableStateListOf() }
val temp = arrayListOf<Item>()
general.forEach { temp.add(Item(it)) }
items = temp
将items
变量指定为MutableList<Item>
似乎有效。
此外,不管上面的问题如何,我将该Log行添加到另一个屏幕中,我在该屏幕中不检索任何数据。在这种情况下,Log应该被触发一次,但是这里的Logs:
......
E/TST_Output: Say Hello, when screen entered! <!!!!!!!!!!----------!!!!!!!!
E/TST_Output: Say Hello, when screen entered! <!!!!!!!!!!----------!!!!!!!!
E/TST_Output: Say Hello, when screen entered! <!!!!!!!!!!----------!!!!!!!!
......
如您所见,那一行(标记为<!!!!!!!!!!----------!!!!!!!!
)被触发了三次。
Jetpack Compose为什么表现得如此奇怪?我做错了什么?
英文:
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.
Why the Jetpack Compose behaves so strangely? Am I doing something wrong?
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)
}
}
答案1
得分: 2
以下是翻译好的部分:
"你的项目列表不断增长的原因是因为你将项目添加到该列表的可组合范围内。所以每当屏幕重新组合时,你的项目都会被重新添加到列表中。
之前添加的Log条目也适用相同的原因。每当屏幕重新组合时,它也会记录日志。
你可以通过将逻辑封装在LaunchedEffect块中来防止这种情况发生。该块仅在该块的关键更改后才会运行。以下是一个示例:
var items: MutableList<Item> = remember { mutableStateListOf() }
val context = LocalContext.current
val general = onGeneral(context)[0]
LaunchedEffect(Unit) {
Log.wtf("TST_Output", "当屏幕进入时,打个招呼!")
general.forEach { items.add(Item(it)) }
}
由于我们将Unit作为LaunchedEffect的关键,所以它只会运行一次,因为Unit不会更改。如果您想每次general更改时运行LaunchedEffect中的代码,您可以将general用作关键,如下所示:
var items: MutableList<Item> = remember { mutableStateListOf() }
val context = LocalContext.current
val general = onGeneral(context)[0]
LaunchedEffect(general) {
Log.wtf("TST_Output", "当屏幕进入时,打个招呼!")
// 清空列表,因为我们要重新添加项目
items.clear()
general.forEach { items.add(Item(it)) }
}
希望这对你有所帮助。如果您想了解更多关于副作用的信息,您可以阅读这个链接。"
英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论