Separate Hilt and Koin into their own product flavours

This commit is contained in:
Gergely Hegedus 2021-09-18 21:10:35 +03:00
parent 682fd71c2d
commit 1b8d0e836c
56 changed files with 496 additions and 72 deletions

View file

@ -2,6 +2,8 @@ plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
// hilt specific
id 'dagger.hilt.android.plugin'
}
android {
@ -25,6 +27,17 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
flavorDimensions 'di'
productFlavors {
hilt {
dimension 'di'
applicationId "org.fnives.test.showcase.hilt"
}
koin {
dimension 'di'
applicationId "org.fnives.test.showcase.koin"
}
}
buildFeatures {
viewBinding true
@ -49,10 +62,17 @@ android {
}
}
hilt {
enableAggregatingTask = true
enableExperimentalClasspathAggregation = true
}
afterEvaluate {
// making sure the :mockserver is assembled after :clean when running tests
testDebugUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
testReleaseUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
testKoinDebugUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
testKoinReleaseUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
testHiltDebugUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
testHiltReleaseUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
}
dependencies {
@ -66,7 +86,14 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:$androidx_swiperefreshlayout_version"
// Koin
implementation "io.insert-koin:koin-android:$koin_version"
koinImplementation "io.insert-koin:koin-android:$koin_version"
// Hilt
// hiltImplementation "com.google.dagger:hilt-android:$hilt_version"
implementation "com.google.dagger:hilt-android:$hilt_version"
// implementation "com.google.dagger:hilt-core:$hilt_version"
kaptHilt "com.google.dagger:hilt-compiler:$hilt_version"
hiltImplementation "androidx.activity:activity-ktx:$activity_ktx_version"
implementation "androidx.room:room-runtime:$androidx_room_version"
kapt "androidx.room:room-compiler:$androidx_room_version"
@ -98,6 +125,8 @@ dependencies {
testImplementation "com.jakewharton.espresso:okhttp3-idling-resource:$testing_okhttp3_idling_resource_version"
testImplementation "androidx.arch.core:core-testing:$testing_androidx_arch_core_version"
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$testing_junit5_version"
testImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptTest "com.google.dagger:hilt-compiler:$hilt_version"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
androidTestImplementation "io.insert-koin:koin-test-junit4:$koin_version"
@ -111,4 +140,6 @@ dependencies {
androidTestImplementation "com.jakewharton.espresso:okhttp3-idling-resource:$testing_okhttp3_idling_resource_version"
androidTestImplementation "androidx.arch.core:core-testing:$testing_androidx_arch_core_version"
androidTestRuntimeOnly "org.junit.vintage:junit-vintage-engine:$testing_junit5_version"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptAndroidTest "com.google.dagger:hilt-compiler:$hilt_version"
}

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.fnives.test.showcase">
<uses-permission android:name="android.permission.INTERNET" />
<application>
<activity
android:name=".ui.splash.HiltSplashActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ui.home.HiltMainActivity" />
<activity android:name=".ui.auth.HiltAuthActivity" />
</application>
</manifest>

View file

@ -0,0 +1,13 @@
package org.fnives.test.showcase
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class TestShowcaseApplication : Application() {
override fun onCreate() {
super.onCreate()
}
}

View file

@ -0,0 +1,53 @@
package org.fnives.test.showcase.di
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.fnives.test.showcase.core.session.SessionExpirationListener
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.session.SessionExpirationListenerImpl
import org.fnives.test.showcase.storage.SharedPreferencesManagerImpl
import org.fnives.test.showcase.storage.database.DatabaseInitialization
import org.fnives.test.showcase.storage.favourite.FavouriteContentLocalStorageImpl
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
object AppModule {
@Provides
fun provideBaseUrl(): String = BaseUrlProvider.get().baseUrl
@Provides
fun enableLogging(): Boolean = true
@Singleton
@Provides
fun provideFavouriteDao(@ApplicationContext context: Context) =
DatabaseInitialization.create(context).favouriteDao
@Provides
fun provideSharedPreferencesManagerImpl(@ApplicationContext context: Context) =
SharedPreferencesManagerImpl.create(context)
@Singleton
@Provides
fun provideUserDataLocalStorage(
sharedPreferencesManagerImpl: SharedPreferencesManagerImpl
): UserDataLocalStorage = sharedPreferencesManagerImpl
@Provides
fun provideFavouriteContentLocalStorage(
favouriteContentLocalStorageImpl: FavouriteContentLocalStorageImpl
): FavouriteContentLocalStorage = favouriteContentLocalStorageImpl
@Provides
internal fun bindSessionExpirationListener(
sessionExpirationListenerImpl: SessionExpirationListenerImpl
) : SessionExpirationListener = sessionExpirationListenerImpl
}

View file

@ -0,0 +1,14 @@
package org.fnives.test.showcase.ui
import android.content.Context
import org.fnives.test.showcase.ui.auth.HiltAuthActivity
import org.fnives.test.showcase.ui.home.HiltMainActivity
object IntentCoordinator {
fun mainActivitygetStartIntent(context: Context) =
HiltMainActivity.getStartIntent(context)
fun authActivitygetStartIntent(context: Context) =
HiltAuthActivity.getStartIntent(context)
}

View file

@ -0,0 +1,12 @@
package org.fnives.test.showcase.ui
import androidx.activity.ComponentActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelStoreOwner
import androidx.activity.viewModels as androidxViewModel
inline fun <reified T : ViewModel> ViewModelStoreOwner.viewModels(): Lazy<T> =
when (this) {
is ComponentActivity -> androidxViewModel()
else -> throw IllegalStateException("Only supports activity viewModel for now")
}

View file

@ -0,0 +1,12 @@
package org.fnives.test.showcase.ui.auth
import android.content.Context
import android.content.Intent
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class HiltAuthActivity : AuthActivity() {
companion object {
fun getStartIntent(context: Context): Intent = Intent(context, HiltAuthActivity::class.java)
}
}

View file

@ -0,0 +1,12 @@
package org.fnives.test.showcase.ui.home
import android.content.Context
import android.content.Intent
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class HiltMainActivity : MainActivity() {
companion object {
fun getStartIntent(context: Context): Intent = Intent(context, HiltMainActivity::class.java)
}
}

View file

@ -0,0 +1,6 @@
package org.fnives.test.showcase.ui.splash
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class HiltSplashActivity : SplashActivity()

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.fnives.test.showcase">
<uses-permission android:name="android.permission.INTERNET" />
<application>
<activity
android:name=".ui.splash.SplashActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ui.home.MainActivity" />
<activity android:name=".ui.auth.AuthActivity" />
</application>
</manifest>

View file

@ -1,6 +1,6 @@
package org.fnives.test.showcase.di
import org.fnives.test.showcase.core.di.createCoreModule
import org.fnives.test.showcase.core.di.koin.createCoreModule
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.session.SessionExpirationListenerImpl
import org.fnives.test.showcase.storage.LocalDatabase

View file

@ -0,0 +1,14 @@
package org.fnives.test.showcase.ui
import android.content.Context
import org.fnives.test.showcase.ui.auth.AuthActivity
import org.fnives.test.showcase.ui.home.MainActivity
object IntentCoordinator {
fun mainActivitygetStartIntent(context: Context) =
MainActivity.getStartIntent(context)
fun authActivitygetStartIntent(context: Context) =
AuthActivity.getStartIntent(context)
}

View file

@ -0,0 +1,8 @@
package org.fnives.test.showcase.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelStoreOwner
import org.koin.androidx.viewmodel.ext.android.viewModel
inline fun <reified T : ViewModel> ViewModelStoreOwner.viewModels(): Lazy<T> =
viewModel()

View file

@ -6,24 +6,13 @@
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".TestShowcaseApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:name=".TestShowcaseApplication"
android:theme="@style/Theme.TestShowCase"
tools:ignore="AllowBackup">
<activity android:name=".ui.splash.SplashActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ui.home.MainActivity"/>
<activity android:name=".ui.auth.AuthActivity"/>
</application>
tools:ignore="AllowBackup"/>
</manifest>

View file

@ -4,15 +4,20 @@ import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import dagger.hilt.android.qualifiers.ApplicationContext
import org.fnives.test.showcase.core.session.SessionExpirationListener
import org.fnives.test.showcase.ui.auth.AuthActivity
import org.fnives.test.showcase.ui.IntentCoordinator
import javax.inject.Inject
class SessionExpirationListenerImpl(private val context: Context) : SessionExpirationListener {
class SessionExpirationListenerImpl @Inject constructor(
@ApplicationContext
private val context: Context
) : SessionExpirationListener {
override fun onSessionExpired() {
Handler(Looper.getMainLooper()).post {
context.startActivity(
AuthActivity.getStartIntent(context)
IntentCoordinator.authActivitygetStartIntent(context)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)

View file

@ -7,7 +7,7 @@ import org.fnives.test.showcase.model.session.Session
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class SharedPreferencesManagerImpl(private val sharedPreferences: SharedPreferences) : UserDataLocalStorage {
class SharedPreferencesManagerImpl constructor(private val sharedPreferences: SharedPreferences) : UserDataLocalStorage {
override var session: Session? by SessionDelegate(SESSION_KEY)

View file

@ -4,8 +4,9 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId
import javax.inject.Inject
class FavouriteContentLocalStorageImpl(private val favouriteDao: FavouriteDao) : FavouriteContentLocalStorage {
class FavouriteContentLocalStorageImpl @Inject constructor(private val favouriteDao: FavouriteDao) : FavouriteContentLocalStorage {
override fun observeFavourites(): Flow<List<ContentId>> =
favouriteDao.get().map { it.map(FavouriteEntity::contentId).map(::ContentId) }

View file

@ -9,12 +9,12 @@ import androidx.core.widget.doAfterTextChanged
import com.google.android.material.snackbar.Snackbar
import org.fnives.test.showcase.R
import org.fnives.test.showcase.databinding.ActivityAuthenticationBinding
import org.fnives.test.showcase.ui.home.MainActivity
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.fnives.test.showcase.ui.IntentCoordinator
import org.fnives.test.showcase.ui.viewModels
class AuthActivity : AppCompatActivity() {
open class AuthActivity : AppCompatActivity() {
private val viewModel by viewModel<AuthViewModel>()
private val viewModel by viewModels<AuthViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -35,7 +35,7 @@ class AuthActivity : AppCompatActivity() {
}
viewModel.navigateToHome.observe(this) {
it.consume() ?: return@observe
startActivity(MainActivity.getStartIntent(this))
startActivity(IntentCoordinator.mainActivitygetStartIntent(this))
finishAffinity()
}
setContentView(binding.root)

View file

@ -4,14 +4,17 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import org.fnives.test.showcase.core.login.LoginUseCase
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.auth.LoginStatus
import org.fnives.test.showcase.model.shared.Answer
import org.fnives.test.showcase.ui.shared.Event
import javax.inject.Inject
class AuthViewModel(private val loginUseCase: LoginUseCase) : ViewModel() {
@HiltViewModel
class AuthViewModel @Inject constructor(private val loginUseCase: LoginUseCase) : ViewModel() {
private val _username = MutableLiveData<String>()
val username: LiveData<String> = _username

View file

@ -6,17 +6,18 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import org.fnives.test.showcase.R
import org.fnives.test.showcase.databinding.ActivityMainBinding
import org.fnives.test.showcase.model.content.ContentId
import org.fnives.test.showcase.ui.auth.AuthActivity
import org.fnives.test.showcase.ui.IntentCoordinator
import org.fnives.test.showcase.ui.shared.VerticalSpaceItemDecoration
import org.fnives.test.showcase.ui.shared.getThemePrimaryColor
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.fnives.test.showcase.ui.viewModels
class MainActivity : AppCompatActivity() {
open class MainActivity : AppCompatActivity() {
private val viewModel by viewModel<MainViewModel>()
private val viewModel by viewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -45,7 +46,7 @@ class MainActivity : AppCompatActivity() {
}
viewModel.navigateToAuth.observe(this) {
it.consume() ?: return@observe
startActivity(AuthActivity.getStartIntent(this))
startActivity(IntentCoordinator.authActivitygetStartIntent(this))
finishAffinity()
}
viewModel.loading.observe(this) {

View file

@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.fnives.test.showcase.core.content.AddContentToFavouriteUseCase
@ -16,8 +17,10 @@ import org.fnives.test.showcase.model.content.ContentId
import org.fnives.test.showcase.model.content.FavouriteContent
import org.fnives.test.showcase.model.shared.Resource
import org.fnives.test.showcase.ui.shared.Event
import javax.inject.Inject
class MainViewModel(
@HiltViewModel
class MainViewModel @Inject constructor(
private val getAllContentUseCase: GetAllContentUseCase,
private val logoutUseCase: LogoutUseCase,
private val fetchContentUseCase: FetchContentUseCase,

View file

@ -3,21 +3,20 @@ package org.fnives.test.showcase.ui.splash
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.fnives.test.showcase.R
import org.fnives.test.showcase.ui.auth.AuthActivity
import org.fnives.test.showcase.ui.home.MainActivity
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.fnives.test.showcase.ui.IntentCoordinator
import org.fnives.test.showcase.ui.viewModels
class SplashActivity : AppCompatActivity() {
open class SplashActivity : AppCompatActivity() {
private val viewModel by viewModel<SplashViewModel>()
private val viewModel by viewModels<SplashViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
viewModel.navigateTo.observe(this) {
val intent = when (it.consume()) {
SplashViewModel.NavigateTo.HOME -> MainActivity.getStartIntent(this)
SplashViewModel.NavigateTo.AUTHENTICATION -> AuthActivity.getStartIntent(this)
SplashViewModel.NavigateTo.HOME -> IntentCoordinator.mainActivitygetStartIntent(this)
SplashViewModel.NavigateTo.AUTHENTICATION -> IntentCoordinator.authActivitygetStartIntent(this)
null -> return@observe
}
startActivity(intent)

View file

@ -4,12 +4,15 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
import org.fnives.test.showcase.ui.shared.Event
import javax.inject.Inject
class SplashViewModel(isUserLoggedInUseCase: IsUserLoggedInUseCase) : ViewModel() {
@HiltViewModel
class SplashViewModel @Inject constructor(isUserLoggedInUseCase: IsUserLoggedInUseCase) : ViewModel() {
private val _navigateTo = MutableLiveData<Event<NavigateTo>>()
val navigateTo: LiveData<Event<NavigateTo>> = _navigateTo

View file

@ -2,12 +2,14 @@
buildscript {
ext.kotlin_version = "1.5.30"
ext.detekt_version = "1.18.1"
ext.hilt_version = "2.38.1"
repositories {
mavenCentral()
google()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
classpath 'com.android.tools.build:gradle:7.0.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.0"
@ -22,6 +24,15 @@ allprojects {
repositories {
mavenCentral()
google()
maven {
url "https://maven.pkg.github.com/fknives/ReloadableHiltModule"
credentials {
username = project.findProperty("GITHUB_USERNAME") ?: System.getenv("GITHUB_USERNAME")
password = project.findProperty("GITHUB_TOKEN") ?: System.getenv("GITHUB_TOKEN")
}
// how to get token
// https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token
}
}
}
@ -29,7 +40,7 @@ task clean(type: Delete) {
delete rootProject.buildDir
}
task unitTests(dependsOn: ["app:testDebugUnitTest", "core:test", "network:test"]){
task unitTests(dependsOn: ["app:testKoinDebugUnitTest", "app:testHiltDebugUnitTest", "core:test", "network:test"]){
group = 'Tests'
description = 'Run all unit tests'
}

View file

@ -1,6 +1,7 @@
plugins {
id 'java-library'
id 'kotlin'
id 'kotlin-kapt'
}
java {
@ -14,12 +15,22 @@ compileKotlin {
}
}
kapt {
correctErrorTypes = true
}
dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
api project(":model")
implementation project(":network")
// hilt
implementation "com.google.dagger:hilt-core:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
implementation "org.fnives.library.reloadable.module:annotation:$reloadable_module_version"
kapt "org.fnives.library.reloadable.module:annotation-processor:$reloadable_module_version"
testImplementation "io.insert-koin:koin-test-junit5:$koin_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
testImplementation "org.mockito.kotlin:mockito-kotlin:$testing_kotlin_mockito_version"

View file

@ -2,8 +2,9 @@ package org.fnives.test.showcase.core.content
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId
import javax.inject.Inject
class AddContentToFavouriteUseCase internal constructor(
class AddContentToFavouriteUseCase @Inject internal constructor(
private val favouriteContentLocalStorage: FavouriteContentLocalStorage
) {

View file

@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import org.fnives.test.showcase.core.di.hilt.LoggedInModuleInject
import org.fnives.test.showcase.core.shared.Optional
import org.fnives.test.showcase.core.shared.mapIntoResource
import org.fnives.test.showcase.core.shared.wrapIntoAnswer
@ -14,7 +15,7 @@ import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.shared.Resource
import org.fnives.test.showcase.network.content.ContentRemoteSource
internal class ContentRepository(private val contentRemoteSource: ContentRemoteSource) {
internal class ContentRepository @LoggedInModuleInject constructor(private val contentRemoteSource: ContentRemoteSource) {
private val mutableContentFlow = MutableStateFlow(Optional<List<Content>>(null))
private val requestFlow: Flow<Resource<List<Content>>> = flow {

View file

@ -1,6 +1,8 @@
package org.fnives.test.showcase.core.content
class FetchContentUseCase internal constructor(private val contentRepository: ContentRepository) {
import javax.inject.Inject
class FetchContentUseCase @Inject internal constructor(private val contentRepository: ContentRepository) {
fun invoke() = contentRepository.fetch()
}

View file

@ -7,8 +7,9 @@ import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.content.ContentId
import org.fnives.test.showcase.model.content.FavouriteContent
import org.fnives.test.showcase.model.shared.Resource
import javax.inject.Inject
class GetAllContentUseCase internal constructor(
class GetAllContentUseCase @Inject internal constructor(
private val contentRepository: ContentRepository,
private val favouriteContentLocalStorage: FavouriteContentLocalStorage
) {

View file

@ -2,8 +2,9 @@ package org.fnives.test.showcase.core.content
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId
import javax.inject.Inject
class RemoveContentFromFavouritesUseCase internal constructor(
class RemoveContentFromFavouritesUseCase @Inject internal constructor(
private val favouriteContentLocalStorage: FavouriteContentLocalStorage
) {

View file

@ -0,0 +1,34 @@
package org.fnives.test.showcase.core.di.hilt
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.fnives.test.showcase.core.login.LogoutUseCase
import org.fnives.test.showcase.core.session.SessionExpirationAdapter
import org.fnives.test.showcase.core.storage.NetworkSessionLocalStorageAdapter
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
import org.fnives.test.showcase.core.di.hilt.ReloadLoggedInModuleInjectModule
@InstallIn(SingletonComponent::class)
@Module
object CoreModule {
@Provides
internal fun bindNetworkSessionLocalStorageAdapter(
networkSessionLocalStorageAdapter: NetworkSessionLocalStorageAdapter
): NetworkSessionLocalStorage = networkSessionLocalStorageAdapter
@Provides
internal fun bindNetworkSessionExpirationListener(
sessionExpirationAdapter: SessionExpirationAdapter
): NetworkSessionExpirationListener = sessionExpirationAdapter
@Provides
fun provideLogoutUseCase(
storage: UserDataLocalStorage,
reloadLoggedInModuleInjectModule: ReloadLoggedInModuleInjectModule
) : LogoutUseCase = LogoutUseCase(storage, reloadLoggedInModuleInjectModule)
}

View file

@ -0,0 +1,8 @@
package org.fnives.test.showcase.core.di.hilt
import org.fnives.library.reloadable.module.annotation.ReloadableModule
@ReloadableModule
@Target(AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.SOURCE)
annotation class LoggedInModuleInject

View file

@ -1,4 +1,4 @@
package org.fnives.test.showcase.core.di
package org.fnives.test.showcase.core.di.koin
import org.fnives.test.showcase.core.content.AddContentToFavouriteUseCase
import org.fnives.test.showcase.core.content.ContentRepository
@ -14,7 +14,7 @@ import org.fnives.test.showcase.core.storage.NetworkSessionLocalStorageAdapter
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.network.di.createNetworkModules
import org.fnives.test.showcase.network.di.koin.createNetworkModules
import org.koin.core.module.Module
import org.koin.core.scope.Scope
import org.koin.dsl.module
@ -42,7 +42,7 @@ fun repositoryModule() = module {
fun useCaseModule() = module {
factory { LoginUseCase(get(), get()) }
factory { LogoutUseCase(get()) }
factory { LogoutUseCase(get(), null) }
factory { GetAllContentUseCase(get(), get()) }
factory { AddContentToFavouriteUseCase(get()) }
factory { RemoveContentFromFavouritesUseCase(get()) }

View file

@ -1,8 +1,11 @@
package org.fnives.test.showcase.core.login
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import javax.inject.Inject
class IsUserLoggedInUseCase(private val userDataLocalStorage: UserDataLocalStorage) {
class IsUserLoggedInUseCase @Inject constructor(
private val userDataLocalStorage: UserDataLocalStorage
) {
fun invoke(): Boolean = userDataLocalStorage.session != null
}

View file

@ -7,8 +7,9 @@ import org.fnives.test.showcase.model.auth.LoginStatus
import org.fnives.test.showcase.model.shared.Answer
import org.fnives.test.showcase.network.auth.LoginRemoteSource
import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
import javax.inject.Inject
class LoginUseCase internal constructor(
class LoginUseCase @Inject internal constructor(
private val loginRemoteSource: LoginRemoteSource,
private val userDataLocalStorage: UserDataLocalStorage
) {

View file

@ -1,13 +1,22 @@
package org.fnives.test.showcase.core.login
import org.fnives.test.showcase.core.di.repositoryModule
import org.fnives.test.showcase.core.di.koin.repositoryModule
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.koin.core.context.loadKoinModules
import org.koin.mp.KoinPlatformTools
import org.fnives.test.showcase.core.di.hilt.ReloadLoggedInModuleInjectModule
class LogoutUseCase(private val storage: UserDataLocalStorage) {
class LogoutUseCase(
private val storage: UserDataLocalStorage,
private val reloadLoggedInModuleInjectModule: ReloadLoggedInModuleInjectModule?
) {
suspend fun invoke() {
if (KoinPlatformTools.defaultContext().getOrNull() == null) {
reloadLoggedInModuleInjectModule?.reload()
} else {
loadKoinModules(repositoryModule())
}
storage.session = null
}
}

View file

@ -1,11 +1,11 @@
package org.fnives.test.showcase.core.session
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
import javax.inject.Inject
internal class SessionExpirationAdapter(
internal class SessionExpirationAdapter @Inject constructor(
private val sessionExpirationListener: SessionExpirationListener
) :
NetworkSessionExpirationListener {
) : NetworkSessionExpirationListener {
override fun onSessionExpired() = sessionExpirationListener.onSessionExpired()
}

View file

@ -2,8 +2,9 @@ package org.fnives.test.showcase.core.storage
import org.fnives.test.showcase.model.session.Session
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
import javax.inject.Inject
internal class NetworkSessionLocalStorageAdapter(
internal class NetworkSessionLocalStorageAdapter @Inject constructor(
private val userDataLocalStorage: UserDataLocalStorage
) : NetworkSessionLocalStorage {

View file

@ -2,7 +2,7 @@ package org.fnives.test.showcase.core.login
import kotlinx.coroutines.test.runBlockingTest
import org.fnives.test.showcase.core.content.ContentRepository
import org.fnives.test.showcase.core.di.createCoreModule
import org.fnives.test.showcase.core.di.koin.createCoreModule
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.model.network.BaseUrl
import org.junit.jupiter.api.AfterEach

View file

@ -6,6 +6,7 @@ project.ext {
androidx_livedata_version = "2.3.1"
androidx_swiperefreshlayout_version = "1.1.0"
androidx_room_version = "2.3.0"
activity_ktx_version = "1.3.1"
coroutines_version = "1.4.3"
koin_version = "3.1.2"
@ -13,6 +14,7 @@ project.ext {
retrofit_version = "2.9.0"
okhttp_version = "4.9.1"
moshi_version = "1.12.0"
reloadable_module_version = "0.1.0"
testing_androidx_code_version = "1.4.0"
testing_androidx_junit_version = "1.1.3"

View file

@ -18,6 +18,8 @@ dependencies {
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
api "io.insert-koin:koin-core:$koin_version"
implementation "com.google.dagger:hilt-core:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
api project(":model")

View file

@ -7,8 +7,9 @@ import org.fnives.test.showcase.network.shared.ExceptionWrapper
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
import retrofit2.HttpException
import retrofit2.Response
import javax.inject.Inject
internal class LoginErrorConverter {
internal class LoginErrorConverter @Inject constructor() {
@Throws(ParsingException::class)
suspend fun invoke(request: suspend () -> Response<LoginResponse>): LoginStatusResponses =

View file

@ -7,8 +7,9 @@ import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
import org.fnives.test.showcase.network.shared.ExceptionWrapper
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
import javax.inject.Inject
internal class LoginRemoteSourceImpl constructor(
internal class LoginRemoteSourceImpl @Inject constructor(
private val loginService: LoginService,
private val loginErrorConverter: LoginErrorConverter
) : LoginRemoteSource {

View file

@ -4,8 +4,9 @@ import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.content.ContentId
import org.fnives.test.showcase.model.content.ImageUrl
import org.fnives.test.showcase.network.shared.ExceptionWrapper
import javax.inject.Inject
internal class ContentRemoteSourceImpl(private val contentService: ContentService) : ContentRemoteSource {
internal class ContentRemoteSourceImpl @Inject constructor(private val contentService: ContentService) : ContentRemoteSource {
override suspend fun get(): List<Content> =
ExceptionWrapper.wrap {

View file

@ -0,0 +1,96 @@
package org.fnives.test.showcase.network.di.hilt
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import org.fnives.test.showcase.network.auth.LoginRemoteSource
import org.fnives.test.showcase.network.auth.LoginRemoteSourceImpl
import org.fnives.test.showcase.network.auth.LoginService
import org.fnives.test.showcase.network.content.ContentRemoteSource
import org.fnives.test.showcase.network.content.ContentRemoteSourceImpl
import org.fnives.test.showcase.network.content.ContentService
import org.fnives.test.showcase.network.di.setupLogging
import org.fnives.test.showcase.network.session.AuthenticationHeaderInterceptor
import org.fnives.test.showcase.network.session.AuthenticationHeaderUtils
import org.fnives.test.showcase.network.session.SessionAuthenticator
import org.fnives.test.showcase.network.shared.PlatformInterceptor
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
object HiltNetworkModule {
@Provides
@Singleton
fun provideConverterFactory(): Converter.Factory = MoshiConverterFactory.create()
@Provides
@Singleton
@SessionLessQualifier
fun provideSessionLessOkHttpClient(enableLogging: Boolean) =
OkHttpClient.Builder()
.addInterceptor(PlatformInterceptor())
.setupLogging(enableLogging)
.build()
@Provides
@Singleton
@SessionLessQualifier
fun provideSessionLessRetrofit(
baseUrl: String,
converterFactory: Converter.Factory,
@SessionLessQualifier okHttpClient: OkHttpClient
) = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(converterFactory)
.client(okHttpClient)
.build()
@Provides
@Singleton
@SessionQualifier
internal fun provideSessionOkHttpClient(
@SessionLessQualifier okHttpClient: OkHttpClient,
sessionAuthenticator: SessionAuthenticator,
authenticationHeaderUtils: AuthenticationHeaderUtils
) =
okHttpClient
.newBuilder()
.authenticator(sessionAuthenticator)
.addInterceptor(AuthenticationHeaderInterceptor(authenticationHeaderUtils))
.build()
@Provides
@Singleton
@SessionQualifier
fun provideSessionRetrofit(
@SessionLessQualifier retrofit: Retrofit,
@SessionQualifier okHttpClient: OkHttpClient
) = retrofit.newBuilder()
.client(okHttpClient)
.build()
@Provides
internal fun bindContentRemoteSource(
contentRemoteSourceImpl: ContentRemoteSourceImpl
): ContentRemoteSource = contentRemoteSourceImpl
@Provides
internal fun bindLoginRemoteSource(
loginRemoteSource: LoginRemoteSourceImpl
): LoginRemoteSource = loginRemoteSource
@Provides
internal fun provideLoginService(@SessionLessQualifier retrofit: Retrofit): LoginService =
retrofit.create(LoginService::class.java)
@Provides
internal fun provideContentService(@SessionQualifier retrofit: Retrofit): ContentService =
retrofit.create(ContentService::class.java)
}

View file

@ -0,0 +1,6 @@
package org.fnives.test.showcase.network.di.hilt
import javax.inject.Qualifier
@Qualifier
annotation class SessionLessQualifier

View file

@ -0,0 +1,6 @@
package org.fnives.test.showcase.network.di.hilt
import javax.inject.Qualifier
@Qualifier
annotation class SessionQualifier

View file

@ -1,4 +1,4 @@
package org.fnives.test.showcase.network.di
package org.fnives.test.showcase.network.di.koin
import okhttp3.OkHttpClient
import org.fnives.test.showcase.model.network.BaseUrl
@ -9,6 +9,7 @@ import org.fnives.test.showcase.network.auth.LoginService
import org.fnives.test.showcase.network.content.ContentRemoteSource
import org.fnives.test.showcase.network.content.ContentRemoteSourceImpl
import org.fnives.test.showcase.network.content.ContentService
import org.fnives.test.showcase.network.di.setupLogging
import org.fnives.test.showcase.network.session.AuthenticationHeaderInterceptor
import org.fnives.test.showcase.network.session.AuthenticationHeaderUtils
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener

View file

@ -1,8 +1,9 @@
package org.fnives.test.showcase.network.session
import okhttp3.Request
import javax.inject.Inject
internal class AuthenticationHeaderUtils(private val networkSessionLocalStorage: NetworkSessionLocalStorage) {
internal class AuthenticationHeaderUtils @Inject constructor(private val networkSessionLocalStorage: NetworkSessionLocalStorage) {
fun hasToken(okhttpRequest: Request): Boolean =
okhttpRequest.header(KEY) == networkSessionLocalStorage.session?.accessToken

View file

@ -6,8 +6,9 @@ import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import org.fnives.test.showcase.network.auth.LoginRemoteSourceImpl
import javax.inject.Inject
internal class SessionAuthenticator(
internal class SessionAuthenticator @Inject constructor(
private val networkSessionLocalStorage: NetworkSessionLocalStorage,
private val loginRemoteSource: LoginRemoteSourceImpl,
private val authenticationHeaderUtils: AuthenticationHeaderUtils,

View file

@ -2,7 +2,7 @@ package org.fnives.test.showcase.network.auth
import kotlinx.coroutines.runBlocking
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.network.di.createNetworkModules
import org.fnives.test.showcase.network.di.koin.createNetworkModules
import org.fnives.test.showcase.network.mockserver.ContentData
import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage

View file

@ -4,7 +4,7 @@ import kotlinx.coroutines.runBlocking
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
import org.fnives.test.showcase.network.di.createNetworkModules
import org.fnives.test.showcase.network.di.koin.createNetworkModules
import org.fnives.test.showcase.network.mockserver.ContentData
import org.fnives.test.showcase.network.mockserver.ContentData.createExpectedLoginRequestJson
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario

View file

@ -3,7 +3,7 @@ package org.fnives.test.showcase.network.content
import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.MockWebServer
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.network.di.createNetworkModules
import org.fnives.test.showcase.network.di.koin.createNetworkModules
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
import org.junit.jupiter.api.AfterEach

View file

@ -2,7 +2,7 @@ package org.fnives.test.showcase.network.content
import kotlinx.coroutines.runBlocking
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.network.di.createNetworkModules
import org.fnives.test.showcase.network.di.koin.createNetworkModules
import org.fnives.test.showcase.network.mockserver.ContentData
import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage

View file

@ -3,7 +3,7 @@ package org.fnives.test.showcase.network.content
import kotlinx.coroutines.runBlocking
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.model.session.Session
import org.fnives.test.showcase.network.di.createNetworkModules
import org.fnives.test.showcase.network.di.koin.createNetworkModules
import org.fnives.test.showcase.network.mockserver.ContentData
import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario