Skip to content

Commit bcddf8a

Browse files
optimize reveal store and id usage for only aniamte true
1 parent a8d257d commit bcddf8a

File tree

5 files changed

+25
-119
lines changed

5 files changed

+25
-119
lines changed

Tutorial4-1ChatBot/src/main/java/com/smarttoolfactory/tutorial4_1chatbot/MainActivity.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ import androidx.activity.ComponentActivity
55
import androidx.activity.compose.setContent
66
import androidx.activity.enableEdgeToEdge
77
import androidx.activity.viewModels
8+
import androidx.compose.runtime.CompositionLocalProvider
9+
import androidx.compose.runtime.remember
810
import androidx.compose.ui.text.TextStyle
911
import androidx.compose.ui.unit.sp
1012
import com.halilibo.richtext.ui.RichTextThemeProvider
13+
import com.smarttoolfactory.tutorial4_1chatbot.markdown.LocalRevealStore
14+
import com.smarttoolfactory.tutorial4_1chatbot.markdown.RevealStore
1115
import com.smarttoolfactory.tutorial4_1chatbot.ui.ChatScreen
1216
import com.smarttoolfactory.tutorial4_1chatbot.ui.ChatViewModel
1317
import dagger.hilt.android.AndroidEntryPoint
@@ -30,9 +34,11 @@ class MainActivity : ComponentActivity() {
3034
)
3135
}
3236
) {
33-
ChatScreen(viewModel)
37+
val revealStore = remember { RevealStore() }
38+
CompositionLocalProvider(LocalRevealStore provides revealStore) {
39+
ChatScreen(viewModel)
40+
}
3441
}
35-
3642
}
3743
}
3844
}

Tutorial4-1ChatBot/src/main/java/com/smarttoolfactory/tutorial4_1chatbot/markdown/MarkdownComposer.kt

Lines changed: 9 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -50,98 +50,6 @@ internal fun MarkdownComposer(
5050
markdown: String,
5151
debug: Boolean = false,
5252
animate: Boolean = true,
53-
segmentation: LineSegmentation = LineSegmentation.None,
54-
onCompleted: () -> Unit = {}
55-
) {
56-
val commonmarkAstNodeParser: CommonmarkAstNodeParser = remember {
57-
CommonmarkAstNodeParser()
58-
}
59-
60-
val astRootNode by produceState<AstNode?>(
61-
initialValue = null,
62-
key1 = commonmarkAstNodeParser,
63-
key2 = markdown
64-
) {
65-
value = commonmarkAstNodeParser.parse(markdown)
66-
}
67-
68-
69-
val tableBlockNodeComposer: AstBlockNodeComposer = remember {
70-
object : AstBlockNodeComposer {
71-
72-
override fun predicate(astBlockNodeType: AstBlockNodeType): Boolean {
73-
// Intercept tables
74-
val isTable = astBlockNodeType == AstTableRoot
75-
// Intercept Text
76-
val isText = astBlockNodeType == AstParagraph
77-
// println(
78-
// "isTable: $isTable, " +
79-
// "isText: $isText," +
80-
// " astBlockNodeType: $astBlockNodeType"
81-
// )
82-
return isTable || isText
83-
}
84-
85-
@Composable
86-
override fun RichTextScope.Compose(
87-
astNode: AstNode,
88-
visitChildren: @Composable ((AstNode) -> Unit)
89-
) {
90-
if (astNode.type is AstTableRoot) {
91-
CustomTable(tableRoot = astNode)
92-
} else if (astNode.type is AstParagraph) {
93-
94-
/**
95-
* Persist startIndex OUTSIDE the node composable so it won't reset to 0 when:
96-
* - the markdown becomes valid (e.g. ** closes),
97-
* - AST is rebuilt,
98-
* - paragraph Text() composable gets recreated.
99-
*/
100-
val startIndexByNodeKey = remember(markdown) {
101-
mutableStateMapOf<String, Int>()
102-
}
103-
104-
val nodeKey = remember(astNode) { astNode.stablePathKey() }
105-
106-
val startIndexForNode = startIndexByNodeKey[nodeKey] ?: -1
107-
108-
// println("✅ nodeKey: $nodeKey, startIndex: $startIndexForNode")
109-
110-
MarkdownFadeInRichText(
111-
modifier = Modifier.border(
112-
2.dp,
113-
if (animate) Color.Cyan else Color.Magenta
114-
),
115-
astNode = astNode,
116-
segmentation = segmentation,
117-
debug = debug,
118-
startIndex = startIndexForNode,
119-
onStartIndexChange = { newStart ->
120-
// monotonic to avoid regressions
121-
val old = startIndexByNodeKey[nodeKey] ?: 0
122-
startIndexByNodeKey[nodeKey] = maxOf(old, newStart)
123-
},
124-
onCompleted = {
125-
onCompleted()
126-
},
127-
animate = animate
128-
)
129-
}
130-
}
131-
}
132-
}
133-
134-
astRootNode?.let { astNode ->
135-
RichTextScope.BasicMarkdown(astNode, tableBlockNodeComposer)
136-
}
137-
}
138-
139-
@Composable
140-
internal fun MarkdownComposer(
141-
markdown: String,
142-
debug: Boolean = false,
143-
revealStore: RevealStore? = null,
144-
animate: Boolean = true,
14553
messageKey: String? = null,
14654
segmentation: LineSegmentation = LineSegmentation.None,
14755
onCompleted: () -> Unit = {}
@@ -162,9 +70,7 @@ internal fun MarkdownComposer(
16270
object : AstBlockNodeComposer {
16371

16472
override fun predicate(astBlockNodeType: AstBlockNodeType): Boolean {
165-
// Intercept tables
16673
val isTable = astBlockNodeType == AstTableRoot
167-
// Intercept Text
16874
val isText = astBlockNodeType == AstParagraph
16975
return isTable || isText
17076
}
@@ -176,30 +82,29 @@ internal fun MarkdownComposer(
17682
) {
17783

17884
if (animate) {
85+
val revealStore = LocalRevealStore.current
86+
17987
val localFallbackStarts = remember { mutableStateMapOf<String, Int>() }
18088
val localFallbackCompleted = remember { mutableStateMapOf<String, Boolean>() }
18189

18290
val startIndexByNodeKey =
183-
revealStore?.startIndexByNodeKey ?: localFallbackStarts
91+
revealStore.startIndexByNodeKey.ifEmpty { localFallbackStarts }
18492
val completedByNodeKey =
185-
revealStore?.completedByNodeKey ?: localFallbackCompleted
93+
revealStore.completedByNodeKey.ifEmpty { localFallbackCompleted }
18694

18795
val rawNodeKey = remember(astNode) { astNode.stablePathKey() }
18896

189-
// ✅ CRITICAL: prefix by messageKey so different messages don't collide
190-
// If messageKey is null, fall back to rawNodeKey (preview/non-lazy uses)
19197
val nodeKey = if (messageKey != null) {
19298
"$messageKey|$rawNodeKey"
19399
} else {
194100
rawNodeKey
195101
}
196102

197-
val startIndexForNode = startIndexByNodeKey[nodeKey] ?: 0
198-
199-
// println("✅ nodeKey: $nodeKey, startIndex: $startIndexForNode")
103+
println("Composer $messageKey, rawNodeKey: $rawNodeKey")
200104

105+
val startIndexForNode = startIndexByNodeKey[nodeKey] ?: 0
201106
val alreadyCompleted = completedByNodeKey[nodeKey] == true
202-
val shouldAnimate = animate && !alreadyCompleted
107+
val shouldAnimate = !alreadyCompleted
203108

204109
if (astNode.type is AstTableRoot) {
205110
CustomTable(tableRoot = astNode)
@@ -214,7 +119,6 @@ internal fun MarkdownComposer(
214119
debug = debug,
215120
startIndex = startIndexForNode,
216121
onStartIndexChange = { newStart ->
217-
// monotonic to avoid regressions
218122
val old = startIndexByNodeKey[nodeKey] ?: 0
219123
startIndexByNodeKey[nodeKey] = maxOf(old, newStart)
220124
},
@@ -226,6 +130,7 @@ internal fun MarkdownComposer(
226130
)
227131
}
228132
} else {
133+
// ✅ untouched non-animated path
229134
if (astNode.type is AstTableRoot) {
230135
CustomTable(tableRoot = astNode)
231136
} else if (astNode.type is AstParagraph) {
@@ -241,4 +146,4 @@ internal fun MarkdownComposer(
241146
astRootNode?.let { astNode ->
242147
RichTextScope.BasicMarkdown(astNode, tableBlockNodeComposer)
243148
}
244-
}
149+
}

Tutorial4-1ChatBot/src/main/java/com/smarttoolfactory/tutorial4_1chatbot/markdown/RevealStore.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@ package com.smarttoolfactory.tutorial4_1chatbot.markdown
22

33
import androidx.compose.runtime.Stable
44
import androidx.compose.runtime.mutableStateMapOf
5+
import androidx.compose.runtime.staticCompositionLocalOf
56
import kotlin.collections.set
67

8+
val LocalRevealStore = staticCompositionLocalOf<RevealStore> {
9+
error("LocalRevealStore not provided")
10+
}
11+
712
@Stable
813
class RevealStore {
914
// paragraph nodeKey -> startIndex (monotonic)

Tutorial4-1ChatBot/src/main/java/com/smarttoolfactory/tutorial4_1chatbot/ui/ChatScreen.kt

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -293,10 +293,6 @@ fun ChatScreen(
293293
)
294294
}
295295
) {
296-
// ✅ This lives at the screen level: it does NOT get disposed when items scroll out.
297-
val revealStoreByMessageKey = remember {
298-
mutableStateMapOf<String, RevealStore>() // message.uiKey -> RevealStore
299-
}
300296

301297
LazyColumn(
302298
modifier = Modifier.fillMaxSize(),
@@ -332,14 +328,10 @@ fun ChatScreen(
332328
// .border(2.dp, Color.Blue)
333329
}
334330

335-
// ✅ IMPORTANT: do NOT create RevealStore with remember{} here.
336-
// Just fetch from the screen-level map.
337-
val revealStore: RevealStore = revealStoreByMessageKey.getOrPut(msg.uiKey) { RevealStore() }
338331

339332
MessageRow(
340333
modifier = modifier,
341-
message = msg,
342-
revealStore = revealStore
334+
message = msg
343335
)
344336
}
345337
}

Tutorial4-1ChatBot/src/main/java/com/smarttoolfactory/tutorial4_1chatbot/ui/component/message/MessageRow.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import com.halilibo.richtext.commonmark.Markdown
1717
import com.halilibo.richtext.ui.BasicRichText
1818
import com.halilibo.richtext.ui.RichTextStyle
1919
import com.smarttoolfactory.tutorial4_1chatbot.markdown.MarkdownComposer
20-
import com.smarttoolfactory.tutorial4_1chatbot.markdown.RevealStore
2120
import com.smarttoolfactory.tutorial4_1chatbot.ui.Message
2221
import com.smarttoolfactory.tutorial4_1chatbot.ui.MessageStatus
2322
import com.smarttoolfactory.tutorial4_1chatbot.ui.Role
@@ -73,8 +72,7 @@ val style = RichTextStyle.Default.copy(
7372
@Composable
7473
fun MessageRow(
7574
modifier: Modifier = Modifier,
76-
message: Message,
77-
revealStore: RevealStore
75+
message: Message
7876
) {
7977
val isUser = message.role == Role.User
8078

@@ -123,7 +121,7 @@ fun MessageRow(
123121
MarkdownComposer(
124122
markdown = message.text,
125123
debug = false,
126-
revealStore = revealStore,
124+
messageKey = message.uiKey,
127125
// ✅ Typical: animate only while streaming.
128126
// Completed paragraphs will auto-disable via completedByNodeKey.
129127
animate = (message.messageStatus == MessageStatus.Streaming),

0 commit comments

Comments
 (0)