new multi module arch

This commit is contained in:
Maxim Bachinsky 2020-06-28 23:18:31 +03:00
parent 838fe53d28
commit 491d86d838
90 changed files with 1357 additions and 602 deletions

View File

@ -1,178 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.firebase.crashlytics'
apply plugin: 'com.google.gms.google-services'
def customEndpoint = System.getenv("CUSTOM_ENDPOINT")
android {
compileSdkVersion versions.compileSdk
defaultConfig {
applicationId "com.touchin.template"
minSdkVersion 21
targetSdkVersion versions.compileSdk
versionCode System.getenv("BUILD_NUMBER") as Integer ?: 10000
versionName "1.0." + versionCode
rootProject.extensions.pathToApiSchemes = "$rootDir/Template-common/api"
rootProject.extensions.applicationId = "com.touchin.template"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
firebaseCrashlytics {
mappingFileUploadEnabled true
}
signingConfigs {
release {
storeFile file("keystore/touchin.jks")
storePassword "iphoneandroidwp7"
keyAlias "touchin"
keyPassword "iphoneandroidwp7"
}
}
buildTypes {
debug {
versionNameSuffix ".debug"
minifyEnabled false
shrinkResources false
ext.enableCrashlytics = false
signingConfig signingConfigs.release
}
release {
minifyEnabled true
shrinkResources true
ext.enableCrashlytics = true
signingConfig signingConfigs.release
}
}
flavorDimensions "proguardSettings", "apiEndpoint", "sslPinning", "testPanel"
/*
Use that guide for adding new server env. flavours https://github.com/TouchInstinct/Styleguide/blob/master/general/setupBuildGuide.md
*/
productFlavors {
noObfuscate {
dimension "proguardSettings"
proguardFiles getDefaultProguardFile('proguard-android.txt'), "$buildScriptsDir/proguard/noObfuscate.pro"
}
obfuscate {
dimension "proguardSettings"
proguardFiles getDefaultProguardFile('proguard-android.txt'), "$buildScriptsDir/proguard/obfuscate.pro"
}
touchinTest {
def endpoint = customEndpoint ?: 'https://template-server.test.touchin.ru'
dimension "apiEndpoint"
buildConfigField "String", "API_URL", """\"${endpoint}/\""""
}
customerProd {
def endpoint = customEndpoint ?: 'https://template-server.prod.customer.ru'
dimension "apiEndpoint"
buildConfigField "String", "API_URL", """\"${endpoint}/\""""
}
withSSLPinning {
dimension "sslPinning"
buildConfigField "Boolean", "PIN_SSL", 'true'
}
withoutSSLPinning {
dimension "sslPinning"
buildConfigField "Boolean", "PIN_SSL", 'false'
}
withTestPanel {
dimension "testPanel"
}
withoutTestPanel {
dimension "testPanel"
}
}
extensions.languageMap = ["ru": "Template-common/strings/default_common_strings_ru.json"]
}
androidExtensions {
experimental = true
}
dependencies {
// RoboSwag
gradle.ext.roboswag.forEach { module ->
implementation project(":$module")
}
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// AndroidX
implementation "androidx.appcompat:appcompat:$versions.appcompat"
implementation "androidx.recyclerview:recyclerview:$versions.androidx"
implementation "androidx.cardview:cardview:$versions.androidx"
implementation "androidx.gridlayout:gridlayout:$versions.androidx"
implementation "androidx.core:core-ktx:$versions.androidxKtx"
implementation "androidx.constraintlayout:constraintlayout:2.0.0-beta4"
implementation "com.google.android.material:material:$versions.material"
// Lifecycle
implementation "androidx.lifecycle:lifecycle-extensions:$versions.lifecycle"
implementation "androidx.lifecycle:lifecycle-reactivestreams:$versions.lifecycle"
// Room
implementation "androidx.room:room-runtime:$versions.room"
implementation "androidx.room:room-rxjava2:$versions.room"
kapt "androidx.room:room-compiler:$versions.room"
// Retrofit
implementation "com.squareup.retrofit2:retrofit:$versions.retrofit"
implementation "com.squareup.retrofit2:adapter-rxjava2:$versions.retrofit"
implementation "com.squareup.okhttp3:logging-interceptor:$versions.okhttp3"
implementation "com.squareup.okhttp3:okhttp-urlconnection:$versions.okhttp3"
// Logan square
implementation "ru.touchin:logansquare:$versions.logansquare"
kapt "ru.touchin:logansquare-compiler:$versions.logansquare"
// Dagger
implementation "com.google.dagger:dagger:$versions.dagger"
kapt "com.google.dagger:dagger-compiler:$versions.dagger"
// RxJava
implementation "io.reactivex.rxjava2:rxjava:$versions.rxJava"
implementation "io.reactivex.rxjava2:rxandroid:$versions.rxAndroid"
// Glide
implementation "com.github.bumptech.glide:glide:$versions.glide"
implementation "com.github.bumptech.glide:okhttp3-integration:$versions.glide"
kapt "com.github.bumptech.glide:compiler:$versions.glide"
// Chucker
withTestPanelImplementation "com.github.ChuckerTeam.Chucker:library:$versions.chucker"
withoutTestPanelImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$versions.chucker"
// LeakCanary
withTestPanelImplementation "com.squareup.leakcanary:leakcanary-android:$versions.leakcanary"
}
//TODO: uncomment, when common repo become plugged
//gradle.projectsEvaluated {
// preBuild.dependsOn('stringGenerator')
//}
//apply from: "$buildScriptsDir/gradle/stringGenerator.gradle"
apply from: "$buildScriptsDir/gradle/apiGenerator.gradle"
apply from: "$buildScriptsDir/gradle/applicationFileNaming.gradle"

97
app/build.gradle.kts Normal file
View File

@ -0,0 +1,97 @@
plugins {
id(Plugins.ANDROID_APP_PLUGIN_WITH_DEFAULT_CONFIG)
id(Plugins.FIREBASE_CRASHLYTICS)
}
android {
signingConfigs {
addConfig(SigningConfig.Test)
addConfig(SigningConfig.Prod)
}
defaultConfig {
applicationId = AndroidConfig.TEST_APP_ID
signingConfig = signingConfigs.getByName(SigningConfig.Test.name)
}
firebaseCrashlytics {
mappingFileUploadEnabled = true
}
buildTypes {
addBuildType(BuildType.Debug)
addBuildType(BuildType.Release)
}
flavorDimensions(
ProguardFlavour.DIMENSION_NAME,
ApiFlavour.DIMENSION_NAME,
SSLPinningFlavour.DIMENSION_NAME,
TestPanelFlavour.DIMENSION_NAME
)
productFlavors {
create(ProguardFlavour.NO_OBFUSCATE) {
dimension = ProguardFlavour.DIMENSION_NAME
setProguardFiles(listOf(
getDefaultProguardFile("proguard-android.txt"),
"$rootProject.projectDir/BuildScripts/proguard/noObfuscate.pro"
))
}
create(ProguardFlavour.OBFUSCATE) {
dimension = ProguardFlavour.DIMENSION_NAME
setProguardFiles(listOf(
getDefaultProguardFile("proguard-android.txt"),
"$rootProject.projectDir/BuildScripts/proguard/obfuscate.pro"
))
}
addEmptyFlavour(ApiFlavour.MockDev)
addEmptyFlavour(ApiFlavour.TouchinTest)
addEmptyFlavour(ApiFlavour.CustomerProd)
addEmptyFlavour(SSLPinningFlavour.OFF)
addEmptyFlavour(SSLPinningFlavour.ON)
addEmptyFlavour(TestPanelFlavour.OFF)
addEmptyFlavour(TestPanelFlavour.ON)
}
extensions.add("languageMap", mapOf("ru" to "Template-common/strings/default_common_strings_ru.json"))
variantFilter = Action {
if (name.contentEquals(AndroidConfig.PROD_BUILD_NAME)) {
(defaultConfig as com.android.build.gradle.internal.dsl.BaseFlavor).apply {
applicationId = AndroidConfig.PROD_APP_ID
signingConfig = signingConfigs.getByName(SigningConfig.Prod.name)
}
}
}
}
androidExtensions {
features = setOf("parcelize")
}
dependencies {
androidX()
featureModules()
mvi()
materialDesign()
dagger()
retrofit()
moshi()
navigation()
coreNetwork()
leakCanary()
sharedPrefs()
}
//gradle.projectsEvaluated {
// preBuild.dependsOn('stringGenerator')
//}
//apply(from = "${rootProject.ext["buildScriptsDir"]}/gradle/stringGenerator.gradle")
apply(from = "${rootProject.ext["buildScriptsDir"]}/gradle/applicationFileNaming.gradle")

View File

@ -1,12 +1,12 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.touchin.template">
package="ru.touchin.template">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:name="ru.touchin.template.TemplateApplication"
android:name="ru.touchin.template.App"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/common_global_app_name"
@ -16,7 +16,7 @@
tools:ignore="GoogleAppIndexingWarning">
<activity
android:name="ru.touchin.template.activities.StartupActivity"
android:name="ru.touchin.template.SingleActivity"
android:screenOrientation="portrait"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize">

View File

@ -0,0 +1,27 @@
package ru.touchin.template
import me.vponomarenko.injectionmanager.IHasComponent
import me.vponomarenko.injectionmanager.x.XInjectionManager
import ru.touchin.roboswag.navigation_base.TouchinApp
import ru.touchin.template.core_prefs.PreferencesModule
import ru.touchin.template.di.ApplicationComponent
import ru.touchin.template.di.DaggerApplicationComponent
class App : TouchinApp(), IHasComponent<ApplicationComponent> {
override fun onCreate() {
super.onCreate()
initDagger()
}
fun initDagger() {
XInjectionManager.init(this)
XInjectionManager.bindComponent(this)
}
override fun getComponent(): ApplicationComponent = DaggerApplicationComponent
.builder()
.preferencesModule(PreferencesModule(this))
.build()
}

View File

@ -0,0 +1,46 @@
package ru.touchin.template
import android.os.Bundle
import me.vponomarenko.injectionmanager.x.XInjectionManager
import ru.terrakok.cicerone.NavigatorHolder
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.StartUpNavigation
import javax.inject.Inject
class SingleActivity : BaseActivity() {
@Inject
@MainNavigation
lateinit var navigatorHolder: NavigatorHolder
@Inject
lateinit var navigation: StartUpNavigation
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
injectDependencies()
lifecycle.addObserver(
CiceroneTuner(
activity = this,
navigatorHolder = navigatorHolder,
fragmentContainerId = R.id.fragment_container
)
)
navigation.start()
}
private fun injectDependencies() {
XInjectionManager
.findComponent<ApplicationComponent>()
.inject(this)
}
}

View File

@ -1,43 +0,0 @@
package ru.touchin.template
import androidx.appcompat.app.AppCompatDelegate
import com.bluelinelabs.logansquare.LoganSquare
import org.joda.time.DateTime
import org.joda.time.format.DateTimeFormat
import ru.touchin.lifecycle.viewmodel.ViewModelFactory
import ru.touchin.lifecycle.viewmodel.ViewModelFactoryProvider
import ru.touchin.roboswag.components.navigation.TouchinApp
import ru.touchin.template.di.app.DaggerApplicationComponent
import ru.touchin.template.di.app.modules.ApplicationModule
import ru.touchin.templates.logansquare.LoganSquareBigDecimalConverter
import ru.touchin.templates.logansquare.LoganSquareJodaTimeConverter
import java.math.BigDecimal
import javax.inject.Inject
class TemplateApplication : TouchinApp(), ViewModelFactoryProvider {
@Inject
override lateinit var viewModelFactory: ViewModelFactory
override fun onCreate() {
super.onCreate()
//TODO remove after init
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
initializeLoganSquare()
initializeDagger()
}
private fun initializeLoganSquare() {
val formatter = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ssZZ")
LoganSquare.registerTypeConverter(BigDecimal::class.java, LoganSquareBigDecimalConverter())
LoganSquare.registerTypeConverter(DateTime::class.java, LoganSquareJodaTimeConverter(formatter))
}
private fun initializeDagger() {
DaggerApplicationComponent.builder()
.applicationModule(ApplicationModule(this))
.build()
.inject(this)
}
}

View File

@ -1,14 +0,0 @@
package ru.touchin.template.activities
import android.os.Bundle
import com.touchin.template.R
import ru.touchin.roboswag.components.navigation.activities.BaseActivity
class StartupActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.common_activity)
}
}

View File

@ -1,41 +0,0 @@
package ru.touchin.template.api
import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.ResponseBody
import org.json.JSONException
import org.json.JSONObject
import ru.touchin.template.api.exceptions.ServerException
import ru.touchin.template.extensions.cloneBody
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,12 +0,0 @@
package ru.touchin.template.api
import io.reactivex.Single
import retrofit2.http.Body
import retrofit2.http.POST
interface UserApi {
@POST("user/session/create")
fun getSession(@Body body: Body): Single<String>
}

View File

@ -0,0 +1,30 @@
package ru.touchin.template.di
import android.content.Context
import dagger.Component
import ru.terrakok.cicerone.Router
import ru.touchin.mvi_test.feature_login.LoginDeps
import ru.touchin.mvitest.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 = [
PreferencesModule::class,
MainNavigationModule::class,
NetworkModule::class,
CoordinatorsImpl::class
])
interface ApplicationComponent : LoginDeps {
@MainNavigation
fun router(): Router
fun inject(application: App)
fun inject(activity: SingleActivity)
}

View File

@ -0,0 +1,9 @@
package ru.touchin.template.di
import dagger.Module
import dagger.Provides
@Module
class ApplicationModule {
}

View File

@ -0,0 +1,13 @@
package ru.touchin.template.di
import dagger.Binds
import dagger.Module
import ru.touchin.mvi_test.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,27 @@
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

@ -1,17 +0,0 @@
package ru.touchin.template.di.app
import dagger.Component
import ru.touchin.template.TemplateApplication
import ru.touchin.template.di.app.modules.ApplicationModule
import ru.touchin.template.di.app.modules.NetworkModule
import ru.touchin.template.di.app.modules.PersistentModule
import ru.touchin.template.di.viewmodel.ViewModelModule
import javax.inject.Singleton
@Singleton
@Component(modules = [ApplicationModule::class, PersistentModule::class, ViewModelModule::class, NetworkModule::class])
interface ApplicationComponent {
fun inject(application: TemplateApplication)
}

View File

@ -1,18 +0,0 @@
package ru.touchin.template.di.app.modules
import android.content.Context
import android.content.SharedPreferences
import android.preference.PreferenceManager
import dagger.Module
import dagger.Provides
@Module
class ApplicationModule(private val context: Context) {
@Provides
fun provideContext(): Context = context
@Provides
fun provideDefaultSharedPreferences(context: Context): SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
}

View File

@ -1,63 +0,0 @@
package ru.touchin.template.di.app.modules
import android.content.Context
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.touchin.template.BuildConfig
import dagger.Module
import dagger.Provides
import io.reactivex.schedulers.Schedulers
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import ru.touchin.template.api.ExceptionsInterceptor
import ru.touchin.template.api.UserApi
import ru.touchin.template.di.qualifiers.PublicApi
import ru.touchin.templates.logansquare.LoganSquareJsonFactory
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
class NetworkModule {
companion object {
private val CONVERTER_FACTORY = LoganSquareJsonFactory()
private val CALL_ADAPTER_FACTORY = RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())
private const val TIMEOUT = 30L
}
@Singleton
@Provides
fun provideUserApi(@PublicApi retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java)
@Singleton
@PublicApi
@Provides
fun providePublicRetrofit(@PublicApi client: OkHttpClient) = buildRetrofitInstance(client, BuildConfig.API_URL)
@Singleton
@PublicApi
@Provides
fun providePublicClient(context: Context, exceptionsInterceptor: ExceptionsInterceptor): OkHttpClient =
buildPublicClient(context, exceptionsInterceptor)
private fun buildRetrofitInstance(client: OkHttpClient, apiUrl: String): Retrofit = Retrofit.Builder()
.baseUrl(apiUrl)
.client(client)
.addCallAdapterFactory(CALL_ADAPTER_FACTORY)
.addConverterFactory(CONVERTER_FACTORY)
.build()
private fun buildPublicClient(context: Context, exceptionsInterceptor: ExceptionsInterceptor): OkHttpClient = OkHttpClient.Builder()
.apply {
connectTimeout(TIMEOUT, TimeUnit.SECONDS)
readTimeout(TIMEOUT, TimeUnit.SECONDS)
writeTimeout(TIMEOUT, TimeUnit.SECONDS)
addInterceptor(exceptionsInterceptor)
addInterceptor(ChuckerInterceptor(context))
if (BuildConfig.DEBUG) {
addNetworkInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY })
}
}.build()
}

View File

@ -1,29 +0,0 @@
package ru.touchin.template.di.app.modules
import android.content.Context
import android.content.SharedPreferences
import androidx.room.Room
import dagger.Module
import dagger.Provides
import ru.touchin.roboswag.components.utils.storables.PreferenceUtils
import ru.touchin.roboswag.core.observables.storable.NonNullStorable
import ru.touchin.template.di.qualifiers.SessionStorable
import ru.touchin.template.persistence.Database
import javax.inject.Singleton
@Module
class PersistentModule {
@Provides
@Singleton
fun provideDatabase(context: Context): Database = Room
.databaseBuilder(context, Database::class.java, "database")
.build()
@Singleton
@Provides
@SessionStorable
fun provideSessionStorable(sharedPreferences: SharedPreferences): NonNullStorable<String, String, String> =
PreferenceUtils.stringStorable("MIDDLE_SESSION", sharedPreferences, "")
}

View File

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

View File

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

View File

@ -1,9 +0,0 @@
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

@ -1,17 +0,0 @@
package ru.touchin.template.di.viewmodel
import androidx.lifecycle.ViewModel
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
import ru.touchin.template.viewmodel.StartupViewModel
@Module
interface ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(StartupViewModel::class)
fun bindStartupViewModel(viewModel: StartupViewModel): ViewModel
}

View File

@ -1,15 +0,0 @@
package ru.touchin.template.model
import com.bluelinelabs.logansquare.annotation.JsonEnum
import com.bluelinelabs.logansquare.annotation.JsonNumberValue
@JsonEnum
enum class TemplateApiError {
@JsonNumberValue(-1)
INVALID_PARAMETERS,
@JsonNumberValue(0)
VALID_RESPONSE
}

View File

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

View File

@ -0,0 +1,13 @@
package ru.touchin.template.navigation
import androidx.fragment.app.Fragment
import ru.terrakok.cicerone.android.support.SupportAppScreen
import ru.touchin.mvi_test.feature_login.presentation.LoginFragment
object Screens {
class Login : SupportAppScreen() {
override fun getFragment(): Fragment = LoginFragment()
}
}

View File

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

View File

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

View File

@ -1,8 +0,0 @@
package ru.touchin.template.persistence
import androidx.room.Database
import androidx.room.RoomDatabase
import ru.touchin.template.persistence.entities.UserEntity
@Database(version = 1, entities = [UserEntity::class], exportSchema = false)
abstract class Database : RoomDatabase()

View File

@ -1,16 +0,0 @@
package ru.touchin.template.persistence.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "user")
data class UserEntity(
@PrimaryKey
val uid: String = "",
@ColumnInfo(name = "session_id")
var sessionId: String?
)

View File

@ -1,6 +0,0 @@
package ru.touchin.template.viewmodel
import ru.touchin.lifecycle.viewmodel.RxViewModel
import javax.inject.Inject
class StartupViewModel @Inject constructor() : RxViewModel()

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

View File

