Skip to content

Commit 32c2a28

Browse files
refactor: Migrate from Hilt to Koin and optimize custom modifiers
- Replace Dagger Hilt with Koin for dependency injection; updated `MultiplyApp` to initialize Koin and converted `AppModule` to use Koin DSL. - Update ViewModels (`GameViewModel`, `QuestionsViewModel`) to remove Hilt annotations and switch navigation injection to `koinViewModel`. - Refactor `neumorphicShadow` custom modifier to use the `Modifier.Node` API (via `ModifierNodeElement` and `DrawModifierNode`) instead of `composed` for improved performance. - Upgrade `kotlin` to `2.3.0` and `agp` to `8.13.2`. - Bump `composeBom` to `2025.12.01`, `material3` to `1.5.0-alpha11`, and `activityCompose` to `1.12.2`. - Add `com.github.skydoves.compose.stability.analyzer` plugin to build scripts.
1 parent c870426 commit 32c2a28

14 files changed

Lines changed: 253 additions & 134 deletions

File tree

app/build.gradle.kts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ plugins {
55
alias(libs.plugins.kotlin.android)
66
alias(libs.plugins.kotlin.compose)
77
alias(libs.plugins.kotlin.serialization)
8-
alias(libs.plugins.hilt.gralde.plugin)
98
alias(libs.plugins.kotlin.ksp)
9+
alias(libs.plugins.stability.analyzer)
1010
}
1111

