@momo-kits/native-kits 0.125.1-securityCustom.0 → 0.127.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/build/kotlin/commonizedNativeDistributionLocation.txt +1 -0
  2. package/compose/build/generated/compose/resourceGenerator/kotlin/androidMainResourceCollectors/vn/momo/uikits/resources/ActualResourceCollectors.kt +49 -0
  3. package/compose/build/generated/compose/resourceGenerator/kotlin/commonMainResourceAccessors/vn/momo/uikits/resources/Font0.commonMain.kt +187 -0
  4. package/compose/build/generated/compose/resourceGenerator/kotlin/commonMainResourceCollectors/vn/momo/uikits/resources/ExpectResourceCollectors.kt +25 -0
  5. package/compose/build/generated/compose/resourceGenerator/kotlin/commonResClass/vn/momo/uikits/resources/Res.kt +49 -0
  6. package/compose/build/generated/compose/resourceGenerator/kotlin/iosArm64MainResourceCollectors/vn/momo/uikits/resources/ActualResourceCollectors.kt +49 -0
  7. package/compose/build/generated/compose/resourceGenerator/preparedResources/commonMain/composeResources/font/momosignature.otf +0 -0
  8. package/compose/build/generated/compose/resourceGenerator/preparedResources/commonMain/composeResources/font/momotrustdisplay.otf +0 -0
  9. package/compose/build/generated/compose/resourceGenerator/preparedResources/commonMain/composeResources/font/sfprotext_black.otf +0 -0
  10. package/compose/build/generated/compose/resourceGenerator/preparedResources/commonMain/composeResources/font/sfprotext_black.ttf +0 -0
  11. package/compose/build/generated/compose/resourceGenerator/preparedResources/commonMain/composeResources/font/sfprotext_bold.ttf +0 -0
  12. package/compose/build/generated/compose/resourceGenerator/preparedResources/commonMain/composeResources/font/sfprotext_heavy.ttf +0 -0
  13. package/compose/build/generated/compose/resourceGenerator/preparedResources/commonMain/composeResources/font/sfprotext_light.ttf +0 -0
  14. package/compose/build/generated/compose/resourceGenerator/preparedResources/commonMain/composeResources/font/sfprotext_medium.ttf +0 -0
  15. package/compose/build/generated/compose/resourceGenerator/preparedResources/commonMain/composeResources/font/sfprotext_regular.ttf +0 -0
  16. package/compose/build/generated/compose/resourceGenerator/preparedResources/commonMain/composeResources/font/sfprotext_semibold.ttf +0 -0
  17. package/compose/build/generated/compose/resourceGenerator/preparedResources/commonMain/composeResources/font/sfprotext_thin.otf +0 -0
  18. package/compose/build/generated/compose/resourceGenerator/preparedResources/commonMain/composeResources/font/sfprotext_thin.ttf +0 -0
  19. package/compose/build/generated/compose/resourceGenerator/preparedResources/commonMain/composeResources/font/sfprotext_ultralight.otf +0 -0
  20. package/compose/build/generated/compose/resourceGenerator/preparedResources/commonMain/composeResources/font/sfprotext_ultralight.ttf +0 -0
  21. package/compose/build.gradle.kts +17 -9
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +63 -1
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +12 -7
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +79 -25
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +87 -72
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +33 -20
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +33 -9
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +61 -39
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +70 -52
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +131 -94
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +39 -19
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +54 -38
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +50 -28
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +29 -5
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +6 -1
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +15 -5
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +3 -2
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +3 -1
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +161 -21
  40. package/local.properties +8 -0
  41. package/package.json +4 -2
  42. package/settings.gradle.kts +3 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/PlatformApi.kt +0 -12
@@ -1,9 +1,13 @@
1
1
  package vn.momo.kits.application
2
2
 
3
3
  import androidx.compose.animation.animateColorAsState
4
+ import androidx.compose.animation.core.AnimationSpec
4
5
  import androidx.compose.animation.core.animateFloatAsState
6
+ import androidx.compose.animation.core.tween
5
7
  import androidx.compose.runtime.Composable
8
+ import androidx.compose.runtime.derivedStateOf
6
9
  import androidx.compose.runtime.getValue
10
+ import androidx.compose.runtime.remember
7
11
  import androidx.compose.ui.graphics.Color
