Compare commits

...

12 Commits

Author SHA1 Message Date
Evgeny Dubravin 7f51e68f46 Auth Component subcomponent dependency 2024-04-05 22:40:51 +07:00
Evgeny Dubravin 9b85bcf562 add Auth Component 2024-04-02 03:42:28 +07:00
Evgeny Dubravin d71fda5853 feature TI-195: [Android] Cicirone Navigation 2024-03-29 22:51:11 +07:00
Evgeny Dubravin c4b740f629 feature TI-194: [Android] DI 2024-03-29 22:36:19 +07:00
Evgeny Dubravin 417547d44e feature TI-193: [Android] Реализовать ViewModel Factory 2024-03-28 17:10:25 +07:00
Evgeny Dubravin 70819e7f41 set common-template submodule 2024-03-25 18:11:23 +07:00
Evgeny Dubravin df7adab2e7 feature TI-188: [Android] Настройка flavor, buildType 2024-03-23 00:24:09 +07:00
Evgeny Dubravin 601fbceb2b feature TI-187: [Android] Настройка зависимостей 2024-03-23 00:15:44 +07:00
Evgeny Dubravin 80681356fa feature TI-186: [Android] Настройка проекта 2024-03-22 20:25:18 +07:00
Evgeny Dubravin b99dec674c add template dependencies 2024-03-21 22:09:35 +07:00
Evgeny Dubravin f77bc0a8c5 удалены лишние модули 2024-03-19 16:47:50 +07:00
Evgeny Dubravin d03d5f9340 set submodules 2024-03-15 17:16:41 +07:00
123 changed files with 1434 additions and 1203 deletions

10
.gitmodules vendored
View File

@ -1,9 +1,9 @@
[submodule "Template-common"]
path = Template-common
url = git@github.com:TouchInstinct/Template-common.git
[submodule "RoboSwag"] [submodule "RoboSwag"]
path = RoboSwag path = RoboSwag
url = git@github.com:TouchInstinct/RoboSwag.git url = https://git.svc.touchin.ru/TouchInstinct/RoboSwag.git
[submodule "BuildScripts"] [submodule "BuildScripts"]
path = BuildScripts path = BuildScripts
url = git@github.com:TouchInstinct/BuildScripts.git url = https://git.svc.touchin.ru/TouchInstinct/BuildScripts.git
[submodule "common-template"]
path = common-template
url = https://git.svc.touchin.ru/TouchInstinct/common-template.git

View File

@ -1,74 +1,99 @@
plugins { plugins {
id(Plugins.ANDROID_APP_PLUGIN_WITH_DEFAULT_CONFIG) id(Plugins.ANDROID_APP_PLUGIN_WITH_DEFAULT_CONFIG)
id(Plugins.FIREBASE_CRASH) alias(libs.plugins.google.services)
id(Plugins.GOOGLE_SERVICES) alias(libs.plugins.firebase.crashlytics)
id(Plugins.LICENCE_PLUGIN) alias(libs.plugins.firebase.perf)
id(libs.plugins.google.oss.licenses.plugin.get().pluginId)
} }
val customEndpoint: String? = Environment.ENDPOINT.getenv()?.takeIf(String::isNotBlank) val customEndpoint: String? = Environment.ENDPOINT.getenv()?.takeIf(String::isNotBlank)
android { android {
namespace = "ru.touchin.template"
configureSigningConfig(this@Build_gradle::file) configureSigningConfig(this@Build_gradle::file)
with(defaultConfig) { with(defaultConfig) {
applicationId = Environment.APP_ID.getenv() ?: AndroidConfig.TEST_APP_ID addResourceConfigurations("ru")
signingConfig = signingConfigs.getByName(SigningConfig.CONFIG_NAME)
} }
firebaseCrashlytics { addBuildType(type = BuildType.Develop, project = rootProject)
mappingFileUploadEnabled = true addBuildType(type = BuildType.Debug, project = rootProject)
} addBuildType(type = BuildType.Customer, project = rootProject)
addBuildType(type = BuildType.Release, project = rootProject)
addBuildType(BuildType.Debug, buildScriptDir = buildScriptDir) addMobileServicesFlavor()
addBuildType(BuildType.Release, buildScriptDir = buildScriptDir)
flavorDimensions( ext["languageMap"] = mapOf("ru" to "${rootProject.projectDir}/${AndroidConfig.COMMON_FOLDER}/strings/default_common_strings.json")
ApiFlavour.DIMENSION_NAME,
SSLPinningFlavour.DIMENSION_NAME,
TestPanelFlavour.DIMENSION_NAME
)
addFlavour(flavour = ApiFlavour.CustomerStage, customEndpoint = customEndpoint)
addFlavour(flavour = ApiFlavour.CustomerProd, customEndpoint = customEndpoint)
addFlavour(SSLPinningFlavour.OFF)
addFlavour(SSLPinningFlavour.ON)
addEmptyFlavour(TestPanelFlavour.OFF)
addEmptyFlavour(TestPanelFlavour.ON)
ignoreCustomerProdFlavourIfReleaseIsDebuggable()
}
androidExtensions {
features = setOf("parcelize")
} }
dependencies { dependencies {
androidX() // AndroidX
featureModules() implementation(libs.bundles.androidX)
mvi()
materialDesign() // KotlinX
dagger() implementation(libs.coroutines)
retrofit()
moshi() // UI
navigation() implementation(libs.bundles.ui)
leakCanary()
sharedPrefs() // Lifecycle
chucker() implementation(libs.bundles.lifecycle)
implementation(Library.FIREBASE_ANAL) kapt(libs.androidx.lifecycle.compiler)
implementation(Library.FIREBASE_CRASH)
implementation(Library.FIREBASE_PERF) // Dagger
implementation(Library.ANDROIDX_SECURE) implementation(libs.bundles.dagger)
coreNetwork() kapt(libs.dagger.compiler)
coreStrings() kapt(libs.dagger.assisted.inject.processor)
implementationModule(Module.Core.UI)
implementationModule(Module.Core.UTILS) // Glide
implementationModule(Module.Core.DATA) implementation(libs.glide)
implementationModule(Module.RoboSwag.UTILS) implementation(libs.glide.okhttp3)
kapt(libs.glide.compiler)
// Retrofit2, OkHttp3
implementation(libs.retrofit)
implementation(libs.retrofit.converter.moshi)
implementation(libs.okhttp)
implementation(libs.okhttp.logging.interceptor)
// Moshi
implementation(libs.moshi)
implementation(libs.moshi.kotlin)
kapt(libs.moshi.codegen)
// Room
implementation(libs.room)
implementation(libs.room.ktx)
kapt(libs.room.compiler)
// LeakCanary
implementation(libs.leakcanary)
// Chucker
debugImplementation(libs.chucker.debug)
releaseImplementation(libs.chucker.release)
// GMS
implementation(platform(libs.firebase.bom))
implementation(libs.bundles.firebase)
implementation(libs.google.oss.licenses)
// Security
implementation(libs.androidx.security.crypto)
// Biometric
implementation(libs.androidx.biometric)
// Groupie
implementation(libs.groupie)
implementation(libs.groupie.viewbinding)
// Cicecrone
implementation(libs.cicerone)
} }
apply(from = "$buildScriptDir/gradle/scripts/applicationFileNaming.gradle") apply(from = "${rootProject.ext["buildScriptsDir"]}/gradle/scripts/stringGenerator.gradle")
val Project.buildScriptDir: String val Project.buildScriptDir: String
get() = rootProject.ext["buildScriptsDir"] as String get() = rootProject.ext["buildScriptsDir"] as String

View File

@ -10,7 +10,7 @@
"client_info": { "client_info": {
"mobilesdk_app_id": "1:1084813714260:android:b6d7bb18a0acfe96255ec1", "mobilesdk_app_id": "1:1084813714260:android:b6d7bb18a0acfe96255ec1",
"android_client_info": { "android_client_info": {
"package_name": "com.touchin.template" "package_name": "ru.touchin.template"
} }
}, },
"oauth_client": [ "oauth_client": [

View File

@ -1,7 +1,6 @@
<manifest <manifest
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="ru.touchin.template">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
@ -9,17 +8,17 @@
android:name="ru.touchin.template.App" android:name="ru.touchin.template.App"
android:allowBackup="false" android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/common_global_app_name" android:label="@string/common_app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false" android:supportsRtl="false"
tools:ignore="GoogleAppIndexingWarning"> tools:ignore="GoogleAppIndexingWarning"
android:theme="@style/AppTheme">
<activity <activity
android:name="ru.touchin.template.SingleActivity" android:name=".feature.SingleActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/AppTheme" android:windowSoftInputMode="adjustResize"
android:windowSoftInputMode="adjustResize"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>

View File

@ -1,25 +1,18 @@
package ru.touchin.template package ru.touchin.template
import me.vponomarenko.injectionmanager.IHasComponent import android.app.Application
import me.vponomarenko.injectionmanager.x.XInjectionManager import ru.touchin.template.di.DI
import ru.touchin.roboswag.navigation_base.TouchinApp import ru.touchin.template.di.SharedComponent
import ru.touchin.template.di.ApplicationComponent import ru.touchin.template.di.SharedComponentProvider
import ru.touchin.template.di.DaggerApplicationComponent
class App : TouchinApp(), IHasComponent<ApplicationComponent> { class App : Application(), SharedComponentProvider {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
initDagger()
DI.initAppComponent(applicationContext)
DI.getComponent().inject(this)
} }
fun initDagger() { override fun getModule(): SharedComponent = DI.getComponent()
XInjectionManager.init(this)
XInjectionManager.bindComponent(this)
}
override fun getComponent(): ApplicationComponent = DaggerApplicationComponent
.factory()
.create(this)
} }

View File

@ -1,62 +0,0 @@
package ru.touchin.template
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import me.vponomarenko.injectionmanager.x.XInjectionManager
import ru.terrakok.cicerone.NavigatorHolder
import ru.terrakok.cicerone.android.support.SupportAppNavigator
import ru.touchin.roboswag.navigation_base.activities.BaseActivity
import ru.touchin.roboswag.navigation_cicerone.CiceroneTuner
import ru.touchin.template.di.ApplicationComponent
import ru.touchin.template.navigation.MainNavigation
import ru.touchin.template.navigation.StartUpCoordinator
import javax.inject.Inject
// TDOD: change package name everywhere
// TODO: change google play config
class SingleActivity : BaseActivity() {
@Inject
@MainNavigation
lateinit var navigatorHolder: NavigatorHolder
@Inject
lateinit var coordinator: StartUpCoordinator
private lateinit var firebaseAnalytics: FirebaseAnalytics
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
firebaseAnalytics = Firebase.analytics
setContentView(R.layout.activity_main)
injectDependencies()
lifecycle.addObserver(
CiceroneTuner(
navigatorHolder = navigatorHolder,
navigator = SupportAppNavigator(this, R.id.fragment_container)
)
)
coordinator.start()
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
coordinator.closeCurrentScreen()
}
})
}
private fun injectDependencies() {
XInjectionManager
.findComponent<ApplicationComponent>()
.inject(this)
}
}

View File

@ -0,0 +1,17 @@
package ru.touchin.template.base.activity
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import ru.touchin.template.di.SharedComponent
import ru.touchin.template.di.getSharedModule
abstract class BaseActivity<T : ViewModel> : AppCompatActivity() {
protected val viewModel: T by lazy { createViewModelLazy().value }
protected val viewModelFactory by lazy { getSharedComponent().viewModelFactory() }
abstract fun createViewModelLazy(): Lazy<T>
protected fun getSharedComponent(): SharedComponent = getSharedModule()
}

View File

@ -0,0 +1,23 @@
package ru.touchin.template.base.fragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import ru.touchin.template.base.viewmodel.BaseController
import ru.touchin.template.di.SharedComponent
import ru.touchin.template.di.getSharedModule
import ru.touchin.template.navigation.backpress.OnBackPressedListener
abstract class BaseFragment<T : ViewModel> : Fragment(), OnBackPressedListener {
protected val viewModel: T by lazy { createViewModelLazy().value }
protected val viewModelFactory by lazy { getSharedComponent().viewModelFactory() }
protected abstract fun createViewModelLazy(): Lazy<T>
protected fun getSharedComponent(): SharedComponent = getSharedModule()
override fun onBackPressed(): Boolean {
return (viewModel as BaseController).onBackClicked()
}
}

View File

@ -0,0 +1,6 @@
package ru.touchin.template.base.viewmodel
interface BaseController {
fun onBackClicked(): Boolean = true
}

View File

@ -0,0 +1,29 @@
package ru.touchin.template.di
import android.content.Context
import dagger.BindsInstance
import dagger.Component
import ru.touchin.template.App
import ru.touchin.template.di.modules.AppModule
import ru.touchin.template.feature.SingleActivity
import ru.touchin.template.feature.second.di.AuthComponent
@Component(modules = [AppModule::class])
@AppScope
interface AppComponent : SharedComponent {
fun authComponent(): AuthComponent.Builder
@Component.Builder
interface Builder {
@BindsInstance
fun appContext(appContext: Context): Builder
fun build(): AppComponent
}
fun inject(entry: App)
fun inject(entry: SingleActivity)
}

View File

@ -0,0 +1,6 @@
package ru.touchin.template.di
import javax.inject.Scope
@Scope
annotation class AppScope

View File

@ -1,37 +0,0 @@
package ru.touchin.template.di
import android.content.Context
import dagger.BindsInstance
import dagger.Component
import ru.terrakok.cicerone.Router
import ru.touchin.template.feature_login.LoginDeps
import ru.touchin.template.network.di.NetworkModule
import ru.touchin.template.App
import ru.touchin.template.SingleActivity
import ru.touchin.template.core_prefs.PreferencesModule
import ru.touchin.template.navigation.MainNavigation
import javax.inject.Singleton
@Singleton
@Component(modules = [
ApplicationModule::class,
PreferencesModule::class,
MainNavigationModule::class,
NetworkModule::class,
CoordinatorsImpl::class
])
interface ApplicationComponent : LoginDeps {
@MainNavigation
fun router(): Router
fun inject(application: App)
fun inject(activity: SingleActivity)
@Component.Factory
interface Factory {
fun create(@BindsInstance context: Context): ApplicationComponent
}
}

View File

@ -1,28 +0,0 @@
package ru.touchin.template.di
import android.content.Context
import com.chuckerteam.chucker.api.ChuckerInterceptor
import dagger.Module
import dagger.Provides
import okhttp3.Interceptor
import ru.touchin.template.network.di.ApiUrl
import ru.touchin.template.network.di.ChuckInterceptor
import ru.touchin.template.network.di.WithSslPinning
import ru.touchin.template.BuildConfig
@Module
class ApplicationModule {
@Provides
@ApiUrl
fun provideApiUrl() = BuildConfig.API_URL
@Provides
@WithSslPinning
fun providePluggerForSsl() = BuildConfig.WithSSLPinning
@Provides
@ChuckInterceptor
fun provideChucker(context: Context): Interceptor = ChuckerInterceptor(context)
}

View File

@ -1,13 +0,0 @@
package ru.touchin.template.di
import dagger.Binds
import dagger.Module
import ru.touchin.template.feature_login.navigation.LoginCoordinator
import ru.touchin.template.navigation.login.LoginCoordinatorImpl
@Module
abstract class CoordinatorsImpl {
@Binds
abstract fun loginCoordinator(impl: LoginCoordinatorImpl): LoginCoordinator
}

View File

@ -0,0 +1,16 @@
package ru.touchin.template.di
import android.content.Context
object DI {
private lateinit var appComponent: AppComponent
fun initAppComponent(context: Context) {
appComponent = DaggerAppComponent.builder()
.appContext(context)
.build()
}
fun getComponent(): AppComponent = appComponent
}

View File

@ -1,27 +0,0 @@
package ru.touchin.template.di
import dagger.Module
import dagger.Provides
import ru.terrakok.cicerone.Cicerone
import ru.terrakok.cicerone.NavigatorHolder
import ru.terrakok.cicerone.Router
import ru.touchin.template.navigation.MainNavigation
import javax.inject.Singleton
@Module
class MainNavigationModule {
@Provides
@Singleton
@MainNavigation
fun provideCicerone(): Cicerone<Router> = Cicerone.create()
@Provides
@MainNavigation
fun provideNavigatorHolder(@MainNavigation cicerone: Cicerone<Router>): NavigatorHolder = cicerone.navigatorHolder
@Provides
@MainNavigation
fun provideRouter(@MainNavigation cicerone: Cicerone<Router>): Router = cicerone.router
}

View File

@ -0,0 +1,17 @@
package ru.touchin.template.di
import android.app.Activity
import android.content.Context
import androidx.fragment.app.Fragment
fun Context.getSharedModule(): SharedComponent {
return (applicationContext as SharedComponentProvider).getModule()
}
fun Activity.getSharedModule(): SharedComponent {
return (applicationContext as SharedComponentProvider).getModule()
}
fun Fragment.getSharedModule(): SharedComponent {
return requireContext().getSharedModule()
}

View File

@ -0,0 +1,17 @@
package ru.touchin.template.di
import dagger.Module
import ru.touchin.template.di.viewmodel.ViewModelFactory
import ru.touchin.template.feature.second.SecondViewModel
interface SharedComponent {
fun viewModelFactory(): ViewModelFactory
fun secondScreenViewModelFactory(): SecondViewModel.Factory
}
@Module
class SharedModule
interface SharedComponentProvider {
fun getModule(): SharedComponent
}

View File

@ -0,0 +1,15 @@
package ru.touchin.template.di.modules
import dagger.Module
import ru.touchin.template.feature.second.di.AuthComponent
@Module(
includes = [
RepositoryModule::class,
NetworkModule::class,
ViewModelModule::class,
NavigationModule::class
],
subcomponents = [AuthComponent::class]
)
class AppModule

View File

@ -0,0 +1,26 @@
package ru.touchin.template.di.modules
import com.github.terrakok.cicerone.Cicerone
import com.github.terrakok.cicerone.NavigatorHolder
import com.github.terrakok.cicerone.Router
import dagger.Module
import dagger.Provides
import ru.touchin.template.di.AppScope
@Module
class NavigationModule {
private val cicerone: Cicerone<Router> = Cicerone.create()
@Provides
@AppScope
fun provideRouter(): Router {
return cicerone.router
}
@Provides
@AppScope
fun provideNavigatorHolder(): NavigatorHolder {
return cicerone.getNavigatorHolder()
}
}

View File

@ -0,0 +1,7 @@
package ru.touchin.template.di.modules
import dagger.Module
@Module
class NetworkModule {
}

View File

@ -0,0 +1,11 @@
package ru.touchin.template.di.modules
import dagger.Module
@Module
interface RepositoryModule {
// @Binds
// @AppScope
// fun bindSecondRepository(secondRepositoryImpl: SecondRepositoryImpl): SecondRepository
}

View File

@ -0,0 +1,29 @@
package ru.touchin.template.di.modules
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
import ru.touchin.template.di.viewmodel.ViewModelFactory
import ru.touchin.template.di.viewmodel.ViewModelKey
import ru.touchin.template.feature.SingleViewModel
import ru.touchin.template.feature.first.FirstViewModel
@Module
interface ViewModelModule {
@Binds
fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
@Binds
@IntoMap
@ViewModelKey(SingleViewModel::class)
fun bindsSingleViewModel(viewModel: SingleViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(FirstViewModel::class)
fun bindsFirstViewModel(viewModel: FirstViewModel): ViewModel
}

View File

@ -0,0 +1,44 @@
package ru.touchin.template.di.viewmodel
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.AbstractSavedStateViewModelFactory
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.savedstate.SavedStateRegistryOwner
import javax.inject.Inject
import javax.inject.Provider
class ViewModelFactory @Inject constructor(
private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val viewModel = viewModels[modelClass]?.get()
return viewModel as T
}
}
inline fun <reified VM : ViewModel> Fragment.assistedViewModel(
crossinline creator: (SavedStateHandle) -> VM,
): Lazy<VM> = viewModels { createAbstractSavedStateViewModelFactory(arguments, creator) }
inline fun <reified T : ViewModel> SavedStateRegistryOwner.createAbstractSavedStateViewModelFactory(
arguments: Bundle? = Bundle(),
crossinline creator: (SavedStateHandle) -> T,
): ViewModelProvider.Factory {
return object : AbstractSavedStateViewModelFactory(
owner = this@createAbstractSavedStateViewModelFactory,
defaultArgs = arguments,
) {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle,
): T = creator(handle) as T
}
}

View File

@ -0,0 +1,9 @@
package ru.touchin.template.di.viewmodel
import androidx.lifecycle.ViewModel
import dagger.MapKey
import kotlin.reflect.KClass
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)

View File

@ -0,0 +1,47 @@
package ru.touchin.template.feature
import android.os.Bundle
import androidx.activity.viewModels
import com.github.terrakok.cicerone.NavigatorHolder
import com.github.terrakok.cicerone.androidx.AppNavigator
import javax.inject.Inject
import ru.touchin.template.R
import ru.touchin.template.base.activity.BaseActivity
import ru.touchin.template.databinding.ActivityMainBinding
import ru.touchin.template.di.DI
class SingleActivity : BaseActivity<SingleViewModel>() {
@Inject
lateinit var navigatorHolder: NavigatorHolder
private val rootNavigator by lazy {
AppNavigator(this, R.id.activityScreensContainer)
}
private lateinit var binding: ActivityMainBinding
override fun createViewModelLazy(): Lazy<SingleViewModel> = viewModels { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
DI.getComponent().inject(this)
viewModel.navigate()
}
override fun onResumeFragments() {
super.onResumeFragments()
navigatorHolder.setNavigator(rootNavigator)
}
override fun onPause() {
navigatorHolder.removeNavigator()
super.onPause()
}
}

View File

@ -0,0 +1,14 @@
package ru.touchin.template.feature
import androidx.lifecycle.ViewModel
import com.github.terrakok.cicerone.Router
import javax.inject.Inject
import ru.touchin.template.navigation.Screens
class SingleViewModel @Inject constructor(
private val router: Router,
) : ViewModel() {
fun navigate() {
router.newRootScreen(Screens.First())
}
}

View File