@ -1,69 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.72'
ext.gradle_version = '3.6.2'
ext.fabric_version = '1.27.1'
repositories {
google()
jcenter()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath "com.android.tools.build:gradle:${gradle_version}"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.0.0-beta04'
classpath 'com.google.gms:google-services:4.3.3'
}
}
plugins {
id "io.gitlab.arturbosch.detekt" version "1.7.4"
id "de.aaschmid.cpd" version "3.1"
}
allprojects {
repositories {
google()
jcenter()
maven { url "http://dl.bintray.com/touchin/touchin-tools" }
maven { url 'https://jitpack.io' }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
ext {
buildScriptsDir = "$rootProject.projectDir/BuildScripts"
versions = [
compileSdk : 29,
androidx : '1.0.0',
fragment : '1.2.1',
appcompat : '1.1.0',
material : '1.1.0',
androidxKtx : '1.2.0',
lifecycle : '2.2.0',
room : '2.2.5',
dagger : '2.17',
retrofit : '2.4.0',
okhttp3 : '3.14.1',
logansquare : '1.4.3',
rxJava : '2.2.3',
rxAndroid : '2.1.1',
crashlytics : '2.10.1',
firebaseCrashlytics: '17.0.0-beta04',
glide : '4.11.0',
location : '16.0.0',
chucker : '3.1.1',
leakcanary : '2.1'
]
}
subprojects {
apply plugin: "io.gitlab.arturbosch.detekt"
}
apply from: "$buildScriptsDir/gradle/staticAnalysis.gradle"

40
build.gradle.kts Normal file
View File

@ -0,0 +1,40 @@
buildscript {
repositories {
mavenCentral()
google()
jcenter()
maven("https://plugins.gradle.org/m2/")
}
dependencies {
classpath("com.android.tools.build:gradle:${Version.ANDROID_PLUGIN}")
classpath(kotlin("gradle-plugin", version = Version.KOTLIN))
classpath("com.google.firebase:firebase-crashlytics-gradle:${Version.FIREBASE_CRASHLYTICS}")
classpath("com.vanniktech:gradle-dependency-graph-generator-plugin:0.5.0")
}
}
plugins {
id(Plugins.DETEKT).version("1.10.0")
id(Plugins.CPD).version("3.1")
}
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"
ext["buildScriptsDir"] = buildScriptsDir
apply(plugin = Plugins.DEPENDENCY_GRAPH)
//TODO: make staticAnalysis work on kotlin DSL
//apply(from = "$buildScriptsDir/gradle/staticAnalysis.gradle")

33
buildSrc/build.gradle.kts Normal file
View File

@ -0,0 +1,33 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
`kotlin-dsl`
`kotlin-dsl-precompiled-script-plugins`
kotlin("jvm") version embeddedKotlinVersion
}
// The kotlin-dsl plugin requires a repository to be declared
repositories {
jcenter()
google()
maven {
url = uri("https://plugins.gradle.org/m2/")
}
}
dependencies {
// android gradle plugin, required by custom plugin
implementation("com.android.tools.build:gradle:4.0.0")
// 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,23 @@
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 = System.getenv("BUILD_NUMBER")?.toIntOrNull() ?: 10000
val VERSION_NAME = "1.0.0"
val PROD_BUILD_NAME = ProguardFlavour.OBFUSCATE +
ApiFlavour.CustomerProd.flavourName +
SSLPinningFlavour.ON.flavourName +
TestPanelFlavour.OFF.flavourName +
BuildType.Release.name
const val TEST_APP_ID = "ru.touchin.template"
const val PROD_APP_ID = "ru.ask.client"
}

View File

@ -0,0 +1,136 @@
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_REFRESH)
}
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.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

@ -0,0 +1,48 @@
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_REFRESH = "androidx.swiperefreshlayout:swiperefreshlayout:${Version.SWIPE_REFRESH}"
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}"
}

View File

@ -0,0 +1,32 @@
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 PAGINATION = "pagination"
const val VIEWS = "views"
const val RECYCLER_VIEW_ADAPTERS = "recyclerview-adapters"
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"
}
}

View File

@ -0,0 +1,18 @@
object Plugins {
const val ANDROID_APPLICATION = "com.android.application"
const val ANDROID_LIBRARY = "com.android.library"
const val ANDROID_APP_PLUGIN_WITH_DEFAULT_CONFIG = "android_app"
const val ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG = "android_lib"
const val KOTLIN_ANDROID = "kotlin-android"
const val KOTLIN_ANDROID_EXTENSIONS = "kotlin-android-extensions"
const val KOTLIN_KAPT = "kotlin-kapt"
const val FIREBASE_CRASHLYTICS = "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

@ -0,0 +1,37 @@
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.kotlin.dsl.NamedDomainObjectContainerScope
import java.io.File
sealed class SigningConfig(
val name: String,
val storeFile: File,
val storePassword: String,
val keyAlias: String,
val keyPassword: String
) {
object Test: SigningConfig(
name = "test",
storeFile = File("file/way"),
storePassword = "pass",
keyAlias = "alias",
keyPassword = "pass"
)
object Prod: SigningConfig(
name = "prod",
storeFile = File("fsdfsd"),
storePassword = "fsdf",
keyAlias = "sdfdsf",
keyPassword = "dsfsdf"
)
}
fun NamedDomainObjectContainer<com.android.build.gradle.internal.dsl.SigningConfig>.addConfig(config: SigningConfig) {
create(config.name) {
storeFile = config.storeFile
storePassword = config.storePassword
keyAlias = config.keyAlias
keyPassword = config.keyPassword
}
}

View File

@ -0,0 +1,37 @@
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 ANDROID_MATERIAL = "1.1.0"
const val ANDROID_LIFECYCLE = "2.2.0"
const val SWIPE_REFRESH = "1.0.0"
const val DAGGER = "2.26"
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.6"
const val CICERONE = "5.1.0"
const val FIREBASE_CRASHLYTICS = "2.1.1"
const val LEAK_CANARY = "2.4"
const val CHUCKER = "3.2.0"
}

View File

@ -0,0 +1,39 @@
import com.android.build.gradle.LibraryExtension
sealed class ApiFlavour(
val name: String,
val apiUrl: String
) : Flavour(name, DIMENSION_NAME) {
companion object {
const val DIMENSION_NAME = "apiEndpoint"
}
object MockDev : ApiFlavour(
name = "mockDev",
apiUrl = "url1"
)
object TouchinTest : ApiFlavour(
name = "touchinTest",
apiUrl = "url2"
)
object CustomerProd : ApiFlavour(
name = "customerProd",
apiUrl = "url3"
)
}
fun LibraryExtension.addFlavour(flavour: ApiFlavour, customEndpoint: String?) {
productFlavors {
create(flavour.name) {
dimension = flavour.dimensionName
buildConfigField("String", "API_URL", "\"${customEndpoint ?: flavour.apiUrl}\"")
}
}
}

View File

@ -0,0 +1,31 @@
import org.gradle.api.NamedDomainObjectContainer
fun NamedDomainObjectContainer<com.android.build.gradle.internal.dsl.BuildType>.addBuildType(type: BuildType) {
getByName(type.name) {
isMinifyEnabled = type.isMinifyEnabled
isShrinkResources = type.isShrinkResources
}
}
sealed class BuildType(
val name: String,
val isMinifyEnabled: Boolean,
val isShrinkResources: Boolean
) {
object Debug : BuildType(
name = "debug",
isMinifyEnabled = false,
isShrinkResources = false
)
object Release : BuildType(
name = "release",
isMinifyEnabled = true,
isShrinkResources = true
)
}

View File

@ -0,0 +1,10 @@
import com.android.build.gradle.internal.dsl.ProductFlavor
import org.gradle.api.NamedDomainObjectContainer
abstract class Flavour(val flavourName: String, val dimensionName: String)
fun NamedDomainObjectContainer<ProductFlavor>.addEmptyFlavour(flavour: Flavour) {
create(flavour.flavourName) {
dimension = flavour.dimensionName
}
}

View File

@ -0,0 +1,6 @@
object ProguardFlavour {
const val DIMENSION_NAME = "proguardSettings"
const val NO_OBFUSCATE = "noObfuscate"
const val OBFUSCATE = "obfuscate"
}

View File

@ -0,0 +1,30 @@
import com.android.build.gradle.LibraryExtension
sealed class SSLPinningFlavour(
val name: String,
val withSslPinning: Boolean
) : Flavour(name, DIMENSION_NAME) {
companion object {
const val DIMENSION_NAME = "sslPinning"
}
object OFF: SSLPinningFlavour(
name = "withoutSSLPinning",
withSslPinning = true
)
object ON: SSLPinningFlavour(
name = "withSSLPinning",
withSslPinning = true
)
}
fun LibraryExtension.addFlavour(flavour: SSLPinningFlavour) {
productFlavors {
create(flavour.name) {
dimension = flavour.dimensionName
buildConfigField("Boolean", "WithSSLPinning", flavour.withSslPinning.toString())
}
}
}

View File

@ -0,0 +1,16 @@
sealed class TestPanelFlavour(
name: String
) : Flavour(name, DIMENSION_NAME) {
companion object {
const val DIMENSION_NAME = "testPanel"
}
object OFF : TestPanelFlavour(
name = "withoutTestPanel"
)
object ON : TestPanelFlavour(
name = "withTestPanel"
)
}

View File

@ -0,0 +1,13 @@
package plugins
import Plugins
import org.gradle.api.Project
class AndroidAppPlugin : BaseAndroidPlugin() {
override fun apply(target: Project) {
target.plugins.apply(Plugins.ANDROID_APPLICATION)
super.apply(target)
}
}

View File

@ -0,0 +1,13 @@
package plugins
import Plugins
import org.gradle.api.Project
class AndroidLibPlugin : BaseAndroidPlugin() {
override fun apply(target: Project) {
target.plugins.apply(Plugins.ANDROID_LIBRARY)
super.apply(target)
}
}

View File

@ -0,0 +1,63 @@
package plugins
import AndroidConfig
import Plugins
import com.android.build.gradle.BaseExtension
import kotlinStd
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
abstract class BaseAndroidPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.configurePlugins()
target.configureAndroid()
target.configureDependencies()
}
private fun Project.configurePlugins() {
plugins.apply(Plugins.KOTLIN_ANDROID)
plugins.apply(Plugins.KOTLIN_ANDROID_EXTENSIONS)
plugins.apply(Plugins.KOTLIN_KAPT)
}
private fun Project.configureAndroid() = extensions.getByType<BaseExtension>().run {
compileSdkVersion(AndroidConfig.COMPILE_SDK_VERSION)
buildToolsVersion = AndroidConfig.BUILD_TOOLS_VERSION
defaultConfig {
minSdkVersion(AndroidConfig.MIN_SDK_VERSION)
targetSdkVersion(AndroidConfig.TARGET_SDK_VERSION)
versionCode = AndroidConfig.VERSION_CODE
versionName = AndroidConfig.VERSION_NAME
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled = true
}
buildFeatures.viewBinding = true
tasks.withType(KotlinCompile::class.java) {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
private fun Project.configureDependencies() = dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
kotlinStd()
add("coreLibraryDesugaring", "com.android.tools:desugar_jdk_libs:1.0.5")
}
private fun DependencyHandler.implementation(dependencyNotation: Any): Dependency? =
add("implementation", dependencyNotation)
}

View File

@ -0,0 +1 @@
implementation-class=plugins.AndroidAppPlugin

View File

@ -0,0 +1 @@
implementation-class=plugins.AndroidLibPlugin

1
core/core_network/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,47 @@
plugins {
id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG)
}
val customEndpoint: String? = System.getenv("CUSTOM_ENDPOINT")?.takeIf(String::isNotBlank)
android {
defaultConfig {
rootProject.extensions.add("pathToApiSchemes", "$rootDir/common/api")
rootProject.extensions.add("applicationId", AndroidConfig.TEST_APP_ID)
}
flavorDimensions(
ApiFlavour.DIMENSION_NAME,
SSLPinningFlavour.DIMENSION_NAME,
TestPanelFlavour.DIMENSION_NAME
)
addFlavour(ApiFlavour.MockDev, customEndpoint)
addFlavour(ApiFlavour.TouchinTest, customEndpoint)
addFlavour(ApiFlavour.CustomerProd, customEndpoint)
addFlavour(SSLPinningFlavour.OFF)
addFlavour(SSLPinningFlavour.ON)
productFlavors {
addEmptyFlavour(TestPanelFlavour.OFF)
addEmptyFlavour(TestPanelFlavour.ON)
}
}
dependencies {
retrofit()
dagger()
moshi()
coroutines()
chucker()
}
//afterEvaluate {
// tasks
// .asIterable()
// .filter { it.name.contains("compile") && it.name.contains("JavaWithJavac") }
// .forEach { it.dependsOn("apiGenerator") }
//}
//
//apply(from = "${rootProject.extra["buildScriptsDir"]}/gradle/apiGenerator.gradle")

View File

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

View File

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

View File

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

View File

@ -0,0 +1,55 @@
package ru.touchin.mvitest.network.di
import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import ru.touchin.mvitest.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): OkHttpClient =
buildPublicClient(exceptionsInterceptor)
@Singleton
@Provides
fun provideMoshi() = buildMoshi()
@Singleton
@Provides
fun provideRetrofit(client: OkHttpClient, moshi: Moshi) = buildRetrofitInstance(client, moshi)
private fun buildMoshi() = Moshi.Builder()
.build()
private fun buildRetrofitInstance(client: OkHttpClient, moshi: Moshi): Retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.API_URL)
.client(client)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
private fun buildPublicClient(exceptionsInterceptor: ExceptionsInterceptor): OkHttpClient = OkHttpClient.Builder()
.apply {
connectTimeout(TIMEOUT, TimeUnit.SECONDS)
readTimeout(TIMEOUT, TimeUnit.SECONDS)
writeTimeout(TIMEOUT, TimeUnit.SECONDS)
addInterceptor(exceptionsInterceptor)
if (BuildConfig.DEBUG) {
addNetworkInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY })
}
}.build()
}

View File

@ -0,0 +1,41 @@
package ru.touchin.mvitest.network.interceptor
import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.ResponseBody
import org.json.JSONException
import org.json.JSONObject
import ru.touchin.mvitest.network.cloneBody
import ru.touchin.mvitest.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,6 +1,5 @@
package ru.touchin.template.api.exceptions
package ru.touchin.mvitest.network.models
import ru.touchin.template.model.TemplateApiError
import java.io.IOException
open class ServerException(val code: Int, message: String? = null) : IOException(message) {

View File

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

View File

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

1
core/core_prefs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

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

View File

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

View File

@ -0,0 +1,24 @@
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(private val context: Context) {
@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)
}

1
core/core_strings/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

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

View File

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

View File

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

View File

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

1
core/core_utils/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

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

View File

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

1
features/feature_login/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,8 @@
plugins {
id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG)
}
dependencies {
dagger()
mvi()
}

View File

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

View File

@ -0,0 +1,7 @@
package ru.touchin.mvi_test.feature_login
import ru.touchin.mvi_test.feature_login.navigation.LoginCoordinator
interface LoginDeps {
fun loginCoordinator(): LoginCoordinator
}

View File

@ -0,0 +1,19 @@
package ru.touchin.mvi_test.feature_login.di
import dagger.Component
import ru.touchin.roboswag.navigation_base.scopes.FragmentScope
import ru.touchin.mvi_test.feature_login.LoginDeps
import ru.touchin.mvi_test.feature_login.presentation.LoginFragment
@FragmentScope
@Component(modules = [ViewModelModule::class], dependencies = [LoginDeps::class])
interface LoginComponent {
fun inject(fragment: LoginFragment)
@Component.Factory
interface Factory {
fun create(deps: LoginDeps): LoginComponent
}
}

View File

