diff --git a/.gitignore b/.gitignore index 73b4ff2..08afcb7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ local.properties /.kotlin/ /.idea/ +/app/kotzilla.json +/app/src/main/assets/kotzilla.key diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b589d56..b86273d 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ffdfd69..05d8449 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,6 +8,8 @@ plugins { alias(libs.plugins.detekt) alias(libs.plugins.google.secrets.gradle.plugin) alias(libs.plugins.automattic.measure.builds) + alias(libs.plugins.allopen) + id("io.kotzilla.kotzilla-plugin") } apply("$rootDir/gradle/report.gradle") @@ -113,6 +115,10 @@ ksp { arg("KOIN_CONFIG_CHECK", "true") } +allOpen { + annotation("org.koin.core.annotation.Monitor") +} + dependencies { implementation(project(":core")) implementation(libs.kotlin.stdlib) @@ -147,6 +153,8 @@ dependencies { implementation(libs.koin.android) implementation(libs.koin.androidx.compose) implementation(libs.koin.androidx.startup) + implementation("io.kotzilla:kotzilla-sdk:1.2.3") + //implementation("io.kotzilla:kotzilla-sdk-compose:1.2.3") compileOnly(libs.koin.annotations.core) ksp(libs.koin.annotations.compiler) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9374963..7ce5aaf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - + @@ -24,8 +25,8 @@ + android:exported="true" + android:label="@string/title_activity_home_view" /> \ No newline at end of file diff --git a/app/src/main/java/com/santimattius/template/MainApplication.kt b/app/src/main/java/com/santimattius/template/MainApplication.kt index 552e0c5..e247c95 100644 --- a/app/src/main/java/com/santimattius/template/MainApplication.kt +++ b/app/src/main/java/com/santimattius/template/MainApplication.kt @@ -1,26 +1,40 @@ package com.santimattius.template import android.app.Application +import android.util.Log +import com.santimattius.core.CoreModule +import com.santimattius.template.di.AppModule +import com.santimattius.template.di.DataModule +import io.kotzilla.sdk.analytics.koin.analytics import org.koin.android.ext.koin.androidContext -import org.koin.android.logger.AndroidLogger import org.koin.androix.startup.KoinStartup +import org.koin.core.annotation.KoinApplication import org.koin.core.annotation.KoinExperimentalAPI import org.koin.dsl.KoinConfiguration -import org.koin.ksp.generated.com_santimattius_template_di_AppModule -import org.koin.ksp.generated.com_santimattius_template_di_DataModule -import org.koin.ksp.generated.defaultModule +import org.koin.dsl.module +import org.koin.ksp.generated.defineComSantimattiusTemplateUiComposeHomeComposeViewModel +import org.koin.ksp.generated.defineComSantimattiusTemplateUiXmlHomeHomeViewModel +import org.koin.ksp.generated.koinConfiguration + +@KoinApplication( + configurations = ["default"], + modules = [CoreModule::class, DataModule::class, AppModule::class] +) +object MainKoinApplication @OptIn(KoinExperimentalAPI::class) class MainApplication : Application(), KoinStartup { + override fun onKoinStartup(): KoinConfiguration { - return KoinConfiguration { + Log.d(this::class.simpleName, "onKoinStartup: ${Thread.currentThread().name}") + val configuration = MainKoinApplication.koinConfiguration { androidContext(this@MainApplication) - logger(AndroidLogger()) - modules( - com_santimattius_template_di_DataModule, - com_santimattius_template_di_AppModule, - defaultModule - ) + analytics() + modules(modules = module { + defineComSantimattiusTemplateUiComposeHomeComposeViewModel() + defineComSantimattiusTemplateUiXmlHomeHomeViewModel() + }) } + return KoinConfiguration(configuration) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/santimattius/template/di/AppModule.kt b/app/src/main/java/com/santimattius/template/di/AppModule.kt index 9280156..1553dc3 100644 --- a/app/src/main/java/com/santimattius/template/di/AppModule.kt +++ b/app/src/main/java/com/santimattius/template/di/AppModule.kt @@ -1,37 +1,21 @@ package com.santimattius.template.di -import android.content.Context -import com.santimattius.core.data.client.database.TheMovieDataBase -import com.santimattius.core.data.client.network.RetrofitServiceCreator -import com.santimattius.core.data.client.network.TheMovieDBService import com.santimattius.template.BuildConfig +import org.koin.core.annotation.Configuration import org.koin.core.annotation.Module import org.koin.core.annotation.Named -import org.koin.core.annotation.Singleton +import org.koin.core.annotation.Single @Module +@Configuration class AppModule { - @Singleton - fun provideAppDatabase(context: Context): TheMovieDataBase = - TheMovieDataBase.get(context) - - @Singleton - fun provideMovieDBService(serviceCreator: RetrofitServiceCreator): TheMovieDBService = - serviceCreator.createService(TheMovieDBService::class.java) - - - @Singleton - fun provideRetrofit(@Named("base_url") baseUrl: String): RetrofitServiceCreator { - return RetrofitServiceCreator( - baseUrl = baseUrl, - apiKey = BuildConfig.apiKey - ) - } - - @Named("base_url") - @Singleton - @Suppress("FunctionOnlyReturningConstant") + @Single(createdAtStart = true) + @Named("baseUrl") fun provideBaseUrl() = "https://api.themoviedb.org" + + @Single(createdAtStart = true) + @Named("apiKey") + fun provideApiKey() = BuildConfig.apiKey } \ No newline at end of file diff --git a/app/src/main/java/com/santimattius/template/di/DataModule.kt b/app/src/main/java/com/santimattius/template/di/DataModule.kt index 7661c2e..b5e60b5 100644 --- a/app/src/main/java/com/santimattius/template/di/DataModule.kt +++ b/app/src/main/java/com/santimattius/template/di/DataModule.kt @@ -8,13 +8,15 @@ import com.santimattius.core.data.datasources.implementation.RetrofitMovieNetwor import com.santimattius.core.data.datasources.implementation.RoomMovieLocalDataSource import com.santimattius.core.data.repositories.TMDbRepository import com.santimattius.core.domain.repositories.MovieRepository +import org.koin.core.annotation.Configuration import org.koin.core.annotation.Module -import org.koin.core.annotation.Singleton +import org.koin.core.annotation.Single @Module +@Configuration class DataModule { - @Singleton + @Single fun provideMovieRepository( movieNetworkDataSource: MovieNetworkDataSource, movieLocalDataSource: MovieLocalDataSource, @@ -23,12 +25,12 @@ class DataModule { movieLocalDataSource = movieLocalDataSource ) - @Singleton + @Single fun provideLocalDataSource(theMovieDataBase: TheMovieDataBase): MovieLocalDataSource { return RoomMovieLocalDataSource(theMovieDataBase = theMovieDataBase) } - @Singleton + @Single fun provideRemoteDataSource(service: TheMovieDBService): MovieNetworkDataSource { return RetrofitMovieNetworkDataSource(service = service) } diff --git a/app/src/main/java/com/santimattius/template/ui/compose/HomeComposeActivity.kt b/app/src/main/java/com/santimattius/template/ui/compose/HomeComposeActivity.kt index 746fa70..72f6e4f 100644 --- a/app/src/main/java/com/santimattius/template/ui/compose/HomeComposeActivity.kt +++ b/app/src/main/java/com/santimattius/template/ui/compose/HomeComposeActivity.kt @@ -40,6 +40,7 @@ import com.santimattius.template.ui.compose.ui.components.snackbar.SnackBarVisua import com.santimattius.template.ui.compose.ui.theme.AndroidTestingTheme import com.santimattius.template.ui.xml.home.HomeViewActivity import org.koin.androidx.compose.koinViewModel +import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.compose.LocalKoinApplication import org.koin.compose.LocalKoinScope import org.koin.core.annotation.KoinInternalApi @@ -47,6 +48,7 @@ import org.koin.mp.KoinPlatformTools class HomeComposeActivity : ComponentActivity() { + private val viewModel: HomeComposeViewModel by viewModel() @OptIn(KoinInternalApi::class) override fun onCreate(savedInstanceState: Bundle?) { @@ -54,18 +56,19 @@ class HomeComposeActivity : ComponentActivity() { setContent { // This shouldn't be needed, but allows robolectric tests to run successfully // TODO remove once a solution is found or a fix in koin? - CompositionLocalProvider( - LocalKoinScope provides KoinPlatformTools.defaultContext() - .get().scopeRegistry.rootScope, - LocalKoinApplication provides KoinPlatformTools.defaultContext().get() - ) { - AndroidTestingTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - HomeRoute() - } +// CompositionLocalProvider( +// LocalKoinScope provides KoinPlatformTools.defaultContext() +// .get().scopeRegistry.rootScope, +// LocalKoinApplication provides KoinPlatformTools.defaultContext().get() +// ) { +// +// } + AndroidTestingTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + HomeRoute(viewModel) } } } @@ -74,7 +77,7 @@ class HomeComposeActivity : ComponentActivity() { @Composable fun HomeRoute( - viewModel: HomeComposeViewModel = koinViewModel() + viewModel: HomeComposeViewModel ) { val snackBarHostState = remember { SnackbarHostState() } Scaffold( diff --git a/app/src/main/java/com/santimattius/template/ui/compose/HomeComposeViewModel.kt b/app/src/main/java/com/santimattius/template/ui/compose/HomeComposeViewModel.kt index 31e5d14..7970f6b 100644 --- a/app/src/main/java/com/santimattius/template/ui/compose/HomeComposeViewModel.kt +++ b/app/src/main/java/com/santimattius/template/ui/compose/HomeComposeViewModel.kt @@ -17,8 +17,12 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.android.annotation.KoinViewModel +import org.koin.core.annotation.Configuration +import org.koin.core.annotation.Monitor @KoinViewModel +@Monitor +@Configuration class HomeComposeViewModel( private val movieRepository: MovieRepository, ) : ViewModel() { @@ -29,7 +33,7 @@ class HomeComposeViewModel( if (newMovies != state.movies) { state.copy(movies = newMovies, isLoading = false) } else { - state // Si no ha cambiado nada, retorna el mismo estado + state } }.onStart { movieRepository.refresh() diff --git a/build.gradle.kts b/build.gradle.kts index 7c820fd..033a144 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,11 +8,13 @@ plugins { alias(libs.plugins.detekt) apply false alias(libs.plugins.google.secrets.gradle.plugin) apply false alias(libs.plugins.automattic.measure.builds) apply false + alias(libs.plugins.allopen) apply false alias(libs.plugins.room) apply false } buildscript { dependencies { classpath(libs.dep.google.secrets.gradle.plugin) + classpath("io.kotzilla:kotzilla-plugin:1.2.3") } } \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 74f026a..c9f30c4 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -72,6 +72,8 @@ dependencies { implementation(platform(libs.koin.bom)) implementation(libs.koin.android) implementation(libs.koin.androidx.compose) + compileOnly(libs.koin.annotations.core) + ksp(libs.koin.annotations.compiler) testImplementation(project(path = ":shared-test")) testImplementation(libs.bundles.unitTesting) diff --git a/core/src/main/java/com/santimattius/core/CoreModule.kt b/core/src/main/java/com/santimattius/core/CoreModule.kt new file mode 100644 index 0000000..f3b84c4 --- /dev/null +++ b/core/src/main/java/com/santimattius/core/CoreModule.kt @@ -0,0 +1,46 @@ +package com.santimattius.core + +import android.content.Context +import com.santimattius.core.data.client.database.TheMovieDataBase +import com.santimattius.core.data.client.network.RequestInterceptor +import com.santimattius.core.data.client.network.TheMovieDBService +import okhttp3.OkHttpClient +import org.koin.core.annotation.Configuration +import org.koin.core.annotation.Module +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +@Module +@Configuration +class CoreModule { + + @Single(createdAtStart = true) + fun provideOkHttpClient(@Named("apiKey") apiKey: String): OkHttpClient { + return OkHttpClient().newBuilder() + .addInterceptor(RequestInterceptor(apiKey)) + .build() + } + + @Single(createdAtStart = true) + fun provideRetrofit( + @Named("baseUrl") baseUrl: String, + client: OkHttpClient + ): Retrofit { + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + @Single(createdAtStart = true) + fun provideService(retrofit: Retrofit): TheMovieDBService { + return retrofit.create(TheMovieDBService::class.java) + } + + @Single(createdAtStart = true) + fun provideAppDatabase(context: Context): TheMovieDataBase = + TheMovieDataBase.get(context) +} \ No newline at end of file diff --git a/core/src/main/java/com/santimattius/core/data/client/database/TheMovieDataBase.kt b/core/src/main/java/com/santimattius/core/data/client/database/TheMovieDataBase.kt index 4197c26..b04cf97 100644 --- a/core/src/main/java/com/santimattius/core/data/client/database/TheMovieDataBase.kt +++ b/core/src/main/java/com/santimattius/core/data/client/database/TheMovieDataBase.kt @@ -13,7 +13,17 @@ abstract class TheMovieDataBase : RoomDatabase() { private const val DATABASE_NAME = "tmdb_database" - fun get(context: Context) = + /*@Volatile + private var INSTANCE: TheMovieDataBase? = null + + fun get(context: Context) = INSTANCE ?: synchronized(this) { + INSTANCE ?: create(context).also { INSTANCE = it } + } + */ + + fun get(context: Context) = create(context) + + private fun create(context: Context) = Room.databaseBuilder(context, TheMovieDataBase::class.java, DATABASE_NAME) .build() } diff --git a/core/src/main/java/com/santimattius/core/data/client/network/RetrofitServiceCreator.kt b/core/src/main/java/com/santimattius/core/data/client/network/RetrofitServiceCreator.kt index 4d1f1bd..0a2f173 100644 --- a/core/src/main/java/com/santimattius/core/data/client/network/RetrofitServiceCreator.kt +++ b/core/src/main/java/com/santimattius/core/data/client/network/RetrofitServiceCreator.kt @@ -4,16 +4,24 @@ import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +//TODO:only for testing class RetrofitServiceCreator(baseUrl: String, apiKey: String) { - val client = OkHttpClient().newBuilder() - .addInterceptor(RequestInterceptor(apiKey)) - .build() - private val retrofit: Retrofit = Retrofit.Builder() - .baseUrl(baseUrl) - .client(client) - .addConverterFactory(GsonConverterFactory.create()) - .build() + // Use 'by lazy' for client + val client: OkHttpClient by lazy { // <--- Made lazy + OkHttpClient().newBuilder() + .addInterceptor(RequestInterceptor(apiKey)) + .build() + } + + // Use 'by lazy' for retrofit instance + private val retrofit: Retrofit by lazy { // <--- Made lazy + Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) // 'client' will be initialized on first access here + .addConverterFactory(GsonConverterFactory.create()) + .build() + } fun createService(serviceClass: Class): T { return retrofit.create(serviceClass) diff --git a/gradle.properties b/gradle.properties index f237e40..2be2282 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,5 +27,5 @@ android.nonFinalResIds=false application_id=com.santimattius.entertainment min_sdk_version=24 target_sdk_version=36 -version_code=1 -version_name=1.0 \ No newline at end of file +version_code=2 +version_name=1.3.2 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8adc0f3..9006a85 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] # Plugins -androidGradlePlugin = "8.12.2" +androidGradlePlugin = "8.13.0" hamcrest = "3.0" -kotlin = "2.2.10" +kotlin = "2.2.20" detektGradlePlugin = "1.23.8" -ksp = "2.2.10-2.0.2" +ksp = "2.2.20-2.0.2" googleSecretsPlugin = "2.0.1" automatticMeasureBuilds = "3.2.1" @@ -15,23 +15,23 @@ appCompat = "1.7.1" fragmentKtx = "1.8.9" constraintLayout = "2.2.1" recyclerView = "1.4.0" -materialVersion = "1.12.0" -lifecycle = "2.9.3" +materialVersion = "1.13.0" +lifecycle = "2.9.4" retrofit = "3.0.0" -okHttp = "5.1.0" +okHttp = "5.2.0" coroutine = "1.10.2" -gson = "2.13.1" -glide = "5.0.0-rc01" +gson = "2.13.2" +glide = "5.0.5" coil = "2.7.0" -room = "2.7.2" +room = "2.8.1" -androidxComposeBom = "2025.08.01" -activityCompose = "1.10.1" +androidxComposeBom = "2025.09.01" +activityCompose = "1.11.0" -koinBom = "4.1.0" -koinAnnotations = "2.1.0" +koinBom = "4.1.1" +koinAnnotations = "2.2.0" #Testing junit = "4.13.2" @@ -44,10 +44,10 @@ fragmentTesting = "1.8.9" espressoCore = "3.7.0" okhttp3IdlingResource = "1.0.0" -mockk = "1.14.5" +mockk = "1.14.6" robolectric = "4.16" turbine = "1.2.1" -mockitoKotlin = "6.0.0" +mockitoKotlin = "6.1.0" [libraries] # Define the libraries @@ -153,6 +153,7 @@ kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } room = { id = "androidx.room", version.ref = "room"} ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +allopen = { id = "org.jetbrains.kotlin.plugin.allopen", version.ref = "kotlin" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detektGradlePlugin" } google-secrets-gradle-plugin = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "googleSecretsPlugin" } automattic-measure-builds = { id = "com.automattic.android.measure-builds", version.ref = "automatticMeasureBuilds" }