@ -0,0 +1,53 @@
package ru.touchin.template.feature.first
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import javax.inject.Inject
import ru.touchin.template.base.fragment.BaseFragment
import ru.touchin.template.databinding.FragmentFirstBinding
import ru.touchin.template.di.DI
import ru.touchin.template.feature.second.SecondRepository
class FirstFragment : BaseFragment<FirstViewModel>() {
@Inject
lateinit var secondRepository: SecondRepository
private var _binding: FragmentFirstBinding? = null
private val binding get() = _binding!!
override fun createViewModelLazy() = viewModels<FirstViewModel> { viewModelFactory }
override fun onAttach(context: Context) {
DI.getComponent().authComponent().build().inject(this)
super.onAttach(context)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentFirstBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
button.apply {
text = "Next"
setOnClickListener { viewModel.onNextButtonClicked(this@FirstFragment::class.toString()) }
}
textView.text = "First Fragment"
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -0,0 +1,16 @@
package ru.touchin.template.feature.first
import androidx.lifecycle.ViewModel
import com.github.terrakok.cicerone.Router
import javax.inject.Inject
import ru.touchin.template.navigation.Screens
class FirstViewModel @Inject constructor(
private val router: Router,
// private val secondRepository: SecondRepository
) : ViewModel() {
fun onNextButtonClicked(from: String) {
router.navigateTo(Screens.Second(from))
}
}

View File

@ -0,0 +1,86 @@
package ru.touchin.template.feature.second
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import javax.inject.Inject
import kotlinx.coroutines.launch
import ru.touchin.template.base.fragment.BaseFragment
import ru.touchin.template.databinding.FragmentSecondBinding
import ru.touchin.template.di.DI
import ru.touchin.template.di.viewmodel.assistedViewModel
class SecondFragment : BaseFragment<SecondViewModel>() {
companion object {
private const val FROM_KEY = "FROM"
private const val SCREEN_NAME_KEY = "SCREEN_NAME"
private const val FRAGMENT_NAME = "Second Fragment"
fun newInstance(from: String): SecondFragment {
val args = bundleOf(FROM_KEY to from, SCREEN_NAME_KEY to FRAGMENT_NAME)
val fragment = SecondFragment().apply {
arguments = args
}
return fragment
}
}
@Inject
lateinit var secondRepository: SecondRepository
private var _binding: FragmentSecondBinding? = null
private val binding get() = _binding!!
private val secondViewModelFactory by lazy { getSharedComponent().secondScreenViewModelFactory() }
override fun createViewModelLazy() = assistedViewModel {
secondViewModelFactory.create(
arguments?.getString(FROM_KEY) ?: "Unknown Fragment",
arguments?.getString(SCREEN_NAME_KEY) ?: "Unknown"
)
}
override fun onAttach(context: Context) {
DI.getComponent().authComponent().build().inject(this)
super.onAttach(context)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentSecondBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
button.apply {
text = "Back"
setOnClickListener { onBackPressed() }
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.state
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect {
textView.text = it
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -0,0 +1,11 @@
package ru.touchin.template.feature.second
import javax.inject.Inject
interface SecondRepository {
}
class SecondRepositoryImpl @Inject constructor() : SecondRepository {
}

View File

@ -0,0 +1,36 @@
package ru.touchin.template.feature.second
import androidx.lifecycle.ViewModel
import com.github.terrakok.cicerone.Router
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import ru.touchin.template.base.viewmodel.BaseController
class SecondViewModel @AssistedInject constructor(
@Assisted("from") from: String,
@Assisted("screenName") screenName: String,
private val rootRouter: Router,
// private val secondRepository: SecondRepository
) : ViewModel(), BaseController {
private val _state = MutableStateFlow("$from to $screenName")
val state: StateFlow<String> = _state.asStateFlow()
@AssistedFactory
interface Factory {
fun create(
@Assisted("from") from: String,
@Assisted("screenName") screenName: String,
): SecondViewModel
}
override fun onBackClicked(): Boolean {
rootRouter.exit()
return super.onBackClicked()
}
}

View File

@ -0,0 +1,20 @@
package ru.touchin.template.feature.second.di
import dagger.Subcomponent
import ru.touchin.template.feature.first.FirstFragment
import ru.touchin.template.feature.second.SecondFragment
@Subcomponent(modules = [AuthModule::class])
@AuthScope
interface AuthComponent {
@Subcomponent.Builder
interface Builder {
fun build(): AuthComponent
}
fun inject(entry: SecondFragment)
fun inject(entry: FirstFragment)
}

View File

@ -0,0 +1,14 @@
package ru.touchin.template.feature.second.di
import dagger.Binds
import dagger.Module
import ru.touchin.template.feature.second.SecondRepository
import ru.touchin.template.feature.second.SecondRepositoryImpl
@Module
interface AuthModule {
@Binds
@AuthScope
fun bindSecondRepository(secondRepositoryImpl: SecondRepositoryImpl): SecondRepository
}

View File

@ -0,0 +1,6 @@
package ru.touchin.template.feature.second.di
import javax.inject.Scope
@Scope
annotation class AuthScope

View File

@ -1,6 +0,0 @@
package ru.touchin.template.navigation
import javax.inject.Qualifier
@Qualifier
annotation class MainNavigation

View File

@ -1,13 +1,16 @@
package ru.touchin.template.navigation package ru.touchin.template.navigation
import androidx.fragment.app.Fragment import com.github.terrakok.cicerone.androidx.FragmentScreen
import ru.terrakok.cicerone.android.support.SupportAppScreen import ru.touchin.template.feature.first.FirstFragment
import ru.touchin.template.feature_login.presentation.LoginFragment import ru.touchin.template.feature.second.SecondFragment
object Screens { object Screens {
class Login : SupportAppScreen() { fun First(): FragmentScreen = FragmentScreen {
override fun getFragment(): Fragment = LoginFragment() FirstFragment()
} }
}
fun Second(from: String): FragmentScreen = FragmentScreen {
SecondFragment.newInstance(from)
}
}

View File

@ -1,18 +0,0 @@
package ru.touchin.template.navigation
import ru.terrakok.cicerone.Router
import javax.inject.Inject
class StartUpCoordinator @Inject constructor(
@MainNavigation private val router: Router
) {
fun start() {
router.newRootScreen(Screens.Login())
}
fun closeCurrentScreen() {
router.exit()
}
}

View File

@ -0,0 +1,6 @@
package ru.touchin.template.navigation.backpress
interface OnBackPressedListener {
fun onBackPressed(): Boolean
}

View File

@ -1,16 +0,0 @@
package ru.touchin.template.navigation.login
import ru.terrakok.cicerone.Router
import ru.touchin.template.feature_login.navigation.LoginCoordinator
import ru.touchin.template.navigation.MainNavigation
import javax.inject.Inject
class LoginCoordinatorImpl @Inject constructor(
@MainNavigation private val router: Router
) : LoginCoordinator {
override fun openMainScreen() {
router.exit()
}
}

View File

@ -0,0 +1,7 @@
package ru.touchin.template.navigation.router
import com.github.terrakok.cicerone.Router
interface RouterProvider {
val router: Router
}

View File

@ -1,6 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"
android:background="@color/biometric_error_color">
<FrameLayout
android:id="@+id/activityScreensContainer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="40dp"
android:background="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toTopOf="@+id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toTopOf="@+id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="common_global_app_name">Template</string> <string name="common_app_name" formatted="false">android-project-template</string>
</resources> </resources>

View File

@ -1,43 +1,18 @@
buildscript { buildscript {
repositories { repositories {
mavenCentral()
google() google()
jcenter() mavenCentral()
maven("https://plugins.gradle.org/m2/") maven("https://plugins.gradle.org/m2/")
} }
dependencies { dependencies {
classpath("com.android.tools.build:gradle:${Version.ANDROID_PLUGIN}") classpath(libs.android.gradle.plugin)
classpath(kotlin("gradle-plugin", version = Version.KOTLIN)) classpath(libs.kotlin.gradle.plugin)
classpath("com.google.gms:google-services:${Version.GOOGLE_SERVICES_PLUGIN}") classpath(libs.google.oss.licenses.plugin) {
classpath("com.google.firebase:firebase-crashlytics-gradle:${Version.FIREBASE_CRASH_PLUGIN}") exclude(group = "com.google.protobuf")
classpath("com.vanniktech:gradle-dependency-graph-generator-plugin:0.5.0") }
classpath("com.google.android.gms:oss-licenses-plugin:0.10.2")
} }
} }
plugins {
id(Plugins.DEPENDENCY_GRAPH).version("0.5.0")
id("static-analysis-android")
}
allprojects {
repositories {
google()
jcenter()
maven("http://dl.bintray.com/touchin/touchin-tools")
maven("https://jitpack.io")
}
}
subprojects {
apply(plugin = Plugins.DETEKT)
}
val buildScriptsDir = "${rootProject.projectDir}/BuildScripts" val buildScriptsDir = "${rootProject.projectDir}/BuildScripts"
ext["buildScriptsDir"] = buildScriptsDir ext["buildScriptsDir"] = buildScriptsDir
apply(plugin = Plugins.DEPENDENCY_GRAPH)
staticAnalysis {
buildScriptDir = buildScriptsDir
}

View File

@ -1,14 +1,11 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
`kotlin-dsl` `kotlin-dsl`
`kotlin-dsl-precompiled-script-plugins` `kotlin-dsl-precompiled-script-plugins`
kotlin("jvm") version embeddedKotlinVersion
} }
// The kotlin-dsl plugin requires a repository to be declared // The kotlin-dsl plugin requires a repository to be declared
repositories { repositories {
jcenter() mavenCentral()
google() google()
maven { maven {
url = uri("https://plugins.gradle.org/m2/") url = uri("https://plugins.gradle.org/m2/")
@ -16,18 +13,7 @@ repositories {
} }
dependencies { dependencies {
// android gradle plugin, required by custom plugin implementation(libs.android.gradle.plugin)
implementation("com.android.tools.build:gradle:4.0.0") implementation(libs.kotlin.gradle.plugin)
// kotlin plugin, required by custom plugin
implementation(kotlin("gradle-plugin", embeddedKotlinVersion))
gradleKotlinDsl()
implementation(kotlin("stdlib-jdk8"))
} }
val compileKotlin: KotlinCompile by tasks
compileKotlin.kotlinOptions {
jvmTarget = "1.8"
}

View File

@ -0,0 +1,10 @@
dependencyResolutionManagement {
repositories {
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}

View File

@ -1,29 +1,14 @@
import com.android.build.gradle.BaseExtension import com.android.build.gradle.BaseExtension
object AndroidConfig { object AndroidConfig {
const val COMPILE_SDK_VERSION = 29
const val MIN_SDK_VERSION = 23
const val TARGET_SDK_VERSION = 29
const val BUILD_TOOLS_VERSION = "29.0.2"
val VERSION_CODE: Int = Environment.BUILD_NUMBER.getenv()?.toIntOrNull() ?: 10000
const val VERSION_NAME = "1.0.0"
// TODO: change test package name // TODO: change test package name
const val TEST_APP_ID = "com.touchin.template" const val TEST_APP_ID = "ru.touchin.template"
// TODO: change common file folder // TODO: change common file folder
const val COMMON_FOLDER = "Template-common" const val COMMON_FOLDER = "common-template"
const val RELEASE_DEBUGGABLE = false
}
fun BaseExtension.ignoreCustomerProdFlavourIfReleaseIsDebuggable() {
variantFilter {
ignore = name.contains(ApiFlavour.CustomerProd.name, ignoreCase = true) && AndroidConfig.RELEASE_DEBUGGABLE
}
} }

View File

@ -0,0 +1,133 @@
import com.android.build.gradle.BaseExtension
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.kotlin.dsl.extra
fun BaseExtension.addFlavors(dimensionName: String, vararg flavorNames: String) {
if (flavorNames.isEmpty()) return
flavorDimensions(dimensionName)
flavorNames.forEach { flavor ->
productFlavors {
create(flavor) {
dimension = dimensionName
}
}
}
}
fun BaseExtension.addMobileServicesFlavor() {
addFlavors(dimensionName = "mobileServices", flavorNames = arrayOf("huawei", "google"))
}
fun BaseExtension.addBuildType(
type: BuildType,
project: Project,
) {
buildTypes {
maybeCreate(type.name)
getByName(type.name) {
isDebuggable = !type.optimizeAndObfuscate
isMinifyEnabled = type.optimizeAndObfuscate
isShrinkResources = type.optimizeAndObfuscate
setMatchingFallbacks(type.matchingFallbacks)
// if (listOf(BuildType.Develop, BuildType.Debug).contains(type)) {
// applicationIdSuffix = ".${type.name}"
// }
if (type.optimizeAndObfuscate) {
setProguardFiles(
listOf(
project.file("proguard").listFiles(),
getDefaultProguardFile("proguard-android-optimize.txt")
).filterNotNull()
)
}
extra.set("enableCrashlytics", type.enableCrashlytics)
buildConfigField("Boolean", "ENABLE_SSL_PINNING", type.enableSslPinning.toString())
buildConfigField("Boolean", "ENABLE_LOGS", type.enabledLogs.toString())
buildConfigField("Boolean", "ENABLE_DEBUG_PANEL", type.enabledDebugPanel.toString())
}
}
}
fun BaseExtension.addLibBuildType(
type: BuildType,
serverType: String? = null,
enableConfig: Boolean = false,
versionCatalog: VersionCatalog
) {
buildTypes {
maybeCreate(type.name)
getByName(type.name) {
isMinifyEnabled = type.optimizeAndObfuscate
setMatchingFallbacks(type.matchingFallbacks)
buildConfigField("String", "VERSION_NAME", "\"${versionCatalog.versionName}\"")
if (enableConfig) {
val server = serverType ?: type.serverType
// buildConfigField("ru.template.data.network.ServerUrl", "DEFAULT_SERVER", type.defaultServer)
buildConfigField("String", "DEFAULT_SERVER_TYPE", "\"$server\"")
buildConfigField("Boolean", "ENABLE_SSL_PINNING", type.enableSslPinning.toString())
buildConfigField("Boolean", "ENABLE_LOGS", type.enabledLogs.toString())
}
}
}
}
sealed class BuildType(
val name: String,
val optimizeAndObfuscate: Boolean,
val enableSslPinning: Boolean,
val enabledLogs: Boolean,
val enabledDebugPanel: Boolean,
val enableCrashlytics: Boolean = true,
val defaultServer: String = "ru.template.data.network.ServerUrl.CUSTOMER_TEST",
val serverType: String,
val matchingFallbacks: String = "debug",
) {
object Develop : BuildType(
name = "develop",
optimizeAndObfuscate = false,
enableSslPinning = false,
enabledLogs = true,
enabledDebugPanel = true,
enableCrashlytics = false,
serverType = "Test",
)
object Debug : BuildType(
name = "debug",
optimizeAndObfuscate = false,
enableSslPinning = false,
enabledLogs = true,
enabledDebugPanel = true,
serverType = "Test",
)
object Customer : BuildType(
name = "customer",
optimizeAndObfuscate = true,
enableSslPinning = true,
enabledLogs = false,
enabledDebugPanel = false,
defaultServer = "ru.template.data.network.ServerUrl.CUSTOMER_PROD",
serverType = "Prod",
matchingFallbacks = "release"
)
object Release : BuildType(
name = "release",
optimizeAndObfuscate = true,
enableSslPinning = true,
enabledLogs = false,
enabledDebugPanel = false,
defaultServer = "ru.template.data.network.ServerUrl.CUSTOMER_PROD",
serverType = "Prod",
matchingFallbacks = "release"
)
}

View File

@ -1,145 +0,0 @@
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.artifacts.dsl.DependencyHandler
fun DependencyHandler.fragment() {
implementation(Library.ANDROIDX_FRAGMENT)
implementation(Library.ANDROIDX_FRAGMENT_KTX)
implementationModule(Module.RoboSwag.NAVIGATION_BASE)
}
fun DependencyHandler.materialDesign() {
implementation(Library.ANDROID_MATERIAL)
implementation(Library.SWIPE_TO_REFRESH)
}
fun DependencyHandler.permissionDispatcher() {
implementation(Library.PERMISSION_DISPATCHER)
kapt(Library.PERMISSION_DISPATCHER_ANNOTATION_PROCESSOR)
}
fun DependencyHandler.constraintLayout() {
implementation(Library.ANDROIDX_CONSTRAINT)
}
fun DependencyHandler.androidX() {
implementation(Library.ANDROIDX_CORE)
implementation(Library.ANDROIDX_APPCOMPAT)
implementationModule(Module.RoboSwag.KOTLIN_EXTENSIONS)
}
fun DependencyHandler.recyclerView() {
implementation(Library.ANDROIDX_RECYCLER)
implementationModule(Module.RoboSwag.RECYCLER_VIEW_ADAPTERS)
}
fun DependencyHandler.kotlinStd() {
implementation(Library.KOTLIN_STDLIB)
}
fun DependencyHandler.navigation() {
implementation(Library.CICERONE)
implementationModule(Module.RoboSwag.NAVIGATION_CICERONE)
}
fun DependencyHandler.featureModules() {
Module.Feature.ALL.forEach(this::implementationModule)
}
fun DependencyHandler.mvi() {
implementationModule(Module.RoboSwag.MVI_ARCH)
fragment()
lifecycle()
}
fun DependencyHandler.coreNetwork() {
implementationModule(Module.Core.NETWORK)
}
fun DependencyHandler.coreStrings() {
implementationModule(Module.Core.STRINGS)
}
fun DependencyHandler.retrofit() {
implementation(Library.RETROFIT)
implementation(Library.OKHTTP_LOGGING_INTERCEPTOR)
implementation(Library.OKHTTP)
implementation(Library.MOSHI_RETROFIT)
}
fun DependencyHandler.dagger(withAssistedInject: Boolean = true) {
implementation(Library.DAGGER)
kapt(Library.DAGGER_COMPILER)
implementation(Library.DAGGER_COMPONENT_MANAGER)
if (withAssistedInject) {
compileOnly(Library.DAGGER_INJECT_ASSISTED_ANNOTATIONS)
kapt(Library.DAGGER_INJECT_ASSISTED_PROCESSOR)
}
}
fun DependencyHandler.glide() {
implementation(Library.GLIDE)
implementation(Library.GLIDE_OKHTTP_INTEGRATION)
kapt(Library.GLIDE_COMPILER)
}
fun DependencyHandler.moshi() {
implementation(Library.MOSHI)
kapt(Library.MOSHI_CODEGEN)
}
fun DependencyHandler.lifecycle() {
implementation(Library.ANDROID_LIFECYCLE_EXTENSIONS)
implementation(Library.ANDROID_LIFECYCLE_VIEW_MODEL_EXTENSIONS)
implementation(Library.ANDROID_LIFECYCLE_LIVE_DATA_EXTENSIONS)
implementationModule(Module.RoboSwag.LIFECYCLE)
}
fun DependencyHandler.coroutines() {
implementation(Library.COROUTINES_CORE)
implementation(Library.COROUTINES_ANDROID)
}
fun DependencyHandler.leakCanary() {
add("withTestPanelImplementation", Library.LEAK_CANARY)
}
fun DependencyHandler.sharedPrefs() {
implementationModule(Module.RoboSwag.STORABLE)
implementationModule(Module.Core.PREFS)
}
fun DependencyHandler.chucker() {
add("withTestPanelImplementation", Library.CHUCKER)
add("withoutTestPanelImplementation", Library.CHUCKER_NO_OP)
}
fun DependencyHandler.implementationModule(moduleName: String) {
implementation(project(":$moduleName"))
}
private fun DependencyHandler.implementation(dependencyNotation: Any): Dependency? =
add("implementation", dependencyNotation)
private fun DependencyHandler.kapt(dependencyNotation: Any): Dependency? =
add("kapt", dependencyNotation)
private fun DependencyHandler.compileOnly(dependencyNotation: Any): Dependency? =
add("compileOnly", dependencyNotation)
private fun DependencyHandler.project(
path: String,
configuration: String? = null
): ProjectDependency {
val notation = if (configuration != null) {
mapOf("path" to path, "configuration" to configuration)
} else {
mapOf("path" to path)
}
return uncheckedCast(project(notation))
}
@Suppress("unchecked_cast", "nothing_to_inline", "detekt.UnsafeCast")
private inline fun <T> uncheckedCast(obj: Any?): T = obj as T

View File

@ -1,13 +1,12 @@
object Environment { object Environment {
const val APP_ID = "BUNDLE_ID" const val APP_ID = "BUNDLE_ID"
const val STORE_PASSWORD = "STORE_PASSWORD" const val STORE_PASSWORD = "STORE_PASSWORD"
const val KEY_ALIAS = "KEY_ALIAS" const val ALIAS = "ALIAS"
const val KEY_PASSWORD = "KEY_PASSWORD" const val KEY_PASSWORD = "KEY_PASSWORD"
const val ENDPOINT = "CUSTOM_ENDPOINT" const val ENDPOINT = "CUSTOM_ENDPOINT"
const val BUILD_NUMBER = "BUILD_NUMBER" const val BUILD_NUMBER = "BUILD_NUMBER"
const val SERVER_ENVIRONMENT = "SERVER_ENVIRONMENT"
const val BUILD_TYPE = "BUILD_TYPE"
} }
fun String.getenv(): String? = System.getenv(this) fun String.getenv(): String? = System.getenv(this)

View File

@ -1,60 +0,0 @@
object Library {
const val KOTLIN_STDLIB = "org.jetbrains.kotlin:kotlin-stdlib:${Version.KOTLIN}"
const val ANDROIDX_APPCOMPAT = "androidx.appcompat:appcompat:${Version.ANDROIDX_APPCOMPAT}"
const val ANDROIDX_CORE = "androidx.core:core-ktx:${Version.ANDROIDX_CORE}"
const val ANDROIDX_RECYCLER = "androidx.recyclerview:recyclerview:${Version.ANDROIDX}"
const val ANDROIDX_CONSTRAINT = "androidx.constraintlayout:constraintlayout:${Version.ANDROIDX_CONSTRAINT}"
const val ANDROIDX_FRAGMENT = "androidx.fragment:fragment:${Version.ANDROIDX_FRAGMENT}"
const val ANDROIDX_FRAGMENT_KTX = "androidx.fragment:fragment-ktx:${Version.ANDROIDX_FRAGMENT}"
const val ANDROID_MATERIAL = "com.google.android.material:material:${Version.ANDROID_MATERIAL}"
const val SWIPE_TO_REFRESH = "androidx.swiperefreshlayout:swiperefreshlayout:${Version.SWIPE_TO_REFRESH}"
const val PERMISSION_DISPATCHER = "org.permissionsdispatcher:permissionsdispatcher:${Version.PERMISSION_DISPATCHER}"
const val PERMISSION_DISPATCHER_ANNOTATION_PROCESSOR = "org.permissionsdispatcher:permissionsdispatcher-processor:${Version.PERMISSION_DISPATCHER}"
const val ANDROID_LIFECYCLE_EXTENSIONS = "androidx.lifecycle:lifecycle-extensions:${Version.ANDROID_LIFECYCLE}"
const val ANDROID_LIFECYCLE_VIEW_MODEL_EXTENSIONS = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Version.ANDROID_LIFECYCLE}"
const val ANDROID_LIFECYCLE_LIVE_DATA_EXTENSIONS = "androidx.lifecycle:lifecycle-livedata-ktx:${Version.ANDROID_LIFECYCLE}"
const val DAGGER = "com.google.dagger:dagger:${Version.DAGGER}"
const val DAGGER_COMPILER = "com.google.dagger:dagger-compiler:${Version.DAGGER}"
const val DAGGER_INJECT_ASSISTED_ANNOTATIONS = "com.squareup.inject:assisted-inject-annotations-dagger2:${Version.DAGGER_INJECT_ASSISTED}"
const val DAGGER_INJECT_ASSISTED_PROCESSOR = "com.squareup.inject:assisted-inject-processor-dagger2:${Version.DAGGER_INJECT_ASSISTED}"
const val DAGGER_COMPONENT_MANAGER = "com.github.valeryponomarenko.componentsmanager:androidx:${Version.DAGGER_COMPONENT_MANAGER}"
const val GLIDE = "com.github.bumptech.glide:glide:${Version.GLIDE}"
const val GLIDE_COMPILER = "com.github.bumptech.glide:compiler:${Version.GLIDE}"
const val GLIDE_OKHTTP_INTEGRATION = "com.github.bumptech.glide:okhttp3-integration:${Version.GLIDE}"
const val RETROFIT = "com.squareup.retrofit2:retrofit:${Version.RETROFIT}"
const val OKHTTP_LOGGING_INTERCEPTOR = "com.squareup.okhttp3:logging-interceptor:${Version.OKHTTP}"
const val OKHTTP = "com.squareup.okhttp3:okhttp-urlconnection:${Version.OKHTTP}"
const val MOSHI = "com.squareup.moshi:moshi:${Version.MOSHI}"
const val MOSHI_CODEGEN = "com.squareup.moshi:moshi-kotlin-codegen:${Version.MOSHI}"
const val MOSHI_RETROFIT = "com.squareup.retrofit2:converter-moshi:${Version.RETROFIT}"
const val COROUTINES_CORE = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Version.COROUTINES}"
const val COROUTINES_ANDROID = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Version.COROUTINES}"
const val CICERONE = "ru.terrakok.cicerone:cicerone:${Version.CICERONE}"
const val LEAK_CANARY = "com.squareup.leakcanary:leakcanary-android:${Version.LEAK_CANARY}"
const val CHUCKER = "com.github.chuckerteam.chucker:library:${Version.CHUCKER}"
const val CHUCKER_NO_OP = "com.github.chuckerteam.chucker:library-no-op:${Version.CHUCKER}"
const val FIREBASE_ANAL = "com.google.firebase:firebase-analytics-ktx:${Version.FIREBASE_ANAL}"
const val FIREBASE_PERF = "com.google.firebase:firebase-perf:${Version.FIREBASE_PERF}"
const val FIREBASE_CRASH = "com.google.firebase:firebase-crashlytics:${Version.FIREBASE_CRASH}"
const val ANDROIDX_SECURE = "androidx.security:security-crypto:${Version.ANDROIDX_SECURE}"
const val ANDROIDX_BIOMETRIC = "androidx.biometric:biometric:${Version.ANDROIDX_BIOMETRIC}"
const val LICENCE_LIBRARY = "com.google.android.gms:play-services-oss-licenses:${Version.LICENCE_LIBRARY}"
}

View File

@ -1,35 +0,0 @@
object Module {
object RoboSwag {
const val UTILS = "utils"
const val LOGGING = "logging"
const val MVI_ARCH = "mvi-arch"
const val NAVIGATION_BASE = "navigation-base"
const val NAVIGATION_CICERONE = "navigation-cicerone"
const val STORABLE = "storable"
const val LIFECYCLE = "lifecycle"
const val VIEWS = "views"
const val RECYCLER_VIEW_ADAPTERS = "recyclerview-adapters"
const val RECYCLER_VIEW_DECORATORS = "recyclerview-decorators"
const val KOTLIN_EXTENSIONS = "kotlin-extensions"
}
object Feature {
const val LOGIN = "feature_login"
val ALL = listOf(
LOGIN
)
}
object Core {
const val NETWORK = "core_network"
const val PREFS = "core_prefs"
const val STRINGS = "core_strings"
const val UTILS = "core_utils"
const val UI = "core_ui"
const val DATA = "core_data"
const val DOMAIN = "core_domain"
}
}

View File

@ -5,16 +5,6 @@ object Plugins {
const val ANDROID_APP_PLUGIN_WITH_DEFAULT_CONFIG = "android_app" const val ANDROID_APP_PLUGIN_WITH_DEFAULT_CONFIG = "android_app"
const val ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG = "android_lib" const val ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG = "android_lib"
const val KOTLIN_ANDROID = "kotlin-android" const val KOTLIN_ANDROID = "org.jetbrains.kotlin.android"
const val KOTLIN_ANDROID_EXTENSIONS = "kotlin-android-extensions"
const val KOTLIN_KAPT = "kotlin-kapt" const val KOTLIN_KAPT = "kotlin-kapt"
const val LICENCE_PLUGIN = "com.google.android.gms.oss-licenses-plugin"
const val GOOGLE_SERVICES = "com.google.gms.google-services"
const val FIREBASE_CRASH = "com.google.firebase.crashlytics"
const val DEPENDENCY_GRAPH = "com.vanniktech.dependency.graph.generator"
const val DETEKT = "io.gitlab.arturbosch.detekt"
const val CPD = "de.aaschmid.cpd"
} }

View File

@ -1,47 +0,0 @@
object Version {
const val ANDROID_PLUGIN = "4.0.0"
const val KOTLIN = "1.3.72"
const val ANDROIDX = "1.1.0"
const val ANDROIDX_CORE = "1.2.0"
const val ANDROIDX_APPCOMPAT = "1.0.2"
const val ANDROIDX_CONSTRAINT = "2.0.0-beta4"
const val ANDROIDX_FRAGMENT = "1.2.1"
const val ANDROIDX_SECURE = "1.0.0-rc02"
const val ANDROIDX_BIOMETRIC = "1.0.1"
const val ANDROID_MATERIAL = "1.2.0-rc01"
const val SWIPE_TO_REFRESH = "1.0.0"
const val ANDROID_LIFECYCLE = "2.2.0"
const val PERMISSION_DISPATCHER = "4.8.0"
const val DAGGER = "2.27"
const val DAGGER_INJECT_ASSISTED = "0.5.2"
const val DAGGER_COMPONENT_MANAGER = "2.1.0"
const val GLIDE = "4.10.0"
const val RETROFIT = "2.8.1"
const val OKHTTP = "3.14.1"
const val MOSHI = "1.9.2"
const val COROUTINES = "1.3.7"
const val CICERONE = "5.1.0"
const val FIREBASE_ANAL = "17.4.3"
const val FIREBASE_CRASH = "17.1.0"
const val FIREBASE_PERF = "19.0.7"
const val GOOGLE_SERVICES_PLUGIN = "4.3.3"
const val FIREBASE_CRASH_PLUGIN = "2.2.0"
const val LEAK_CANARY = "2.4"
const val CHUCKER = "3.2.0"
const val LICENCE_LIBRARY = "17.0.0"
}

View File

@ -0,0 +1,6 @@
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.provider.Provider
private fun VersionCatalog.getLibrary(library: String) = findLibrary(library).get()

View File

@ -0,0 +1,19 @@
import org.gradle.api.artifacts.VersionCatalog
val VersionCatalog.sdkCompile: String
get() = findVersion("compileSdk").get().requiredVersion
val VersionCatalog.sdkMin: String
get() = findVersion("minSdk").get().requiredVersion
val VersionCatalog.sdkTarget: String
get() = findVersion("targetSdk").get().requiredVersion
val VersionCatalog.jvmBytecode: String
get() = findVersion("jvmBytecode").get().requiredVersion
val VersionCatalog.versionCode: String
get() = findVersion("versionCode").get().requiredVersion
val VersionCatalog.versionName: String
get() = findVersion("versionName").get().requiredVersion

View File

@ -1,41 +0,0 @@
import com.android.build.gradle.BaseExtension
fun BaseExtension.addBuildType(
type: BuildType,
buildScriptDir: String
) {
buildTypes {
getByName(type.name) {
isMinifyEnabled = type.optimizeAndObfuscate
isShrinkResources = type.optimizeAndObfuscate
if (type.optimizeAndObfuscate) {
val proguardFile = if (AndroidConfig.RELEASE_DEBUGGABLE) "noObfuscate.pro" else "obfuscate.pro"
setProguardFiles(listOfNotNull(
getDefaultProguardFile("proguard-android-optimize.txt"),
"$buildScriptDir/proguard/$proguardFile",
"proguard/projectConfig.pro"
))
}
}
}
}
sealed class BuildType(
val name: String,
val optimizeAndObfuscate: Boolean
) {
object Debug : BuildType(
name = "debug",
optimizeAndObfuscate = false
)
object Release : BuildType(
name = "release",
optimizeAndObfuscate = true
)
}

View File

@ -11,7 +11,7 @@ sealed class SSLPinningFlavour(
object OFF : SSLPinningFlavour( object OFF : SSLPinningFlavour(
name = "withoutSSLPinning", name = "withoutSSLPinning",
withSslPinning = true withSslPinning = false
) )
object ON : SSLPinningFlavour( object ON : SSLPinningFlavour(

View File

@ -15,7 +15,7 @@ fun BaseExtension.configureSigningConfig(getRelativeFile: (String) -> File) {
create(SigningConfig.CONFIG_NAME) { create(SigningConfig.CONFIG_NAME) {
storeFile = getRelativeFile(SigningConfig.PATH_TO_KEYSTORE_FILE) storeFile = getRelativeFile(SigningConfig.PATH_TO_KEYSTORE_FILE)
storePassword = Environment.STORE_PASSWORD.getenv() ?: SigningConfig.DEFAULT_STORE_PASSWORD storePassword = Environment.STORE_PASSWORD.getenv() ?: SigningConfig.DEFAULT_STORE_PASSWORD
keyAlias = Environment.KEY_ALIAS.getenv() ?: SigningConfig.DEFAULT_KEY_ALIAS keyAlias = Environment.ALIAS.getenv() ?: SigningConfig.DEFAULT_KEY_ALIAS
keyPassword = Environment.KEY_PASSWORD.getenv() ?: SigningConfig.DEFAULT_KEY_PASSWORD keyPassword = Environment.KEY_PASSWORD.getenv() ?: SigningConfig.DEFAULT_KEY_PASSWORD
} }
} }

View File

@ -2,6 +2,9 @@ package plugins
import Plugins import Plugins
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType
class AndroidAppPlugin : BaseAndroidPlugin() { class AndroidAppPlugin : BaseAndroidPlugin() {

View File

@ -4,15 +4,23 @@ import AndroidConfig
import BuildType import BuildType
import Plugins import Plugins
import com.android.build.gradle.BaseExtension import com.android.build.gradle.BaseExtension
import kotlinStd import jvmBytecode
import org.gradle.api.JavaVersion import org.gradle.api.JavaVersion
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.artifacts.dsl.DependencyHandler import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.internal.impldep.junit.runner.Version.id
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import sdkCompile
import sdkMin
import sdkTarget
import versionCode
import versionName
abstract class BaseAndroidPlugin : Plugin<Project> { abstract class BaseAndroidPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
@ -23,50 +31,45 @@ abstract class BaseAndroidPlugin : Plugin<Project> {
private fun Project.configurePlugins() { private fun Project.configurePlugins() {
plugins.apply(Plugins.KOTLIN_ANDROID) plugins.apply(Plugins.KOTLIN_ANDROID)
plugins.apply(Plugins.KOTLIN_ANDROID_EXTENSIONS)
plugins.apply(Plugins.KOTLIN_KAPT) plugins.apply(Plugins.KOTLIN_KAPT)
} }
private fun Project.configureAndroid() = extensions.getByType<BaseExtension>().run { private fun Project.configureAndroid() = extensions.getByType<BaseExtension>().run {
compileSdkVersion(AndroidConfig.COMPILE_SDK_VERSION) compileSdkVersion(libs.sdkCompile.toInt())
buildToolsVersion = AndroidConfig.BUILD_TOOLS_VERSION
defaultConfig { defaultConfig {
minSdkVersion(AndroidConfig.MIN_SDK_VERSION) minSdk = libs.sdkMin.toInt()
targetSdkVersion(AndroidConfig.TARGET_SDK_VERSION) targetSdk = libs.sdkTarget.toInt()
versionCode = AndroidConfig.VERSION_CODE versionCode = libs.versionCode.toInt()
versionName = AndroidConfig.VERSION_NAME versionName = libs.versionName
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.toVersion(libs.jvmBytecode)
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.toVersion(libs.jvmBytecode)
coreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
} }
buildFeatures.viewBinding = true buildFeatures.apply {
buildConfig = true
viewBinding = true
}
tasks.withType(KotlinCompile::class.java) { tasks.withType(KotlinCompile::class.java) {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = libs.jvmBytecode
}
}
if (AndroidConfig.RELEASE_DEBUGGABLE) {
buildTypes {
getByName(BuildType.Release.name) {
isDebuggable = true
}
} }
} }
} }
private fun Project.configureDependencies() = dependencies { private fun Project.configureDependencies() = dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
kotlinStd() add("coreLibraryDesugaring", "com.android.tools:desugar_jdk_libs:2.0.4")
add("coreLibraryDesugaring", "com.android.tools:desugar_jdk_libs:1.0.5")
} }
private fun DependencyHandler.implementation(dependencyNotation: Any): Dependency? = private fun DependencyHandler.implementation(dependencyNotation: Any): Dependency? =
add("implementation", dependencyNotation) add("implementation", dependencyNotation)
} }
internal val Project.libs: VersionCatalog
get() = extensions.getByType<VersionCatalogsExtension>().named("libs")

1
common-template Submodule

@ -0,0 +1 @@
Subproject commit d6f303bf879a2da1706cfdacaf2bbe0c326044bd

View File

@ -1,3 +0,0 @@
plugins {
id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG)
}

View File

@ -1 +0,0 @@
<manifest package="ru.touchin.template.core_data" />

View File

@ -1,3 +0,0 @@
plugins {
id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG)
}

View File

@ -1 +0,0 @@
<manifest package="ru.touchin.template.core_domain" />

View File

@ -1 +0,0 @@
/build

View File

@ -1,20 +0,0 @@
plugins {
id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG)
// id("api-generator-android")
}
// TODO: uncomment api generator
//apiGenerator {
// pathToApiSchemes = "${AndroidConfig.COMMON_FOLDER}/api"
// outputPackageName = AndroidConfig.TEST_APP_ID
// outputLanguage = apigen.OutputLanguage.KotlinAndroid(
// methodOutputType = apigen.MethodOutputType.Coroutine
// )
//}
dependencies {
retrofit()
dagger()
moshi()
coroutines()
}

View File

@ -1 +0,0 @@
<manifest package="ru.touchin.template.core_network" />

View File

@ -1,10 +0,0 @@
package ru.touchin.template.network
import okhttp3.ResponseBody
import java.nio.charset.Charset
fun ResponseBody.cloneBody(): String? = source()
.also { it.request(Long.MAX_VALUE) }
.buffer
?.clone()
?.readString(Charset.forName("UTF-8"))

View File

@ -1,6 +0,0 @@
package ru.touchin.template.network.di
import javax.inject.Qualifier
@Qualifier
annotation class ApiUrl

View File

@ -1,6 +0,0 @@
package ru.touchin.template.network.di
import javax.inject.Qualifier
@Qualifier
annotation class ChuckInterceptor

View File

@ -1,69 +0,0 @@
package ru.touchin.template.network.di
import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
import okhttp3.CertificatePinner
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import ru.touchin.template.network.interceptor.ExceptionsInterceptor
import ru.touchin.template.core_network.BuildConfig
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
class NetworkModule {
companion object {
private const val TIMEOUT = 30L
}
@Singleton
@Provides
fun providePublicClient(
exceptionsInterceptor: ExceptionsInterceptor,
@ChuckInterceptor chuckerInterceptor: Interceptor,
@WithSslPinning withSslPinning: Boolean
): OkHttpClient =
buildPublicClient(exceptionsInterceptor, chuckerInterceptor, withSslPinning)
@Singleton
@Provides
fun provideMoshi() = buildMoshi()
@Singleton
@Provides
fun provideRetrofit(client: OkHttpClient, moshi: Moshi, @ApiUrl apiUrl: String) = buildRetrofitInstance(client, moshi, apiUrl)
private fun buildMoshi() = Moshi.Builder()
.build()
private fun buildRetrofitInstance(client: OkHttpClient, moshi: Moshi, apiUrl: String): Retrofit = Retrofit.Builder()
.baseUrl(apiUrl)
.client(client)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
private fun buildPublicClient(
exceptionsInterceptor: ExceptionsInterceptor,
chuckerInterceptor: Interceptor,
withSslPinning: Boolean
): OkHttpClient = OkHttpClient.Builder()
.apply {
connectTimeout(TIMEOUT, TimeUnit.SECONDS)
readTimeout(TIMEOUT, TimeUnit.SECONDS)
writeTimeout(TIMEOUT, TimeUnit.SECONDS)
addInterceptor(exceptionsInterceptor)
addInterceptor(chuckerInterceptor)
if (BuildConfig.DEBUG) {
addNetworkInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY })
}
if (withSslPinning) {
certificatePinner(CertificatePinner.DEFAULT)
}
}.build()
}

View File

@ -1,6 +0,0 @@
package ru.touchin.template.network.di
import javax.inject.Qualifier
@Qualifier
annotation class WithSslPinning

View File

@ -1,41 +0,0 @@
package ru.touchin.template.network.interceptor
import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.ResponseBody
import org.json.JSONException
import org.json.JSONObject
import ru.touchin.template.network.cloneBody
import ru.touchin.template.network.models.ServerException
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ExceptionsInterceptor @Inject constructor() : Interceptor {
companion object {
private const val ERROR_MESSAGE_FIELD = "errorMessage"
}
override fun intercept(chain: Interceptor.Chain): Response = chain
.proceed(chain.request())
.also { getError(it, it.body())?.let { exception -> throw exception } }
@Suppress("detekt.NestedBlockDepth")
private fun getError(response: Response, body: ResponseBody?): IOException? = body
?.cloneBody()
?.let { responseBody ->
try {
val jsonObject = JSONObject(responseBody)
val message = jsonObject.optString(ERROR_MESSAGE_FIELD)
when {
response.code() != 200 -> ServerException(response.code(), message)
else -> null
}
} catch (error: JSONException) {
null
}
}
}

View File

@ -1,18 +0,0 @@
package ru.touchin.template.network.models
import java.io.IOException
open class ServerException(val code: Int, message: String? = null) : IOException(message) {
companion object {
private val codeToErrorTypeMap = mapOf(
1 to TemplateApiError.VALID_RESPONSE,
2 to TemplateApiError.INVALID_PARAMETERS
)
fun getErrorTypeByCode(code: Int) = codeToErrorTypeMap[code]
}
fun getErrorType(): TemplateApiError? = codeToErrorTypeMap[code]
}

