From 5c59f6f97c0b3b23579840daf60917f56be968bb Mon Sep 17 00:00:00 2001 From: Simonas Sankauskas Date: Sun, 10 May 2026 20:47:27 +0300 Subject: [PATCH] migrate to metro for DI --- app/build.gradle.kts | 28 ++---- .../java/dev/simonas/quies/ComponentTest.kt | 1 - .../{HiltTestRunner.kt => MetroTestRunner.kt} | 5 +- .../java/dev/simonas/quies/Screenshots.kt | 2 - .../dev/simonas/quies/TestAnalyticsModule.kt | 30 ------- .../java/dev/simonas/quies/TestApp.kt | 88 +++++++++++++++++++ .../java/dev/simonas/quies/TestAppModule.kt | 21 ----- .../java/dev/simonas/quies/UITest.kt | 24 ++--- .../dev/simonas/quies/card/CardScreenTest.kt | 2 - .../java/dev/simonas/quies/card/CardTest.kt | 2 - .../java/dev/simonas/quies/card/CardUITest.kt | 2 - .../simonas/quies/gamesets/GameSetsUITest.kt | 2 - .../quies/onboarding/OnboardingUITest.kt | 3 - .../dev/simonas/quies/router/RouterUITest.kt | 3 - app/src/main/java/dev/simonas/quies/App.kt | 24 +++-- .../main/java/dev/simonas/quies/AppGraph.kt | 88 +++++++++++++++++++ .../main/java/dev/simonas/quies/AppModule.kt | 16 ---- .../main/java/dev/simonas/quies/CoreModule.kt | 36 -------- .../java/dev/simonas/quies/CoroutineScopes.kt | 13 +++ .../main/java/dev/simonas/quies/DataModule.kt | 47 ---------- .../java/dev/simonas/quies/MainActivity.kt | 24 +++-- .../quies/analytics/AnalyticsEntryPoint.kt | 31 ++----- .../quies/analytics/AnalyticsModule.kt | 21 ----- .../quies/analytics/MixpanelEventTracker.kt | 14 +-- .../dev/simonas/quies/card/CardScreen2.kt | 4 +- .../dev/simonas/quies/card/CardViewModel.kt | 27 ++++-- .../dev/simonas/quies/card/CardViewModel2.kt | 27 ++++-- .../dev/simonas/quies/card/GetNextQuestion.kt | 10 ++- .../simonas/quies/card/ShuffleQuestionDeck.kt | 10 ++- .../simonas/quies/gamesets/GameSetsScreen.kt | 4 +- .../quies/gamesets/GameSetsViewModel.kt | 12 ++- .../quies/onboarding/OnboardingRepository.kt | 5 +- .../quies/onboarding/OnboardingScreen.kt | 4 +- .../quies/onboarding/OnboardingViewModel.kt | 16 ++-- .../dev/simonas/quies/router/RouterScreen.kt | 4 +- .../quies/router/RouterScreenViewModel.kt | 12 ++- .../dev/simonas/quies/storage/JetpackStore.kt | 10 ++- .../simonas/quies/storage/StorageModule.kt | 21 ----- .../java/dev/simonas/quies/utils/SeqRandom.kt | 3 +- build.gradle.kts | 3 +- buildSrc/build.gradle.kts | 3 - gradle/libs.versions.toml | 15 ++-- 42 files changed, 363 insertions(+), 354 deletions(-) rename app/src/androidTest/java/dev/simonas/quies/{HiltTestRunner.kt => MetroTestRunner.kt} (69%) delete mode 100644 app/src/androidTest/java/dev/simonas/quies/TestAnalyticsModule.kt create mode 100644 app/src/androidTest/java/dev/simonas/quies/TestApp.kt delete mode 100644 app/src/androidTest/java/dev/simonas/quies/TestAppModule.kt create mode 100644 app/src/main/java/dev/simonas/quies/AppGraph.kt delete mode 100644 app/src/main/java/dev/simonas/quies/AppModule.kt delete mode 100644 app/src/main/java/dev/simonas/quies/CoreModule.kt create mode 100644 app/src/main/java/dev/simonas/quies/CoroutineScopes.kt delete mode 100644 app/src/main/java/dev/simonas/quies/DataModule.kt delete mode 100644 app/src/main/java/dev/simonas/quies/analytics/AnalyticsModule.kt delete mode 100644 app/src/main/java/dev/simonas/quies/storage/StorageModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 99b870b..bb51607 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,15 +3,14 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.konan.properties.Properties plugins { + id("com.android.application") + kotlin("android") + alias(libs.plugins.compose.compiler) + alias(libs.plugins.metro) alias(libs.plugins.google.services) - alias(libs.plugins.hilt) alias(libs.plugins.play.publisher) alias(libs.plugins.simple.flank) - id("com.android.application") - kotlin("android") id("com.google.firebase.crashlytics") - alias(libs.plugins.compose.compiler) - id("com.google.devtools.ksp") } android { @@ -25,7 +24,7 @@ android { applicationId = AppCoordinates.APP_ID versionCode = AppCoordinates.APP_VERSION_CODE versionName = AppCoordinates.APP_VERSION_NAME - testInstrumentationRunner = "dev.simonas.quies.HiltTestRunner" + testInstrumentationRunner = "dev.simonas.quies.MetroTestRunner" vectorDrawables { useSupportLibrary = true } @@ -94,9 +93,6 @@ android { packaging { resources.excludes += "DebugProbesKt.bin" } - hilt { - enableAggregatingTask = true - } } kotlin { @@ -124,9 +120,10 @@ dependencies { implementation(libs.firebase.analytics) implementation(libs.firebase.analytics.ktx) implementation(libs.firebase.crashlytics) - implementation(libs.hilt.android) - implementation(libs.hilt.navigation.compose) - implementation(libs.javapoet) + implementation(libs.metro.runtime) + implementation(libs.metrox.android) + implementation(libs.metrox.viewmodel) + implementation(libs.metrox.viewmodel.compose) implementation(libs.kotlin.math) implementation(platform(libs.compose.bom)) implementation(platform(libs.firebase.bom)) @@ -135,14 +132,10 @@ dependencies { implementation(libs.datastore.preferences) implementation(libs.mixpanel) - ksp(libs.hilt.android.compiler) - debugImplementation(libs.compose.ui.test.manifest) - kspTest(libs.hilt.android.compiler) testImplementation(libs.coroutines.android) testImplementation(libs.coroutines.test) - testImplementation(libs.hilt.android) testImplementation(libs.junit) testImplementation(libs.junit.jupiter) testImplementation(libs.mockito.kotlin) @@ -150,16 +143,13 @@ dependencies { testImplementation(libs.truth) androidTestImplementation(platform(libs.compose.bom)) - androidTestImplementation(libs.hilt.android) androidTestImplementation(libs.androidx.test.ext.junit) androidTestImplementation(libs.androidx.test.ext.junit.ktx) androidTestImplementation(libs.androidx.test.rules) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.compose.ui.test.junit4) androidTestImplementation(libs.espresso.core) - androidTestImplementation(libs.hilt.testing) androidTestImplementation(libs.mockito.android) - kspAndroidTest(libs.hilt.android.compiler) } simpleFlank { diff --git a/app/src/androidTest/java/dev/simonas/quies/ComponentTest.kt b/app/src/androidTest/java/dev/simonas/quies/ComponentTest.kt index 54a1ac7..b09a181 100644 --- a/app/src/androidTest/java/dev/simonas/quies/ComponentTest.kt +++ b/app/src/androidTest/java/dev/simonas/quies/ComponentTest.kt @@ -13,7 +13,6 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.device.DeviceController import androidx.test.rule.GrantPermissionRule -import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.BeforeClass import org.junit.Rule diff --git a/app/src/androidTest/java/dev/simonas/quies/HiltTestRunner.kt b/app/src/androidTest/java/dev/simonas/quies/MetroTestRunner.kt similarity index 69% rename from app/src/androidTest/java/dev/simonas/quies/HiltTestRunner.kt rename to app/src/androidTest/java/dev/simonas/quies/MetroTestRunner.kt index e631441..fa15fa1 100644 --- a/app/src/androidTest/java/dev/simonas/quies/HiltTestRunner.kt +++ b/app/src/androidTest/java/dev/simonas/quies/MetroTestRunner.kt @@ -3,9 +3,8 @@ package dev.simonas.quies import android.app.Application import android.content.Context import androidx.test.runner.AndroidJUnitRunner -import dagger.hilt.android.testing.HiltTestApplication -class HiltTestRunner : AndroidJUnitRunner() { +class MetroTestRunner : AndroidJUnitRunner() { override fun newApplication( cl: ClassLoader?, @@ -14,7 +13,7 @@ class HiltTestRunner : AndroidJUnitRunner() { ): Application = super.newApplication( cl, - HiltTestApplication::class.java.name, + TestApp::class.java.name, context, ) } diff --git a/app/src/androidTest/java/dev/simonas/quies/Screenshots.kt b/app/src/androidTest/java/dev/simonas/quies/Screenshots.kt index 1264b0c..c0d21ef 100644 --- a/app/src/androidTest/java/dev/simonas/quies/Screenshots.kt +++ b/app/src/androidTest/java/dev/simonas/quies/Screenshots.kt @@ -2,11 +2,9 @@ package dev.simonas.quies import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Ignore import org.junit.Test -@HiltAndroidTest internal class Screenshots : UITest() { @Ignore("Needs Impl") diff --git a/app/src/androidTest/java/dev/simonas/quies/TestAnalyticsModule.kt b/app/src/androidTest/java/dev/simonas/quies/TestAnalyticsModule.kt deleted file mode 100644 index 3cf3630..0000000 --- a/app/src/androidTest/java/dev/simonas/quies/TestAnalyticsModule.kt +++ /dev/null @@ -1,30 +0,0 @@ -package dev.simonas.quies - -import dagger.Module -import dagger.Provides -import dagger.hilt.components.SingletonComponent -import dagger.hilt.testing.TestInstallIn -import dev.simonas.quies.analytics.AnalyticsModule -import dev.simonas.quies.analytics.EventTracker -import dev.simonas.quies.utils.SeqRandom -import dev.simonas.quies.utils.logd -import dev.simonas.quies.utils.loge -import javax.inject.Singleton -import kotlin.random.Random - -@Module -@TestInstallIn( - components = [SingletonComponent::class], - replaces = [AnalyticsModule::class], -) -class TestAnalyticsModule { - - @Provides - @Singleton - fun eventTracker(): EventTracker = - object : EventTracker { - override fun send(key: String, meta: Map) { - logd("EventTracker: $key and $meta") - } - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/dev/simonas/quies/TestApp.kt b/app/src/androidTest/java/dev/simonas/quies/TestApp.kt new file mode 100644 index 0000000..f666932 --- /dev/null +++ b/app/src/androidTest/java/dev/simonas/quies/TestApp.kt @@ -0,0 +1,88 @@ +package dev.simonas.quies + +import android.app.Application +import android.content.Context +import dev.simonas.quies.analytics.EventTracker +import dev.simonas.quies.data.DataSource +import dev.simonas.quies.data.EncryptedDataSource +import dev.simonas.quies.data.GameSetRepository +import dev.simonas.quies.data.QuestionRepository +import dev.simonas.quies.data.SourceGameSetRepository +import dev.simonas.quies.data.SourceQuestionsRepository +import dev.simonas.quies.storage.JetpackStore +import dev.simonas.quies.storage.Store +import dev.simonas.quies.utils.SeqRandom +import dev.simonas.quies.utils.logd +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.DependencyGraph +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn +import dev.zacsweers.metro.createGraphFactory +import dev.zacsweers.metrox.android.MetroAppComponentProviders +import dev.zacsweers.metrox.android.MetroApplication +import dev.zacsweers.metrox.viewmodel.ViewModelGraph +import kotlin.random.Random + +@DependencyGraph(AppScope::class) +internal interface TestAppGraph : MetroAppComponentProviders, ViewModelGraph { + + val store: Store + val eventTracker: EventTracker + + @Provides + @SingleIn(AppScope::class) + fun random(): Random = SeqRandom() + + @Provides + @SingleIn(AppScope::class) + fun appCoroutineScope(): AppCoroutineScope = GlobalAppScope() + + @Provides + fun context(application: Application): Context = application + + @Provides + @SingleIn(AppScope::class) + fun dataSource(context: Context): DataSource = + EncryptedDataSource(context = context) + + @Provides + @SingleIn(AppScope::class) + fun questionRepository(source: DataSource): QuestionRepository = + SourceQuestionsRepository(dataSource = source) + + @Provides + @SingleIn(AppScope::class) + fun gameSetRepository(source: DataSource): GameSetRepository = + SourceGameSetRepository(dataSource = source) + + @Provides + @SingleIn(AppScope::class) + fun eventTracker(): EventTracker = + object : EventTracker { + override fun send(key: String, meta: Map) { + logd("EventTracker: $key and $meta") + } + } + + @Binds + val JetpackStore.bindStore: Store + + @DependencyGraph.Factory + fun interface Factory { + fun create(@Provides application: Application): TestAppGraph + } +} + +internal class TestApp : Application(), MetroApplication, AppGraphHolder { + + private val testGraph: TestAppGraph by lazy { + createGraphFactory().create(this) + } + + override val appComponentProviders: MetroAppComponentProviders + get() = testGraph + + override val store: Store get() = testGraph.store + override val eventTracker get() = testGraph.eventTracker +} diff --git a/app/src/androidTest/java/dev/simonas/quies/TestAppModule.kt b/app/src/androidTest/java/dev/simonas/quies/TestAppModule.kt deleted file mode 100644 index 6a68f6f..0000000 --- a/app/src/androidTest/java/dev/simonas/quies/TestAppModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package dev.simonas.quies - -import dagger.Module -import dagger.Provides -import dagger.hilt.components.SingletonComponent -import dagger.hilt.testing.TestInstallIn -import dev.simonas.quies.utils.SeqRandom -import javax.inject.Singleton -import kotlin.random.Random - -@Module -@TestInstallIn( - components = [SingletonComponent::class], - replaces = [AppModule::class], -) -class TestAppModule { - - @Provides - @Singleton - fun random(): Random = SeqRandom() -} diff --git a/app/src/androidTest/java/dev/simonas/quies/UITest.kt b/app/src/androidTest/java/dev/simonas/quies/UITest.kt index c4bfa6f..6d3bf0a 100644 --- a/app/src/androidTest/java/dev/simonas/quies/UITest.kt +++ b/app/src/androidTest/java/dev/simonas/quies/UITest.kt @@ -3,42 +3,30 @@ package dev.simonas.quies import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.rules.ActivityScenarioRule -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest import dev.simonas.quies.onboarding.OnboardingRepository import dev.simonas.quies.storage.Store -import dev.simonas.quies.utils.instrumentationExec -import javax.inject.Inject import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Rule -import org.junit.rules.RuleChain internal typealias ActivityRule = AndroidComposeTestRule, MainActivity> -/** - * Children must be annotated with [HiltAndroidTest] - */ internal abstract class UITest( val composeRule: ActivityRule = createAndroidComposeRule() -): ComposeContentTestRule by composeRule { +) : ComposeContentTestRule by composeRule { open val skipsOnboarding: Boolean = true - var hiltRule = HiltAndroidRule(this) - @get:Rule - val rule = RuleChain - .outerRule(hiltRule) - .around(composeRule) + val rule = composeRule - @Inject - lateinit var store: Store + val store: Store + get() = (ApplicationProvider.getApplicationContext()).store @Before - fun hiltInit() { - hiltRule.inject() + fun setUpTestApp() { runBlocking { if (skipsOnboarding) { store.set(OnboardingRepository.KEY_ONBOARDING_COMPLETED, "true") diff --git a/app/src/androidTest/java/dev/simonas/quies/card/CardScreenTest.kt b/app/src/androidTest/java/dev/simonas/quies/card/CardScreenTest.kt index a766fdd..46b352d 100644 --- a/app/src/androidTest/java/dev/simonas/quies/card/CardScreenTest.kt +++ b/app/src/androidTest/java/dev/simonas/quies/card/CardScreenTest.kt @@ -6,7 +6,6 @@ import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import dagger.hilt.android.testing.HiltAndroidTest import dev.simonas.quies.ComponentTest import dev.simonas.quies.data.Question import org.junit.Ignore @@ -15,7 +14,6 @@ import org.mockito.Mockito.mock import org.mockito.Mockito.verify @Ignore("broken due to LocalUiGuides") -@HiltAndroidTest internal class CardScreenTest : ComponentTest() { val questionClosed: (Question) -> Unit = mock() diff --git a/app/src/androidTest/java/dev/simonas/quies/card/CardTest.kt b/app/src/androidTest/java/dev/simonas/quies/card/CardTest.kt index 63eca14..44e3f2e 100644 --- a/app/src/androidTest/java/dev/simonas/quies/card/CardTest.kt +++ b/app/src/androidTest/java/dev/simonas/quies/card/CardTest.kt @@ -2,13 +2,11 @@ package dev.simonas.quies.card import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.onNodeWithText -import dagger.hilt.android.testing.HiltAndroidTest import dev.simonas.quies.ComponentTest import org.junit.Ignore import org.junit.Test @Ignore("broken due to LocalUiGuides") -@HiltAndroidTest class CardTest : ComponentTest() { @Test diff --git a/app/src/androidTest/java/dev/simonas/quies/card/CardUITest.kt b/app/src/androidTest/java/dev/simonas/quies/card/CardUITest.kt index 4185f82..164f91b 100644 --- a/app/src/androidTest/java/dev/simonas/quies/card/CardUITest.kt +++ b/app/src/androidTest/java/dev/simonas/quies/card/CardUITest.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput -import dagger.hilt.android.testing.HiltAndroidTest import dev.simonas.quies.UITest import dev.simonas.quies.card.CardScreen2.questionState import dev.simonas.quies.gamesets.GameSetsScreen @@ -22,7 +21,6 @@ import org.junit.Test import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds -@HiltAndroidTest internal class CardUITest : UITest() { private fun onNodeWithState(state: QuestionComponent.State) = diff --git a/app/src/androidTest/java/dev/simonas/quies/gamesets/GameSetsUITest.kt b/app/src/androidTest/java/dev/simonas/quies/gamesets/GameSetsUITest.kt index cc77993..5b26a7c 100644 --- a/app/src/androidTest/java/dev/simonas/quies/gamesets/GameSetsUITest.kt +++ b/app/src/androidTest/java/dev/simonas/quies/gamesets/GameSetsUITest.kt @@ -2,12 +2,10 @@ package dev.simonas.quies.gamesets import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import dagger.hilt.android.testing.HiltAndroidTest import dev.simonas.quies.UITest import dev.simonas.quies.card.showsCardScreen import org.junit.Test -@HiltAndroidTest internal class GameSetsUITest : UITest() { @Test diff --git a/app/src/androidTest/java/dev/simonas/quies/onboarding/OnboardingUITest.kt b/app/src/androidTest/java/dev/simonas/quies/onboarding/OnboardingUITest.kt index 73bf929..c5d898c 100644 --- a/app/src/androidTest/java/dev/simonas/quies/onboarding/OnboardingUITest.kt +++ b/app/src/androidTest/java/dev/simonas/quies/onboarding/OnboardingUITest.kt @@ -5,19 +5,16 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.onNodeWithTag import androidx.test.espresso.IdlingPolicies import androidx.test.espresso.IdlingRegistry -import dagger.hilt.android.testing.HiltAndroidTest import dev.simonas.quies.UITest import dev.simonas.quies.gamesets.GameSetsScreen import dev.simonas.quies.gamesets.showsGameSetsScreen import dev.simonas.quies.onboarding.OnboardingRepository.Companion.KEY_ONBOARDING_COMPLETED import dev.simonas.quies.storage.Store -import javax.inject.Inject import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.junit.Ignore import org.junit.Test -@HiltAndroidTest internal class OnboardingUITest : UITest() { override val skipsOnboarding: Boolean = false diff --git a/app/src/androidTest/java/dev/simonas/quies/router/RouterUITest.kt b/app/src/androidTest/java/dev/simonas/quies/router/RouterUITest.kt index bb230a1..ef28627 100644 --- a/app/src/androidTest/java/dev/simonas/quies/router/RouterUITest.kt +++ b/app/src/androidTest/java/dev/simonas/quies/router/RouterUITest.kt @@ -2,17 +2,14 @@ package dev.simonas.quies.router import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.onNodeWithTag -import dagger.hilt.android.testing.HiltAndroidTest import dev.simonas.quies.UITest import dev.simonas.quies.gamesets.GameSetsScreen import dev.simonas.quies.onboarding.OnboardingRepository.Companion.KEY_ONBOARDING_COMPLETED import dev.simonas.quies.onboarding.OnboardingScreen import dev.simonas.quies.storage.Store -import javax.inject.Inject import kotlinx.coroutines.runBlocking import org.junit.Test -@HiltAndroidTest internal class RouterUITest : UITest() { @Test diff --git a/app/src/main/java/dev/simonas/quies/App.kt b/app/src/main/java/dev/simonas/quies/App.kt index 9d49acd..421bba4 100644 --- a/app/src/main/java/dev/simonas/quies/App.kt +++ b/app/src/main/java/dev/simonas/quies/App.kt @@ -2,15 +2,27 @@ package dev.simonas.quies import android.app.Application import android.content.Context -import dagger.hilt.android.HiltAndroidApp import dev.simonas.quies.storage.Store -import javax.inject.Inject +import dev.zacsweers.metro.createGraphFactory +import dev.zacsweers.metrox.android.MetroAppComponentProviders +import dev.zacsweers.metrox.android.MetroApplication -@HiltAndroidApp -internal class App : Application() { +internal interface AppGraphHolder { + val eventTracker: dev.simonas.quies.analytics.EventTracker + val store: Store +} + +internal class App : Application(), MetroApplication, AppGraphHolder { + + private val appGraph: AppGraph by lazy { + createGraphFactory().create(this) + } + + override val appComponentProviders: MetroAppComponentProviders + get() = appGraph - @Inject - lateinit var store: Store + override val store: Store get() = appGraph.store + override val eventTracker get() = appGraph.eventTracker } fun isRunningTests(context: Context): Boolean { diff --git a/app/src/main/java/dev/simonas/quies/AppGraph.kt b/app/src/main/java/dev/simonas/quies/AppGraph.kt new file mode 100644 index 0000000..b84c553 --- /dev/null +++ b/app/src/main/java/dev/simonas/quies/AppGraph.kt @@ -0,0 +1,88 @@ +package dev.simonas.quies + +import android.app.Application +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dev.simonas.quies.analytics.EventTracker +import dev.simonas.quies.analytics.MixpanelEventTracker +import dev.simonas.quies.analytics.aggregateEventTracker +import dev.simonas.quies.data.DataSource +import dev.simonas.quies.data.EncryptedDataSource +import dev.simonas.quies.data.GameSetRepository +import dev.simonas.quies.data.QuestionRepository +import dev.simonas.quies.data.SourceGameSetRepository +import dev.simonas.quies.data.SourceQuestionsRepository +import dev.simonas.quies.storage.JetpackStore +import dev.simonas.quies.storage.Store +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.DependencyGraph +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn +import dev.zacsweers.metro.binding +import dev.zacsweers.metrox.android.MetroAppComponentProviders +import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory +import dev.zacsweers.metrox.viewmodel.MetroViewModelFactory +import dev.zacsweers.metrox.viewmodel.ViewModelAssistedFactory +import dev.zacsweers.metrox.viewmodel.ViewModelGraph +import kotlin.random.Random +import kotlin.reflect.KClass + +@DependencyGraph(AppScope::class) +internal interface AppGraph : MetroAppComponentProviders, ViewModelGraph { + + val store: Store + val eventTracker: EventTracker + + @Provides + fun random(): Random = Random.Default + + @Provides + @SingleIn(AppScope::class) + fun appCoroutineScope(): AppCoroutineScope = GlobalAppScope() + + @Provides + fun context(application: Application): Context = application + + @Provides + @SingleIn(AppScope::class) + fun dataSource(context: Context): DataSource = + EncryptedDataSource(context = context) + + @Provides + @SingleIn(AppScope::class) + fun questionRepository(source: DataSource): QuestionRepository = + SourceQuestionsRepository(dataSource = source) + + @Provides + @SingleIn(AppScope::class) + fun gameSetRepository(source: DataSource): GameSetRepository = + SourceGameSetRepository(dataSource = source) + + @Provides + @SingleIn(AppScope::class) + fun eventTracker(mixpanel: MixpanelEventTracker): EventTracker = + aggregateEventTracker(mixpanel) + + @Binds + val JetpackStore.bindStore: Store + + @DependencyGraph.Factory + fun interface Factory { + fun create(@Provides application: Application): AppGraph + } +} + +@Inject +@ContributesBinding(AppScope::class) +@ContributesBinding(AppScope::class, binding()) +@SingleIn(AppScope::class) +internal class InjectedViewModelFactory( + override val viewModelProviders: Map, () -> ViewModel>, + override val assistedFactoryProviders: Map, () -> ViewModelAssistedFactory>, + override val manualAssistedFactoryProviders: + Map, () -> ManualViewModelAssistedFactory>, +) : MetroViewModelFactory() diff --git a/app/src/main/java/dev/simonas/quies/AppModule.kt b/app/src/main/java/dev/simonas/quies/AppModule.kt deleted file mode 100644 index 1d82724..0000000 --- a/app/src/main/java/dev/simonas/quies/AppModule.kt +++ /dev/null @@ -1,16 +0,0 @@ -package dev.simonas.quies - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import kotlin.random.Random - -@Module -@InstallIn(SingletonComponent::class) -internal class AppModule { - - @Provides - fun random(): Random = - Random.Default -} diff --git a/app/src/main/java/dev/simonas/quies/CoreModule.kt b/app/src/main/java/dev/simonas/quies/CoreModule.kt deleted file mode 100644 index 55da972..0000000 --- a/app/src/main/java/dev/simonas/quies/CoreModule.kt +++ /dev/null @@ -1,36 +0,0 @@ -package dev.simonas.quies - -import android.content.Context -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlin.coroutines.CoroutineContext - -interface AppScope : CoroutineScope - -private class GlobalAppScope : AppScope { - @OptIn(DelicateCoroutinesApi::class) - override val coroutineContext: CoroutineContext = GlobalScope.coroutineContext -} - -@Module -@InstallIn(SingletonComponent::class) -internal class CoreModule { - - @Provides - fun appScope(): AppScope = GlobalAppScope() - - @Module - @InstallIn(SingletonComponent::class) - interface Binders { - - @Binds - fun context(@ApplicationContext context: Context): Context - } -} diff --git a/app/src/main/java/dev/simonas/quies/CoroutineScopes.kt b/app/src/main/java/dev/simonas/quies/CoroutineScopes.kt new file mode 100644 index 0000000..8caaa91 --- /dev/null +++ b/app/src/main/java/dev/simonas/quies/CoroutineScopes.kt @@ -0,0 +1,13 @@ +package dev.simonas.quies + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlin.coroutines.CoroutineContext + +interface AppCoroutineScope : CoroutineScope + +internal class GlobalAppScope : AppCoroutineScope { + @OptIn(DelicateCoroutinesApi::class) + override val coroutineContext: CoroutineContext = GlobalScope.coroutineContext +} diff --git a/app/src/main/java/dev/simonas/quies/DataModule.kt b/app/src/main/java/dev/simonas/quies/DataModule.kt deleted file mode 100644 index 6b17fee..0000000 --- a/app/src/main/java/dev/simonas/quies/DataModule.kt +++ /dev/null @@ -1,47 +0,0 @@ -package dev.simonas.quies - -import android.content.Context -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import dev.simonas.quies.data.DataSource -import dev.simonas.quies.data.EncryptedDataSource -import dev.simonas.quies.data.GameSetRepository -import dev.simonas.quies.data.QuestionRepository -import dev.simonas.quies.data.SourceGameSetRepository -import dev.simonas.quies.data.SourceQuestionsRepository -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal class DataModule { - - @Singleton - @Provides - fun dataSource( - @ApplicationContext context: Context, - ): DataSource = - EncryptedDataSource( - context = context, - ) - - @Singleton - @Provides - fun questionRepository( - source: DataSource, - ): QuestionRepository = - SourceQuestionsRepository( - dataSource = source, - ) - - @Singleton - @Provides - fun gameSetRepository( - source: DataSource, - ): GameSetRepository = - SourceGameSetRepository( - dataSource = source, - ) -} diff --git a/app/src/main/java/dev/simonas/quies/MainActivity.kt b/app/src/main/java/dev/simonas/quies/MainActivity.kt index 70a2f10..e7dd596 100644 --- a/app/src/main/java/dev/simonas/quies/MainActivity.kt +++ b/app/src/main/java/dev/simonas/quies/MainActivity.kt @@ -1,5 +1,6 @@ package dev.simonas.quies +import android.app.Activity import android.os.Build import android.os.Bundle import android.view.View @@ -8,16 +9,27 @@ import android.view.WindowInsetsController.OnControllableInsetsChangedListener import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.annotation.RequiresApi +import androidx.compose.runtime.CompositionLocalProvider import androidx.lifecycle.lifecycleScope -import dagger.hilt.android.AndroidEntryPoint import dev.simonas.quies.router.RouterScreen import dev.simonas.quies.utils.logd +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoMap +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.binding +import dev.zacsweers.metrox.android.ActivityKey +import dev.zacsweers.metrox.viewmodel.LocalMetroViewModelFactory +import dev.zacsweers.metrox.viewmodel.MetroViewModelFactory import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch -@AndroidEntryPoint -internal class MainActivity : ComponentActivity() { +@ContributesIntoMap(AppScope::class, binding()) +@ActivityKey +@Inject +internal class MainActivity( + private val viewModelFactory: MetroViewModelFactory, +) : ComponentActivity() { @RequiresApi(Build.VERSION_CODES.R) private var insetsUpdateListener: Any? = null @@ -27,8 +39,10 @@ internal class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - AppTheme { - RouterScreen() + CompositionLocalProvider(LocalMetroViewModelFactory provides viewModelFactory) { + AppTheme { + RouterScreen() + } } } } diff --git a/app/src/main/java/dev/simonas/quies/analytics/AnalyticsEntryPoint.kt b/app/src/main/java/dev/simonas/quies/analytics/AnalyticsEntryPoint.kt index 565f70c..3cc2a5d 100644 --- a/app/src/main/java/dev/simonas/quies/analytics/AnalyticsEntryPoint.kt +++ b/app/src/main/java/dev/simonas/quies/analytics/AnalyticsEntryPoint.kt @@ -1,33 +1,14 @@ package dev.simonas.quies.analytics import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext -import dagger.hilt.EntryPoint -import dagger.hilt.EntryPoints -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@EntryPoint -@InstallIn(SingletonComponent::class) -interface AnalyticsEntryPoint { - val eventTracker: EventTracker -} - -private lateinit var analyticsEntryPoint: AnalyticsEntryPoint - -@Composable -private fun requireTrackerEntryPoint(): AnalyticsEntryPoint { - if (!::analyticsEntryPoint.isInitialized) { - analyticsEntryPoint = - EntryPoints.get( - LocalContext.current.applicationContext, - AnalyticsEntryPoint::class.java, - ) - } - return analyticsEntryPoint -} +import dev.simonas.quies.AppGraphHolder @Composable fun eventTracker(): EventTracker { - return requireTrackerEntryPoint().eventTracker + val context = LocalContext.current + return remember(context) { + (context.applicationContext as AppGraphHolder).eventTracker + } } diff --git a/app/src/main/java/dev/simonas/quies/analytics/AnalyticsModule.kt b/app/src/main/java/dev/simonas/quies/analytics/AnalyticsModule.kt deleted file mode 100644 index 2888303..0000000 --- a/app/src/main/java/dev/simonas/quies/analytics/AnalyticsModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package dev.simonas.quies.analytics - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal class AnalyticsModule { - - @Singleton - @Provides - fun eventTracker( - mixpanel: MixpanelEventTracker, - ): EventTracker = - aggregateEventTracker( - mixpanel, - ) -} diff --git a/app/src/main/java/dev/simonas/quies/analytics/MixpanelEventTracker.kt b/app/src/main/java/dev/simonas/quies/analytics/MixpanelEventTracker.kt index 075a8ae..75c0f85 100644 --- a/app/src/main/java/dev/simonas/quies/analytics/MixpanelEventTracker.kt +++ b/app/src/main/java/dev/simonas/quies/analytics/MixpanelEventTracker.kt @@ -2,19 +2,19 @@ package dev.simonas.quies.analytics import android.content.Context import com.mixpanel.android.mpmetrics.MixpanelAPI -import dev.simonas.quies.AppScope +import dev.simonas.quies.AppCoroutineScope import dev.simonas.quies.storage.Store +import dev.zacsweers.metro.Inject import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.json.JSONObject import java.util.UUID -import javax.inject.Inject -import javax.inject.Provider -internal class MixpanelEventTracker @Inject constructor( - private val context: Provider, +@Inject +internal class MixpanelEventTracker( + private val context: () -> Context, private val store: Store, - scope: AppScope, + scope: AppCoroutineScope, ) : EventTracker { init { @@ -34,7 +34,7 @@ internal class MixpanelEventTracker @Inject constructor( } private fun get(): MixpanelAPI { - return MixpanelAPI.getInstance(context.get(), MIXPANEL_TOKEN, true) + return MixpanelAPI.getInstance(context(), MIXPANEL_TOKEN, true) } companion object { diff --git a/app/src/main/java/dev/simonas/quies/card/CardScreen2.kt b/app/src/main/java/dev/simonas/quies/card/CardScreen2.kt index 372b55f..846fb1e 100644 --- a/app/src/main/java/dev/simonas/quies/card/CardScreen2.kt +++ b/app/src/main/java/dev/simonas/quies/card/CardScreen2.kt @@ -49,7 +49,7 @@ import androidx.compose.ui.semantics.SemanticsPropertyReceiver import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import dev.simonas.quies.AppTheme import dev.simonas.quies.AppTheme.SCREEN_SAVER_FADE_FRAC @@ -92,7 +92,7 @@ internal object CardScreen2 { @Composable internal fun CardScreen2( onBack: () -> Unit, - cardViewModel: CardViewModel2 = hiltViewModel(), + cardViewModel: CardViewModel2 = assistedMetroViewModel(), tracker: EventTracker = eventTracker(), ) { val questions = cardViewModel.questions.collectAsStateWithLifecycle() diff --git a/app/src/main/java/dev/simonas/quies/card/CardViewModel.kt b/app/src/main/java/dev/simonas/quies/card/CardViewModel.kt index 357ac44..24307f1 100644 --- a/app/src/main/java/dev/simonas/quies/card/CardViewModel.kt +++ b/app/src/main/java/dev/simonas/quies/card/CardViewModel.kt @@ -2,17 +2,24 @@ package dev.simonas.quies.card import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel +import androidx.lifecycle.viewmodel.CreationExtras +import androidx.lifecycle.createSavedStateHandle import dev.simonas.quies.data.Question import dev.simonas.quies.router.NavRoutes +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.ContributesIntoMap +import dev.zacsweers.metrox.viewmodel.ViewModelAssistedFactory +import dev.zacsweers.metrox.viewmodel.ViewModelAssistedFactoryKey import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import javax.inject.Inject -@HiltViewModel -internal class CardViewModel @Inject constructor( - stateHandle: SavedStateHandle, +@AssistedInject +internal class CardViewModel( + @Assisted stateHandle: SavedStateHandle, private val getNextQuestion: GetNextQuestion, ) : ViewModel() { @@ -71,4 +78,14 @@ internal class CardViewModel @Inject constructor( val nextLevel: Question.Level, ) : State() } + + @AssistedFactory + @ViewModelAssistedFactoryKey(CardViewModel::class) + @ContributesIntoMap(AppScope::class) + fun interface Factory : ViewModelAssistedFactory { + override fun create(extras: CreationExtras): CardViewModel = + create(extras.createSavedStateHandle()) + + fun create(stateHandle: SavedStateHandle): CardViewModel + } } diff --git a/app/src/main/java/dev/simonas/quies/card/CardViewModel2.kt b/app/src/main/java/dev/simonas/quies/card/CardViewModel2.kt index f35699e..775abc6 100644 --- a/app/src/main/java/dev/simonas/quies/card/CardViewModel2.kt +++ b/app/src/main/java/dev/simonas/quies/card/CardViewModel2.kt @@ -4,7 +4,8 @@ import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel +import androidx.lifecycle.viewmodel.CreationExtras +import androidx.lifecycle.createSavedStateHandle import dev.simonas.quies.data.Question import dev.simonas.quies.millisSinceLaunch import dev.simonas.quies.router.NavRoutes @@ -28,12 +29,18 @@ import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.update -import javax.inject.Inject +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.ContributesIntoMap +import dev.zacsweers.metrox.viewmodel.ViewModelAssistedFactory +import dev.zacsweers.metrox.viewmodel.ViewModelAssistedFactoryKey import kotlin.time.Duration.Companion.minutes -@HiltViewModel -internal class CardViewModel2 @Inject constructor( - stateHandle: SavedStateHandle, +@AssistedInject +internal class CardViewModel2( + @Assisted stateHandle: SavedStateHandle, private val shuffleQuestionDeck: ShuffleQuestionDeck, ) : ViewModel() { @@ -358,6 +365,16 @@ internal class CardViewModel2 @Inject constructor( } } } + + @AssistedFactory + @ViewModelAssistedFactoryKey(CardViewModel2::class) + @ContributesIntoMap(AppScope::class) + fun interface Factory : ViewModelAssistedFactory { + override fun create(extras: CreationExtras): CardViewModel2 = + create(extras.createSavedStateHandle()) + + fun create(stateHandle: SavedStateHandle): CardViewModel2 + } } private inline fun MutableStateFlow.updateComponents( diff --git a/app/src/main/java/dev/simonas/quies/card/GetNextQuestion.kt b/app/src/main/java/dev/simonas/quies/card/GetNextQuestion.kt index f37718e..ad933d0 100644 --- a/app/src/main/java/dev/simonas/quies/card/GetNextQuestion.kt +++ b/app/src/main/java/dev/simonas/quies/card/GetNextQuestion.kt @@ -2,12 +2,14 @@ package dev.simonas.quies.card import dev.simonas.quies.data.Question import dev.simonas.quies.data.QuestionRepository -import javax.inject.Inject -import javax.inject.Singleton +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import kotlin.random.Random -@Singleton -internal class GetNextQuestion @Inject constructor( +@SingleIn(AppScope::class) +@Inject +internal class GetNextQuestion( private val random: Random, private val questionRepository: QuestionRepository, ) { diff --git a/app/src/main/java/dev/simonas/quies/card/ShuffleQuestionDeck.kt b/app/src/main/java/dev/simonas/quies/card/ShuffleQuestionDeck.kt index 9a46ed8..7614342 100644 --- a/app/src/main/java/dev/simonas/quies/card/ShuffleQuestionDeck.kt +++ b/app/src/main/java/dev/simonas/quies/card/ShuffleQuestionDeck.kt @@ -2,12 +2,14 @@ package dev.simonas.quies.card import dev.simonas.quies.data.Question import dev.simonas.quies.data.QuestionRepository -import javax.inject.Inject -import javax.inject.Singleton +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import kotlin.random.Random -@Singleton -internal class ShuffleQuestionDeck @Inject constructor( +@SingleIn(AppScope::class) +@Inject +internal class ShuffleQuestionDeck( private val random: Random, private val questionRepository: QuestionRepository, ) { diff --git a/app/src/main/java/dev/simonas/quies/gamesets/GameSetsScreen.kt b/app/src/main/java/dev/simonas/quies/gamesets/GameSetsScreen.kt index 0dfa0ce..d5a1bd2 100644 --- a/app/src/main/java/dev/simonas/quies/gamesets/GameSetsScreen.kt +++ b/app/src/main/java/dev/simonas/quies/gamesets/GameSetsScreen.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex -import androidx.hilt.navigation.compose.hiltViewModel +import dev.zacsweers.metrox.viewmodel.metroViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import dev.simonas.quies.AppTheme import dev.simonas.quies.LocalUiGuide @@ -44,7 +44,7 @@ internal object GameSetsScreen { @Composable internal fun GameSetsScreen( - viewModel: GameSetsViewModel = hiltViewModel(), + viewModel: GameSetsViewModel = metroViewModel(), onGameSetSelected: (id: String) -> Unit, tracker: EventTracker = eventTracker(), ) { diff --git a/app/src/main/java/dev/simonas/quies/gamesets/GameSetsViewModel.kt b/app/src/main/java/dev/simonas/quies/gamesets/GameSetsViewModel.kt index b3ba72b..a763434 100644 --- a/app/src/main/java/dev/simonas/quies/gamesets/GameSetsViewModel.kt +++ b/app/src/main/java/dev/simonas/quies/gamesets/GameSetsViewModel.kt @@ -1,15 +1,19 @@ package dev.simonas.quies.gamesets import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel import dev.simonas.quies.data.GameSet import dev.simonas.quies.data.GameSetRepository +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoMap +import dev.zacsweers.metro.Inject +import dev.zacsweers.metrox.viewmodel.ViewModelKey import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject -@HiltViewModel -internal class GameSetsViewModel @Inject constructor( +@ContributesIntoMap(AppScope::class) +@ViewModelKey +@Inject +internal class GameSetsViewModel( gameSetRepository: GameSetRepository, ) : ViewModel() { diff --git a/app/src/main/java/dev/simonas/quies/onboarding/OnboardingRepository.kt b/app/src/main/java/dev/simonas/quies/onboarding/OnboardingRepository.kt index e9ef3a5..4db3dae 100644 --- a/app/src/main/java/dev/simonas/quies/onboarding/OnboardingRepository.kt +++ b/app/src/main/java/dev/simonas/quies/onboarding/OnboardingRepository.kt @@ -1,11 +1,12 @@ package dev.simonas.quies.onboarding import dev.simonas.quies.storage.Store +import dev.zacsweers.metro.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject -class OnboardingRepository @Inject constructor( +@Inject +class OnboardingRepository( private val store: Store, ) { fun isOnboardingCompleted(): Flow { diff --git a/app/src/main/java/dev/simonas/quies/onboarding/OnboardingScreen.kt b/app/src/main/java/dev/simonas/quies/onboarding/OnboardingScreen.kt index 9315a1d..fabc91d 100644 --- a/app/src/main/java/dev/simonas/quies/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/dev/simonas/quies/onboarding/OnboardingScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import dev.zacsweers.metrox.viewmodel.metroViewModel import dev.simonas.quies.AppTheme import dev.simonas.quies.analytics.EventTracker import dev.simonas.quies.analytics.eventTracker @@ -46,7 +46,7 @@ internal object OnboardingScreen { @Composable internal fun OnboardingScreen( onboardingCompleted: () -> Unit, - viewModel: OnboardingViewModel = hiltViewModel(), + viewModel: OnboardingViewModel = metroViewModel(), tracker: EventTracker = eventTracker(), ) { val startedAt = remember { System.currentTimeMillis() } diff --git a/app/src/main/java/dev/simonas/quies/onboarding/OnboardingViewModel.kt b/app/src/main/java/dev/simonas/quies/onboarding/OnboardingViewModel.kt index e626a0f..889aaa8 100644 --- a/app/src/main/java/dev/simonas/quies/onboarding/OnboardingViewModel.kt +++ b/app/src/main/java/dev/simonas/quies/onboarding/OnboardingViewModel.kt @@ -1,15 +1,19 @@ package dev.simonas.quies.onboarding import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import dev.simonas.quies.AppScope +import dev.simonas.quies.AppCoroutineScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoMap +import dev.zacsweers.metro.Inject +import dev.zacsweers.metrox.viewmodel.ViewModelKey import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class OnboardingViewModel @Inject constructor( +@ContributesIntoMap(AppScope::class) +@ViewModelKey +@Inject +class OnboardingViewModel( private val onboardingRepository: OnboardingRepository, - private val appScope: AppScope, + private val appScope: AppCoroutineScope, ) : ViewModel() { fun on(interaction: Interaction) { diff --git a/app/src/main/java/dev/simonas/quies/router/RouterScreen.kt b/app/src/main/java/dev/simonas/quies/router/RouterScreen.kt index db3a857..0c478ce 100644 --- a/app/src/main/java/dev/simonas/quies/router/RouterScreen.kt +++ b/app/src/main/java/dev/simonas/quies/router/RouterScreen.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntSize -import androidx.hilt.navigation.compose.hiltViewModel +import dev.zacsweers.metrox.viewmodel.metroViewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.NavHost @@ -37,7 +37,7 @@ internal object RouterScreen { @Composable internal fun RouterScreen( - viewModel: RouterScreenViewModel = hiltViewModel() + viewModel: RouterScreenViewModel = metroViewModel() ) { val state = viewModel.state.collectAsState() var screenSize by remember { mutableStateOf(IntSize(0, 0)) } diff --git a/app/src/main/java/dev/simonas/quies/router/RouterScreenViewModel.kt b/app/src/main/java/dev/simonas/quies/router/RouterScreenViewModel.kt index 0e357ea..e041799 100644 --- a/app/src/main/java/dev/simonas/quies/router/RouterScreenViewModel.kt +++ b/app/src/main/java/dev/simonas/quies/router/RouterScreenViewModel.kt @@ -2,17 +2,21 @@ package dev.simonas.quies.router import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel import dev.simonas.quies.onboarding.OnboardingRepository +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoMap +import dev.zacsweers.metro.Inject +import dev.zacsweers.metrox.viewmodel.ViewModelKey import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class RouterScreenViewModel @Inject constructor( +@ContributesIntoMap(AppScope::class) +@ViewModelKey +@Inject +class RouterScreenViewModel( private val onboardingRepository: OnboardingRepository, ) : ViewModel() { diff --git a/app/src/main/java/dev/simonas/quies/storage/JetpackStore.kt b/app/src/main/java/dev/simonas/quies/storage/JetpackStore.kt index 560c999..e2ff2b9 100644 --- a/app/src/main/java/dev/simonas/quies/storage/JetpackStore.kt +++ b/app/src/main/java/dev/simonas/quies/storage/JetpackStore.kt @@ -6,15 +6,17 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject -import javax.inject.Singleton private val Context.dataStore: DataStore by preferencesDataStore(name = "quies") -@Singleton -class JetpackStore @Inject constructor( +@SingleIn(AppScope::class) +@Inject +class JetpackStore( private val context: Context, ) : Store { diff --git a/app/src/main/java/dev/simonas/quies/storage/StorageModule.kt b/app/src/main/java/dev/simonas/quies/storage/StorageModule.kt deleted file mode 100644 index 7f5a984..0000000 --- a/app/src/main/java/dev/simonas/quies/storage/StorageModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package dev.simonas.quies.storage - -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -class StorageModule { - - @Module - @InstallIn(SingletonComponent::class) - interface Binders { - - @Binds - @Singleton - fun store(store: JetpackStore): Store - } -} diff --git a/app/src/main/java/dev/simonas/quies/utils/SeqRandom.kt b/app/src/main/java/dev/simonas/quies/utils/SeqRandom.kt index 383bdd5..b760873 100644 --- a/app/src/main/java/dev/simonas/quies/utils/SeqRandom.kt +++ b/app/src/main/java/dev/simonas/quies/utils/SeqRandom.kt @@ -1,10 +1,9 @@ package dev.simonas.quies.utils import java.util.concurrent.atomic.AtomicInteger -import javax.inject.Inject import kotlin.random.Random -internal class SeqRandom @Inject constructor() : Random() { +internal class SeqRandom : Random() { private val counter = AtomicInteger(0) diff --git a/build.gradle.kts b/build.gradle.kts index fb718ab..dfc03cb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,12 +3,11 @@ import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask plugins { alias(libs.plugins.detekt) alias(libs.plugins.google.services) apply false - alias(libs.plugins.hilt) apply false + alias(libs.plugins.metro) apply false alias(libs.plugins.versions) base id("com.android.application") apply false id("com.android.library") apply false - id("com.google.devtools.ksp") version libs.versions.ksp apply false kotlin("android") apply false id("com.google.firebase.crashlytics") version "3.0.1" apply false alias(libs.plugins.compose.compiler) apply false diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index b4e9aee..9303e97 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -9,9 +9,6 @@ repositories { dependencies { implementation(libs.kgp) implementation(libs.agp) - // otherwise app:hiltAggregateDepsDebug fails because it's failing to find - // 'java.lang.String com.squareup.javapoet.ClassName.canonicalName()' - implementation(libs.javapoet) } kotlin { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6d4d30d..18b7fb7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,15 +23,13 @@ firebase_bom = "33.0.0" firebase_crashlytics = "19.0.0" firebase_ktx = "22.0.0" google_services = "4.4.1" -hilt = "2.56.2" -hilt-navigation-compose = "1.2.0" +metro = "1.0.0" javapoet = "1.13.0" junit = "4.13.2" junit-jupiter = "5.11.4" kaml = "0.55.0" kotlin = "2.2.20" kotlin-math = "1.5.3" -ksp = "2.3.2" ktlint_gradle = "11.5.0" min_sdk_version = "28" mixpanel-android = "8.2.1" @@ -76,11 +74,10 @@ firebase-analytics = { module = "com.google.firebase:firebase-analytics", versi firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx", version.ref = "firebase_ktx" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics", version.ref = "firebase_crashlytics" } firebase_bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase_bom" } -hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } -hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } -hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } -hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt-navigation-compose" } -hilt-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } +metro-runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" } +metrox-android = { module = "dev.zacsweers.metro:metrox-android", version.ref = "metro" } +metrox-viewmodel = { module = "dev.zacsweers.metro:metrox-viewmodel", version.ref = "metro" } +metrox-viewmodel-compose = { module = "dev.zacsweers.metro:metrox-viewmodel-compose", version.ref = "metro" } javapoet = { module = "com.squareup:javapoet", version.ref = "javapoet" } junit = { module = "junit:junit", version.ref = "junit" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } @@ -97,7 +94,7 @@ material = { group = "com.google.android.material", name = "material", version.r [plugins] detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } google_services = { id = "com.google.gms.google-services", version.ref = "google_services" } -hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +metro = { id = "dev.zacsweers.metro", version.ref = "metro" } kotlin_android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint.gradle" } play_publisher = { id = "com.github.triplet.play", version.ref = "play_publisher" }