@ -0,0 +1,24 @@
package ru.touchin.mvi_test.feature_login.di
import androidx.lifecycle.ViewModel
import com.squareup.inject.assisted.dagger2.AssistedModule
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
import ru.touchin.mvi_test.feature_login.presentation.LoginViewModel
import ru.touchin.roboswag.mvi_arch.di.ViewModelAssistedFactory
import ru.touchin.roboswag.mvi_arch.di.ViewModelKey
@Module(includes = [ViewModelAssistedFactoriesModule::class])
interface ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(LoginViewModel::class)
fun bindLoginByPinFactory(factory: LoginViewModel.Factory): ViewModelAssistedFactory<out ViewModel>
}
@AssistedModule
@Module(includes = [AssistedInject_ViewModelAssistedFactoriesModule::class])
abstract class ViewModelAssistedFactoriesModule

View File

@ -0,0 +1,8 @@
package ru.touchin.mvi_test.feature_login.navigation
import ru.touchin.roboswag.navigation_base.scopes.FragmentScope
@FragmentScope
interface LoginCoordinator {
fun openMainScreen()
}

View File

@ -0,0 +1,43 @@
package ru.touchin.mvi_test.feature_login.presentation
import android.os.Bundle
import android.view.View
import me.vponomarenko.injectionmanager.IHasComponent
import me.vponomarenko.injectionmanager.x.XInjectionManager
import ru.touchin.mvi_test.feature_login.R
import ru.touchin.mvi_test.feature_login.databinding.FragmentLoginBinding
import ru.touchin.mvi_test.feature_login.di.DaggerLoginComponent
import ru.touchin.mvi_test.feature_login.di.LoginComponent
import ru.touchin.roboswag.mvi_arch.core.MviFragment
import ru.touchin.roboswag.navigation_base.fragments.EmptyState
import ru.touchin.roboswag.navigation_base.fragments.viewBinding
class LoginFragment : MviFragment<EmptyState, LoginViewState, LoginViewAction, LoginViewModel>(R.layout.fragment_login),
IHasComponent<LoginComponent> {
private val binding by viewBinding(FragmentLoginBinding::bind)
override val viewModel: LoginViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
injectDependencies()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.goToMainScreenButton.dispatchActionOnRippleClick(LoginViewAction.GoToMainScreenButtonClicked)
}
private fun injectDependencies() {
XInjectionManager.bindComponent(this)
.inject(this)
}
override fun getComponent(): LoginComponent = DaggerLoginComponent
.factory()
.create(XInjectionManager.findComponent())
}

View File

@ -0,0 +1,7 @@
package ru.touchin.mvi_test.feature_login.presentation
import ru.touchin.roboswag.mvi_arch.marker.ViewAction
sealed class LoginViewAction : ViewAction {
object GoToMainScreenButtonClicked : LoginViewAction()
}

View File

@ -0,0 +1,25 @@
package ru.touchin.mvi_test.feature_login.presentation
import androidx.lifecycle.SavedStateHandle
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import ru.touchin.mvi_test.feature_login.navigation.LoginCoordinator
import ru.touchin.roboswag.mvi_arch.core.MviViewModel
import ru.touchin.roboswag.mvi_arch.di.ViewModelAssistedFactory
import ru.touchin.roboswag.navigation_base.fragments.EmptyState
class LoginViewModel @AssistedInject constructor(
@Assisted arg0: SavedStateHandle,
private val coordinator: LoginCoordinator
) : MviViewModel<EmptyState, LoginViewAction, LoginViewState>(LoginViewState, arg0) {
override fun dispatchAction(action: LoginViewAction) {
when (action) {
LoginViewAction.GoToMainScreenButtonClicked -> coordinator.openMainScreen()
}
}
@AssistedInject.Factory
interface Factory : ViewModelAssistedFactory<LoginViewModel>
}

View File

@ -0,0 +1,5 @@
package ru.touchin.mvi_test.feature_login.presentation
import ru.touchin.roboswag.mvi_arch.marker.ViewState
object LoginViewState : ViewState

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="8dp"
android:text="Экран логина" />
<Button
android:id="@+id/go_to_main_screen_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="На главный экран" />
</LinearLayout>

View File

@ -14,3 +14,4 @@ org.gradle.parallel=true
android.useAndroidX=true
android.enableJetifier=true

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip

View File

@ -1,20 +0,0 @@
include ':app'
gradle.ext.roboswag = [
'utils',
'logging',
'navigation',
'storable',
'api-logansquare',
'lifecycle',
'lifecycle-rx',
'views',
'recyclerview-adapters',
'kotlin-extensions'
]
gradle.ext.roboswag.forEach { module ->
include ":$module"
project(":$module").projectDir = file("RoboSwag/$module")
}

33
settings.gradle.kts Normal file
View File

@ -0,0 +1,33 @@
fun includeModulesFromFolder(folderName: String) {
file(folderName)
.walk()
.maxDepth(1)
.forEach { moduleFolder ->
include(":${moduleFolder.name}")
project(":${moduleFolder.name}").projectDir = moduleFolder
}
}
val roboswagModules = listOf(
"utils",
"logging",
"mvi-arch",
"pagination",
"navigation-cicerone",
"navigation-base",
"storable",
"lifecycle",
"views",
"recyclerview-adapters",
"kotlin-extensions"
)
roboswagModules.forEach { module ->
include(":$module")
project(":$module").projectDir = file("RoboSwag/$module")
}
includeModulesFromFolder("core")
includeModulesFromFolder("features")
include(":app")