diff --git a/app/build.gradle b/app/build.gradle index 95f780ba..0a60eb34 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ if (file("$rootDir/secrets.gradle").exists()) { } else { // Dummy secrets file for local development println "[WARNING] Using dummy secrets file. You will only be able to create debug builds. Please create a secrets.gradle file in the project root with your signing secrets if you want to create release builds." - apply from: "$rootDir/dummy_secrets.gradle" + apply from: "$rootDir/dummy_secrets.gradle" } apply from: "$rootDir/build.module.feature-and-app.gradle" apply from: "$rootDir/build.dep.navigation.gradle" @@ -37,7 +37,7 @@ android { resources { // Exclude duplicate META-INF files from dependencies to avoid build conflicts // okhttp3:logging-interceptor and jspecify both contain META-INF/versions/9/OSGI-INF/MANIFEST.MF - excludes += '/META-INF/versions/9/OSGI-INF/MANIFEST.MF' + excludes += '**/META-INF/versions/*/OSGI-INF/MANIFEST.MF' } } @@ -93,6 +93,11 @@ dependencies { api platform(libs.firebaseBoM) implementation(libs.firebaseCrashlytics) + + testImplementation libs.junit + testImplementation libs.kotlin.test + testImplementation libs.koin.test + } tasks.register("checkSigningConfig") { diff --git a/app/src/main/kotlin/nl/q42/template/MainActivity.kt b/app/src/main/kotlin/nl/q42/template/MainActivity.kt index eaa7bdcc..690e879a 100644 --- a/app/src/main/kotlin/nl/q42/template/MainActivity.kt +++ b/app/src/main/kotlin/nl/q42/template/MainActivity.kt @@ -17,28 +17,23 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController -import dagger.hilt.android.AndroidEntryPoint import io.github.aakira.napier.Napier -import nl.q42.template.core.utils.di.ConfigAppScheme +import nl.q42.template.core.utils.config.AppScheme import nl.q42.template.navigation.Destination import nl.q42.template.navigation.homeGraph import nl.q42.template.navigation.onboardingDestinations import nl.q42.template.ui.compose.composables.widgets.AppSurface import nl.q42.template.ui.compose.composables.window.LocalSnackbarHostState import nl.q42.template.ui.compose.composables.window.toSnackBarVisuals -import nl.q42.template.ui.presentation.SnackbarManager +import nl.q42.template.ui.presentation.SnackbarPresenter import nl.q42.template.ui.theme.AppTheme -import javax.inject.Inject +import org.koin.android.ext.android.inject -@AndroidEntryPoint class MainActivity : ComponentActivity() { - @Inject - @ConfigAppScheme - lateinit var appDeepLinkScheme: String + private val appDeepLinkScheme: AppScheme by inject() - @Inject - lateinit var snackbarManager: SnackbarManager + private val snackbarPresenter: SnackbarPresenter by inject() @OptIn(ExperimentalAnimationApi::class) override fun onCreate(savedInstanceState: Bundle?) { @@ -47,7 +42,6 @@ class MainActivity : ComponentActivity() { Napier.d { "onCreate received, ${intent.data}" } - setContent { val snackbarHostState = remember { SnackbarHostState() } @@ -88,12 +82,12 @@ class MainActivity : ComponentActivity() { } /** - * May set a Snackbar on the [snackbarHostState] if the [SnackbarManager] has a snackbar available. + * May set a Snackbar on the [snackbarHostState] if the [SnackbarPresenter] has a snackbar available. * To actually show the snackbar, snackbarHostState has to be used in a Scaffold, such as ScaffoldWithAppBar. */ @Composable private fun SnackbarChangedEffect(snackbarHostState: SnackbarHostState) { - val snackbarSpec by snackbarManager.uiState.collectAsStateWithLifecycle( + val snackbarSpec by snackbarPresenter.uiState.collectAsStateWithLifecycle( initialValue = null ) val snackbarVisuals = snackbarSpec?.toSnackBarVisuals() diff --git a/app/src/main/kotlin/nl/q42/template/MainApplication.kt b/app/src/main/kotlin/nl/q42/template/MainApplication.kt index c2005c82..9134ca14 100644 --- a/app/src/main/kotlin/nl/q42/template/MainApplication.kt +++ b/app/src/main/kotlin/nl/q42/template/MainApplication.kt @@ -3,19 +3,18 @@ package nl.q42.template import android.app.Application import android.os.StrictMode import com.google.firebase.crashlytics.FirebaseCrashlytics -import dagger.hilt.android.HiltAndroidApp import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier +import nl.q42.template.di.initDependencyInjection import nl.q42.template.logging.CrashlyticsLogger -@HiltAndroidApp class MainApplication : Application() { override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { - FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false) + FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = false Napier.base(DebugAntilog()) StrictMode.setThreadPolicy( @@ -27,8 +26,10 @@ class MainApplication : Application() { .build() ) } else { - FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true) + FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = true Napier.base(CrashlyticsLogger()) } + + initDependencyInjection(this) } } diff --git a/app/src/main/kotlin/nl/q42/template/di/AppModule.kt b/app/src/main/kotlin/nl/q42/template/di/AppModule.kt new file mode 100644 index 00000000..19a4f833 --- /dev/null +++ b/app/src/main/kotlin/nl/q42/template/di/AppModule.kt @@ -0,0 +1,48 @@ +package nl.q42.template.di + +import nl.q42.template.MainApplication +import nl.q42.template.core.network.di.networkModule +import nl.q42.template.data.main.di.dataModule +import nl.q42.template.domain.main.di.domainModule +import nl.q42.template.home.di.homeModule +import nl.q42.template.navigation.di.navigationModule +import nl.q42.template.onboarding.di.onboardingModule +import nl.q42.template.ui.di.presentationModule +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.GlobalContext.startKoin +import org.koin.dsl.module + + +/** + * Initializes the Koin dependency injection framework, setting up the complete + * dependency graph for the application. Call on Application start. + */ +fun initDependencyInjection(application: MainApplication) { + startKoin { + androidLogger() + androidContext(application) + modules(appModule) + } +} + +val appModule = module { + includes( + configModule, + + // features + homeModule, + onboardingModule, + + // core + networkModule, + navigationModule, + presentationModule, + + // data + dataModule, + + // domain + domainModule + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/nl/q42/template/di/ConfigModule.kt b/app/src/main/kotlin/nl/q42/template/di/ConfigModule.kt index b12c350e..f2cf2644 100644 --- a/app/src/main/kotlin/nl/q42/template/di/ConfigModule.kt +++ b/app/src/main/kotlin/nl/q42/template/di/ConfigModule.kt @@ -1,48 +1,21 @@ package nl.q42.template.di -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent import nl.q42.template.BuildConfig -import nl.q42.template.core.utils.di.ConfigApiMainPath -import nl.q42.template.core.utils.di.ConfigAppScheme -import nl.q42.template.core.utils.di.ConfigAppVersionCode -import nl.q42.template.core.utils.di.ConfigAppVersionName -import nl.q42.template.core.utils.di.ConfigLogHttpCalls -import javax.inject.Singleton +import nl.q42.template.core.utils.config.ApiMainPath +import nl.q42.template.core.utils.config.AppScheme +import nl.q42.template.core.utils.config.AppVersionCode +import nl.q42.template.core.utils.config.AppVersionName +import nl.q42.template.core.utils.config.IsLogHttpCalls +import org.koin.dsl.module /** * All application wide config can go in here. Used so that other modules don't need to access the BuildConfig, which has drawbacks and can cause bugs: * https://blog.dipien.com/stop-generating-the-buildconfig-on-your-android-modules-7d82dd7f20f1 */ -@Module -@InstallIn(SingletonComponent::class) -class ConfigModule { - - @Provides - @Singleton - @ConfigApiMainPath - fun providesApiMainPath(): String = BuildConfig.config_api_main_url - - @Provides - @Singleton - @ConfigLogHttpCalls - fun configIsLoggingHttpCalls(): Boolean = BuildConfig.config_log_http_calls - - @Provides - @Singleton - @ConfigAppScheme - fun providesAppScheme(): String = BuildConfig.config_app_scheme - - @Provides - @Singleton - @ConfigAppVersionName - fun providesAppVersionName(): String = BuildConfig.VERSION_NAME - - @Provides - @Singleton - @ConfigAppVersionCode - fun providesAppVersionCode(): Int = BuildConfig.VERSION_CODE - +val configModule = module { + single { ApiMainPath(BuildConfig.config_api_main_url) } + single { IsLogHttpCalls(BuildConfig.config_log_http_calls) } + single { AppScheme(BuildConfig.config_app_scheme) } + single { AppVersionName(BuildConfig.VERSION_NAME) } + single { AppVersionCode(BuildConfig.VERSION_CODE) } } \ No newline at end of file diff --git a/app/src/main/kotlin/nl/q42/template/navigation/HomeGraph.kt b/app/src/main/kotlin/nl/q42/template/navigation/HomeGraph.kt index 62bda55f..68675b97 100644 --- a/app/src/main/kotlin/nl/q42/template/navigation/HomeGraph.kt +++ b/app/src/main/kotlin/nl/q42/template/navigation/HomeGraph.kt @@ -1,25 +1,26 @@ package nl.q42.template.navigation -import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import androidx.navigation.navDeepLink +import nl.q42.template.core.utils.config.AppScheme import nl.q42.template.home.main.presentation.HomeViewModel import nl.q42.template.home.main.ui.HomeScreen import nl.q42.template.home.second.presentation.HomeSecondViewModel import nl.q42.template.home.second.ui.HomeSecondScreen import nl.q42.template.navigation.viewmodel.InitNavigator +import org.koin.androidx.compose.koinViewModel internal fun NavGraphBuilder.homeGraph( navController: NavHostController, - appDeepLinkScheme: String, + appDeepLinkScheme: AppScheme, ) { navigation(startDestination = Destination.Home) { composable { - val viewModel: HomeViewModel = hiltViewModel() + val viewModel: HomeViewModel = koinViewModel() InitNavigator(navController = navController, routeNavigator = viewModel) HomeScreen(viewModel = viewModel) @@ -28,11 +29,11 @@ internal fun NavGraphBuilder.homeGraph( deepLinks = listOf( // keep in sync with Destinations.HomeSecond: // title should be the name of a parameter of Destinations.HomeSecond - navDeepLink { uriPattern = "$appDeepLinkScheme://home/second/{title}" } + navDeepLink { uriPattern = "${appDeepLinkScheme.value}://home/second/{title}" } ) ) { - val viewModel: HomeSecondViewModel = hiltViewModel() + val viewModel: HomeSecondViewModel = koinViewModel() InitNavigator(navController = navController, routeNavigator = viewModel) HomeSecondScreen(viewModel = viewModel) diff --git a/app/src/main/kotlin/nl/q42/template/navigation/OnboardingDestinations.kt b/app/src/main/kotlin/nl/q42/template/navigation/OnboardingDestinations.kt index f6cab226..66a692f1 100644 --- a/app/src/main/kotlin/nl/q42/template/navigation/OnboardingDestinations.kt +++ b/app/src/main/kotlin/nl/q42/template/navigation/OnboardingDestinations.kt @@ -1,17 +1,17 @@ package nl.q42.template.navigation -import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import nl.q42.template.navigation.viewmodel.InitNavigator import nl.q42.template.onboarding.start.presentation.OnboardingStartViewModel import nl.q42.template.onboarding.start.ui.OnboardingStartScreen +import org.koin.androidx.compose.koinViewModel internal fun NavGraphBuilder.onboardingDestinations(navController: NavHostController) { composable { - val viewModel: OnboardingStartViewModel = hiltViewModel() + val viewModel: OnboardingStartViewModel = koinViewModel() InitNavigator(navController = navController, viewModel) OnboardingStartScreen(viewModel = viewModel) diff --git a/app/src/test/kotlin/nl/q42/template/di/KoinCheckModulesTest.kt b/app/src/test/kotlin/nl/q42/template/di/KoinCheckModulesTest.kt new file mode 100644 index 00000000..a27ba3dc --- /dev/null +++ b/app/src/test/kotlin/nl/q42/template/di/KoinCheckModulesTest.kt @@ -0,0 +1,33 @@ +package nl.q42.template.di + +import androidx.lifecycle.SavedStateHandle +import kotlinx.coroutines.CoroutineScope +import org.junit.Test +import org.koin.core.annotation.KoinExperimentalAPI +import org.koin.test.KoinTest +import org.koin.test.verify.verify + +class KoinCheckModulesTest : KoinTest { + + /** + * Note: this checks that all the dependencies in the DI configuration work well among them, + * but it doesn't prevent crashes at runtime if the consumer code of the DI dependencies misses + * a required parameter. This will happen for example with a ViewModel(val myID: String) where + * the caller of koinViewModel() forgets to pass the myID parameter or the parameter's type doesn't match. + */ + @OptIn(KoinExperimentalAPI::class) + @Test + fun `Check all Koin modules and their dependencies`() { + appModule.verify( + extraTypes = listOf( + // Primitive types used in value classes + String::class, + Boolean::class, + Int::class, + // Android framework types injected at runtime + SavedStateHandle::class, + CoroutineScope::class, + ) + ) + } +} \ No newline at end of file diff --git a/build.dep.compose.gradle b/build.dep.compose.gradle index 1dd024de..69f1fb54 100644 --- a/build.dep.compose.gradle +++ b/build.dep.compose.gradle @@ -14,4 +14,5 @@ dependencies { implementation(libs.composeMaterial3) implementation(libs.composeLifecycle) implementation(libs.composeStateEvents) + implementation(libs.koin.compose) } diff --git a/build.dep.di.gradle b/build.dep.di.gradle index 83ca6514..770bec8a 100644 --- a/build.dep.di.gradle +++ b/build.dep.di.gradle @@ -1,9 +1,3 @@ -apply { - plugin(libs.plugins.hilt.get().getPluginId()) - plugin(libs.plugins.ksp.get().getPluginId()) -} - dependencies { - implementation(libs.hilt) - ksp(libs.hilt.ksp) + implementation(libs.koin) } diff --git a/build.gradle b/build.gradle index b264dd93..fec2e1f4 100644 --- a/build.gradle +++ b/build.gradle @@ -13,8 +13,6 @@ plugins { // sets class paths only (because of 'apply false') alias libs.plugins.androidLibrary apply false alias libs.plugins.jetbrainsKotlinAndroid apply false alias libs.plugins.kotlinSerialization apply false - alias libs.plugins.hilt apply false - alias libs.plugins.ksp apply false alias libs.plugins.googleServices apply false alias libs.plugins.firebaseCrashlyticsPlugin apply false alias libs.plugins.compose.compiler apply false diff --git a/build.module.feature-and-app.gradle b/build.module.feature-and-app.gradle index 63953aea..d3c61092 100644 --- a/build.module.feature-and-app.gradle +++ b/build.module.feature-and-app.gradle @@ -33,7 +33,7 @@ dependencies { implementation project(':core:utils') implementation(libs.napier) implementation(libs.activityCompose) - implementation(libs.hiltNavigationCompose) + implementation(libs.koin) testImplementation libs.junit testImplementation libs.kotlinx.coroutines.test diff --git a/core/navigation/src/main/kotlin/nl/q42/template/navigation/di/ConfigModule.kt b/core/navigation/src/main/kotlin/nl/q42/template/navigation/di/ConfigModule.kt deleted file mode 100644 index 319e4987..00000000 --- a/core/navigation/src/main/kotlin/nl/q42/template/navigation/di/ConfigModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package nl.q42.template.navigation.di - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped -import nl.q42.template.navigation.viewmodel.MyRouteNavigator -import nl.q42.template.navigation.viewmodel.RouteNavigator - -@Module -@InstallIn(ViewModelComponent::class) -class NavigationModule { - - @Provides - @ViewModelScoped - fun bindRouteNavigator(): RouteNavigator = MyRouteNavigator() -} \ No newline at end of file diff --git a/core/navigation/src/main/kotlin/nl/q42/template/navigation/di/NavigationModule.kt b/core/navigation/src/main/kotlin/nl/q42/template/navigation/di/NavigationModule.kt new file mode 100644 index 00000000..000c00a5 --- /dev/null +++ b/core/navigation/src/main/kotlin/nl/q42/template/navigation/di/NavigationModule.kt @@ -0,0 +1,11 @@ +package nl.q42.template.navigation.di + +import nl.q42.template.navigation.viewmodel.MyRouteNavigator +import nl.q42.template.navigation.viewmodel.RouteNavigator +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.factoryOf +import org.koin.dsl.module + +val navigationModule = module { + factoryOf(::MyRouteNavigator) { bind() } +} \ No newline at end of file diff --git a/core/network/src/main/kotlin/nl/q42/template/core/network/di/NetworkModule.kt b/core/network/src/main/kotlin/nl/q42/template/core/network/di/NetworkModule.kt index 36054646..80f4f7cc 100644 --- a/core/network/src/main/kotlin/nl/q42/template/core/network/di/NetworkModule.kt +++ b/core/network/src/main/kotlin/nl/q42/template/core/network/di/NetworkModule.kt @@ -2,62 +2,65 @@ package nl.q42.template.core.network.di import com.haroldadmin.cnradapter.NetworkResponseAdapterFactory import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent import kotlinx.serialization.json.Json import nl.q42.template.core.network.interceptor.UserAgentHeaderInterceptor import nl.q42.template.core.network.logger.JsonFormattedHttpLogger -import nl.q42.template.core.utils.di.ConfigApiMainPath -import nl.q42.template.core.utils.di.ConfigLogHttpCalls +import nl.q42.template.core.utils.config.ApiMainPath +import nl.q42.template.core.utils.config.IsLogHttpCalls import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module import retrofit2.Retrofit import java.util.concurrent.TimeUnit -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal class NetworkModule { - - @Provides - @Singleton - fun providesOkhttpClient( - @ConfigLogHttpCalls logHttpCalls: Boolean, - userAgentHeaderInterceptor: UserAgentHeaderInterceptor, - ) = - OkHttpClient.Builder() - .apply { - connectTimeout(1, TimeUnit.MINUTES) - .readTimeout(1, TimeUnit.MINUTES) - .writeTimeout(1, TimeUnit.MINUTES) - - if (logHttpCalls) addInterceptor( - HttpLoggingInterceptor(JsonFormattedHttpLogger()) - .apply { level = HttpLoggingInterceptor.Level.BODY }) - - addInterceptor(userAgentHeaderInterceptor) - }.build() - - @Singleton - @Provides - fun provideRetrofit( - httpClient: OkHttpClient, - @ConfigApiMainPath apiMainPath: String, - ): Retrofit { - val contentType = "application/json".toMediaType() - - // When the server adds new fields to the response, we don't want to crash - val json = Json { ignoreUnknownKeys = true } - - return Retrofit.Builder() - .baseUrl(apiMainPath) - .addConverterFactory(json.asConverterFactory(contentType)) - .addCallAdapterFactory(NetworkResponseAdapterFactory()) - .client(httpClient) - .build() + +val networkModule = module { + singleOf(::UserAgentHeaderInterceptor) + + single { + provideOkHttpClient(get(), get()) } + single { + provideRetrofit(get(), get()) + } +} + +internal fun provideOkHttpClient( + logHttpCalls: IsLogHttpCalls, + userAgentHeaderInterceptor: UserAgentHeaderInterceptor, +): OkHttpClient { + return OkHttpClient.Builder() + .apply { + connectTimeout(1, TimeUnit.MINUTES) + .readTimeout(1, TimeUnit.MINUTES) + .writeTimeout(1, TimeUnit.MINUTES) + + if (logHttpCalls.value) { + addInterceptor( + HttpLoggingInterceptor(JsonFormattedHttpLogger()) + .apply { level = HttpLoggingInterceptor.Level.BODY } + ) + } + + addInterceptor(userAgentHeaderInterceptor) + }.build() } + +internal fun provideRetrofit( + httpClient: OkHttpClient, + apiMainPath: ApiMainPath, +): Retrofit { + val contentType = "application/json".toMediaType() + + // When the server adds new fields to the response, we don't want to crash + val json = Json { ignoreUnknownKeys = true } + + return Retrofit.Builder() + .baseUrl(apiMainPath.value) + .addConverterFactory(json.asConverterFactory(contentType)) + .addCallAdapterFactory(NetworkResponseAdapterFactory()) + .client(httpClient) + .build() +} \ No newline at end of file diff --git a/core/network/src/main/kotlin/nl/q42/template/core/network/interceptor/UserAgentHeaderInterceptor.kt b/core/network/src/main/kotlin/nl/q42/template/core/network/interceptor/UserAgentHeaderInterceptor.kt index c2d8f788..b9434952 100644 --- a/core/network/src/main/kotlin/nl/q42/template/core/network/interceptor/UserAgentHeaderInterceptor.kt +++ b/core/network/src/main/kotlin/nl/q42/template/core/network/interceptor/UserAgentHeaderInterceptor.kt @@ -1,28 +1,26 @@ package nl.q42.template.core.network.interceptor import android.os.Build -import nl.q42.template.core.utils.di.ConfigAppVersionCode -import nl.q42.template.core.utils.di.ConfigAppVersionName +import nl.q42.template.core.utils.config.AppVersionCode +import nl.q42.template.core.utils.config.AppVersionName import okhttp3.Interceptor import okhttp3.Response -import javax.inject.Inject -import javax.inject.Singleton /** * Adds a user agent with app name, app version, app versionCode, os version * which can be useful for request logging and debugging. - */ -@Singleton -class UserAgentHeaderInterceptor @Inject constructor( - @param:ConfigAppVersionName private val appVersionName: String, - @param:ConfigAppVersionCode private val appVersionCode: Int, + */ +class UserAgentHeaderInterceptor( + private val appVersionName: AppVersionName, + private val appVersionCode: AppVersionCode, ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().newBuilder() .apply { val androidVersionRelease = Build.VERSION.RELEASE - val userAgentString = "Template/$appVersionName ($appVersionCode; Android/$androidVersionRelease; ${Build.BRAND} ${Build.MODEL})" + val userAgentString = + "Template/${appVersionName.value} (${appVersionCode.value}; Android/$androidVersionRelease; ${Build.BRAND} ${Build.MODEL})" header("User-Agent", userAgentString) } .build() diff --git a/core/ui/src/main/kotlin/nl/q42/template/ui/compose/MainCoroutineScope.kt b/core/ui/src/main/kotlin/nl/q42/template/ui/compose/MainCoroutineScope.kt new file mode 100644 index 00000000..04f894ff --- /dev/null +++ b/core/ui/src/main/kotlin/nl/q42/template/ui/compose/MainCoroutineScope.kt @@ -0,0 +1,6 @@ +package nl.q42.template.ui.compose + +import kotlinx.coroutines.CoroutineScope + +@JvmInline +value class MainCoroutineScope(val value: CoroutineScope) \ No newline at end of file diff --git a/core/ui/src/main/kotlin/nl/q42/template/ui/di/PresentationModule.kt b/core/ui/src/main/kotlin/nl/q42/template/ui/di/PresentationModule.kt index c6aaed55..fe85a93c 100644 --- a/core/ui/src/main/kotlin/nl/q42/template/ui/di/PresentationModule.kt +++ b/core/ui/src/main/kotlin/nl/q42/template/ui/di/PresentationModule.kt @@ -1,28 +1,28 @@ package nl.q42.template.ui.di import android.app.Application +import android.content.Context import android.view.accessibility.AccessibilityManager -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.MainScope +import nl.q42.template.ui.compose.MainCoroutineScope +import nl.q42.template.ui.presentation.SnackbarPresenter import nl.q42.template.ui.presentation.dialog.DialogPresenter import nl.q42.template.ui.presentation.dialog.DialogPresenterImpl -import javax.inject.Singleton +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module -@Module -@InstallIn(SingletonComponent::class) -internal class PresentationModule { - - @Singleton - @Provides - fun provideAccessibilityManager(application: Application): AccessibilityManager = - application.getSystemService( - Application.ACCESSIBILITY_SERVICE - ) as AccessibilityManager - - @Provides - fun providesDialogPresenter(dialogPresenter: DialogPresenterImpl): DialogPresenter = - dialogPresenter +val presentationModule = module { + single { + provideAccessibilityManager(get()) + } + single { MainCoroutineScope(MainScope()) } + singleOf(::DialogPresenterImpl) { bind() } + singleOf(::SnackbarPresenter) } + +internal fun provideAccessibilityManager(applicationContext: Context): AccessibilityManager = + applicationContext.getSystemService( + Application.ACCESSIBILITY_SERVICE + ) as AccessibilityManager \ No newline at end of file diff --git a/core/ui/src/main/kotlin/nl/q42/template/ui/presentation/SnackbarManager.kt b/core/ui/src/main/kotlin/nl/q42/template/ui/presentation/SnackbarPresenter.kt similarity index 85% rename from core/ui/src/main/kotlin/nl/q42/template/ui/presentation/SnackbarManager.kt rename to core/ui/src/main/kotlin/nl/q42/template/ui/presentation/SnackbarPresenter.kt index 27d2023a..48b2220d 100644 --- a/core/ui/src/main/kotlin/nl/q42/template/ui/presentation/SnackbarManager.kt +++ b/core/ui/src/main/kotlin/nl/q42/template/ui/presentation/SnackbarPresenter.kt @@ -1,21 +1,20 @@ package nl.q42.template.ui.presentation import android.view.accessibility.AccessibilityManager -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch +import nl.q42.template.ui.compose.MainCoroutineScope import java.util.UUID -import javax.inject.Inject -import javax.inject.Singleton private const val DEFAULT_SNACKBAR_DURATION_MILLIS = 4000L -@Singleton -class SnackbarManager @Inject constructor(private val accessibilityManager: AccessibilityManager) : ViewModel() { +class SnackbarPresenter( + private val accessibilityManager: AccessibilityManager, + private val mainCoroutineScope: MainCoroutineScope, +) { private var showSnackbarJob: Job? = null @@ -25,7 +24,7 @@ class SnackbarManager @Inject constructor(private val accessibilityManager: Acce /** No queueing is implemented. When a Snackbar is currently showing, it will be replaced */ fun showSnackbar(spec: SnackBarSpec) { showSnackbarJob?.cancel() - showSnackbarJob = viewModelScope.launch { + showSnackbarJob = mainCoroutineScope.value.launch { _uiState.value = spec delay(snackbarDuration(spec)) _uiState.value = null diff --git a/core/ui/src/main/kotlin/nl/q42/template/ui/presentation/dialog/DialogPresenter.kt b/core/ui/src/main/kotlin/nl/q42/template/ui/presentation/dialog/DialogPresenter.kt index 5218e211..9390b5de 100644 --- a/core/ui/src/main/kotlin/nl/q42/template/ui/presentation/dialog/DialogPresenter.kt +++ b/core/ui/src/main/kotlin/nl/q42/template/ui/presentation/dialog/DialogPresenter.kt @@ -4,14 +4,13 @@ import androidx.annotation.CallSuper import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -import javax.inject.Inject /** * Usage: * * * ViewModel: * ``` - * @inject constructor ( + * constructor ( * private val dialogPresenter: DialogPresenter * ) : ViewModel(), DialogPresenter by dialogPresenter { * override fun onDialogConfirmed(tag: Any) { @@ -47,7 +46,7 @@ interface DialogPresenter { val dialogUIState: Flow } -internal class DialogPresenterImpl @Inject constructor() : DialogPresenter { +internal class DialogPresenterImpl : DialogPresenter { private val _dialogUIState = MutableStateFlow(DialogViewState.None) override val dialogUIState: Flow = _dialogUIState diff --git a/core/utils/src/main/kotlin/nl/q42/template/core/utils/config/ConfigModels.kt b/core/utils/src/main/kotlin/nl/q42/template/core/utils/config/ConfigModels.kt new file mode 100644 index 00000000..821364ed --- /dev/null +++ b/core/utils/src/main/kotlin/nl/q42/template/core/utils/config/ConfigModels.kt @@ -0,0 +1,16 @@ +package nl.q42.template.core.utils.config + +@JvmInline +value class ApiMainPath(val value: String) + +@JvmInline +value class IsLogHttpCalls(val value: Boolean) + +@JvmInline +value class AppScheme(val value: String) + +@JvmInline +value class AppVersionName(val value: String) + +@JvmInline +value class AppVersionCode(val value: Int) \ No newline at end of file diff --git a/core/utils/src/main/kotlin/nl/q42/template/core/utils/di/QualifierAnnotations.kt b/core/utils/src/main/kotlin/nl/q42/template/core/utils/di/QualifierAnnotations.kt deleted file mode 100644 index b6744686..00000000 --- a/core/utils/src/main/kotlin/nl/q42/template/core/utils/di/QualifierAnnotations.kt +++ /dev/null @@ -1,23 +0,0 @@ -package nl.q42.template.core.utils.di - -import javax.inject.Qualifier - -@Qualifier -@Retention(AnnotationRetention.RUNTIME) -annotation class ConfigApiMainPath - -@Qualifier -@Retention(AnnotationRetention.RUNTIME) -annotation class ConfigLogHttpCalls - -@Qualifier -@Retention(AnnotationRetention.RUNTIME) -annotation class ConfigAppScheme - -@Qualifier -@Retention(AnnotationRetention.RUNTIME) -annotation class ConfigAppVersionName - -@Qualifier -@Retention(AnnotationRetention.RUNTIME) -annotation class ConfigAppVersionCode \ No newline at end of file diff --git a/data/main/src/main/kotlin/nl/q42/template/data/main/UserRepositoryImpl.kt b/data/main/src/main/kotlin/nl/q42/template/data/main/UserRepositoryImpl.kt index b271789d..2d6a774f 100644 --- a/data/main/src/main/kotlin/nl/q42/template/data/main/UserRepositoryImpl.kt +++ b/data/main/src/main/kotlin/nl/q42/template/data/main/UserRepositoryImpl.kt @@ -10,9 +10,8 @@ import nl.q42.template.data.main.local.model.mapToUser import nl.q42.template.data.main.remote.UserRemoteDataSource import nl.q42.template.domain.main.model.User import nl.q42.template.domain.main.repo.UserRepository -import javax.inject.Inject -internal class UserRepositoryImpl @Inject constructor( +internal class UserRepositoryImpl( private val userRemoteDataSource: UserRemoteDataSource, private val userLocalDataSource: UserLocalDataSource, ) : UserRepository { diff --git a/data/main/src/main/kotlin/nl/q42/template/data/main/di/DataModule.kt b/data/main/src/main/kotlin/nl/q42/template/data/main/di/DataModule.kt new file mode 100644 index 00000000..cdd33879 --- /dev/null +++ b/data/main/src/main/kotlin/nl/q42/template/data/main/di/DataModule.kt @@ -0,0 +1,25 @@ +package nl.q42.template.data.main.di + +import nl.q42.template.data.main.UserRepositoryImpl +import nl.q42.template.data.main.local.UserLocalDataSource +import nl.q42.template.data.main.remote.MainApi +import nl.q42.template.data.main.remote.UserRemoteDataSource +import nl.q42.template.domain.main.repo.UserRepository +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module +import retrofit2.Retrofit + +val dataModule = module { + + single { + provideRetrofitApi(get()) + } + + singleOf(::UserRepositoryImpl) { bind() } + singleOf(::UserLocalDataSource) + singleOf(::UserRemoteDataSource) +} + +internal fun provideRetrofitApi(retrofit: Retrofit): MainApi = + retrofit.create(MainApi::class.java) \ No newline at end of file diff --git a/data/main/src/main/kotlin/nl/q42/template/data/main/di/MainDataModule.kt b/data/main/src/main/kotlin/nl/q42/template/data/main/di/MainDataModule.kt deleted file mode 100644 index 9bab4e6f..00000000 --- a/data/main/src/main/kotlin/nl/q42/template/data/main/di/MainDataModule.kt +++ /dev/null @@ -1,32 +0,0 @@ -package nl.q42.template.data.main.di - -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import nl.q42.template.data.main.UserRepositoryImpl -import nl.q42.template.data.main.remote.UserApi -import nl.q42.template.domain.main.repo.UserRepository -import retrofit2.Retrofit -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal class UserDataModule { - - @Provides - @Singleton - fun providesUserApi( - retrofit: Retrofit, - ): UserApi = retrofit.create(UserApi::class.java) -} - -@Module -@InstallIn(SingletonComponent::class) -internal interface UserRepoModule { - - @Binds - @Singleton - fun bindUserRepository(impl: UserRepositoryImpl): UserRepository -} diff --git a/data/main/src/main/kotlin/nl/q42/template/data/main/local/UserLocalDataSource.kt b/data/main/src/main/kotlin/nl/q42/template/data/main/local/UserLocalDataSource.kt index 40fd90ac..07d1ed13 100644 --- a/data/main/src/main/kotlin/nl/q42/template/data/main/local/UserLocalDataSource.kt +++ b/data/main/src/main/kotlin/nl/q42/template/data/main/local/UserLocalDataSource.kt @@ -2,16 +2,12 @@ package nl.q42.template.data.main.local import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update import nl.q42.template.data.main.local.model.UserEntity -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -internal class UserLocalDataSource @Inject constructor() { +internal class UserLocalDataSource() { - private val userFlow = MutableSharedFlow() // this is dummy code, replace it with your own local storage implementation. + private val userFlow = + MutableSharedFlow() // this is dummy code, replace it with your own local storage implementation. suspend fun setUser(userEntity: UserEntity) { diff --git a/data/main/src/main/kotlin/nl/q42/template/data/main/remote/UserApi.kt b/data/main/src/main/kotlin/nl/q42/template/data/main/remote/MainApi.kt similarity index 94% rename from data/main/src/main/kotlin/nl/q42/template/data/main/remote/UserApi.kt rename to data/main/src/main/kotlin/nl/q42/template/data/main/remote/MainApi.kt index 29add4cb..392aa100 100644 --- a/data/main/src/main/kotlin/nl/q42/template/data/main/remote/UserApi.kt +++ b/data/main/src/main/kotlin/nl/q42/template/data/main/remote/MainApi.kt @@ -6,7 +6,7 @@ import nl.q42.template.data.main.remote.model.UserDTO import retrofit2.http.GET import retrofit2.http.Query -internal interface UserApi { +internal interface MainApi { /** * note: [dummyEmailForResponse] is set because this test server mirrors the request as response. diff --git a/data/main/src/main/kotlin/nl/q42/template/data/main/remote/UserRemoteDataSource.kt b/data/main/src/main/kotlin/nl/q42/template/data/main/remote/UserRemoteDataSource.kt index 9e319e94..965bfe8b 100644 --- a/data/main/src/main/kotlin/nl/q42/template/data/main/remote/UserRemoteDataSource.kt +++ b/data/main/src/main/kotlin/nl/q42/template/data/main/remote/UserRemoteDataSource.kt @@ -9,17 +9,14 @@ import nl.q42.template.actionresult.domain.map import nl.q42.template.data.main.local.model.UserEntity import nl.q42.template.data.main.mapper.mapToEntity import nl.q42.template.data.main.remote.model.UserDTO -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -internal class UserRemoteDataSource @Inject constructor( - private val userApi: UserApi +internal class UserRemoteDataSource( + private val mainApi: MainApi ) { suspend fun getUser(): ActionResult = withContext(Dispatchers.IO) { val apiActionResult = mapToActionResult { - userApi.getUsers("test@test.com") + mainApi.getUsers("test@test.com") } when (apiActionResult) { diff --git a/domain/main/src/main/kotlin/nl/q42/template/domain/main/di/DomainModule.kt b/domain/main/src/main/kotlin/nl/q42/template/domain/main/di/DomainModule.kt new file mode 100644 index 00000000..ded514b1 --- /dev/null +++ b/domain/main/src/main/kotlin/nl/q42/template/domain/main/di/DomainModule.kt @@ -0,0 +1,12 @@ +package nl.q42.template.domain.main.di + +import nl.q42.template.domain.main.usecase.FetchUserUseCase +import nl.q42.template.domain.main.usecase.GetUserFlowUseCase +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +val domainModule = module { + + singleOf(::FetchUserUseCase) + singleOf(::GetUserFlowUseCase) +} \ No newline at end of file diff --git a/domain/main/src/main/kotlin/nl/q42/template/domain/main/usecase/FetchUserUseCase.kt b/domain/main/src/main/kotlin/nl/q42/template/domain/main/usecase/FetchUserUseCase.kt index 20d52ad0..6b3d99c9 100644 --- a/domain/main/src/main/kotlin/nl/q42/template/domain/main/usecase/FetchUserUseCase.kt +++ b/domain/main/src/main/kotlin/nl/q42/template/domain/main/usecase/FetchUserUseCase.kt @@ -4,10 +4,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import nl.q42.template.actionresult.domain.ActionResult import nl.q42.template.domain.main.repo.UserRepository -import javax.inject.Inject // A UseCase models an action so the name should begin with a verb. For Flows, use: GetSomethingFlowUseCase -class FetchUserUseCase @Inject constructor(private val userRepository: UserRepository) { +class FetchUserUseCase(private val userRepository: UserRepository) { suspend operator fun invoke(): ActionResult = withContext(Dispatchers.Default) { userRepository.fetchUser() diff --git a/domain/main/src/main/kotlin/nl/q42/template/domain/main/usecase/GetUserFlowUseCase.kt b/domain/main/src/main/kotlin/nl/q42/template/domain/main/usecase/GetUserFlowUseCase.kt index f225243c..de55eabe 100644 --- a/domain/main/src/main/kotlin/nl/q42/template/domain/main/usecase/GetUserFlowUseCase.kt +++ b/domain/main/src/main/kotlin/nl/q42/template/domain/main/usecase/GetUserFlowUseCase.kt @@ -5,9 +5,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import nl.q42.template.domain.main.model.User import nl.q42.template.domain.main.repo.UserRepository -import javax.inject.Inject -class GetUserFlowUseCase @Inject constructor(private val userRepository: UserRepository) { +class GetUserFlowUseCase(private val userRepository: UserRepository) { operator fun invoke(): Flow = userRepository diff --git a/feature/home/src/main/kotlin/nl/q42/template/home/di/HomeModule.kt b/feature/home/src/main/kotlin/nl/q42/template/home/di/HomeModule.kt new file mode 100644 index 00000000..c54e57d1 --- /dev/null +++ b/feature/home/src/main/kotlin/nl/q42/template/home/di/HomeModule.kt @@ -0,0 +1,12 @@ +package nl.q42.template.home.di + +import nl.q42.template.home.main.presentation.HomeViewModel +import nl.q42.template.home.second.presentation.HomeSecondViewModel +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val homeModule = module { + + viewModelOf(::HomeViewModel) + viewModelOf(::HomeSecondViewModel) +} \ No newline at end of file diff --git a/feature/home/src/main/kotlin/nl/q42/template/home/main/presentation/HomeViewModel.kt b/feature/home/src/main/kotlin/nl/q42/template/home/main/presentation/HomeViewModel.kt index ddf400c9..86dd67c2 100644 --- a/feature/home/src/main/kotlin/nl/q42/template/home/main/presentation/HomeViewModel.kt +++ b/feature/home/src/main/kotlin/nl/q42/template/home/main/presentation/HomeViewModel.kt @@ -2,7 +2,6 @@ package nl.q42.template.home.main.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -17,19 +16,17 @@ import nl.q42.template.domain.main.usecase.GetUserFlowUseCase import nl.q42.template.feature.home.R import nl.q42.template.navigation.Destination import nl.q42.template.navigation.viewmodel.RouteNavigator -import nl.q42.template.ui.presentation.SnackbarManager +import nl.q42.template.ui.presentation.SnackbarPresenter import nl.q42.template.ui.presentation.ViewStateString import nl.q42.template.ui.presentation.dialog.DialogData import nl.q42.template.ui.presentation.dialog.DialogPresenter -import javax.inject.Inject import kotlin.random.Random -@HiltViewModel -class HomeViewModel @Inject constructor( +class HomeViewModel( private val fetchUserUseCase: FetchUserUseCase, private val getUserFlowUseCase: GetUserFlowUseCase, private val navigator: RouteNavigator, - private val snackbarManager: SnackbarManager, + private val snackbarPresenter: SnackbarPresenter, private val dialogPresenter: DialogPresenter ) : ViewModel(), RouteNavigator by navigator, DialogPresenter by dialogPresenter { @@ -44,7 +41,7 @@ class HomeViewModel @Inject constructor( override fun onDialogConfirmed(tag: Any) { dialogPresenter.onDialogConfirmed(tag) // take action based on dialog tag - snackbarManager.showSnackbar(message = ViewStateString.Basic("Dialog confirmed with tag: $tag")) + snackbarPresenter.showSnackbar(message = ViewStateString.Basic("Dialog confirmed with tag: $tag")) } fun onScreenResumed() { @@ -64,7 +61,7 @@ class HomeViewModel @Inject constructor( } fun onShowDummySnackBarClicked() { - snackbarManager.showSnackbar( + snackbarPresenter.showSnackbar( message = ViewStateString.Basic("A SnackBar message. Random: " + Random.nextInt() % 100), isError = false ) diff --git a/feature/home/src/main/kotlin/nl/q42/template/home/main/ui/HomeScreen.kt b/feature/home/src/main/kotlin/nl/q42/template/home/main/ui/HomeScreen.kt index 8c17a895..c24b5f41 100644 --- a/feature/home/src/main/kotlin/nl/q42/template/home/main/ui/HomeScreen.kt +++ b/feature/home/src/main/kotlin/nl/q42/template/home/main/ui/HomeScreen.kt @@ -2,16 +2,16 @@ package nl.q42.template.home.main.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import nl.q42.template.home.main.presentation.HomeViewModel import nl.q42.template.ui.compose.OnLifecycleResume import nl.q42.template.ui.compose.composables.dialog.InitDialogPresenter import nl.q42.template.ui.compose.composables.window.ScaffoldWithAppBar +import org.koin.androidx.compose.koinViewModel @Composable fun HomeScreen( - viewModel: HomeViewModel = hiltViewModel() + viewModel: HomeViewModel = koinViewModel() ) { OnLifecycleResume(viewModel::onScreenResumed) diff --git a/feature/home/src/main/kotlin/nl/q42/template/home/second/presentation/HomeSecondViewModel.kt b/feature/home/src/main/kotlin/nl/q42/template/home/second/presentation/HomeSecondViewModel.kt index d327aa62..33e56a37 100644 --- a/feature/home/src/main/kotlin/nl/q42/template/home/second/presentation/HomeSecondViewModel.kt +++ b/feature/home/src/main/kotlin/nl/q42/template/home/second/presentation/HomeSecondViewModel.kt @@ -3,16 +3,13 @@ package nl.q42.template.home.second.presentation import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.navigation.toRoute -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import nl.q42.template.navigation.Destination import nl.q42.template.navigation.viewmodel.RouteNavigator -import javax.inject.Inject -@HiltViewModel -class HomeSecondViewModel @Inject constructor( +class HomeSecondViewModel( private val navigator: RouteNavigator, savedStateHandle: SavedStateHandle, ) : ViewModel(), RouteNavigator by navigator { diff --git a/feature/home/src/main/kotlin/nl/q42/template/home/second/ui/HomeSecondScreen.kt b/feature/home/src/main/kotlin/nl/q42/template/home/second/ui/HomeSecondScreen.kt index a2a4d9ef..fde3116c 100644 --- a/feature/home/src/main/kotlin/nl/q42/template/home/second/ui/HomeSecondScreen.kt +++ b/feature/home/src/main/kotlin/nl/q42/template/home/second/ui/HomeSecondScreen.kt @@ -2,14 +2,14 @@ package nl.q42.template.home.second.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import nl.q42.template.home.second.presentation.HomeSecondViewModel import nl.q42.template.ui.compose.composables.window.ScaffoldWithAppBar +import org.koin.androidx.compose.koinViewModel @Composable fun HomeSecondScreen( - viewModel: HomeSecondViewModel = hiltViewModel(), + viewModel: HomeSecondViewModel = koinViewModel(), ) { val viewState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/feature/home/src/test/kotlin/nl/q42/template/home/main/presentation/HomeViewModelTest.kt b/feature/home/src/test/kotlin/nl/q42/template/home/main/presentation/HomeViewModelTest.kt index 97816053..63778d1e 100644 --- a/feature/home/src/test/kotlin/nl/q42/template/home/main/presentation/HomeViewModelTest.kt +++ b/feature/home/src/test/kotlin/nl/q42/template/home/main/presentation/HomeViewModelTest.kt @@ -39,7 +39,7 @@ class HomeViewModelTest { val viewModel = HomeViewModel( fetchUserUseCase = fetchUserUseCaseMock, getUserFlowUseCase = getUserFlowUseCaseMock, - snackbarManager = mockk(), + snackbarPresenter = mockk(), dialogPresenter = mockk(), navigator = mockk(), ) @@ -63,7 +63,7 @@ class HomeViewModelTest { val viewModel = HomeViewModel( fetchUserUseCase = fetchUserUseCaseMock, getUserFlowUseCase = getUserFlowUseCaseMock, - snackbarManager = mockk(), + snackbarPresenter = mockk(), dialogPresenter = mockk(), navigator = mockk() ) diff --git a/feature/onboarding/src/main/kotlin/nl/q42/template/onboarding/di/OnboardingModule.kt b/feature/onboarding/src/main/kotlin/nl/q42/template/onboarding/di/OnboardingModule.kt new file mode 100644 index 00000000..c9aa8fc3 --- /dev/null +++ b/feature/onboarding/src/main/kotlin/nl/q42/template/onboarding/di/OnboardingModule.kt @@ -0,0 +1,10 @@ +package nl.q42.template.onboarding.di + +import nl.q42.template.onboarding.start.presentation.OnboardingStartViewModel +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val onboardingModule = module { + + viewModelOf(::OnboardingStartViewModel) +} \ No newline at end of file diff --git a/feature/onboarding/src/main/kotlin/nl/q42/template/onboarding/start/presentation/OnboardingStartViewModel.kt b/feature/onboarding/src/main/kotlin/nl/q42/template/onboarding/start/presentation/OnboardingStartViewModel.kt index 8dd2d0b7..a3f6cf57 100644 --- a/feature/onboarding/src/main/kotlin/nl/q42/template/onboarding/start/presentation/OnboardingStartViewModel.kt +++ b/feature/onboarding/src/main/kotlin/nl/q42/template/onboarding/start/presentation/OnboardingStartViewModel.kt @@ -1,18 +1,13 @@ package nl.q42.template.onboarding.start.presentation -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import nl.q42.template.navigation.viewmodel.RouteNavigator -import javax.inject.Inject -@HiltViewModel -class OnboardingStartViewModel @Inject constructor( +class OnboardingStartViewModel( private val navigator: RouteNavigator, - savedStateHandle: SavedStateHandle, ) : ViewModel(), RouteNavigator by navigator { private val _uiState = MutableStateFlow(OnboardingStartViewState("Onboarding start")) diff --git a/feature/onboarding/src/main/kotlin/nl/q42/template/onboarding/start/ui/OnboardingStartScreen.kt b/feature/onboarding/src/main/kotlin/nl/q42/template/onboarding/start/ui/OnboardingStartScreen.kt index 44af9d81..beb90d76 100644 --- a/feature/onboarding/src/main/kotlin/nl/q42/template/onboarding/start/ui/OnboardingStartScreen.kt +++ b/feature/onboarding/src/main/kotlin/nl/q42/template/onboarding/start/ui/OnboardingStartScreen.kt @@ -10,13 +10,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import nl.q42.template.onboarding.start.presentation.OnboardingStartViewModel +import org.koin.androidx.compose.koinViewModel @Composable fun OnboardingStartScreen( - viewModel: OnboardingStartViewModel = hiltViewModel(), + viewModel: OnboardingStartViewModel = koinViewModel(), ) { val viewState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 820d502e..76d1285a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,12 +2,10 @@ jvmTarget = "21" kotlin = "2.2.20" # should match kotlin https://github.com/google/ksp/releases -ksp = "2.2.20-2.0.4" gradlePlugin = "8.11.2" googleServices = "4.4.4" crashlyticsPlugin = "3.0.6" firebaseBOM = "34.4.0" -hilt = "2.57.2" retrofit = "3.0.0" kotlinx-serialization = "1.9.0" retrofit2KotlinxSerializationConverter = "1.0.0" @@ -17,7 +15,6 @@ composeNavigation = "2.9.5" okhttp = "5.2.1" composePlatform = "2025.10.00" activityCompose = "1.11.0" -hiltNavigationCompose = "1.3.0" composeLifecycle = "2.9.4" # Test dependencies kotlinxCoroutinesTest = "1.10.2" @@ -25,11 +22,11 @@ junit = "4.13.2" mockkAndroid = "1.14.6" turbine = "1.2.1" composeStateEvents = "2.2.0" +koin = "4.1.1" [libraries] -hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } -hilt-ksp = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } junit = { module = "junit:junit", version.ref = "junit" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" } mockk-agent = { module = "io.mockk:mockk-agent", version.ref = "mockkAndroid" } mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockkAndroid" } @@ -48,19 +45,19 @@ composePlatform = { module = "androidx.compose:compose-bom", version.ref = "comp firebaseBoM = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBOM" } firebaseCrashlytics = { module = "com.google.firebase:firebase-crashlytics" } activityCompose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } -hiltNavigationCompose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } composeLifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "composeLifecycle" } turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } composeStateEvents = { module = "com.github.leonard-palm:compose-state-events", version.ref = "composeStateEvents" } composeNavigation = { module = "androidx.navigation:navigation-compose", version.ref = "composeNavigation" } +koin = { module = "io.insert-koin:koin-android", version.ref = "koin" } +koin-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" } +koin-test = { module = "io.insert-koin:koin-test-junit4", version.ref = "koin" } [plugins] androidApplication = { id = "com.android.application", version.ref = "gradlePlugin" } androidLibrary = { id = "com.android.library", version.ref = "gradlePlugin" } kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } -ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } googleServices = { id = "com.google.gms.google-services", version.ref = "googleServices" } firebaseCrashlyticsPlugin = { id = "com.google.firebase.crashlytics", version.ref = "crashlyticsPlugin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }