diff --git a/.gitignore b/.gitignore index 4b2cb01..eb1b6a4 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,8 @@ local.properties *.iml app/src/main/res/*/*strings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties diff --git a/.gitmodules b/.gitmodules index aa5ba1d..2346927 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,9 @@ -[submodule "Template-common"] - path = Template-common - url = git@github.com:TouchInstinct/Template-common.git [submodule "RoboSwag"] path = RoboSwag - url = git@github.com:TouchInstinct/RoboSwag.git + url = https://git.svc.touchin.ru/TouchInstinct/RoboSwag.git [submodule "BuildScripts"] path = BuildScripts - url = git@github.com:TouchInstinct/BuildScripts.git + url = https://git.svc.touchin.ru/TouchInstinct/BuildScripts.git +[submodule "common-template"] + path = common-template + url = https://git.svc.touchin.ru/TouchInstinct/common-template.git diff --git a/app/.gitignore b/app/.gitignore index 796b96d..b615a9d 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,3 @@ /build + +app/src/main/res/*/*strings.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a40f352..0c17aaf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,74 +1,98 @@ plugins { - id(Plugins.ANDROID_APP_PLUGIN_WITH_DEFAULT_CONFIG) - id(Plugins.FIREBASE_CRASH) - id(Plugins.GOOGLE_SERVICES) - id(Plugins.LICENCE_PLUGIN) + id(libs.plugins.android.app.get().pluginId) + alias(libs.plugins.firebase.crashlytics) + alias(libs.plugins.firebase.perf) + id(libs.plugins.google.oss.licenses.plugin.get().pluginId) } val customEndpoint: String? = Environment.ENDPOINT.getenv()?.takeIf(String::isNotBlank) android { + namespace = "ru.touchin.template" + configureSigningConfig(this@Build_gradle::file) with(defaultConfig) { - applicationId = Environment.APP_ID.getenv() ?: AndroidConfig.TEST_APP_ID - signingConfig = signingConfigs.getByName(SigningConfig.CONFIG_NAME) + addResourceConfigurations("ru") } - firebaseCrashlytics { - mappingFileUploadEnabled = true - } + addBuildType(type = BuildType.Develop, project = rootProject) + addBuildType(type = BuildType.Debug, project = rootProject) + addBuildType(type = BuildType.Customer, project = rootProject) + addBuildType(type = BuildType.Release, project = rootProject) - addBuildType(BuildType.Debug, buildScriptDir = buildScriptDir) - addBuildType(BuildType.Release, buildScriptDir = buildScriptDir) + addMobileServicesFlavor() - 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") + ext["languageMap"] = mapOf("ru" to "${rootProject.projectDir}/${AndroidConfig.COMMON_FOLDER}/strings/default_common_strings.json") } 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) + // AndroidX + implementation(libs.bundles.androidX) + + // KotlinX + implementation(libs.coroutines) + + // UI + implementation(libs.bundles.ui) + + // Lifecycle + implementation(libs.bundles.lifecycle) + kapt(libs.androidx.lifecycle.compiler) + + // Dagger + implementation(libs.bundles.dagger) + kapt(libs.dagger.compiler) + kapt(libs.dagger.assisted.inject.processor) + + // Glide + implementation(libs.glide) + implementation(libs.glide.okhttp3) + kapt(libs.glide.compiler) + + // Retrofit2, OkHttp3 + implementation(libs.retrofit) + implementation(libs.retrofit.converter.moshi) + implementation(libs.okhttp) + implementation(libs.okhttp.logging.interceptor) + + // Moshi + implementation(libs.moshi) + implementation(libs.moshi.kotlin) + kapt(libs.moshi.codegen) + + // Room + implementation(libs.room) + implementation(libs.room.ktx) + kapt(libs.room.compiler) + + // LeakCanary + implementation(libs.leakcanary) + + // Chucker + debugImplementation(libs.chucker.debug) + releaseImplementation(libs.chucker.release) + + // GMS + implementation(platform(libs.firebase.bom)) + implementation(libs.bundles.firebase) + implementation(libs.google.oss.licenses) + + // Security + implementation(libs.androidx.security.crypto) + + // Biometric + implementation(libs.androidx.biometric) + + // Groupie + implementation(libs.groupie) + implementation(libs.groupie.viewbinding) + + // Cicecrone + implementation(libs.cicerone) } -apply(from = "$buildScriptDir/gradle/scripts/applicationFileNaming.gradle") +apply(from = "${rootProject.ext["buildScriptsDir"]}/gradle/scripts/stringGenerator.gradle") val Project.buildScriptDir: String get() = rootProject.ext["buildScriptsDir"] as String diff --git a/app/google-services.json b/app/google-services.json deleted file mode 100644 index 89951fd..0000000 --- a/app/google-services.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "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" -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fd2e16f..0f6717f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> @@ -9,17 +8,17 @@ android:name="ru.touchin.template.App" android:allowBackup="false" android:icon="@mipmap/ic_launcher" - android:label="@string/common_global_app_name" + android:label="@string/common_app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="false" - tools:ignore="GoogleAppIndexingWarning"> + tools:ignore="GoogleAppIndexingWarning" + android:theme="@style/AppTheme"> - + android:windowSoftInputMode="adjustResize" + android:exported="true"> diff --git a/app/src/main/java/ru/touchin/template/App.kt b/app/src/main/java/ru/touchin/template/App.kt index dc4afba..0048de1 100644 --- a/app/src/main/java/ru/touchin/template/App.kt +++ b/app/src/main/java/ru/touchin/template/App.kt @@ -1,25 +1,18 @@ 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 +import android.app.Application +import ru.touchin.template.di.DI +import ru.touchin.template.di.SharedComponent +import ru.touchin.template.di.SharedComponentProvider -class App : TouchinApp(), IHasComponent { +class App : Application(), SharedComponentProvider { override fun onCreate() { super.onCreate() - initDagger() + + DI.init(applicationContext) + DI.getComponent().inject(this) } - fun initDagger() { - XInjectionManager.init(this) - XInjectionManager.bindComponent(this) - } - - override fun getComponent(): ApplicationComponent = DaggerApplicationComponent - .factory() - .create(this) - + override fun getModule(): SharedComponent = DI.getComponent() } diff --git a/app/src/main/java/ru/touchin/template/SingleActivity.kt b/app/src/main/java/ru/touchin/template/SingleActivity.kt deleted file mode 100644 index 5e7a377..0000000 --- a/app/src/main/java/ru/touchin/template/SingleActivity.kt +++ /dev/null @@ -1,62 +0,0 @@ -package ru.touchin.template - -import android.os.Bundle -import androidx.activity.OnBackPressedCallback -import com.google.firebase.analytics.FirebaseAnalytics -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.ktx.Firebase -import me.vponomarenko.injectionmanager.x.XInjectionManager -import ru.terrakok.cicerone.NavigatorHolder -import ru.terrakok.cicerone.android.support.SupportAppNavigator -import ru.touchin.roboswag.navigation_base.activities.BaseActivity -import ru.touchin.roboswag.navigation_cicerone.CiceroneTuner -import ru.touchin.template.di.ApplicationComponent -import ru.touchin.template.navigation.MainNavigation -import ru.touchin.template.navigation.StartUpCoordinator -import javax.inject.Inject - -// TDOD: change package name everywhere -// TODO: change google play config -class SingleActivity : BaseActivity() { - - @Inject - @MainNavigation - lateinit var navigatorHolder: NavigatorHolder - - @Inject - lateinit var coordinator: StartUpCoordinator - - private lateinit var firebaseAnalytics: FirebaseAnalytics - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - firebaseAnalytics = Firebase.analytics - - setContentView(R.layout.activity_main) - - injectDependencies() - - lifecycle.addObserver( - CiceroneTuner( - navigatorHolder = navigatorHolder, - navigator = SupportAppNavigator(this, R.id.fragment_container) - ) - ) - - coordinator.start() - - onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - coordinator.closeCurrentScreen() - } - }) - } - - private fun injectDependencies() { - XInjectionManager - .findComponent() - .inject(this) - } - -} diff --git a/app/src/main/java/ru/touchin/template/base/activity/BaseActivity.kt b/app/src/main/java/ru/touchin/template/base/activity/BaseActivity.kt new file mode 100644 index 0000000..0100cf3 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/base/activity/BaseActivity.kt @@ -0,0 +1,17 @@ +package ru.touchin.template.base.activity + +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModel +import ru.touchin.template.di.SharedComponent +import ru.touchin.template.di.getSharedModule + +abstract class BaseActivity : AppCompatActivity() { + + protected val viewModel: T by lazy { createViewModelLazy().value } + + protected val viewModelFactory by lazy { getSharedComponent().viewModelFactory() } + + abstract fun createViewModelLazy(): Lazy + + protected fun getSharedComponent(): SharedComponent = getSharedModule() +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/base/fragment/BaseFragment.kt b/app/src/main/java/ru/touchin/template/base/fragment/BaseFragment.kt new file mode 100644 index 0000000..39384c7 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/base/fragment/BaseFragment.kt @@ -0,0 +1,23 @@ +package ru.touchin.template.base.fragment + +import androidx.annotation.LayoutRes +import androidx.fragment.app.Fragment +import ru.touchin.template.base.viewmodel.BaseViewModel +import ru.touchin.template.di.SharedComponent +import ru.touchin.template.di.getSharedModule +import ru.touchin.template.navigation.backpress.OnBackPressedListener + +abstract class BaseFragment(@LayoutRes layoutId: Int) : Fragment(layoutId), OnBackPressedListener { + + protected val viewModel: T by lazy { createViewModelLazy().value } + + protected val viewModelFactory by lazy { getSharedComponent().viewModelFactory() } + + protected abstract fun createViewModelLazy(): Lazy + + protected fun getSharedComponent(): SharedComponent = getSharedModule() + + override fun onBackPressed(): Boolean { + return viewModel.onBackClicked() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/base/viewmodel/BaseController.kt b/app/src/main/java/ru/touchin/template/base/viewmodel/BaseController.kt new file mode 100644 index 0000000..106042e --- /dev/null +++ b/app/src/main/java/ru/touchin/template/base/viewmodel/BaseController.kt @@ -0,0 +1,6 @@ +package ru.touchin.template.base.viewmodel + +interface BaseController { + + fun onBackClicked(): Boolean = true +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/base/viewmodel/BaseViewModel.kt b/app/src/main/java/ru/touchin/template/base/viewmodel/BaseViewModel.kt new file mode 100644 index 0000000..cb7386f --- /dev/null +++ b/app/src/main/java/ru/touchin/template/base/viewmodel/BaseViewModel.kt @@ -0,0 +1,5 @@ +package ru.touchin.template.base.viewmodel + +import androidx.lifecycle.ViewModel + +abstract class BaseViewModel : ViewModel(), BaseController \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/AppComponent.kt b/app/src/main/java/ru/touchin/template/di/AppComponent.kt new file mode 100644 index 0000000..27efa14 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/AppComponent.kt @@ -0,0 +1,37 @@ +package ru.touchin.template.di + +import android.content.Context +import dagger.BindsInstance +import dagger.Component +import ru.touchin.template.App +import ru.touchin.template.di.auth.AuthComponent +import ru.touchin.template.di.modules.AppModule +import ru.touchin.template.di.modules.NavigationModule +import ru.touchin.template.di.modules.ViewModelModule +import ru.touchin.template.feature.SingleActivity + +@Component( + modules = [ + AppModule::class, + ViewModelModule::class, + NavigationModule::class + ] +) +@AppScope +interface AppComponent : SharedComponent { + + fun authComponent(): AuthComponent.Builder + + @Component.Builder + interface Builder { + + @BindsInstance + fun appContext(appContext: Context): Builder + + fun build(): AppComponent + } + + fun inject(entry: App) + + fun inject(entry: SingleActivity) +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/AppScope.kt b/app/src/main/java/ru/touchin/template/di/AppScope.kt new file mode 100644 index 0000000..f062861 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/AppScope.kt @@ -0,0 +1,6 @@ +package ru.touchin.template.di + +import javax.inject.Scope + +@Scope +annotation class AppScope \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/ApplicationComponent.kt b/app/src/main/java/ru/touchin/template/di/ApplicationComponent.kt deleted file mode 100644 index a1475a4..0000000 --- a/app/src/main/java/ru/touchin/template/di/ApplicationComponent.kt +++ /dev/null @@ -1,37 +0,0 @@ -package ru.touchin.template.di - -import android.content.Context -import dagger.BindsInstance -import dagger.Component -import ru.terrakok.cicerone.Router -import ru.touchin.template.feature_login.LoginDeps -import ru.touchin.template.network.di.NetworkModule -import ru.touchin.template.App -import ru.touchin.template.SingleActivity -import ru.touchin.template.core_prefs.PreferencesModule -import ru.touchin.template.navigation.MainNavigation -import javax.inject.Singleton - -@Singleton -@Component(modules = [ - ApplicationModule::class, - PreferencesModule::class, - MainNavigationModule::class, - NetworkModule::class, - CoordinatorsImpl::class -]) -interface ApplicationComponent : LoginDeps { - - @MainNavigation - fun router(): Router - - fun inject(application: App) - - fun inject(activity: SingleActivity) - - @Component.Factory - interface Factory { - fun create(@BindsInstance context: Context): ApplicationComponent - } - -} diff --git a/app/src/main/java/ru/touchin/template/di/ApplicationModule.kt b/app/src/main/java/ru/touchin/template/di/ApplicationModule.kt deleted file mode 100644 index 90758d1..0000000 --- a/app/src/main/java/ru/touchin/template/di/ApplicationModule.kt +++ /dev/null @@ -1,28 +0,0 @@ -package ru.touchin.template.di - -import android.content.Context -import com.chuckerteam.chucker.api.ChuckerInterceptor -import dagger.Module -import dagger.Provides -import okhttp3.Interceptor -import ru.touchin.template.network.di.ApiUrl -import ru.touchin.template.network.di.ChuckInterceptor -import ru.touchin.template.network.di.WithSslPinning -import ru.touchin.template.BuildConfig - -@Module -class ApplicationModule { - - @Provides - @ApiUrl - fun provideApiUrl() = BuildConfig.API_URL - - @Provides - @WithSslPinning - fun providePluggerForSsl() = BuildConfig.WithSSLPinning - - @Provides - @ChuckInterceptor - fun provideChucker(context: Context): Interceptor = ChuckerInterceptor(context) - -} diff --git a/app/src/main/java/ru/touchin/template/di/CoordinatorsImpl.kt b/app/src/main/java/ru/touchin/template/di/CoordinatorsImpl.kt deleted file mode 100644 index d43191f..0000000 --- a/app/src/main/java/ru/touchin/template/di/CoordinatorsImpl.kt +++ /dev/null @@ -1,13 +0,0 @@ -package ru.touchin.template.di - -import dagger.Binds -import dagger.Module -import ru.touchin.template.feature_login.navigation.LoginCoordinator -import ru.touchin.template.navigation.login.LoginCoordinatorImpl - -@Module -abstract class CoordinatorsImpl { - - @Binds - abstract fun loginCoordinator(impl: LoginCoordinatorImpl): LoginCoordinator -} diff --git a/app/src/main/java/ru/touchin/template/di/DI.kt b/app/src/main/java/ru/touchin/template/di/DI.kt new file mode 100644 index 0000000..626c007 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/DI.kt @@ -0,0 +1,16 @@ +package ru.touchin.template.di + +import android.content.Context + +object DI { + + private lateinit var appComponent: AppComponent + + fun init(context: Context) { + appComponent = DaggerAppComponent.builder() + .appContext(context) + .build() + } + + fun getComponent(): AppComponent = appComponent +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/MainNavigationModule.kt b/app/src/main/java/ru/touchin/template/di/MainNavigationModule.kt deleted file mode 100644 index d7c055d..0000000 --- a/app/src/main/java/ru/touchin/template/di/MainNavigationModule.kt +++ /dev/null @@ -1,27 +0,0 @@ -package ru.touchin.template.di - -import dagger.Module -import dagger.Provides -import ru.terrakok.cicerone.Cicerone -import ru.terrakok.cicerone.NavigatorHolder -import ru.terrakok.cicerone.Router -import ru.touchin.template.navigation.MainNavigation -import javax.inject.Singleton - -@Module -class MainNavigationModule { - - @Provides - @Singleton - @MainNavigation - fun provideCicerone(): Cicerone = Cicerone.create() - - @Provides - @MainNavigation - fun provideNavigatorHolder(@MainNavigation cicerone: Cicerone): NavigatorHolder = cicerone.navigatorHolder - - @Provides - @MainNavigation - fun provideRouter(@MainNavigation cicerone: Cicerone): Router = cicerone.router - -} diff --git a/app/src/main/java/ru/touchin/template/di/SharedComponentExt.kt b/app/src/main/java/ru/touchin/template/di/SharedComponentExt.kt new file mode 100644 index 0000000..36993ee --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/SharedComponentExt.kt @@ -0,0 +1,17 @@ +package ru.touchin.template.di + +import android.app.Activity +import android.content.Context +import androidx.fragment.app.Fragment + +fun Context.getSharedModule(): SharedComponent { + return (applicationContext as SharedComponentProvider).getModule() +} + +fun Activity.getSharedModule(): SharedComponent { + return (applicationContext as SharedComponentProvider).getModule() +} + +fun Fragment.getSharedModule(): SharedComponent { + return requireContext().getSharedModule() +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/SharedComponentProvider.kt b/app/src/main/java/ru/touchin/template/di/SharedComponentProvider.kt new file mode 100644 index 0000000..771a420 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/SharedComponentProvider.kt @@ -0,0 +1,17 @@ +package ru.touchin.template.di + +import dagger.Module +import ru.touchin.template.di.viewmodel.ViewModelFactory +import ru.touchin.template.feature.second.SecondViewModel + +interface SharedComponent { + fun viewModelFactory(): ViewModelFactory + fun secondScreenViewModelFactory(): SecondViewModel.Factory +} + +@Module +class SharedModule + +interface SharedComponentProvider { + fun getModule(): SharedComponent +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/auth/AuthComponent.kt b/app/src/main/java/ru/touchin/template/di/auth/AuthComponent.kt new file mode 100644 index 0000000..e3c5dfc --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/auth/AuthComponent.kt @@ -0,0 +1,17 @@ +package ru.touchin.template.di.auth + +import dagger.Subcomponent +import ru.touchin.template.feature.first.FirstFragment + +@Subcomponent(modules = [AuthModule::class]) +@AuthScope +interface AuthComponent { + + @Subcomponent.Builder + interface Builder { + + fun build(): AuthComponent + } + + fun inject(entry: FirstFragment) +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/auth/AuthModule.kt b/app/src/main/java/ru/touchin/template/di/auth/AuthModule.kt new file mode 100644 index 0000000..e2e325a --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/auth/AuthModule.kt @@ -0,0 +1,13 @@ +package ru.touchin.template.di.auth + +import dagger.Module + +@Module +class AuthModule { + +// @Provides +// @AuthScope +// internal fun providesSecondRepository(): SecondRepository { +// return SecondRepositoryImpl() +// } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/auth/AuthScope.kt b/app/src/main/java/ru/touchin/template/di/auth/AuthScope.kt new file mode 100644 index 0000000..64dbe7a --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/auth/AuthScope.kt @@ -0,0 +1,6 @@ +package ru.touchin.template.di.auth + +import javax.inject.Scope + +@Scope +annotation class AuthScope \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/modules/AppModule.kt b/app/src/main/java/ru/touchin/template/di/modules/AppModule.kt new file mode 100644 index 0000000..aab5a49 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/modules/AppModule.kt @@ -0,0 +1,13 @@ +package ru.touchin.template.di.modules + +import dagger.Module +import ru.touchin.template.di.auth.AuthComponent + +@Module( + includes = [ + RepositoryModule::class, + NetworkModule::class + ], + subcomponents = [AuthComponent::class] +) +class AppModule \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/modules/NavigationModule.kt b/app/src/main/java/ru/touchin/template/di/modules/NavigationModule.kt new file mode 100644 index 0000000..4838b23 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/modules/NavigationModule.kt @@ -0,0 +1,26 @@ +package ru.touchin.template.di.modules + +import com.github.terrakok.cicerone.Cicerone +import com.github.terrakok.cicerone.NavigatorHolder +import com.github.terrakok.cicerone.Router +import dagger.Module +import dagger.Provides +import ru.touchin.template.di.AppScope + +@Module +class NavigationModule { + + private val cicerone: Cicerone = Cicerone.create() + + @Provides + @AppScope + fun provideRouter(): Router { + return cicerone.router + } + + @Provides + @AppScope + fun provideNavigatorHolder(): NavigatorHolder { + return cicerone.getNavigatorHolder() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/modules/NetworkModule.kt b/app/src/main/java/ru/touchin/template/di/modules/NetworkModule.kt new file mode 100644 index 0000000..8c14718 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/modules/NetworkModule.kt @@ -0,0 +1,7 @@ +package ru.touchin.template.di.modules + +import dagger.Module + +@Module +class NetworkModule { +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/modules/RepositoryModule.kt b/app/src/main/java/ru/touchin/template/di/modules/RepositoryModule.kt new file mode 100644 index 0000000..f995563 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/modules/RepositoryModule.kt @@ -0,0 +1,7 @@ +package ru.touchin.template.di.modules + +import dagger.Module + +@Module +class RepositoryModule { +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/modules/ViewModelModule.kt b/app/src/main/java/ru/touchin/template/di/modules/ViewModelModule.kt new file mode 100644 index 0000000..781d5ad --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/modules/ViewModelModule.kt @@ -0,0 +1,29 @@ +package ru.touchin.template.di.modules + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import ru.touchin.template.di.viewmodel.ViewModelFactory +import ru.touchin.template.di.viewmodel.ViewModelKey +import ru.touchin.template.feature.SingleViewModel +import ru.touchin.template.feature.first.FirstViewModel + +@Module +interface ViewModelModule { + + @Binds + fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory + + @Binds + @IntoMap + @ViewModelKey(SingleViewModel::class) + fun bindsSingleViewModel(viewModel: SingleViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(FirstViewModel::class) + fun bindsFirstViewModel(viewModel: FirstViewModel): ViewModel + +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/viewmodel/ViewModelFactory.kt b/app/src/main/java/ru/touchin/template/di/viewmodel/ViewModelFactory.kt new file mode 100644 index 0000000..699e68c --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/viewmodel/ViewModelFactory.kt @@ -0,0 +1,44 @@ +package ru.touchin.template.di.viewmodel + +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.AbstractSavedStateViewModelFactory +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.savedstate.SavedStateRegistryOwner +import javax.inject.Inject +import javax.inject.Provider + +class ViewModelFactory @Inject constructor( + private val viewModels: MutableMap, Provider> +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + val viewModel = viewModels[modelClass]?.get() + return viewModel as T + } +} + +inline fun Fragment.assistedViewModel( + crossinline creator: (SavedStateHandle) -> VM, +): Lazy = viewModels { createAbstractSavedStateViewModelFactory(arguments, creator) } + +inline fun SavedStateRegistryOwner.createAbstractSavedStateViewModelFactory( + arguments: Bundle? = Bundle(), + crossinline creator: (SavedStateHandle) -> T, +): ViewModelProvider.Factory { + return object : AbstractSavedStateViewModelFactory( + owner = this@createAbstractSavedStateViewModelFactory, + defaultArgs = arguments, + ) { + @Suppress("UNCHECKED_CAST") + override fun create( + key: String, + modelClass: Class, + handle: SavedStateHandle, + ): T = creator(handle) as T + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/viewmodel/ViewModelKey.kt b/app/src/main/java/ru/touchin/template/di/viewmodel/ViewModelKey.kt new file mode 100644 index 0000000..1f76f60 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/viewmodel/ViewModelKey.kt @@ -0,0 +1,9 @@ +package ru.touchin.template.di.viewmodel + +import androidx.lifecycle.ViewModel +import dagger.MapKey +import kotlin.reflect.KClass + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +@MapKey +annotation class ViewModelKey(val value: KClass) \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/feature/SingleActivity.kt b/app/src/main/java/ru/touchin/template/feature/SingleActivity.kt new file mode 100644 index 0000000..45cbb69 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/feature/SingleActivity.kt @@ -0,0 +1,47 @@ +package ru.touchin.template.feature + +import android.os.Bundle +import androidx.activity.viewModels +import com.github.terrakok.cicerone.NavigatorHolder +import com.github.terrakok.cicerone.androidx.AppNavigator +import javax.inject.Inject +import ru.touchin.template.R +import ru.touchin.template.base.activity.BaseActivity +import ru.touchin.template.databinding.ActivityMainBinding +import ru.touchin.template.di.DI + +class SingleActivity : BaseActivity() { + + @Inject + lateinit var navigatorHolder: NavigatorHolder + + private val rootNavigator by lazy { + AppNavigator(this, R.id.activityScreensContainer) + } + + private lateinit var binding: ActivityMainBinding + + override fun createViewModelLazy(): Lazy = viewModels { viewModelFactory } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + DI.getComponent().inject(this) + + viewModel.navigate() + + } + + override fun onResumeFragments() { + super.onResumeFragments() + navigatorHolder.setNavigator(rootNavigator) + } + + override fun onPause() { + navigatorHolder.removeNavigator() + super.onPause() + } + +} diff --git a/app/src/main/java/ru/touchin/template/feature/SingleViewModel.kt b/app/src/main/java/ru/touchin/template/feature/SingleViewModel.kt new file mode 100644 index 0000000..394f3d5 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/feature/SingleViewModel.kt @@ -0,0 +1,14 @@ +package ru.touchin.template.feature + +import androidx.lifecycle.ViewModel +import com.github.terrakok.cicerone.Router +import javax.inject.Inject +import ru.touchin.template.navigation.Screens + +class SingleViewModel @Inject constructor( + private val router: Router, +) : ViewModel() { + fun navigate() { + router.newRootScreen(Screens.First()) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/feature/first/FirstFragment.kt b/app/src/main/java/ru/touchin/template/feature/first/FirstFragment.kt new file mode 100644 index 0000000..128df58 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/feature/first/FirstFragment.kt @@ -0,0 +1,29 @@ +package ru.touchin.template.feature.first + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import ru.touchin.template.R +import ru.touchin.template.base.fragment.BaseFragment +import ru.touchin.template.databinding.FragmentFirstBinding +import ru.touchin.template.utils.binding.viewBinding + +class FirstFragment : BaseFragment(R.layout.fragment_first) { + + private val binding by viewBinding { FragmentFirstBinding.bind(it) } + + override fun createViewModelLazy() = viewModels { viewModelFactory } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding) { + button.apply { + text = "Next" + setOnClickListener { viewModel.onNextButtonClicked(this@FirstFragment::class.toString()) } + } + + textView.text = "First Fragment" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/feature/first/FirstViewModel.kt b/app/src/main/java/ru/touchin/template/feature/first/FirstViewModel.kt new file mode 100644 index 0000000..02941c6 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/feature/first/FirstViewModel.kt @@ -0,0 +1,16 @@ +package ru.touchin.template.feature.first + +import com.github.terrakok.cicerone.Router +import javax.inject.Inject +import ru.touchin.template.base.viewmodel.BaseViewModel +import ru.touchin.template.navigation.Screens + +class FirstViewModel @Inject constructor( + private val router: Router +) : BaseViewModel() { + + fun onNextButtonClicked(from: String) { + router.navigateTo(Screens.Second(from)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/feature/second/SecondFragment.kt b/app/src/main/java/ru/touchin/template/feature/second/SecondFragment.kt new file mode 100644 index 0000000..7e17c92 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/feature/second/SecondFragment.kt @@ -0,0 +1,64 @@ +package ru.touchin.template.feature.second + +import android.os.Bundle +import android.view.View +import androidx.core.os.bundleOf +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import ru.touchin.template.R +import ru.touchin.template.base.fragment.BaseFragment +import ru.touchin.template.databinding.FragmentSecondBinding +import ru.touchin.template.di.viewmodel.assistedViewModel +import ru.touchin.template.utils.binding.viewBinding + +class SecondFragment : BaseFragment(R.layout.fragment_second) { + + companion object { + private const val FROM_KEY = "FROM" + private const val SCREEN_NAME_KEY = "SCREEN_NAME" + private const val FRAGMENT_NAME = "Second Fragment" + + fun newInstance(from: String): SecondFragment { + val args = bundleOf(FROM_KEY to from, SCREEN_NAME_KEY to FRAGMENT_NAME) + + val fragment = SecondFragment().apply { + arguments = args + } + + return fragment + } + } + + private val binding by viewBinding { FragmentSecondBinding.bind(it) } + + private val secondViewModelFactory by lazy { getSharedComponent().secondScreenViewModelFactory() } + + override fun createViewModelLazy() = assistedViewModel { + secondViewModelFactory.create( + arguments?.getString(FROM_KEY) ?: "Unkown Fragment", + arguments?.getString(SCREEN_NAME_KEY) ?: "Unknown" + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding) { + button.apply { + text = "Back" + setOnClickListener { onBackPressed() } + } + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.state + .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) + .collect { + textView.text = it + } + } + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/feature/second/SecondViewModel.kt b/app/src/main/java/ru/touchin/template/feature/second/SecondViewModel.kt new file mode 100644 index 0000000..cc9fd9c --- /dev/null +++ b/app/src/main/java/ru/touchin/template/feature/second/SecondViewModel.kt @@ -0,0 +1,33 @@ +package ru.touchin.template.feature.second + +import com.github.terrakok.cicerone.Router +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import ru.touchin.template.base.viewmodel.BaseViewModel + +class SecondViewModel @AssistedInject constructor( + @Assisted("from") from: String, + @Assisted("screenName") screenName: String, + private val rootRouter: Router +) : BaseViewModel() { + + private val _state = MutableStateFlow("$from to $screenName") + val state: StateFlow = _state.asStateFlow() + + @AssistedFactory + interface Factory { + fun create( + @Assisted("from") from: String, + @Assisted("screenName") screenName: String + ): SecondViewModel + } + + override fun onBackClicked(): Boolean { + rootRouter.exit() + return super.onBackClicked() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/navigation/MainNavigation.kt b/app/src/main/java/ru/touchin/template/navigation/MainNavigation.kt deleted file mode 100644 index e63ac31..0000000 --- a/app/src/main/java/ru/touchin/template/navigation/MainNavigation.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.touchin.template.navigation - -import javax.inject.Qualifier - -@Qualifier -annotation class MainNavigation diff --git a/app/src/main/java/ru/touchin/template/navigation/Screens.kt b/app/src/main/java/ru/touchin/template/navigation/Screens.kt index 3ed2df7..51dabfd 100644 --- a/app/src/main/java/ru/touchin/template/navigation/Screens.kt +++ b/app/src/main/java/ru/touchin/template/navigation/Screens.kt @@ -1,13 +1,16 @@ 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 +import com.github.terrakok.cicerone.androidx.FragmentScreen +import ru.touchin.template.feature.first.FirstFragment +import ru.touchin.template.feature.second.SecondFragment object Screens { - class Login : SupportAppScreen() { - override fun getFragment(): Fragment = LoginFragment() - + fun First(): FragmentScreen = FragmentScreen { + FirstFragment() } -} + + fun Second(from: String): FragmentScreen = FragmentScreen { + SecondFragment.newInstance(from) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/navigation/StartUpCoordinator.kt b/app/src/main/java/ru/touchin/template/navigation/StartUpCoordinator.kt deleted file mode 100644 index b926336..0000000 --- a/app/src/main/java/ru/touchin/template/navigation/StartUpCoordinator.kt +++ /dev/null @@ -1,18 +0,0 @@ -package ru.touchin.template.navigation - -import ru.terrakok.cicerone.Router -import javax.inject.Inject - -class StartUpCoordinator @Inject constructor( - @MainNavigation private val router: Router -) { - - fun start() { - router.newRootScreen(Screens.Login()) - } - - fun closeCurrentScreen() { - router.exit() - } - -} diff --git a/app/src/main/java/ru/touchin/template/navigation/backpress/OnBackPressedListener.kt b/app/src/main/java/ru/touchin/template/navigation/backpress/OnBackPressedListener.kt new file mode 100644 index 0000000..1679240 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/navigation/backpress/OnBackPressedListener.kt @@ -0,0 +1,6 @@ +package ru.touchin.template.navigation.backpress + +interface OnBackPressedListener { + + fun onBackPressed(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/navigation/login/LoginCoordinatorImpl.kt b/app/src/main/java/ru/touchin/template/navigation/login/LoginCoordinatorImpl.kt deleted file mode 100644 index f7ed3a7..0000000 --- a/app/src/main/java/ru/touchin/template/navigation/login/LoginCoordinatorImpl.kt +++ /dev/null @@ -1,16 +0,0 @@ -package ru.touchin.template.navigation.login - -import ru.terrakok.cicerone.Router -import ru.touchin.template.feature_login.navigation.LoginCoordinator -import ru.touchin.template.navigation.MainNavigation -import javax.inject.Inject - -class LoginCoordinatorImpl @Inject constructor( - @MainNavigation private val router: Router -) : LoginCoordinator { - - override fun openMainScreen() { - router.exit() - } - -} diff --git a/app/src/main/java/ru/touchin/template/navigation/router/RouterProvider.kt b/app/src/main/java/ru/touchin/template/navigation/router/RouterProvider.kt new file mode 100644 index 0000000..14426c4 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/navigation/router/RouterProvider.kt @@ -0,0 +1,7 @@ +package ru.touchin.template.navigation.router + +import com.github.terrakok.cicerone.Router + +interface RouterProvider { + val router: Router +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/utils/binding/FragmentViewBindingDelegate.kt b/app/src/main/java/ru/touchin/template/utils/binding/FragmentViewBindingDelegate.kt new file mode 100644 index 0000000..f5e15b1 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/utils/binding/FragmentViewBindingDelegate.kt @@ -0,0 +1,50 @@ +package ru.touchin.template.utils.binding + +import android.view.View +import androidx.fragment.app.Fragment +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.viewbinding.ViewBinding +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +typealias ViewBindingFactory = (View) -> T + +class FragmentViewBindingDelegate( + val fragment: Fragment, + val viewBindingFactory: ViewBindingFactory, +) : ReadOnlyProperty { + private var binding: T? = null + + init { + fragment.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onCreate(owner: LifecycleOwner) { + fragment.viewLifecycleOwnerLiveData.observe(fragment) { lifecylelOwner -> + lifecylelOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + binding = null + } + }) + } + } + }) + } + + override fun getValue(thisRef: Fragment, property: KProperty<*>): T { + val binding = this.binding + + if (binding != null) return binding + + val lifecycle = fragment.viewLifecycleOwner.lifecycle + + if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { + throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.") + } + + return viewBindingFactory(thisRef.requireView()).also { this.binding = it } + } +} + +fun Fragment.viewBinding(viewBindingFactory: ViewBindingFactory) = + FragmentViewBindingDelegate(this, viewBindingFactory) \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index a066a0d..305c3c3 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,6 +1,22 @@ - - + android:layout_height="match_parent" + android:background="@color/colorPrimaryDark"> + + + + + + diff --git a/app/src/main/res/layout/fragment_first.xml b/app/src/main/res/layout/fragment_first.xml new file mode 100644 index 0000000..a36f0ea --- /dev/null +++ b/app/src/main/res/layout/fragment_first.xml @@ -0,0 +1,27 @@ + + + + + +