8
12
  import androidx.compose.ui.unit.Dp
9
13
  import androidx.compose.ui.unit.dp
@@ -21,6 +25,23 @@ data class HeaderAnimations(
21
25
 
22
26
  private const val SCREEN_PADDING = 12
23
27
  private const val BACK_WIDTH = 28
28
+ private const val HEADER_HEIGHT_CONST = 100f
29
+
30
+ // Optimized animation spec for better performance
31
+ private val defaultAnimationSpec: AnimationSpec<Float> = tween(
32
+ durationMillis = 300,
33
+ delayMillis = 0
34
+ )
35
+
36
+ // Helper function for color interpolation - moved outside composable for better performance
37
+ private fun interpolateColor(from: Color, to: Color, progress: Float): Color {
38
+ return Color(
39
+ red = from.red + (to.red - from.red) * progress,
40
+ green = from.green + (to.green - from.green) * progress,
41
+ blue = from.blue + (to.blue - from.blue) * progress,
42
+ alpha = from.alpha + (to.alpha - from.alpha) * progress
43
+ )
44
+ }
24
45
 
25
46
  @Composable
26
47
  fun useHeaderSearchAnimation(
@@ -29,40 +50,73 @@ fun useHeaderSearchAnimation(
29
50
  headerRightWidth: Dp,
30
51
  isBack: Boolean
31
52
  ): HeaderAnimations {
32
- val screenWidth = getScreenDimensions().width
33
- val leftPosition = if (isBack) (BACK_WIDTH + 20).dp else 12.dp
34
- val searchWidth = screenWidth - (SCREEN_PADDING * 2)
53
+ // Cache screen calculations - call getScreenDimensions directly in composable context
54
+ val screenDimensions = getScreenDimensions()
55
+ val screenWidth = remember(screenDimensions) { screenDimensions.width }
56
+
57
+ // Pre-calculate static values
58
+ val leftPosition = remember(isBack) {
59
+ if (isBack) (BACK_WIDTH + 20).dp else 12.dp
60
+ }
35
61
 
62
+ val searchWidth = remember(screenWidth) {
63
+ screenWidth - (SCREEN_PADDING * 2)
64
+ }
65
+
66
+ // Use derivedStateOf for computed scroll progress to avoid unnecessary recompositions
67
+ val scrollProgress by remember {
68
+ derivedStateOf {
69
+ (scrollState / HEADER_HEIGHT_CONST).coerceIn(0f, 1f)
70
+ }
71
+ }
72
+
73
+ // Optimize color animation with remember
74
+ val theme = AppTheme.current
36
75
  val backgroundSearch by animateColorAsState(
37
- targetValue = animateColor(
38
- Colors.black_01,
39
- AppTheme.current.colors.background.default,
40
- opacityAni
41
- ),
76
+ targetValue = remember(opacityAni, theme) {
77
+ interpolateColor(
78
+ Colors.black_01,
79
+ theme.colors.background.default,
80
+ opacityAni
81
+ )
82
+ },
83
+ animationSpec = tween(durationMillis = 200),
84
+ label = "backgroundSearch"
42
85
  )
43
86
 
87
+ // Consolidate translation and width calculations
44
88
  val animatedTranslateX by animateFloatAsState(
45
- targetValue = (scrollState / HEADER_HEIGHT * 1f).coerceIn(
46
- 0f,
47
- 1f
48
- ) * (leftPosition.value - 12) + 12,
89
+ targetValue = remember(scrollProgress, leftPosition) {
90
+ scrollProgress * (leftPosition.value - 12) + 12
91
+ },
92
+ animationSpec = defaultAnimationSpec,
93
+ label = "translateX"
49
94
  )
50
95
 
51
96
  val animatedWidth by animateFloatAsState(
52
- targetValue = (scrollState / HEADER_HEIGHT * 1f).coerceIn(
53
- 0f,
54
- 1f
55
- ) * ((searchWidth - leftPosition.value - headerRightWidth.value + 12) - searchWidth) + searchWidth,
97
+ targetValue = remember(scrollProgress, searchWidth, leftPosition, headerRightWidth) {
98
+ val targetWidth = searchWidth - leftPosition.value - headerRightWidth.value + 12
99
+ scrollProgress * (targetWidth - searchWidth) + searchWidth
100
+ },
101
+ animationSpec = defaultAnimationSpec,
102
+ label = "width"
56
103
  )
57
104
 
58
105
  val animatedTranslateY by animateFloatAsState(
59
- targetValue = (1 - (scrollState / HEADER_HEIGHT * 1f).coerceIn(0f, 1f)) * HEADER_HEIGHT
106
+ targetValue = remember(scrollProgress) {
107
+ (1 - scrollProgress) * HEADER_HEIGHT_CONST
108
+ },
109
+ animationSpec = defaultAnimationSpec,
110
+ label = "translateY"
60
111
  )
61
- return HeaderAnimations(
62
- opacity = opacityAni,
63
- backgroundSearch = backgroundSearch,
64
- translateX = animatedTranslateX,
65
- width = animatedWidth,
66
- translateY = animatedTranslateY
67
- )
68
- }
112
+
113
+ return remember(opacityAni, backgroundSearch, animatedTranslateX, animatedWidth, animatedTranslateY) {
114
+ HeaderAnimations(
115
+ opacity = opacityAni,
116
+ backgroundSearch = backgroundSearch,
117
+ translateX = animatedTranslateX,
118
+ width = animatedWidth,
119
+ translateY = animatedTranslateY
120
+ )
121
+ }
122
+ }
@@ -2,7 +2,6 @@ package vn.momo.kits.components
2
2
 
3
3
  import androidx.compose.foundation.background
4
4
  import androidx.compose.foundation.border
5
- import androidx.compose.foundation.clickable
6
5
  import androidx.compose.foundation.layout.Arrangement
7
6
  import androidx.compose.foundation.layout.Box
8
7
  import androidx.compose.foundation.layout.Row
@@ -14,6 +13,7 @@ import androidx.compose.foundation.layout.size
14
13
  import androidx.compose.foundation.shape.RoundedCornerShape
15
14
  import androidx.compose.material3.CircularProgressIndicator
16
15
  import androidx.compose.runtime.Composable
16
+ import androidx.compose.runtime.remember
17
17
  import androidx.compose.ui.Alignment
18
18
  import androidx.compose.ui.Modifier
19
19
  import androidx.compose.ui.draw.clip
@@ -73,47 +73,56 @@ enum class Size(val value: ButtonSpecs) {
73
73
  )
74
74
  }
