Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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'
}
}

Expand Down Expand Up @@ -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") {
Expand Down
20 changes: 7 additions & 13 deletions app/src/main/kotlin/nl/q42/template/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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?) {
Expand All @@ -47,7 +42,6 @@ class MainActivity : ComponentActivity() {

Napier.d { "onCreate received, ${intent.data}" }


setContent {

val snackbarHostState = remember { SnackbarHostState() }
Expand Down Expand Up @@ -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()
Expand Down
9 changes: 5 additions & 4 deletions app/src/main/kotlin/nl/q42/template/MainApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -27,8 +26,10 @@ class MainApplication : Application() {
.build()
)
} else {
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true)
FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = true
Napier.base(CrashlyticsLogger())
}

initDependencyInjection(this)
}
}
48 changes: 48 additions & 0 deletions app/src/main/kotlin/nl/q42/template/di/AppModule.kt
Original file line number Diff line number Diff line change
@@ -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
)
}
51 changes: 12 additions & 39 deletions app/src/main/kotlin/nl/q42/template/di/ConfigModule.kt
Original file line number Diff line number Diff line change
@@ -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) }
}
11 changes: 6 additions & 5 deletions app/src/main/kotlin/nl/q42/template/navigation/HomeGraph.kt
Original file line number Diff line number Diff line change
@@ -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<Destination.HomeGraph>(startDestination = Destination.Home) {
composable<Destination.Home> {

val viewModel: HomeViewModel = hiltViewModel()
val viewModel: HomeViewModel = koinViewModel()
InitNavigator(navController = navController, routeNavigator = viewModel)

HomeScreen(viewModel = viewModel)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Destination.Onboarding> {

val viewModel: OnboardingStartViewModel = hiltViewModel()
val viewModel: OnboardingStartViewModel = koinViewModel()
InitNavigator(navController = navController, viewModel)

OnboardingStartScreen(viewModel = viewModel)
Expand Down
33 changes: 33 additions & 0 deletions app/src/test/kotlin/nl/q42/template/di/KoinCheckModulesTest.kt
Original file line number Diff line number Diff line change
@@ -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,
)
)
}
}
1 change: 1 addition & 0 deletions build.dep.compose.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ dependencies {
implementation(libs.composeMaterial3)
implementation(libs.composeLifecycle)
implementation(libs.composeStateEvents)
implementation(libs.koin.compose)
}
8 changes: 1 addition & 7 deletions build.dep.di.gradle
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 0 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion build.module.feature-and-app.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<RouteNavigator>() }
}
Loading