Merge pull request #4 from TouchInstinct/new_arch

New arch
This commit is contained in:
Maxim Bachinsky 2020-09-29 19:35:38 +03:00 committed by GitHub
commit a2cad6cba2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 1505 additions and 605 deletions

@ -1 +1 @@
Subproject commit c957a38784af59fb341e3df651a60b0861aba50b
Subproject commit be9bbb971e0864178ebf6d2f22bcd40d7fecd24e

@ -1 +1 @@
Subproject commit dc5cb987cfbc6ba37e59870650fae8c34b1a9c32
Subproject commit 09061c1bcfd951127f1a62c25990a5c9d9cbbbce

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"

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

@ -0,0 +1,74 @@
plugins {
id(Plugins.ANDROID_APP_PLUGIN_WITH_DEFAULT_CONFIG)
id(Plugins.FIREBASE_CRASH)
id(Plugins.GOOGLE_SERVICES)
id(Plugins.LICENCE_PLUGIN)
}
val customEndpoint: String? = Environment.ENDPOINT.getenv()?.takeIf(String::isNotBlank)
android {
configureSigningConfig(this@Build_gradle::file)
with(defaultConfig) {
applicationId = Environment.APP_ID.getenv() ?: AndroidConfig.TEST_APP_ID
signingConfig = signingConfigs.getByName(SigningConfig.CONFIG_NAME)
}
firebaseCrashlytics {
mappingFileUploadEnabled = true
}
addBuildType(BuildType.Debug, buildScriptDir = buildScriptDir)
addBuildType(BuildType.Release, buildScriptDir = buildScriptDir)
flavorDimensions(
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 {
androidX()
featureModules()
mvi()
materialDesign()
dagger()
retrofit()
moshi()
navigation()
leakCanary()
sharedPrefs()
chucker()
implementation(Library.FIREBASE_ANAL)
implementation(Library.FIREBASE_CRASH)
implementation(Library.FIREBASE_PERF)
implementation(Library.ANDROIDX_SECURE)
coreNetwork()
coreStrings()
implementationModule(Module.Core.UI)
implementationModule(Module.Core.UTILS)
implementationModule(Module.Core.DATA)
implementationModule(Module.RoboSwag.UTILS)
}
apply(from = "$buildScriptDir/gradle/scripts/applicationFileNaming.gradle")
val Project.buildScriptDir: String
get() = rootProject.ext["buildScriptsDir"] as String

40
app/google-services.json Normal file
View File

@ -0,0 +1,40 @@
{
"project_info": {
"project_number": "1084813714260",
"firebase_url": "https://testproject-ac7fe.firebaseio.com",
"project_id": "testproject-ac7fe",
"storage_bucket": "testproject-ac7fe.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:1084813714260:android:b6d7bb18a0acfe96255ec1",
"android_client_info": {
"package_name": "com.touchin.template"
}
},
"oauth_client": [
{
"client_id": "1084813714260-ijq13dkdc1h5i5j87t45tiibl8eg2v9e.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBVsh_CN-RCfU3LkHuvhLdqVS-ZUJbOljE"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "1084813714260-ijq13dkdc1h5i5j87t45tiibl8eg2v9e.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

Binary file not shown.

BIN
app/signing_key.jks Normal file

Binary file not shown.

View File

@ -1,22 +1,21 @@
<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"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false"
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,25 @@
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.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
.factory()
.create(this)
}

View File

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

@ -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,37 @@
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

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

@ -0,0 +1,13 @@
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,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.template.feature_login.presentation.LoginFragment
object Screens {
class Login : SupportAppScreen() {
override fun getFragment(): Fragment = LoginFragment()
}
}

View File

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

@ -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

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:networkSecurityConfig="@xml/network_security_config">
</application>
</manifest>

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"

43
build.gradle.kts Normal file
View File

@ -0,0 +1,43 @@
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.gms:google-services:${Version.GOOGLE_SERVICES_PLUGIN}")
classpath("com.google.firebase:firebase-crashlytics-gradle:${Version.FIREBASE_CRASH_PLUGIN}")
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"
ext["buildScriptsDir"] = buildScriptsDir
apply(plugin = Plugins.DEPENDENCY_GRAPH)
staticAnalysis {
buildScriptDir = buildScriptsDir
}

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,29 @@
import com.android.build.gradle.BaseExtension
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
const val TEST_APP_ID = "com.touchin.template"
// TODO: change common file folder
const val COMMON_FOLDER = "Template-common"
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,145 @@
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

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

View File

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

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

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

@ -0,0 +1,47 @@
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,36 @@
import com.android.build.gradle.BaseExtension
sealed class ApiFlavour(
val name: String,
val apiUrl: String
) : Flavour(name, DIMENSION_NAME) {
companion object {
const val DIMENSION_NAME = "apiEndpoint"
}
// TODO: change url
object CustomerStage : ApiFlavour(
name = "customerStage",
apiUrl = "https://wallet-api.staging.mnxsc.tech"
)
// TODO: change url
object CustomerProd : ApiFlavour(
name = "customerProd",
apiUrl = "https://wallet-api.prod.mnxsc.tech"
)
}
fun BaseExtension.addFlavour(flavour: ApiFlavour, customEndpoint: String?) {
productFlavors {
create(flavour.name) {
dimension = flavour.dimensionName
buildConfigField("String", "API_URL", "\"${customEndpoint ?: flavour.apiUrl}\"")
}
}
}

View File

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

@ -0,0 +1,11 @@
import com.android.build.gradle.BaseExtension
abstract class Flavour(val flavourName: String, val dimensionName: String)
fun BaseExtension.addEmptyFlavour(flavour: Flavour) {
productFlavors {
create(flavour.flavourName) {
dimension = flavour.dimensionName
}
}
}

View File

@ -0,0 +1,30 @@
import com.android.build.gradle.BaseExtension
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 BaseExtension.addFlavour(flavour: SSLPinningFlavour) {
productFlavors {
create(flavour.name) {
dimension = flavour.dimensionName
buildConfigField("Boolean", "WithSSLPinning", flavour.withSslPinning.toString())
}
}
}

View File

@ -0,0 +1,23 @@
import com.android.build.gradle.BaseExtension
import java.io.File
object SigningConfig {
const val CONFIG_NAME: String = "signing_key"
const val PATH_TO_KEYSTORE_FILE: String = "signing_key.jks"
const val DEFAULT_STORE_PASSWORD: String = "iphoneandroidwp7"
const val DEFAULT_KEY_ALIAS: String = "touchin"
const val DEFAULT_KEY_PASSWORD: String = "iphoneandroidwp7"
}
fun BaseExtension.configureSigningConfig(getRelativeFile: (String) -> File) {
signingConfigs {
create(SigningConfig.CONFIG_NAME) {
storeFile = getRelativeFile(SigningConfig.PATH_TO_KEYSTORE_FILE)
storePassword = Environment.STORE_PASSWORD.getenv() ?: SigningConfig.DEFAULT_STORE_PASSWORD
keyAlias = Environment.KEY_ALIAS.getenv() ?: SigningConfig.DEFAULT_KEY_ALIAS
keyPassword = Environment.KEY_PASSWORD.getenv() ?: SigningConfig.DEFAULT_KEY_PASSWORD
}
}
}

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,72 @@
package plugins
import AndroidConfig
import BuildType
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"
}
}
if (AndroidConfig.RELEASE_DEBUGGABLE) {
buildTypes {
getByName(BuildType.Release.name) {
isDebuggable = true
}
}
}
}
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

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_data" />

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_domain" />

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

@ -0,0 +1 @@
/build

View File

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

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

View File

@ -1,10 +1,10 @@
package ru.touchin.template.extensions
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()
.also { it.request(Long.MAX_VALUE) }
.buffer
?.clone()
?.readString(Charset.forName("UTF-8"))

View File

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

View File

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

View File

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

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

View File

@ -0,0 +1,41 @@
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,6 +1,5 @@
package ru.touchin.template.api.exceptions
package ru.touchin.template.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.template.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 {
@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,15 @@
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

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

View File

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

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.template.feature_login">
</manifest>

View File

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

View File

@ -0,0 +1,19 @@
package ru.touchin.template.feature_login.di
import dagger.Component
import ru.touchin.roboswag.navigation_base.scopes.FragmentScope
import ru.touchin.template.feature_login.LoginDeps
import ru.touchin.template.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.template.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.template.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.template.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.template.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.template.feature_login.R
import ru.touchin.template.feature_login.databinding.FragmentLoginBinding
import ru.touchin.template.feature_login.di.DaggerLoginComponent
import ru.touchin.template.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.template.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.template.feature_login.presentation
import androidx.lifecycle.SavedStateHandle
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import ru.touchin.template.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.template.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>

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