75
75
 
76
+ // Cache for style calculations to avoid repeated computations
77
+ private val styleCache = mapOf(
78
+ Size.LARGE to Typography.actionDefaultBold,
79
+ Size.MEDIUM to Typography.actionSBold,
80
+ Size.SMALL to Typography.actionXsBold
81
+ )
82
+
83
+ private val iconSizeCache = mapOf(
84
+ Size.LARGE to 24.dp,
85
+ Size.MEDIUM to 16.dp,
86
+ Size.SMALL to 16.dp
87
+ )
88
+
89
+ private val iconSpaceCache = mapOf(
90
+ Size.SMALL to Spacing.XS,
91
+ Size.MEDIUM to Spacing.S,
92
+ Size.LARGE to Spacing.S
93
+ )
94
+
76
95
  fun getStyle(size: Size): TextStyle {
77
- return when (size.name) {
78
- Size.LARGE.name -> Typography.actionDefaultBold
79
- Size.MEDIUM.name -> Typography.actionSBold
80
- Size.SMALL.name -> Typography.actionXsBold
81
- else -> Typography.actionDefaultBold
82
- }
96
+ return styleCache[size] ?: Typography.actionDefaultBold
83
97
  }
84
98
 
85
99
  fun getIconSize(size: Size): Dp {
86
- return when (size.name) {
87
- Size.LARGE.name -> 24.dp
88
- Size.MEDIUM.name -> 16.dp
89
- Size.SMALL.name -> 16.dp
90
- else -> 24.dp
91
- }
100
+ return iconSizeCache[size] ?: 24.dp
92
101
  }
93
102
 
94
103
  fun getIconSpace(size: Size): Dp {
95
- return when (size.name) {
96
- Size.SMALL.name -> Spacing.XS
97
- else -> Spacing.S
98
- }
104
+ return iconSpaceCache[size] ?: Spacing.S
99
105
  }
100
106
 
101
107
  @Composable
102
108
  fun getTextColor(type: ButtonType): Color {
103
- return when (type) {
104
- ButtonType.DISABLED -> AppTheme.current.colors.text.disable
105
- ButtonType.PRIMARY -> Colors.black_01
106
- ButtonType.SECONDARY -> AppTheme.current.colors.text.default
107
- ButtonType.OUTLINE -> AppTheme.current.colors.primary
108
- ButtonType.TONAL -> AppTheme.current.colors.primary
109
- ButtonType.DANGER -> Colors.black_01
110
- ButtonType.TEXT -> AppTheme.current.colors.primary
109
+ val theme = AppTheme.current
110
+ return remember(type, theme) {
111
+ when (type) {
112
+ ButtonType.DISABLED -> theme.colors.text.disable
113
+ ButtonType.PRIMARY -> Colors.black_01
114
+ ButtonType.SECONDARY -> theme.colors.text.default
115
+ ButtonType.OUTLINE -> theme.colors.primary
116
+ ButtonType.TONAL -> theme.colors.primary
117
+ ButtonType.DANGER -> Colors.black_01
118
+ ButtonType.TEXT -> theme.colors.primary
119
+ }
111
120
  }
112
121
  }
113
122
 
114
123
  @Composable
115
124
  fun RenderTitle(size: Size, title: String = "", type: ButtonType) {
116
- val style = getStyle(size)
125
+ val style = remember(size) { getStyle(size) }
117
126
  val color = getTextColor(type)
118
127
  Text(style = style, text = title, color = color, overflow = TextOverflow.Ellipsis, maxLines = 1)
119
128
  }
@@ -126,9 +135,10 @@ fun RenderLeading(
126
135
  type: ButtonType,
127
136
  iconLeft: String = "",
128
137
  ) {
129
- val iconSize = getIconSize(size)
130
- val marginRight = getIconSpace(size)
138
+ val iconSize = remember(size) { getIconSize(size) }
139
+ val marginRight = remember(size) { getIconSpace(size) }
131
140
  val color = if (useTintColor) getTextColor(type) else Color.Unspecified
141
+
132
142
  Row(
133
143
  verticalAlignment = Alignment.CenterVertically,
134
144
  horizontalArrangement = Arrangement.Center
@@ -142,7 +152,6 @@ fun RenderLeading(
142
152
  strokeWidth = 2.dp
143
153
  )
144
154
  }
145
-
146
155
  }
147
156
  if (iconLeft.isNotEmpty()) {
148
157
  Icon(
@@ -163,9 +172,10 @@ fun RenderTrailing(
163
172
  type: ButtonType,
164
173
  ) {
165
174
  val color = if (useTintColor) getTextColor(type) else Color.Unspecified
166
- val marginLeft = getIconSpace(size)
175
+ val marginLeft = remember(size) { getIconSpace(size) }
176
+
167
177
  if (iconRight.isNotEmpty()) {
168
- val iconSize = getIconSize(size)
178
+ val iconSize = remember(size) { getIconSize(size) }
169
179
  Icon(
170
180
  source = iconRight,
171
181
  color = color,
@@ -182,33 +192,36 @@ fun getTypeStyle(
182
192
  size: Size,
183
193
  ): Modifier {
184
194
  val theme = AppTheme.current
195
+ val radius = remember(size) { size.value.radius }
185
196
 
186
- return when (type) {
187
- ButtonType.DISABLED -> Modifier.background(theme.colors.background.disable)
188
- .border(0.dp, Color.Unspecified, RoundedCornerShape(size.value.radius))
197
+ return remember(type, color, theme, radius) {
198
+ when (type) {
199
+ ButtonType.DISABLED -> Modifier.background(theme.colors.background.disable)
200
+ .border(0.dp, Color.Unspecified, RoundedCornerShape(radius))
189
201
 
190
- ButtonType.PRIMARY -> Modifier.background(theme.colors.primary)
191
- .border(0.dp, Color.Unspecified, RoundedCornerShape(size.value.radius))
202
+ ButtonType.PRIMARY -> Modifier.background(theme.colors.primary)
203
+ .border(0.dp, Color.Unspecified, RoundedCornerShape(radius))
192
204
 
193
- ButtonType.SECONDARY -> Modifier
194
- .background(theme.colors.background.surface)
195
- .border(1.dp, theme.colors.border.default, RoundedCornerShape(size.value.radius))
205
+ ButtonType.SECONDARY -> Modifier
206
+ .background(theme.colors.background.surface)
207
+ .border(1.dp, theme.colors.border.default, RoundedCornerShape(radius))
196
208
 
197
- ButtonType.OUTLINE -> Modifier
198
- .background(theme.colors.background.surface)
199
- .border(
200
- 1.dp,
201
- color ?: theme.colors.primary,
202
- RoundedCornerShape(size.value.radius)
203
- )
209
+ ButtonType.OUTLINE -> Modifier
210
+ .background(theme.colors.background.surface)
211
+ .border(
212
+ 1.dp,
213
+ color ?: theme.colors.primary,
214
+ RoundedCornerShape(radius)
215
+ )
204
216
 
205
- ButtonType.TONAL -> Modifier.background(theme.colors.background.tonal)
206
- .border(0.dp, Color.Unspecified, RoundedCornerShape(size.value.radius))
217
+ ButtonType.TONAL -> Modifier.background(theme.colors.background.tonal)
218
+ .border(0.dp, Color.Unspecified, RoundedCornerShape(radius))
207
219
 
208
- ButtonType.DANGER -> Modifier.background(theme.colors.error.primary)
209
- .border(0.dp, Color.Unspecified, RoundedCornerShape(size.value.radius))
220
+ ButtonType.DANGER -> Modifier.background(theme.colors.error.primary)
221
+ .border(0.dp, Color.Unspecified, RoundedCornerShape(radius))
210
222
 
211
- ButtonType.TEXT -> Modifier
223
+ ButtonType.TEXT -> Modifier
224
+ }
212
225
  }
213
226
  }
214
227
 
@@ -234,30 +247,32 @@ fun Button(
234
247
  isFull: Boolean = true,
235
248
  modifier: Modifier = Modifier,
236
249
  ) {
237
- val customModifier = if (isFull) {
238
- modifier
239
- .fillMaxWidth()
240
- .clip(RoundedCornerShape(size.value.radius)).activeOpacityClickable(
241
- activeOpacity = 0.5f,
242
- enabled = type != ButtonType.DISABLED,
243
- onClick = onClick
244
- )
250
+ val radius = remember(size) { size.value.radius }
251
+ val isEnabled = remember(type) { type != ButtonType.DISABLED }
245
252
 
246
- } else {
247
- modifier
248
- .clip(RoundedCornerShape(size.value.radius)).activeOpacityClickable(
249
- activeOpacity = 0.5f,
250
- enabled = type != ButtonType.DISABLED,
251
- onClick = onClick
252
- )
253
- }
253
+ val customModifier = remember(isFull, radius, isEnabled, modifier) {
254
+ if (isFull) {
255
+ modifier
256
+ .fillMaxWidth()
257
+ .clip(RoundedCornerShape(radius))
258
+ } else {
259
+ modifier
260
+ .clip(RoundedCornerShape(radius))
261
+ }
262
+ }.activeOpacityClickable(
263
+ activeOpacity = 0.5f,
264
+ enabled = isEnabled,
265
+ onClick = onClick
266
+ )
267
+
268
+ val sizeSpecs = remember(size) { size.value }
254
269
 
255
270
  Row(
256
- modifier = customModifier.then(getTypeStyle(type, size = size))
257
- .padding(horizontal = size.value.padding)
258
- .defaultMinSize(
259
- minWidth = size.value.width
260
- ).height(size.value.height),
271
+ modifier = customModifier
272
+ .then(getTypeStyle(type, size = size))
273
+ .padding(horizontal = sizeSpecs.padding)
274
+ .defaultMinSize(minWidth = sizeSpecs.width)
275
+ .height(sizeSpecs.height),
261
276
  horizontalArrangement = Arrangement.Center,
262
277
  verticalAlignment = Alignment.CenterVertically,
263
278
  ) {
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Spacer
9
9
  import androidx.compose.foundation.layout.size
10
10
  import androidx.compose.foundation.shape.RoundedCornerShape
11
11
  import androidx.compose.runtime.Composable
12
+ import androidx.compose.runtime.remember
12
13
  import androidx.compose.ui.Alignment
13
14
  import androidx.compose.ui.Modifier
14
15
  import androidx.compose.ui.graphics.Color
@@ -20,6 +21,9 @@ import vn.momo.kits.const.Spacing
20
21
  import vn.momo.kits.const.Typography
21
22
  import vn.momo.kits.modifier.activeOpacityClickable
22
23
 
24
+ // Pre-computed shape for performance
25
+ private val checkboxShape = RoundedCornerShape(Radius.XS)
26
+
23
27
  @Composable
24
28
  fun CheckBox(
25
29
  disabled: Boolean = false,
@@ -28,32 +32,40 @@ fun CheckBox(
28
32
  indeterminate: Boolean = false,
29
33
  title: String = "",
30
34
  ) {
31
- var borderColor = AppTheme.current.colors.text.default
32
- var backgroundColor = Color.Transparent
33
- val iconSource = if (indeterminate) "navigation_minus" else "ic_checked"
35
+ // Cache theme access
36
+ val theme = AppTheme.current
34
37
 
35
- if (checked) {
36
- borderColor = AppTheme.current.colors.primary
37
- backgroundColor = AppTheme.current.colors.primary
38
- }
39
- if (disabled) {
40
- borderColor = AppTheme.current.colors.border.disable
41
- backgroundColor = Color.Transparent
42
- if (checked) {
43
- borderColor = AppTheme.current.colors.background.tonal
44
- backgroundColor = AppTheme.current.colors.background.tonal
38
+ // Memoize color calculations to avoid repeated conditional logic
39
+ val colors = remember(checked, disabled, theme) {
40
+ when {
41
+ disabled && checked -> Pair(theme.colors.background.tonal, theme.colors.background.tonal)
42
+ disabled -> Pair(theme.colors.border.disable, Color.Transparent)
43
+ checked -> Pair(theme.colors.primary, theme.colors.primary)
44
+ else -> Pair(theme.colors.text.default, Color.Transparent)
45
45
  }
46
46
  }
47
- Row(
48
- verticalAlignment = Alignment.CenterVertically,
49
- ) {
47
+
48
+ val (borderColor, backgroundColor) = colors
49
+
50
+ // Memoize icon source to avoid repeated conditional check
51
+ val iconSource = remember(indeterminate) {
52
+ if (indeterminate) "navigation_minus" else "ic_checked"
53
+ }
54
+
55
+ // Memoize click handler to avoid lambda recreation
56
+ val onClickHandler = remember(onCheckedChange, checked) {
57
+ { onCheckedChange(!checked) }
58
+ }
59
+
60
+ Row(verticalAlignment = Alignment.CenterVertically) {
50
61
  Box(
51
- modifier = Modifier.size(20.dp)
52
- .background(backgroundColor, RoundedCornerShape(Radius.XS))
53
- .border(Spacing.XXS, borderColor, RoundedCornerShape(Radius.XS))
62
+ modifier = Modifier
63
+ .size(20.dp)
64
+ .background(backgroundColor, checkboxShape)
65
+ .border(Spacing.XXS, borderColor, checkboxShape)
54
66
  .activeOpacityClickable(
55
67
  activeOpacity = 0.8f,
56
- onClick = { onCheckedChange(!checked) },
68
+ onClick = onClickHandler,
57
69
  enabled = !disabled,
58
70
  ),
59
71
  ) {
@@ -65,6 +77,7 @@ fun CheckBox(
65
77
  )
66
78
  }
67
79
  }
80
+
68
81
  if (title.isNotEmpty()) {
69
82
  Spacer(Modifier.size(Spacing.S))
70
83
  Text(
@@ -3,6 +3,7 @@ package vn.momo.kits.components
3
3
  import androidx.compose.foundation.layout.Box
4
4
  import androidx.compose.foundation.layout.size
5
5
  import androidx.compose.runtime.Composable
6
+ import androidx.compose.runtime.remember
6
7
  import androidx.compose.ui.Modifier
7
8
  import androidx.compose.ui.graphics.Color
8
9
  import androidx.compose.ui.graphics.ColorFilter
@@ -19,22 +20,45 @@ import vn.momo.kits.utils.Icons
19
20
  fun Icon(
20
21
  source: String,
21
22
  size: Dp = 24.dp,
22
- color: Color? = AppTheme.current.colors.text.default,
23
+ color: Color? = null,
23
24
  modifier: Modifier = Modifier,
24
25
  ) {
25
- val icon = if (source.contains("https")) {
26
- source
27
- } else {
28
- Icons[source] ?: ""
26
+ // Cache theme access
27
+ val theme = AppTheme.current
28
+
29
+ // Memoize icon resolution to avoid repeated string operations and map lookups
30
+ val iconUrl = remember(source) {
31
+ if (source.contains("https")) {
32
+ source
33
+ } else {
34
+ Icons[source] ?: ""
35
+ }
36
+ }
37
+
38
+ // Memoize color calculation
39
+ val iconColor = remember(color, theme) {
40
+ color ?: theme.colors.text.default
41
+ }
42
+
43
+ // Memoize ColorFilter creation
44
+ val colorFilter = remember(iconColor) {
45
+ ColorFilter.tint(iconColor)
29
46
  }
30
- Box(modifier = modifier.size(size).semantics { contentDescription = "img|$icon" }) {
47
+
48
+ // Memoize content description
49
+ val contentDesc = remember(iconUrl) { "img|$iconUrl" }
50
+
51
+ Box(
52
+ modifier = modifier
53
+ .size(size)
54
+ .semantics { contentDescription = contentDesc }
55
+ ) {
31
56
  AsyncImage(
32
57
  modifier = Modifier.matchParentSize(),
33
- model = icon,
58
+ model = iconUrl,
34
59
  contentDescription = null,
35
60
  contentScale = ContentScale.Fit,
36
- colorFilter = color?.let { ColorFilter.tint(it) },
61
+ colorFilter = colorFilter,
37
62
  )
38
63
  }
39
64
  }
40
-
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Box
7
7
  import androidx.compose.foundation.layout.size
8
8
  import androidx.compose.foundation.shape.RoundedCornerShape
9
9
  import androidx.compose.runtime.Composable
10
+ import androidx.compose.runtime.remember
10
11
  import androidx.compose.ui.Alignment
11
12
  import androidx.compose.ui.Modifier
12
13
  import androidx.compose.ui.draw.clip
@@ -43,53 +44,63 @@ enum class IconSize(val value: IconSpecs) {
43
44
  )
44
45
  }
45
46
 
47
+ // Pre-computed shape to avoid repeated creation
48
+ private val iconButtonShape = RoundedCornerShape(Radius.XL)
49
+
50
+ // Cache for icon sizes to avoid repeated calculations
51
+ private val iconSizeMap = mapOf(
52
+ IconSize.SMALL to 16.dp,
53
+ IconSize.LARGE to 24.dp
54
+ )
55
+
46
56
  @Composable
47
- fun getIconStyle(type: ButtonType, color: Color? = AppTheme.current.colors.primary): Modifier {
57
+ fun getIconStyle(type: ButtonType, color: Color? = null): Modifier {
48
58
  val theme = AppTheme.current
49
59
 
50
- return when (type) {
51
- ButtonType.DISABLED -> Modifier.background(
52
- theme.colors.background.disable,
53
- RoundedCornerShape(Radius.XL)
54
- ).border(0.dp, Color.Unspecified, RoundedCornerShape(Radius.XL))
60
+ return remember(type, color, theme) {
61
+ when (type) {
62
+ ButtonType.DISABLED -> Modifier
63
+ .background(theme.colors.background.disable, iconButtonShape)
64
+ .border(0.dp, Color.Unspecified, iconButtonShape)
55
65
 
56
- ButtonType.PRIMARY -> Modifier.background(
57
- theme.colors.primary,
58
- RoundedCornerShape(Radius.XL)
59
- ).border(0.dp, Color.Unspecified, RoundedCornerShape(Radius.XL))
66
+ ButtonType.PRIMARY -> Modifier
67
+ .background(theme.colors.primary, iconButtonShape)
68
+ .border(0.dp, Color.Unspecified, iconButtonShape)
60
69
 
61
- ButtonType.SECONDARY -> Modifier
62
- .background(theme.colors.background.surface, RoundedCornerShape(Radius.XL))
63
- .border(1.dp, theme.colors.border.default, RoundedCornerShape(Radius.XL))
70
+ ButtonType.SECONDARY -> Modifier
71
+ .background(theme.colors.background.surface, iconButtonShape)
72
+ .border(1.dp, theme.colors.border.default, iconButtonShape)
64
73
 
65
- ButtonType.OUTLINE -> Modifier
66
- .background(theme.colors.background.surface, RoundedCornerShape(Radius.XL))
67
- .border(1.dp, color ?: theme.colors.primary, RoundedCornerShape(Radius.XL))
74
+ ButtonType.OUTLINE -> Modifier
75
+ .background(theme.colors.background.surface, iconButtonShape)
76
+ .border(1.dp, color ?: theme.colors.primary, iconButtonShape)
68
77
 
69
- ButtonType.TONAL -> Modifier.background(
70
- theme.colors.background.tonal,
71
- RoundedCornerShape(Radius.XL)
72
- ).border(0.dp, Color.Unspecified, RoundedCornerShape(Radius.XL))
78
+ ButtonType.TONAL -> Modifier
79
+ .background(theme.colors.background.tonal, iconButtonShape)
80
+ .border(0.dp, Color.Unspecified, iconButtonShape)
73
81
 
74
- ButtonType.DANGER -> Modifier.background(
75
- theme.colors.error.primary,
76
- RoundedCornerShape(Radius.XL)
77
- ).border(0.dp, Color.Unspecified, RoundedCornerShape(Radius.XL))
82
+ ButtonType.DANGER -> Modifier
83
+ .background(theme.colors.error.primary, iconButtonShape)
84
+ .border(0.dp, Color.Unspecified, iconButtonShape)
78
85
 
79
- ButtonType.TEXT -> Modifier
86
+ ButtonType.TEXT -> Modifier
87
+ }
80
88
  }
81
89
  }
82
90
 
83
91
  @Composable
84
92
  fun getIconColor(type: ButtonType): Color {
85
- return when (type) {
86
- ButtonType.DISABLED -> AppTheme.current.colors.text.disable
87
- ButtonType.PRIMARY -> Colors.black_01
88
- ButtonType.SECONDARY -> AppTheme.current.colors.text.default
89
- ButtonType.OUTLINE -> AppTheme.current.colors.primary
90
- ButtonType.TONAL -> AppTheme.current.colors.primary
91
- ButtonType.DANGER -> Colors.black_01
92
- ButtonType.TEXT -> AppTheme.current.colors.primary
93
+ val theme = AppTheme.current
94
+ return remember(type, theme) {
95
+ when (type) {
96
+ ButtonType.DISABLED -> theme.colors.text.disable
97
+ ButtonType.PRIMARY -> Colors.black_01
98
+ ButtonType.SECONDARY -> theme.colors.text.default
99
+ ButtonType.OUTLINE -> theme.colors.primary
100
+ ButtonType.TONAL -> theme.colors.primary
101
+ ButtonType.DANGER -> Colors.black_01
102
+ ButtonType.TEXT -> theme.colors.primary
103
+ }
93
104
  }
94
105
  }
95
106
 
@@ -100,16 +111,27 @@ fun IconButton(
100
111
  size: IconSize = IconSize.SMALL,
101
112
  icon: String = "",
102
113
  ) {
103
- val iconSize = if (size.name == IconSize.SMALL.name) 16.dp else 24.dp
114
+ // Memoize icon size calculation
115
+ val iconSize = remember(size) {
116
+ iconSizeMap[size] ?: 16.dp
117
+ }
118
+
119
+ // Memoize enabled state
120
+ val isEnabled = remember(type) {
121
+ type != ButtonType.DISABLED
122
+ }
123
+
124
+ // Memoize radius for clipping
125
+ val radius = remember(size) {
126
+ size.value.radius
127
+ }
128
+
104
129
  Box(
105
130
  modifier = Modifier
106
131
  .size(size.value.size)
107
132
  .then(getIconStyle(type))
108
- .clip(RoundedCornerShape(size.value.radius))
109
- .clickable(
110
- enabled = type != ButtonType.DISABLED,
111
- onClick = onClick
112
- ),
133
+ .clip(RoundedCornerShape(radius))
134
+ .clickable(enabled = isEnabled, onClick = onClick),
113
135
  contentAlignment = Alignment.Center
114
136
  ) {
115
137
  Icon(