From 4f88eb423d34d5cafe32f8d85db2a323308c4718 Mon Sep 17 00:00:00 2001 From: Frank1234 Date: Mon, 13 Oct 2025 14:32:18 +0200 Subject: [PATCH 1/3] CHANGE hilt to koin --- app/build.gradle | 5 + .../kotlin/nl/q42/template/MainActivity.kt | 20 ++-- .../kotlin/nl/q42/template/MainApplication.kt | 9 +- .../kotlin/nl/q42/template/di/AppModule.kt | 43 ++++++++ .../kotlin/nl/q42/template/di/ConfigModule.kt | 51 +++------- .../nl/q42/template/navigation/HomeGraph.kt | 11 ++- .../navigation/OnboardingDestinations.kt | 4 +- .../q42/template/di/KoinCheckModulesTest.kt | 27 +++++ build.dep.compose.gradle | 1 + build.dep.di.gradle | 4 +- build.gradle | 1 - build.module.feature-and-app.gradle | 2 +- .../template/navigation/di/ConfigModule.kt | 18 ---- .../navigation/di/NavigationModule.kt | 11 +++ .../template/core/network/di/NetworkModule.kt | 99 ++++++++++--------- .../interceptor/UserAgentHeaderInterceptor.kt | 18 ++-- .../template/ui/compose/MainCoroutineScope.kt | 6 ++ .../q42/template/ui/di/PresentationModule.kt | 37 +++---- ...nackbarManager.kt => SnackbarPresenter.kt} | 13 ++- .../ui/presentation/dialog/DialogPresenter.kt | 5 +- .../core/utils/config/ConfigModels.kt | 16 +++ .../core/utils/di/QualifierAnnotations.kt | 23 ----- .../template/data/main/UserRepositoryImpl.kt | 3 +- .../q42/template/data/main/di/DataModule.kt | 25 +++++ .../template/data/main/di/MainDataModule.kt | 32 ------ .../data/main/local/UserLocalDataSource.kt | 10 +- .../main/remote/{UserApi.kt => MainApi.kt} | 2 +- .../data/main/remote/UserRemoteDataSource.kt | 9 +- .../template/domain/main/di/DomainModule.kt | 12 +++ .../domain/main/usecase/FetchUserUseCase.kt | 3 +- .../domain/main/usecase/GetUserFlowUseCase.kt | 3 +- .../nl/q42/template/home/di/HomeModule.kt | 12 +++ .../home/main/presentation/HomeViewModel.kt | 13 +-- .../q42/template/home/main/ui/HomeScreen.kt | 4 +- .../presentation/HomeSecondViewModel.kt | 5 +- .../home/second/ui/HomeSecondScreen.kt | 4 +- .../main/presentation/HomeViewModelTest.kt | 4 +- .../onboarding/di/OnboardingModule.kt | 10 ++ .../presentation/OnboardingStartViewModel.kt | 7 +- .../start/ui/OnboardingStartScreen.kt | 4 +- gradle/libs.versions.toml | 11 +-- 41 files changed, 318 insertions(+), 279 deletions(-) create mode 100644 app/src/main/kotlin/nl/q42/template/di/AppModule.kt create mode 100644 app/src/test/kotlin/nl/q42/template/di/KoinCheckModulesTest.kt delete mode 100644 core/navigation/src/main/kotlin/nl/q42/template/navigation/di/ConfigModule.kt create mode 100644 core/navigation/src/main/kotlin/nl/q42/template/navigation/di/NavigationModule.kt create mode 100644 core/ui/src/main/kotlin/nl/q42/template/ui/compose/MainCoroutineScope.kt rename core/ui/src/main/kotlin/nl/q42/template/ui/presentation/{SnackbarManager.kt => SnackbarPresenter.kt} (85%) create mode 100644 core/utils/src/main/kotlin/nl/q42/template/core/utils/config/ConfigModels.kt delete mode 100644 core/utils/src/main/kotlin/nl/q42/template/core/utils/di/QualifierAnnotations.kt create mode 100644 data/main/src/main/kotlin/nl/q42/template/data/main/di/DataModule.kt delete mode 100644 data/main/src/main/kotlin/nl/q42/template/data/main/di/MainDataModule.kt rename data/main/src/main/kotlin/nl/q42/template/data/main/remote/{UserApi.kt => MainApi.kt} (94%) create mode 100644 domain/main/src/main/kotlin/nl/q42/template/domain/main/di/DomainModule.kt create mode 100644 feature/home/src/main/kotlin/nl/q42/template/home/di/HomeModule.kt create mode 100644 feature/onboarding/src/main/kotlin/nl/q42/template/onboarding/di/OnboardingModule.kt diff --git a/app/build.gradle b/app/build.gradle index 2e614af7..dfd70b63 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -86,4 +86,9 @@ dependencies { api platform(libs.firebaseBoM) implementation(libs.firebaseCrashlytics) + + testImplementation libs.junit + testImplementation libs.kotlin.test + testImplementation libs.koin.test + } 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..ce82c67e --- /dev/null +++ b/app/src/main/kotlin/nl/q42/template/di/AppModule.kt @@ -0,0 +1,43 @@ +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 + +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..2ad784ad --- /dev/null +++ b/app/src/test/kotlin/nl/q42/template/di/KoinCheckModulesTest.kt @@ -0,0 +1,27 @@ +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 { + + @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..aae5c0d9 100644 --- a/build.dep.di.gradle +++ b/build.dep.di.gradle @@ -1,9 +1,7 @@ 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..5288124c 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +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 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..70b47e65 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,29 @@ 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 { +val presentationModule = module { + single { + provideAccessibilityManager(get()) + } - @Singleton - @Provides - fun provideAccessibilityManager(application: Application): AccessibilityManager = - application.getSystemService( - Application.ACCESSIBILITY_SERVICE - ) as AccessibilityManager - - @Provides - fun providesDialogPresenter(dialogPresenter: DialogPresenterImpl): DialogPresenter = - dialogPresenter + factory { 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 4b0373a9..cba55404 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..0ac131e1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,6 @@ 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 +16,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 +23,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,18 +46,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" } From 18dae6cb450ab21febfdeaa6bf7d2461a2f5cc45 Mon Sep 17 00:00:00 2001 From: Frank1234 Date: Wed, 29 Oct 2025 13:30:10 +0100 Subject: [PATCH 2/3] CHANGE DI improvements --- app/build.gradle | 4 ++-- app/src/main/kotlin/nl/q42/template/di/AppModule.kt | 5 +++++ .../test/kotlin/nl/q42/template/di/KoinCheckModulesTest.kt | 6 ++++++ .../main/kotlin/nl/q42/template/ui/di/PresentationModule.kt | 3 +-- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f80277e8..4524bf91 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' } } diff --git a/app/src/main/kotlin/nl/q42/template/di/AppModule.kt b/app/src/main/kotlin/nl/q42/template/di/AppModule.kt index ce82c67e..19a4f833 100644 --- a/app/src/main/kotlin/nl/q42/template/di/AppModule.kt +++ b/app/src/main/kotlin/nl/q42/template/di/AppModule.kt @@ -13,6 +13,11 @@ 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() diff --git a/app/src/test/kotlin/nl/q42/template/di/KoinCheckModulesTest.kt b/app/src/test/kotlin/nl/q42/template/di/KoinCheckModulesTest.kt index 2ad784ad..a27ba3dc 100644 --- a/app/src/test/kotlin/nl/q42/template/di/KoinCheckModulesTest.kt +++ b/app/src/test/kotlin/nl/q42/template/di/KoinCheckModulesTest.kt @@ -9,6 +9,12 @@ 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`() { 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 70b47e65..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 @@ -17,8 +17,7 @@ val presentationModule = module { provideAccessibilityManager(get()) } - factory { MainCoroutineScope(MainScope()) } - + single { MainCoroutineScope(MainScope()) } singleOf(::DialogPresenterImpl) { bind() } singleOf(::SnackbarPresenter) } From 176c0b696cb18738438e4615f72d4ca4474b431e Mon Sep 17 00:00:00 2001 From: Frank1234 Date: Wed, 29 Oct 2025 14:52:51 +0100 Subject: [PATCH 3/3] REMOVE ksp --- build.dep.di.gradle | 4 ---- build.gradle | 1 - gradle/libs.versions.toml | 2 -- 3 files changed, 7 deletions(-) diff --git a/build.dep.di.gradle b/build.dep.di.gradle index aae5c0d9..770bec8a 100644 --- a/build.dep.di.gradle +++ b/build.dep.di.gradle @@ -1,7 +1,3 @@ -apply { - plugin(libs.plugins.ksp.get().getPluginId()) -} - dependencies { implementation(libs.koin) } diff --git a/build.gradle b/build.gradle index 5288124c..fec2e1f4 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +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.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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0ac131e1..76d1285a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,6 @@ 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" @@ -59,7 +58,6 @@ androidApplication = { id = "com.android.application", version.ref = "gradlePlug 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" } -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" }