View File

@ -1,9 +0,0 @@
package ru.touchin.template.network.models
enum class TemplateApiError {
INVALID_PARAMETERS,
VALID_RESPONSE
}

View File

@ -1,3 +0,0 @@
<resources>
<string name="app_name">My Library</string>
</resources>

View File

@ -1 +0,0 @@
/build

View File

@ -1,8 +0,0 @@
plugins {
id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG)
}
dependencies {
implementation(Library.DAGGER)
implementationModule(Module.RoboSwag.STORABLE)
}

View File

@ -1 +0,0 @@
<manifest package="ru.touchin.template.core_prefs" />

View File

@ -1,24 +0,0 @@
package ru.touchin.template.core_prefs
import android.content.Context
import android.content.SharedPreferences
import android.preference.PreferenceManager
import dagger.Module
import dagger.Provides
import ru.touchin.roboswag.components.utils.storables.PreferenceUtils
import ru.touchin.roboswag.core.observables.storable.NonNullStorable
import javax.inject.Singleton
@Module
class PreferencesModule {
@Provides
@Singleton
fun provideDefaultSharedPreferences(context: Context): SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
@Provides
@Singleton
fun provideTutorialStorable(sharedPreferences: SharedPreferences): NonNullStorable<String, Boolean, Boolean> = PreferenceUtils
.booleanStorable("TUTORIAL_STORABLE", sharedPreferences, false)
}