1212
android {
@@ -70,11 +70,11 @@ dependencies {
7070
debugImplementation(libs.androidx.ui.tooling)
7171
debugImplementation(libs.androidx.ui.test.manifest)
7272

73-
implementation(libs.bundles.hilt)
73+
implementation(platform(libs.koin.bom))
74+
implementation(libs.bundles.koin)
7475
implementation(libs.androidx.material3.adaptive.navigation.suite)
7576
implementation(libs.androidx.datastore.preferences)
7677
implementation(libs.androidx.compose.material3.adaptive.navigation)
77-
ksp(libs.hilt.compiler)
7878
implementation(libs.kotlin.serialization.kotlinx.json)
7979
implementation(libs.androidx.navigation.compose)
8080
implementation(libs.compose.material.icons.extended)

app/release/app-release.apk

-415 Bytes
Binary file not shown.
-32 Bytes
Binary file not shown.
-35 Bytes
Binary file not shown.

app/src/main/java/com/stephenwanjala/multiply/MainActivity.kt

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,15 @@ import androidx.activity.ComponentActivity
55
import androidx.activity.SystemBarStyle
66
import androidx.activity.compose.setContent
77
import androidx.activity.enableEdgeToEdge
8-
import androidx.compose.foundation.layout.consumeWindowInsets
9-
import androidx.compose.foundation.layout.fillMaxSize
10-
import androidx.compose.foundation.layout.padding
11-
import androidx.compose.foundation.layout.safeContentPadding
12-
import androidx.compose.material3.Scaffold
138
import androidx.compose.ui.Modifier
149
import androidx.compose.ui.graphics.Color
1510
import androidx.compose.ui.graphics.toArgb
1611
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
1712
import androidx.navigation.compose.rememberNavController
1813
import com.stephenwanjala.multiply.ui.navigation.MultiplyNav
1914
import com.stephenwanjala.multiply.ui.theme.MultiplyTheme
20-
import dagger.hilt.android.AndroidEntryPoint
2115

22-
@AndroidEntryPoint
16+
2317
class MainActivity : ComponentActivity() {
2418
override fun onCreate(savedInstanceState: Bundle?) {
2519
installSplashScreen()
@@ -34,10 +28,10 @@ class MainActivity : ComponentActivity() {
3428
setContent {
3529
val navHostController = rememberNavController()
3630
MultiplyTheme {
37-
MultiplyNav(
38-
navHostController = navHostController,
39-
modifier = Modifier
40-
)
31+
MultiplyNav(
32+
navHostController = navHostController,
33+
modifier = Modifier
34+
)
4135
}
4236
}
4337
}
Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
package com.stephenwanjala.multiply
22

33
import android.app.Application
4-
import dagger.hilt.android.HiltAndroidApp
4+
import com.stephenwanjala.multiply.di.appModule
5+
import org.koin.android.ext.koin.androidContext
6+
import org.koin.android.ext.koin.androidLogger
7+
import org.koin.core.context.GlobalContext.startKoin
58

6-
@HiltAndroidApp
7-
class MultiplyApp:Application()
9+
10+
class MultiplyApp : Application(){
11+
override fun onCreate() {
12+
super.onCreate()
13+
14+
startKoin {
15+
androidLogger()
16+
androidContext(this@MultiplyApp)
17+
modules(appModule)
18+
}
19+
}
20+
}

app/src/main/java/com/stephenwanjala/multiply/di/AppModule.kt

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,20 @@ import androidx.datastore.core.DataStore
55
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
66
import androidx.datastore.preferences.core.Preferences
77
import androidx.datastore.preferences.preferencesDataStoreFile
8-
import dagger.Module
9-
import dagger.Provides
10-
import dagger.hilt.InstallIn
11-
import dagger.hilt.components.SingletonComponent
12-
import javax.inject.Singleton
8+
import com.stephenwanjala.multiply.game.feat_bubblemode.GameViewModel
9+
import com.stephenwanjala.multiply.game.feat_quizmode.QuestionsViewModel
10+
import org.koin.core.module.dsl.viewModelOf
11+
import org.koin.dsl.module
1312

14-
@Module
15-
@InstallIn(SingletonComponent::class)
16-
object AppModule {
17-
private const val MULTIPLYPREFRENCES ="MULTIPLYPREFRENCES"
13+
private const val MULTIPLY_PREFERENCES = "MULTIPLYPREFRENCES"
1814

19-
@Provides
20-
@Singleton
21-
fun provideDataStorePreferences(app: Application): DataStore<Preferences> =
15+
val appModule = module {
16+
single<DataStore<Preferences>> {
17+
val app: Application = get()
2218
PreferenceDataStoreFactory.create(
23-
produceFile = {
24-
app.preferencesDataStoreFile(MULTIPLYPREFRENCES)
25-
}
19+
produceFile = { app.preferencesDataStoreFile(MULTIPLY_PREFERENCES) }
2620
)
21+
}
22+
viewModelOf(::QuestionsViewModel)
23+
viewModelOf(::GameViewModel)
2724
}

app/src/main/java/com/stephenwanjala/multiply/game/components/EffectsUtils.kt

Lines changed: 141 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,75 +3,170 @@ package com.stephenwanjala.multiply.game.components
33
import androidx.compose.foundation.shape.RoundedCornerShape
44
import androidx.compose.runtime.Composable
55
import androidx.compose.ui.Modifier
6-
import androidx.compose.ui.composed
76
import androidx.compose.ui.draw.drawBehind
8-
import androidx.compose.ui.draw.drawWithCache
97
import androidx.compose.ui.geometry.Offset
8+
import androidx.compose.ui.geometry.Size
109
import androidx.compose.ui.graphics.BlendMode
1110
import androidx.compose.ui.graphics.Brush
1211
import androidx.compose.ui.graphics.Color
12+
import androidx.compose.ui.graphics.Outline
1313
import androidx.compose.ui.graphics.Path
1414
import androidx.compose.ui.graphics.Shape
1515
import androidx.compose.ui.graphics.drawOutline
16+
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
1617
import androidx.compose.ui.graphics.drawscope.Stroke
1718
import androidx.compose.ui.graphics.drawscope.translate
19+
import androidx.compose.ui.node.DrawModifierNode
20+
import androidx.compose.ui.node.ModifierNodeElement
21+
import androidx.compose.ui.platform.InspectorInfo
1822
import androidx.compose.ui.unit.Dp
23+
import androidx.compose.ui.unit.LayoutDirection
1924
import androidx.compose.ui.unit.dp
2025

26+
/**
27+
**
28+
* Applies a neumorphic shadow effect to the component.
29+
*
30+
* @param offset The offset distance for the shadow
31+
* @param blurRadius The blur radius (note: actual blur requires API 31+, this uses alpha for effect)
32+
* @param shape The shape of the shadow
33+
* @param lightColor The color of the light shadow
34+
* @param darkColor The color of the dark shadow
35+
* @param inverted Whether to invert the shadow direction (for pressed effect)
36+
*/
2137
fun Modifier.neumorphicShadow(
2238
offset: Dp = 6.dp,
2339
blurRadius: Dp = 6.dp,
2440
shape: Shape = RoundedCornerShape(8.dp),
2541
lightColor: Color = Color.White.copy(alpha = 0.7f),
2642
darkColor: Color = Color.Black.copy(alpha = 0.2f),
2743
inverted: Boolean = false
28-
): Modifier =
29-
composed {
30-
// We use drawWithCache for better performance as it caches the outline
31-
// when the size or shape remains the same.
32-
Modifier.drawWithCache {
33-
val shadowOffsetPx = offset.toPx()
34-
blurRadius.toPx()
35-
36-
// For a "soft" shadow, we don't draw with Stroke directly on the outline.
37-
// Instead, we translate the drawing area and draw the outline itself.
38-
// The blurring effect is typically handled by RenderEffect (requires API 31+)
39-
// or by drawing multiple slightly offset, transparent layers.
40-
// For a simpler, cross-API neumorphic look, we'll draw the solid shape
41-
// with an offset and rely on the alpha for the "blur" feel without actual blur.
42-
43-
val outline = shape.createOutline(size, layoutDirection, this)
44-
45-
onDrawBehind {
46-
val lightOffset = if (inverted) Offset(
47-
shadowOffsetPx,
48-
shadowOffsetPx
49-
) else Offset(-shadowOffsetPx, -shadowOffsetPx)
50-
val darkOffset = if (inverted) Offset(-shadowOffsetPx, -shadowOffsetPx) else Offset(
51-
shadowOffsetPx,
52-
shadowOffsetPx
53-
)
54-
55-
// Draw dark shadow
56-
translate(left = darkOffset.x, top = darkOffset.y) {
57-
drawOutline(
58-
outline = outline,
59-
color = darkColor,
60-
alpha = darkColor.alpha
61-
)
62-
}
44+
): Modifier = this then NeumorphicShadowElement(
45+
offset = offset,
46+
blurRadius = blurRadius,
47+
shape = shape,
48+
lightColor = lightColor,
49+
darkColor = darkColor,
50+
inverted = inverted
51+
)
6352

64-
// Draw light shadow
65-
translate(left = lightOffset.x, top = lightOffset.y) {
66-
drawOutline(
67-
outline = outline,
68-
color = lightColor,
69-
alpha = lightColor.alpha
70-
)
71-
}
72-
}
53+
/**
54+
* ModifierNodeElement that creates and updates NeumorphicShadowNode
55+
*/
56+
private data class NeumorphicShadowElement(
57+
val offset: Dp,
58+
val blurRadius: Dp,
59+
val shape: Shape,
60+
val lightColor: Color,
61+
val darkColor: Color,
62+
val inverted: Boolean
63+
) : ModifierNodeElement<NeumorphicShadowNode>() {
64+
65+
override fun create(): NeumorphicShadowNode {
66+
return NeumorphicShadowNode(
67+
offset = offset,
68+
blurRadius = blurRadius,
69+
shape = shape,
70+
lightColor = lightColor,
71+
darkColor = darkColor,
72+
inverted = inverted
73+
)
74+
}
75+
76+
override fun update(node: NeumorphicShadowNode) {
77+
node.offset = offset
78+
node.blurRadius = blurRadius
79+
node.shape = shape
80+
node.lightColor = lightColor
81+
node.darkColor = darkColor
82+
node.inverted = inverted
83+
}
84+
85+
override fun InspectorInfo.inspectableProperties() {
86+
name = "neumorphicShadow"
87+
properties["offset"] = offset
88+
properties["blurRadius"] = blurRadius
89+
properties["shape"] = shape
90+
properties["lightColor"] = lightColor
91+
properties["darkColor"] = darkColor
92+
properties["inverted"] = inverted
93+
}
94+
}
95+
96+
/**
97+
* DrawModifierNode that performs the neumorphic shadow drawing
98+
*/
99+
private class NeumorphicShadowNode(
100+
var offset: Dp,
101+
var blurRadius: Dp,
102+
var shape: Shape,
103+
var lightColor: Color,
104+
var darkColor: Color,
105+
var inverted: Boolean
106+
) : Modifier.Node(), DrawModifierNode {
107+
108+
private var cachedOutline: Outline? = null
109+
private var cachedSize: Size? = null
110+
private var cachedLayoutDirection: LayoutDirection? = null
111+
112+
override fun ContentDrawScope.draw() {
113+
val shadowOffsetPx = offset.toPx()
114+
val currentSize = size
115+
116+
// Cache the outline for better performance
117+
if (cachedOutline == null ||
118+
cachedSize != currentSize ||
119+
cachedLayoutDirection != layoutDirection
120+
) {
121+
cachedOutline = shape.createOutline(currentSize, layoutDirection, this)
122+
cachedSize = currentSize
123+
cachedLayoutDirection = layoutDirection
73124
}
125+
126+
val outline = cachedOutline!!
127+
128+
// Calculate shadow offsets based on inverted state
129+
val lightOffset = if (inverted) {
130+
Offset(shadowOffsetPx, shadowOffsetPx)
131+
} else {
132+
Offset(-shadowOffsetPx, -shadowOffsetPx)
133+
}
134+
135+
val darkOffset = if (inverted) {
136+
Offset(-shadowOffsetPx, -shadowOffsetPx)
137+
} else {
138+
Offset(shadowOffsetPx, shadowOffsetPx)
139+
}
140+
141+
// Draw dark shadow
142+
translate(left = darkOffset.x, top = darkOffset.y) {
143+
drawOutline(
144+
outline = outline,
145+
color = darkColor,
146+
alpha = darkColor.alpha
147+
)
148+
}
149+
150+
// Draw light shadow
151+
translate(left = lightOffset.x, top = lightOffset.y) {
152+
drawOutline(
153+
outline = outline,
154+
color = lightColor,
155+
alpha = lightColor.alpha
156+
)
157+
}
158+
159+
// Draw the actual content on top of shadows
160+
drawContent()
161+
}
162+
163+
override fun onDetach() {
164+
// Clear cache when node is detached
165+
cachedOutline = null
166+
cachedSize = null
167+
cachedLayoutDirection = null
74168
}
169+
}
75170

76171

77172
// Background Effects

0 commit comments

Comments
 (0)