View File

@ -1 +0,0 @@
/build

View File

@ -1,15 +0,0 @@
plugins {
id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG)
}
android {
ext["languageMap"] = mapOf("ru" to "${AndroidConfig.COMMON_FOLDER}/strings/default_common_strings_ru.json")
ext["rootPath"] = "core/core_strings"
}
//gradle.projectsEvaluated {
// tasks.named("preBuild") {
// dependsOn("stringGenerator")
// }
//}
//
//apply(from = "${rootProject.ext["buildScriptsDir"]}/gradle/stringGenerator.gradle")

View File

@ -1 +0,0 @@
<manifest package="ru.touchin.template" />

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="common_global_yes" formatted="false">Да</string>
</resources>

View File

@ -1,3 +0,0 @@
plugins {
id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG)
}

View File

@ -1 +0,0 @@
<manifest package="ru.touchin.template.core_ui" />

View File

@ -1 +0,0 @@
/build

View File

@ -1,3 +0,0 @@
plugins {
id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG)
}

View File

@ -1 +0,0 @@
<manifest package="ru.touchin.template.core_utils" />

1
data/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

48
data/build.gradle.kts Normal file
View File

@ -0,0 +1,48 @@
plugins {
id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG)
}
private val serverType = Environment.SERVER_ENVIRONMENT.getenv()?.takeIf(String::isNotBlank)
private val versionCatalog: VersionCatalog
get() = extensions.getByType<VersionCatalogsExtension>().named("libs")
android {
namespace = "ru.template.data"
addLibBuildType(type = BuildType.Develop, enableConfig = true, versionCatalog = versionCatalog)
addLibBuildType(type = BuildType.Debug, enableConfig = true, versionCatalog = versionCatalog)
addLibBuildType(type = BuildType.Customer, enableConfig = true, versionCatalog = versionCatalog)
addLibBuildType(type = BuildType.Release, enableConfig = true, versionCatalog = versionCatalog)
sourceSets {
getByName("main") {
java.srcDirs("src/main/kotlin")
}
getByName("androidTest") {
java.srcDirs("src/androidTest/kotlin")
}
getByName("test") {
java.srcDirs("src/test/kotlin")
}
}
addMobileServicesFlavor()
testOptions {
unitTests {
isReturnDefaultValues = true
}
}
}
dependencies {
implementation(project(":domain"))
implementation(project(":mobile_services"))
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.espresso.core)
}
val Project.buildScriptDir: String
get() = rootProject.ext["buildScriptsDir"] as String

View File

@ -0,0 +1,24 @@
package ru.template.data
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("ru.template.data.test", appContext.packageName)
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -0,0 +1,17 @@
package ru.template.data
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

1
domain/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

29
domain/build.gradle.kts Normal file
View File

@ -0,0 +1,29 @@
plugins {
id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG)
}
private val versionCatalog: VersionCatalog
get() = extensions.getByType<VersionCatalogsExtension>().named("libs")
android {
namespace = "ru.template.domain"
buildTypes {
addLibBuildType(BuildType.Develop, versionCatalog = versionCatalog)
addLibBuildType(BuildType.Debug, versionCatalog = versionCatalog)
addLibBuildType(BuildType.Customer, versionCatalog = versionCatalog)
addLibBuildType(BuildType.Release, versionCatalog = versionCatalog)
}
addMobileServicesFlavor()
sourceSets {
getByName("main") {
java.srcDirs("src/main/kotlin")
}
}
}
dependencies {
implementation(project(":mobile_services"))
}

Some files were not shown because too many files have changed in this diff Show More