├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml ├── jarRepositories.xml └── misc.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── template │ │ └── cleanarch │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── template │ │ │ └── cleanarch │ │ │ ├── CleanApp.kt │ │ │ ├── di │ │ │ ├── AppInjector.kt │ │ │ ├── component │ │ │ │ └── ApplicationComponent.kt │ │ │ └── module │ │ │ │ ├── ApplicationModule.kt │ │ │ │ ├── DbModule.kt │ │ │ │ └── ViewModelModule.kt │ │ │ ├── presentation │ │ │ └── view │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainViewModel.kt │ │ │ └── utils │ │ │ ├── LocaleManager.kt │ │ │ ├── PreferenceUtil.kt │ │ │ └── SharedPrefHelper.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ └── nav_graph_main.xml │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ ├── java │ └── com │ │ └── template │ │ └── cleanarch │ │ ├── LoginViewModelTest.kt │ │ ├── base │ │ ├── BaseTest.kt │ │ └── MainCoroutineRule.kt │ │ └── di │ │ ├── MainDIComponent.kt │ │ ├── MockWebServerDITest.kt │ │ └── NetworkDITest.kt │ └── resources │ └── login_success.json ├── benchmark ├── benchmark-proguard-rules.pro ├── build.gradle └── src │ ├── androidTest │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── template │ │ └── benchmark │ │ └── ExampleBenchmark.kt │ └── main │ └── AndroidManifest.xml ├── build.gradle ├── commons.features.gradle ├── commons.gradle ├── core ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── template │ │ └── core │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── template │ │ │ └── core │ │ │ ├── arc │ │ │ └── SingleLiveEvent.kt │ │ │ ├── common │ │ │ ├── Constants.kt │ │ │ └── SharedPrefConstants.kt │ │ │ ├── config │ │ │ └── Configuration.kt │ │ │ ├── contract │ │ │ └── SubscriptionContract.kt │ │ │ ├── di │ │ │ ├── Injectable.kt │ │ │ └── module │ │ │ │ └── application │ │ │ │ ├── OkhttpModule.kt │ │ │ │ └── RetrofitModule.kt │ │ │ ├── interceptors │ │ │ └── HeaderInterceptor.kt │ │ │ ├── listeners │ │ │ ├── BackButtonHandlerListener.kt │ │ │ └── BackPressListner.kt │ │ │ ├── ui │ │ │ └── base │ │ │ │ ├── BaseActivity.kt │ │ │ │ └── BaseFragment.kt │ │ │ ├── utils │ │ │ ├── LocaleManager.kt │ │ │ ├── NavigationCommand.kt │ │ │ ├── PreferenceUtil.kt │ │ │ └── SharedPrefHelper.kt │ │ │ └── viewmodel │ │ │ ├── SharedViewModel.kt │ │ │ ├── ToolbarPropertyViewModel.kt │ │ │ └── base │ │ │ └── BaseViewModel.kt │ └── res │ │ ├── anim │ │ ├── anim_enter.xml │ │ ├── anim_exit.xml │ │ ├── anim_frag_fade_in.xml │ │ ├── anim_frag_fade_out.xml │ │ ├── anim_pop_enter.xml │ │ ├── anim_pop_exit.xml │ │ ├── anim_slide_in_up.xml │ │ ├── anim_slide_out_up.xml │ │ ├── pop_enter.xml │ │ ├── pop_exit.xml │ │ ├── right_in.xml │ │ ├── right_out.xml │ │ ├── slide_down.xml │ │ ├── slide_down_fade_out_anim.xml │ │ ├── slide_down_half.xml │ │ ├── slide_from_top_right.xml │ │ ├── slide_in_from_left.xml │ │ ├── slide_in_from_rigth.xml │ │ ├── slide_left_fade_out_anim.xml │ │ ├── slide_out_to_left.xml │ │ ├── slide_out_to_right.xml │ │ └── slide_up.xml │ │ └── values │ │ ├── do_not_translate.xml │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── template │ └── core │ └── ExampleUnitTest.kt ├── data ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── template │ │ └── data │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── template │ │ └── data │ │ ├── constants │ │ └── NetworkConstants.kt │ │ ├── datasource │ │ ├── local │ │ │ ├── dao │ │ │ │ ├── BaseDao.kt │ │ │ │ └── BranchDao.kt │ │ │ ├── database │ │ │ │ └── AppDb.kt │ │ │ └── entity │ │ │ │ └── Branch.kt │ │ └── remote │ │ │ ├── api │ │ │ └── IAuthApi.kt │ │ │ └── dto │ │ │ ├── AuthDto.kt │ │ │ ├── CommonDto.kt │ │ │ └── ErrorDto.kt │ │ ├── di │ │ ├── ApiModule.kt │ │ └── RepositoryModule.kt │ │ ├── mapper │ │ └── dtotoentity │ │ │ ├── AuthDtoToEntity.kt │ │ │ └── CommonDtoToEntity.kt │ │ └── repository │ │ ├── AuthRepositoryImpl.kt │ │ └── BaseRepositoryImpl.kt │ └── test │ └── java │ └── com │ └── template │ └── data │ └── ExampleUnitTest.kt ├── domain ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── com │ └── template │ └── domain │ ├── common │ └── ResultState.kt │ ├── di │ └── UseCaseModule.kt │ ├── entity │ ├── request │ │ └── AuthRequest.kt │ └── response │ │ ├── auth │ │ └── AuthEntity.kt │ │ └── common │ │ ├── CommonEntity.kt │ │ └── ErrorEntity.kt │ ├── repository │ └── IAuthRepository.kt │ └── usecases │ ├── auth │ ├── AuthUseCaseImpl.kt │ └── IAuthUseCase.kt │ └── base │ └── BaseUseCase.kt ├── features ├── onboarding │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── template │ │ │ └── feature_onboarding │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── template │ │ │ │ └── feature_onboarding │ │ │ │ ├── di │ │ │ │ └── OnboardingViewModelModule.kt │ │ │ │ └── view │ │ │ │ └── fragment │ │ │ │ ├── signin │ │ │ │ ├── LoginFragment.kt │ │ │ │ └── LoginViewModel.kt │ │ │ │ └── splash │ │ │ │ ├── SplashFragment.kt │ │ │ │ └── SplashViewModel.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── ic_launcher_background.xml │ │ │ ├── drawable-mdpi │ │ │ └── ic_launcher_background.xml │ │ │ ├── drawable-xhdpi │ │ │ └── ic_launcher_background.xml │ │ │ ├── drawable-xxhdpi │ │ │ └── ic_launcher_background.xml │ │ │ ├── drawable-xxxhdpi │ │ │ └── ic_launcher_background.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ ├── fragment_login.xml │ │ │ └── fragment_splash.xml │ │ │ ├── navigation │ │ │ └── nav_graph_onboarding.xml │ │ │ └── values │ │ │ └── ids.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── template │ │ └── feature_onboarding │ │ └── ExampleUnitTest.kt └── profile │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── template │ │ └── feature_profile │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── template │ │ │ └── feature_profile │ │ │ ├── di │ │ │ └── ProfileViewModelModule.kt │ │ │ ├── edit │ │ │ ├── EditProfileFragment.kt │ │ │ └── EditProfileViewModel.kt │ │ │ └── profile │ │ │ ├── ProfileFragment.kt │ │ │ └── ProfileViewModel.kt │ └── res │ │ ├── drawable-hdpi │ │ └── ic_launcher_background.xml │ │ ├── drawable-mdpi │ │ └── ic_launcher_background.xml │ │ ├── drawable-xhdpi │ │ └── ic_launcher_background.xml │ │ ├── drawable-xxhdpi │ │ └── ic_launcher_background.xml │ │ ├── drawable-xxxhdpi │ │ └── ic_launcher_background.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── fragment_edit_profile.xml │ │ └── fragment_profile.xml │ │ ├── navigation │ │ └── nav_graph_profile.xml │ │ └── values │ │ └── ids.xml │ └── test │ └── java │ └── com │ └── template │ └── feature_profile │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots ├── app_modules.png ├── arch.png ├── deeplink_onboarding.png ├── goback.png ├── ids.png ├── main_nav_graph.png ├── module_to_module.png ├── onboarding_login.png ├── onboarding_nav_graph.png ├── profile.png ├── profile_deeplink.png ├── profile_ids.png └── profile_nav_graph.png ├── settings.gradle └── thirdpartys └── analyticslib ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── template │ └── analyticslib │ └── ExampleInstrumentedTest.kt ├── main ├── AndroidManifest.xml └── java │ └── com │ └── template │ └── analyticslib │ ├── AnalyicsHelper.kt │ └── FirebaseAnalyticsHelper.kt └── test └── java └── com └── template └── analyticslib └── ExampleUnitTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | CleanArchApp -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | id 'androidx.navigation.safeargs' 6 | } 7 | apply from: "$rootProject.projectDir/commons.gradle" 8 | 9 | android { 10 | compileSdkVersion rootProject.compileSdkVersion 11 | buildToolsVersion rootProject.buildToolsVersion 12 | 13 | defaultConfig { 14 | applicationId "com.template.cleanarch" 15 | minSdkVersion rootProject.minSdkVersion 16 | targetSdkVersion rootProject.targetSdkVersion 17 | versionCode 1 18 | versionName "1.0" 19 | 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | kotlinOptions { 34 | jvmTarget = '1.8' 35 | } 36 | 37 | buildFeatures { 38 | dataBinding true 39 | } 40 | 41 | flavorDimensions "server" 42 | 43 | productFlavors { 44 | 45 | dev { 46 | dimension "server" 47 | } 48 | 49 | uat { 50 | dimension "server" 51 | } 52 | 53 | pilot { 54 | dimension "server" 55 | } 56 | 57 | prod { 58 | dimension "server" 59 | } 60 | } 61 | 62 | publishNonDefault true 63 | 64 | configurations { 65 | devDebugImplementation 66 | devReleaseImplementation 67 | uatDebugImplementation 68 | uatReleaseImplementation 69 | pilotDebugImplementation 70 | pilotReleaseImplementation 71 | prodDebugImplementation 72 | prodReleaseImplementation 73 | } 74 | 75 | packagingOptions { 76 | exclude 'META-INF/DEPENDENCIES' 77 | exclude 'META-INF/LICENSE' 78 | exclude 'META-INF/LICENSE.txt' 79 | exclude 'META-INF/license.txt' 80 | exclude 'META-INF/NOTICE' 81 | exclude 'META-INF/NOTICE.txt' 82 | exclude 'META-INF/notice.txt' 83 | exclude 'META-INF/AL2.0' 84 | exclude 'META-INF/LGPL2.1' 85 | exclude("META-INF/*.kotlin_module") 86 | } 87 | 88 | } 89 | 90 | dependencies { 91 | 92 | api project(':core') 93 | implementation project(':features:onboarding') 94 | implementation project(':features:profile') 95 | 96 | // Koin AndroidX Scope features 97 | implementation "org.koin:koin-androidx-scope:$koinVersion" 98 | implementation "org.koin:koin-androidx-viewmodel:$koinVersion" 99 | implementation "org.koin:koin-androidx-fragment:$koinVersion" 100 | implementation "org.koin:koin-androidx-ext:$koinVersion" 101 | implementation "org.koin:koin-androidx-scope:$koinVersion" 102 | implementation "org.koin:koin-androidx-viewmodel:$koinVersion" 103 | implementation "org.koin:koin-core:$koinVersion" 104 | 105 | //Calligraphy 106 | implementation "io.github.inflationx:calligraphy3:$calligrapyVersion" 107 | implementation "io.github.inflationx:viewpump:$viewpumpVersion" 108 | 109 | // Navigation Jetpack 110 | implementation "androidx.navigation:navigation-fragment-ktx:$navVersion" 111 | implementation "androidx.navigation:navigation-ui-ktx:$navVersion" 112 | 113 | //Retrofit 114 | implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" 115 | implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" 116 | implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion" 117 | implementation "com.squareup.okhttp3:logging-interceptor:$okhttpLoggingVersion" 118 | implementation "com.squareup.okio:okio:$okioVersion" 119 | implementation "com.squareup.okhttp3:okhttp-urlconnection:$urlConnectionVersion" 120 | 121 | 122 | //Coroutine 123 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" 124 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" 125 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutineVersion" 126 | implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$coroutineAdapterVersion" 127 | 128 | /* Android Architecture Component - Room Persistance Lib */ 129 | implementation "androidx.room:room-runtime:$roomVersion" 130 | kapt "androidx.room:room-compiler:$roomVersion" 131 | implementation "androidx.room:room-common:$roomVersion" 132 | 133 | implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion" 134 | 135 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/template/cleanarch/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.template.cleanarch", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/template/cleanarch/CleanApp.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.template.cleanarch.di.AppInjector 6 | import com.template.cleanarch.di.component.ApplicationComponent.loadAllModules 7 | import org.koin.android.ext.koin.androidContext 8 | import org.koin.core.context.startKoin 9 | 10 | /**** 11 | * Application class 12 | * Author: Lajesh Dineshkumar 13 | * Created on: 1/31/21 14 | * Modified on: 1/31/21 15 | *****/ 16 | class CleanApp : Application() { 17 | 18 | private var localeContext: Context? = null 19 | 20 | init { 21 | instance = this 22 | } 23 | 24 | companion object { 25 | private lateinit var instance: CleanApp 26 | 27 | private var isAppVisible: Boolean = false 28 | 29 | fun applicationContext(): Context { 30 | return instance.applicationContext 31 | } 32 | 33 | fun localeContext(): Context { 34 | return instance.localeContext ?: instance.applicationContext 35 | } 36 | 37 | fun getInstance(): CleanApp { 38 | return instance 39 | } 40 | 41 | fun setInstance(application: CleanApp) { 42 | instance = application 43 | } 44 | 45 | fun isApplicationVisible(): Boolean { 46 | return isAppVisible 47 | } 48 | 49 | fun setAppVisible(isVisible: Boolean) { 50 | isAppVisible = isVisible 51 | } 52 | } 53 | 54 | override fun onCreate() { 55 | super.onCreate() 56 | 57 | AppInjector.init(this) 58 | 59 | startKoin { 60 | androidContext(this@CleanApp) 61 | } 62 | 63 | loadAllModules() 64 | } 65 | 66 | fun setLocaleContext(context: Context) { 67 | this.localeContext = context 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/java/com/template/cleanarch/di/AppInjector.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch.di 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.os.Bundle 6 | import com.template.cleanarch.CleanApp 7 | 8 | /** 9 | * Helper class to automatically inject fragments if they implement [Injectable]. 10 | * Created by Lajesh Dineshkumar on 10/30/2019. 11 | * Company: 12 | * Email: lajeshds2007@gmail.com 13 | */ 14 | object AppInjector { 15 | 16 | private var resumed = 0 17 | private var paused = 0 18 | 19 | fun init(cleanApp: CleanApp) { 20 | cleanApp.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks { 21 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { 22 | cleanApp.setLocaleContext(activity) 23 | } 24 | 25 | override fun onActivityStarted(activity: Activity) { 26 | // Nothing goes here 27 | } 28 | 29 | override fun onActivityResumed(activity: Activity) { 30 | ++resumed 31 | CleanApp.setAppVisible(true) 32 | } 33 | 34 | override fun onActivityPaused(activity: Activity) { 35 | ++paused 36 | } 37 | 38 | override fun onActivityStopped(activity: Activity) { 39 | CleanApp.setAppVisible(resumed > paused) 40 | } 41 | 42 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { 43 | // Nothing goes here 44 | } 45 | 46 | override fun onActivityDestroyed(activity: Activity) { 47 | // Nothing goes here 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/template/cleanarch/di/component/ApplicationComponent.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch.di.component 2 | 3 | import com.template.cleanarch.di.module.DbModule 4 | import com.template.cleanarch.di.module.ViewModelModule 5 | import com.template.core.di.module.application.RetrofitModule 6 | import com.template.cleanarch.di.module.ApplicationModule 7 | import com.template.core.di.module.application.OkhttpModule 8 | import com.template.data.di.ApiModule 9 | import com.template.data.di.RepositoryModule 10 | import com.template.domain.di.UseCaseModule 11 | import com.template.feature_onboarding.di.OnboardingViewModelModule 12 | import com.template.feature_profile.di.ProfileViewModelModule 13 | 14 | /**** 15 | * Application component which loads all the Koin Modules 16 | * Author: Lajesh Dineshkumar 17 | * Company: 18 | * Created on: 2/2/21 19 | * Modified on: 2/2/21 20 | *****/ 21 | object ApplicationComponent { 22 | fun loadAllModules(){ 23 | RetrofitModule.load() 24 | OkhttpModule.load() 25 | RepositoryModule.load() 26 | UseCaseModule.load() 27 | ApiModule.load() 28 | ApplicationModule.load() 29 | ViewModelModule.load() 30 | OnboardingViewModelModule.load() 31 | ProfileViewModelModule.load() 32 | DbModule.load() 33 | 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/template/cleanarch/di/module/ApplicationModule.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch.di.module 2 | 3 | import org.koin.core.context.loadKoinModules 4 | import org.koin.dsl.module 5 | 6 | /**** 7 | * File Description 8 | * Author: Lajesh Dineshkumar 9 | * Company: 10 | * Created on: 2/2/21 11 | * Modified on: 2/2/21 12 | *****/ 13 | object ApplicationModule { 14 | fun load() { 15 | loadKoinModules(module { 16 | 17 | }) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/template/cleanarch/di/module/DbModule.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch.di.module 2 | 3 | import androidx.room.Room 4 | import androidx.room.RoomDatabase 5 | import androidx.sqlite.db.SupportSQLiteDatabase 6 | import com.template.cleanarch.CleanApp 7 | import com.template.data.datasource.local.database.AppDb 8 | import org.koin.core.context.loadKoinModules 9 | import org.koin.dsl.module 10 | 11 | /**** 12 | * Database Module 13 | * Author: Lajesh Dineshkumar 14 | * Company: 15 | * Created on: 3/14/21 16 | * Modified on: 3/14/21 17 | *****/ 18 | object DbModule { 19 | fun load() { 20 | loadKoinModules(dbModules ) 21 | } 22 | 23 | val dbModules = module { 24 | 25 | single { 26 | Room.databaseBuilder( 27 | CleanApp.applicationContext(), AppDb::class.java, "cleanapp.db" 28 | ).allowMainThreadQueries() 29 | .addCallback(object : RoomDatabase.Callback() { 30 | override fun onDestructiveMigration(db: SupportSQLiteDatabase) { 31 | // Clear data 32 | } 33 | }) 34 | // Clear DB while upgrade or downgrade 35 | .fallbackToDestructiveMigration() 36 | .build() 37 | } 38 | 39 | single { 40 | get().branchDao() 41 | } 42 | 43 | 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/template/cleanarch/di/module/ViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch.di.module 2 | 3 | import com.template.cleanarch.presentation.view.MainViewModel 4 | import com.template.core.viewmodel.SharedViewModel 5 | import com.template.core.viewmodel.ToolbarPropertyViewModel 6 | import org.koin.androidx.viewmodel.dsl.viewModel 7 | import org.koin.core.context.loadKoinModules 8 | import org.koin.dsl.module 9 | 10 | /**** 11 | * DI module which provides all the viewmodel instances 12 | * Author: Lajesh Dineshkumar 13 | * Company: 14 | * Created on: 2/3/21 15 | * Modified on: 2/3/21 16 | *****/ 17 | object ViewModelModule { 18 | fun load() { 19 | loadKoinModules(module { 20 | viewModel { 21 | MainViewModel() 22 | } 23 | 24 | viewModel { 25 | ToolbarPropertyViewModel() 26 | } 27 | 28 | viewModel { 29 | SharedViewModel() 30 | } 31 | 32 | }) 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/template/cleanarch/presentation/view/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch.presentation.view 2 | 3 | 4 | import android.os.Bundle 5 | import androidx.navigation.Navigation 6 | import androidx.navigation.ui.NavigationUI 7 | import com.template.cleanarch.R 8 | import com.template.cleanarch.databinding.ActivityMainBinding 9 | import com.template.cleanarch.BR 10 | import com.template.core.ui.base.BaseActivity 11 | 12 | 13 | class MainActivity : BaseActivity(MainViewModel::class){ 14 | override val layoutRes: Int 15 | get() = R.layout.activity_main 16 | override val bindingVariable: Int 17 | get() = BR.viewModel 18 | 19 | override fun getViewModel(): Class { 20 | return MainViewModel::class.java 21 | } 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | NavigationUI.setupActionBarWithNavController(this, 26 | Navigation.findNavController(this, R.id.fragment_container_view)); 27 | } 28 | 29 | override fun onSupportNavigateUp(): Boolean { 30 | return Navigation.findNavController(this, R.id.fragment_container_view).navigateUp() 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/template/cleanarch/presentation/view/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch.presentation.view 2 | 3 | import com.template.core.viewmodel.base.BaseViewModel 4 | 5 | /**** 6 | * File Description 7 | * Author: Lajesh Dineshkumar 8 | * Company: 9 | * Created on: 2/3/21 10 | * Modified on: 2/3/21 11 | *****/ 12 | class MainViewModel : BaseViewModel() { 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/template/cleanarch/utils/PreferenceUtil.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch.utils 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | 6 | /**** 7 | * Shared Preference util class 8 | * ---------------------------- 9 | * How to use ? 10 | * val prefs = PreferenceUtil.defaultPrefs(this) 11 | * set any type of value in prefs 12 | * prefs[PREFUSERID] = "name" 13 | * ---------------------------- 14 | * Author: Lajesh Dineshkumar 15 | * Company: 16 | * Created on: 2020-03-02 17 | * Modified on: 2020-03-02 18 | *****/ 19 | object PreferenceUtil { 20 | 21 | fun customPrefs(context: Context, name: String): SharedPreferences = 22 | context.getSharedPreferences(name, Context.MODE_PRIVATE) 23 | 24 | private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) { 25 | val editor = this.edit() 26 | operation(editor) 27 | editor.apply() 28 | } 29 | 30 | /** 31 | * puts a key value pair in shared prefs if doesn't exists, otherwise updates value on given [key] 32 | */ 33 | operator fun SharedPreferences.set(key: String, value: Any?) { 34 | when (value) { 35 | is String? -> edit { it.putString(key, value) } 36 | is Int -> edit { it.putInt(key, value) } 37 | is Boolean -> edit { it.putBoolean(key, value) } 38 | is Float -> edit { it.putFloat(key, value) } 39 | is Long -> edit { it.putLong(key, value) } 40 | else -> throw UnsupportedOperationException("Not yet implemented") 41 | } 42 | } 43 | 44 | /** 45 | * finds value on given key. 46 | * [T] is the type of value 47 | * @param defaultValue optional default value - will take null for strings, false for bool and -1 for numeric values if [defaultValue] is not specified 48 | */ 49 | inline operator fun SharedPreferences.get(key: String, defaultValue: T? = null): T? { 50 | return when (T::class) { 51 | String::class -> getString(key, defaultValue as? String) as T? 52 | Int::class -> getInt(key, defaultValue as? Int ?: -1) as T? 53 | Boolean::class -> getBoolean(key, defaultValue as? Boolean ?: false) as T? 54 | Float::class -> getFloat(key, defaultValue as? Float ?: -1f) as T? 55 | Long::class -> getLong(key, defaultValue as? Long ?: -1) as T? 56 | else -> throw UnsupportedOperationException("Not yet implemented") 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/template/cleanarch/utils/SharedPrefHelper.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch.utils 2 | 3 | import android.content.SharedPreferences 4 | import com.template.cleanarch.CleanApp 5 | import com.template.cleanarch.utils.PreferenceUtil.get 6 | import com.template.cleanarch.utils.PreferenceUtil.set 7 | import com.template.core.common.SharedPrefConstants 8 | import com.template.core.common.SharedPrefConstants.DID_FAVORITE_OFFER 9 | import com.template.core.common.SharedPrefConstants.WALLET_ONBOARDING_COMPLETED 10 | 11 | /**** 12 | * Keep all shared preference related methods here 13 | * Author: Lajesh Dineshkumar 14 | * Company: 15 | * Created on: 2020-03-02 16 | * Modified on: 2020-03-02 17 | *****/ 18 | class SharedPrefHelper { 19 | 20 | private var sharedPreferences: SharedPreferences = 21 | PreferenceUtil.customPrefs(CleanApp.applicationContext(), SharedPrefConstants.APP_PREFS) 22 | 23 | fun getToken(): String? { 24 | return sharedPreferences[SharedPrefConstants.TOKEN] 25 | } 26 | 27 | fun setToken(token: String?) { 28 | sharedPreferences[SharedPrefConstants.TOKEN] = token 29 | } 30 | 31 | fun saveFcmToken(token: String?) { 32 | sharedPreferences[SharedPrefConstants.PREF_FCM_TOKEN] = token 33 | } 34 | 35 | fun getFCMToken(): String? { 36 | return sharedPreferences[SharedPrefConstants.PREF_FCM_TOKEN] 37 | } 38 | 39 | fun isUserLogged(): Boolean { 40 | return getUserID() != 0 41 | } 42 | 43 | fun setWalletOnBoardingCompleted(completed: Boolean) { 44 | sharedPreferences[WALLET_ONBOARDING_COMPLETED] = completed 45 | } 46 | 47 | fun doFavoriteOffer() { 48 | sharedPreferences[DID_FAVORITE_OFFER] = true 49 | } 50 | 51 | fun didFavoriteOffer(): Boolean = sharedPreferences[DID_FAVORITE_OFFER] ?: false 52 | 53 | fun isWalletOnBoardingCompleted(): Boolean { 54 | return sharedPreferences[WALLET_ONBOARDING_COMPLETED] ?: false 55 | } 56 | 57 | private fun getUserID(): Int { 58 | return (sharedPreferences[SharedPrefConstants.USER_ID, "0"] ?: "0").toInt() 59 | } 60 | 61 | fun getCurrentLocale(): LocaleManager.LocaleInfo? { 62 | return LocaleManager.getCurrentLocaleInfo(CleanApp.localeContext()) 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 19 | 20 | 27 | 28 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lajesh/clean-architecture-kotlin/af1fe7b55f7585e1406d93a02b7d336a0b81ad45/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lajesh/clean-architecture-kotlin/af1fe7b55f7585e1406d93a02b7d336a0b81ad45/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lajesh/clean-architecture-kotlin/af1fe7b55f7585e1406d93a02b7d336a0b81ad45/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lajesh/clean-architecture-kotlin/af1fe7b55f7585e1406d93a02b7d336a0b81ad45/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lajesh/clean-architecture-kotlin/af1fe7b55f7585e1406d93a02b7d336a0b81ad45/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lajesh/clean-architecture-kotlin/af1fe7b55f7585e1406d93a02b7d336a0b81ad45/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lajesh/clean-architecture-kotlin/af1fe7b55f7585e1406d93a02b7d336a0b81ad45/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lajesh/clean-architecture-kotlin/af1fe7b55f7585e1406d93a02b7d336a0b81ad45/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lajesh/clean-architecture-kotlin/af1fe7b55f7585e1406d93a02b7d336a0b81ad45/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lajesh/clean-architecture-kotlin/af1fe7b55f7585e1406d93a02b7d336a0b81ad45/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CleanArchApp 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/test/java/com/template/cleanarch/LoginViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import com.template.cleanarch.base.BaseTest 5 | import com.template.domain.usecases.auth.IAuthUseCase 6 | import com.template.feature_onboarding.view.fragment.signin.LoginViewModel 7 | import junit.framework.Assert.assertTrue 8 | import kotlinx.coroutines.ExperimentalCoroutinesApi 9 | import kotlinx.coroutines.test.runBlockingTest 10 | import okhttp3.mockwebserver.MockWebServer 11 | import org.junit.Assert 12 | import org.junit.Rule 13 | import org.junit.Test 14 | import org.junit.runner.RunWith 15 | import org.junit.runners.JUnit4 16 | import org.koin.test.inject 17 | import java.net.HttpURLConnection 18 | 19 | /**** 20 | * Unit test for LogiViewModel 21 | * Author: Lajesh Dineshkumar 22 | * Company: 23 | * Created on: 3/1/21 24 | * Modified on: 3/1/21 25 | *****/ 26 | @RunWith(JUnit4::class) 27 | @ExperimentalCoroutinesApi 28 | class LoginViewModelTest : BaseTest() { 29 | private lateinit var loginViewModel: LoginViewModel 30 | 31 | //Inject api service created with koin 32 | val authUseCase: IAuthUseCase by inject() 33 | 34 | //Inject Mockwebserver created with koin 35 | val mockWebServer: MockWebServer by inject() 36 | 37 | @get:Rule 38 | var instantExecutorRule = InstantTaskExecutorRule() 39 | 40 | override fun setUp() { 41 | super.setUp() 42 | loginViewModel = LoginViewModel(authUseCase, mainCoroutineRule.testDispatcher) 43 | } 44 | 45 | @Test 46 | @ExperimentalCoroutinesApi 47 | fun test_login_success_case() { 48 | mainCoroutineRule.testDispatcher.runBlockingTest { 49 | mockNetworkResponseWithFileContent("login_success.json", HttpURLConnection.HTTP_OK) 50 | loginViewModel.login() 51 | loginViewModel.addLikeCount() 52 | Assert.assertEquals(1, loginViewModel.getLikeCount()) 53 | Assert.assertEquals(false, loginViewModel.data.value) 54 | testCoroutineScope.advanceTimeBy(2000) 55 | 56 | loginViewModel.errorEvent.observeForever { 57 | assertTrue(it.errorCode == 503) 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/test/java/com/template/cleanarch/base/BaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch.base 2 | 3 | import com.template.cleanarch.di.configureTestAppComponent 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.test.TestCoroutineDispatcher 7 | import kotlinx.coroutines.test.TestCoroutineScope 8 | import kotlinx.coroutines.test.resetMain 9 | import kotlinx.coroutines.test.setMain 10 | import okhttp3.mockwebserver.MockResponse 11 | import okhttp3.mockwebserver.MockWebServer 12 | import org.junit.After 13 | import org.junit.Before 14 | import org.junit.Rule 15 | import org.koin.core.context.startKoin 16 | import org.koin.core.context.stopKoin 17 | import org.koin.test.KoinTest 18 | import java.io.File 19 | 20 | /**** 21 | * Base class for all the unit tests 22 | * Author: Lajesh Dineshkumar 23 | * Company: 24 | * Created on: 2/22/21 25 | * Modified on: 2/22/21 26 | *****/ 27 | @ExperimentalCoroutinesApi 28 | abstract class BaseTest : KoinTest { 29 | /** 30 | * For MockWebServer instance 31 | */ 32 | private lateinit var mMockServerInstance: MockWebServer 33 | 34 | /** 35 | * Default, let server be shut down 36 | */ 37 | private var mShouldStart = false 38 | 39 | val dispatcher = TestCoroutineDispatcher() 40 | 41 | val testCoroutineScope = TestCoroutineScope(dispatcher) 42 | 43 | @get:Rule 44 | var mainCoroutineRule = MainCoroutineRule() 45 | 46 | @Before 47 | open fun setUp(){ 48 | startMockServer(true) 49 | startKoin{ modules(configureTestAppComponent(getMockWebServerUrl()))} 50 | Dispatchers.setMain(dispatcher) 51 | } 52 | 53 | /** 54 | * Helps to read input file returns the respective data in mocked call 55 | */ 56 | fun mockNetworkResponseWithFileContent(fileName: String, responseCode: Int) = mMockServerInstance.enqueue( 57 | MockResponse() 58 | .setResponseCode(responseCode) 59 | .setBody(getJson(fileName))) 60 | 61 | /** 62 | * Reads input file and converts to json 63 | */ 64 | fun getJson(path : String) : String { 65 | val uri = javaClass.classLoader!!.getResource(path) 66 | val file = File(uri.path) 67 | return String(file.readBytes()) 68 | } 69 | 70 | /** 71 | * Start Mockwebserver 72 | */ 73 | private fun startMockServer(shouldStart:Boolean){ 74 | if (shouldStart){ 75 | mShouldStart = shouldStart 76 | mMockServerInstance = MockWebServer() 77 | mMockServerInstance.start() 78 | } 79 | } 80 | 81 | /** 82 | * Set Mockwebserver url 83 | */ 84 | fun getMockWebServerUrl() = mMockServerInstance.url("/").toString() 85 | 86 | /** 87 | * Stop Mockwebserver 88 | */ 89 | private fun stopMockServer() { 90 | if (mShouldStart){ 91 | mMockServerInstance.shutdown() 92 | } 93 | } 94 | 95 | @After 96 | open fun tearDown(){ 97 | //Stop Mock server 98 | stopMockServer() 99 | //Stop Koin as well 100 | stopKoin() 101 | 102 | Dispatchers.resetMain() 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /app/src/test/java/com/template/cleanarch/base/MainCoroutineRule.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch.base 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.TestCoroutineDispatcher 6 | import kotlinx.coroutines.test.resetMain 7 | import kotlinx.coroutines.test.setMain 8 | import org.junit.rules.TestWatcher 9 | import org.junit.runner.Description 10 | 11 | /**** 12 | * File Description 13 | * Author: Lajesh Dineshkumar 14 | 15 | * Created on: 3/1/21 16 | * Modified on: 3/1/21 17 | *****/ 18 | @ExperimentalCoroutinesApi 19 | class MainCoroutineRule( 20 | val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() 21 | ) : TestWatcher() { 22 | 23 | override fun starting(description: Description?) { 24 | super.starting(description) 25 | Dispatchers.setMain(testDispatcher) 26 | } 27 | 28 | override fun finished(description: Description?) { 29 | super.finished(description) 30 | Dispatchers.resetMain() 31 | testDispatcher.cleanupTestCoroutines() 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/test/java/com/template/cleanarch/di/MainDIComponent.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch.di 2 | 3 | import com.template.data.di.ApiModule 4 | import com.template.data.di.RepositoryModule 5 | import com.template.domain.di.UseCaseModule 6 | 7 | /**** 8 | * Main Koin DI component which helps to configure 9 | * Mockwebserver, Usecase and repository 10 | * Author: Lajesh Dineshkumar 11 | * Company: 12 | * Created on: 2/22/21 13 | * Modified on: 2/22/21 14 | *****/ 15 | 16 | fun configureTestAppComponent(baseApi: String) = listOf( 17 | mockwebserverDITest, 18 | configureNetworkModuleForTest(baseApi), 19 | ApiModule.apiModules, 20 | UseCaseModule.authUSeCaseModules, 21 | RepositoryModule.repositoryModules 22 | ) -------------------------------------------------------------------------------- /app/src/test/java/com/template/cleanarch/di/MockWebServerDITest.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch.di 2 | 3 | import okhttp3.mockwebserver.MockWebServer 4 | import org.koin.dsl.module 5 | 6 | /**** 7 | * Creates Mockwebserver instance for testing 8 | * Author: Lajesh Dineshkumar 9 | * Company: 10 | * Created on: 2/22/21 11 | * Modified on: 2/22/21 12 | *****/ 13 | 14 | val mockwebserverDITest = module { 15 | factory { 16 | MockWebServer() 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/test/java/com/template/cleanarch/di/NetworkDITest.kt: -------------------------------------------------------------------------------- 1 | package com.template.cleanarch.di 2 | 3 | import com.google.gson.GsonBuilder 4 | import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory 5 | import com.template.data.datasource.remote.api.IAuthApi 6 | import org.koin.core.qualifier.named 7 | import org.koin.dsl.module 8 | import retrofit2.Retrofit 9 | import retrofit2.converter.gson.GsonConverterFactory 10 | 11 | /**** 12 | * Network module test configuration with mockserver url 13 | * Author: Lajesh Dineshkumar 14 | * Company: 15 | * Created on: 2/22/21 16 | * Modified on: 2/22/21 17 | *****/ 18 | fun configureNetworkModuleForTest(baseApi: String) = module { 19 | single { 20 | Retrofit.Builder() 21 | .baseUrl(baseApi) 22 | .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create())) 23 | .addCallAdapterFactory(CoroutineCallAdapterFactory()) 24 | .build() 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /app/src/test/resources/login_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": 1, 3 | "data" : { 4 | "name": "Lajesh" 5 | } 6 | } -------------------------------------------------------------------------------- /benchmark/benchmark-proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -dontobfuscate 24 | 25 | -ignorewarnings 26 | 27 | -keepattributes *Annotation* 28 | 29 | -dontnote junit.framework.** 30 | -dontnote junit.runner.** 31 | 32 | -dontwarn androidx.test.** 33 | -dontwarn org.junit.** 34 | -dontwarn org.hamcrest.** 35 | -dontwarn com.squareup.javawriter.JavaWriter 36 | 37 | -keepclasseswithmembers @org.junit.runner.RunWith public class * -------------------------------------------------------------------------------- /benchmark/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'androidx.benchmark' 4 | id 'kotlin-android' 5 | } 6 | 7 | android { 8 | compileSdkVersion rootProject.compileSdkVersion 9 | buildToolsVersion rootProject.buildToolsVersion 10 | 11 | compileOptions { 12 | sourceCompatibility = 1.8 13 | targetCompatibility = 1.8 14 | } 15 | 16 | kotlinOptions { 17 | jvmTarget = "1.8" 18 | } 19 | 20 | defaultConfig { 21 | minSdkVersion rootProject.minSdkVersion 22 | targetSdkVersion rootProject.targetSdkVersion 23 | versionCode 1 24 | versionName "1.0" 25 | 26 | testInstrumentationRunner 'androidx.benchmark.junit4.AndroidBenchmarkRunner' 27 | } 28 | 29 | testBuildType = "release" 30 | buildTypes { 31 | debug { 32 | // Since debuggable can"t be modified by gradle for library modules, 33 | // it must be done in a manifest - see src/androidTest/AndroidManifest.xml 34 | minifyEnabled true 35 | proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro" 36 | } 37 | release { 38 | isDefault = true 39 | } 40 | } 41 | } 42 | 43 | dependencies { 44 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 45 | androidTestImplementation "androidx.test:runner:$testRunnerVersion" 46 | androidTestImplementation "androidx.test.ext:junit:$junitExtVersion" 47 | androidTestImplementation "junit:junit:$junitVersion" 48 | androidTestImplementation "androidx.benchmark:benchmark-junit4:$benchmarkVersion" 49 | 50 | // Add your dependencies here. Note that you cannot benchmark code 51 | // in an app module this way - you will need to move any code you 52 | // want to benchmark to a library module: 53 | // https://developer.android.com/studio/projects/android-library#Convert 54 | 55 | } -------------------------------------------------------------------------------- /benchmark/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 16 | -------------------------------------------------------------------------------- /benchmark/src/androidTest/java/com/template/benchmark/ExampleBenchmark.kt: -------------------------------------------------------------------------------- 1 | package com.template.benchmark 2 | 3 | import android.util.Log 4 | import androidx.benchmark.junit4.BenchmarkRule 5 | import androidx.benchmark.junit4.measureRepeated 6 | import androidx.test.ext.junit.runners.AndroidJUnit4 7 | import org.junit.Rule 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | 11 | /** 12 | * Benchmark, which will execute on an Android device. 13 | * 14 | * The body of [BenchmarkRule.measureRepeated] is measured in a loop, and Studio will 15 | * output the result. Modify your code to see how it affects performance. 16 | */ 17 | @RunWith(AndroidJUnit4::class) 18 | class ExampleBenchmark { 19 | 20 | @get:Rule 21 | val benchmarkRule = BenchmarkRule() 22 | 23 | @Test 24 | fun log() { 25 | benchmarkRule.measureRepeated { 26 | Log.d("LogBenchmark", "the cost of writing this log method will be measured") 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /benchmark/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = "1.4.21" 4 | ext{ 5 | minSdkVersion = 25 6 | targetSdkVersion = 30 7 | compileSdkVersion = 30 8 | buildToolsVersion = '30.0.2' 9 | coreKtxVersion = '1.3.2' 10 | appCompatVersion = '1.2.0' 11 | materialVersion = '1.2.1' 12 | constraintLayoutVersion = '2.0.4' 13 | junitVersion = '4.13.1' 14 | junitExtVersion = '1.1.2' 15 | testXRulesVersion = '1.3.0' 16 | coreTestingVersion = '1.1.1' 17 | retrofitMockVersion = '2.3.0' 18 | mockVersion = '1.9.3' 19 | espressoVersion = '3.3.0' 20 | espressoCoreVersion = '3.3.0' 21 | mockWebserverVersion = '4.1.0' 22 | benchmarkVersion = '1.0.0' 23 | testRunnerVersion = '1.3.0' 24 | lifecycleVersion = '2.2.0' 25 | koinVersion = '2.1.6' 26 | retrofitVersion = '2.9.0' 27 | okioVersion = '2.8.0' 28 | okhttpLoggingVersion = '4.9.0' 29 | coroutineVersion = '1.3.8' 30 | roomVersion = '2.2.6' 31 | timberVersion = '4.7.1' 32 | urlConnectionVersion = '3.11.0' 33 | timberVersion = '4.7.1' 34 | calligrapyVersion = '3.1.1' 35 | viewpumpVersion = '2.0.3' 36 | navVersion = "2.3.3" 37 | lifecycleVersion = '2.2.0' 38 | analyticsVersion = '26.4.0' 39 | coroutineAdapterVersion = '0.9.2' 40 | 41 | } 42 | repositories { 43 | google() 44 | jcenter() 45 | maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' } 46 | } 47 | dependencies { 48 | classpath "com.android.tools.build:gradle:4.1.2" 49 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 50 | classpath 'androidx.benchmark:benchmark-gradle-plugin:1.0.0' 51 | classpath "org.koin:koin-gradle-plugin:$koinVersion" 52 | classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion" 53 | 54 | } 55 | } 56 | 57 | allprojects { 58 | repositories { 59 | google() 60 | jcenter() 61 | } 62 | } 63 | 64 | task clean(type: Delete) { 65 | delete rootProject.buildDir 66 | } -------------------------------------------------------------------------------- /commons.features.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | // Keep all dependencies here 3 | implementation project(':core') 4 | implementation project(':domain') 5 | implementation project(':data') 6 | 7 | // Koin AndroidX Scope features 8 | implementation "org.koin:koin-androidx-scope:$koinVersion" 9 | implementation "org.koin:koin-androidx-viewmodel:$koinVersion" 10 | implementation "org.koin:koin-androidx-fragment:$koinVersion" 11 | implementation "org.koin:koin-androidx-ext:$koinVersion" 12 | implementation "org.koin:koin-androidx-scope:$koinVersion" 13 | implementation "org.koin:koin-androidx-viewmodel:$koinVersion" 14 | implementation "org.koin:koin-core:$koinVersion" 15 | 16 | // Lifecycle 17 | implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion" 18 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" 19 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" 20 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" 21 | 22 | // Navigation Jetpack 23 | implementation "androidx.navigation:navigation-fragment-ktx:$navVersion" 24 | implementation "androidx.navigation:navigation-ui-ktx:$navVersion" 25 | 26 | implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion" 27 | } -------------------------------------------------------------------------------- /commons.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 3 | implementation "androidx.core:core-ktx:$coreKtxVersion" 4 | implementation "androidx.appcompat:appcompat:$appCompatVersion" 5 | implementation "com.google.android.material:material:$materialVersion" 6 | testImplementation "junit:junit:$junitVersion" 7 | androidTestImplementation "androidx.test.espresso:espresso-core:$espressoCoreVersion" 8 | 9 | androidTestImplementation "androidx.test.ext:junit:$junitExtVersion" 10 | androidTestImplementation "androidx.test:rules:$testXRulesVersion" 11 | androidTestImplementation "android.arch.core:core-testing:$coreTestingVersion" 12 | androidTestImplementation "com.squareup.okhttp3:mockwebserver:$mockWebserverVersion" 13 | androidTestImplementation "org.koin:koin-test:$koinVersion" 14 | androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion" 15 | 16 | testImplementation "androidx.test.ext:junit:$junitExtVersion" 17 | testImplementation "android.arch.core:core-testing:$coreTestingVersion" 18 | testImplementation "com.squareup.okhttp3:mockwebserver:$mockWebserverVersion" 19 | testImplementation "org.koin:koin-test:$koinVersion" 20 | testImplementation "io.mockk:mockk:$mockVersion" 21 | } -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | id 'androidx.navigation.safeargs' 6 | } 7 | apply from: "$rootProject.projectDir/commons.gradle" 8 | 9 | android { 10 | compileSdkVersion 30 11 | buildToolsVersion "30.0.2" 12 | 13 | defaultConfig { 14 | minSdkVersion 25 15 | targetSdkVersion 30 16 | versionCode 1 17 | versionName "1.0" 18 | 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | consumerProguardFiles "consumer-rules.pro" 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | kotlinOptions { 34 | jvmTarget = '1.8' 35 | } 36 | 37 | buildFeatures { 38 | dataBinding true 39 | } 40 | 41 | flavorDimensions "server" 42 | 43 | productFlavors { 44 | 45 | dev { 46 | dimension "server" 47 | buildConfigField "Boolean", "IS_PINNING_ENABLED", 'false' 48 | } 49 | 50 | uat { 51 | dimension "server" 52 | buildConfigField "Boolean", "IS_PINNING_ENABLED", 'false' 53 | } 54 | 55 | pilot { 56 | dimension "server" 57 | buildConfigField "Boolean", "IS_PINNING_ENABLED", 'false' 58 | } 59 | 60 | prod { 61 | dimension "server" 62 | buildConfigField "Boolean", "IS_PINNING_ENABLED", 'false' 63 | } 64 | } 65 | 66 | publishNonDefault true 67 | 68 | configurations { 69 | devDebugImplementation 70 | devReleaseImplementation 71 | uatDebugImplementation 72 | uatReleaseImplementation 73 | pilotDebugImplementation 74 | pilotReleaseImplementation 75 | prodDebugImplementation 76 | prodReleaseImplementation 77 | } 78 | 79 | } 80 | 81 | dependencies { 82 | 83 | api project(':domain') 84 | api project(':data') 85 | implementation project(':thirdpartys:analyticslib') 86 | 87 | // Koin AndroidX Scope features 88 | implementation "org.koin:koin-androidx-scope:$koinVersion" 89 | implementation "org.koin:koin-androidx-viewmodel:$koinVersion" 90 | implementation "org.koin:koin-androidx-fragment:$koinVersion" 91 | implementation "org.koin:koin-androidx-ext:$koinVersion" 92 | implementation "org.koin:koin-androidx-scope:$koinVersion" 93 | implementation "org.koin:koin-androidx-viewmodel:$koinVersion" 94 | implementation "org.koin:koin-core:$koinVersion" 95 | 96 | //Retrofit 97 | implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" 98 | implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" 99 | implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion" 100 | implementation "com.squareup.okhttp3:logging-interceptor:$okhttpLoggingVersion" 101 | implementation "com.squareup.okio:okio:$okioVersion" 102 | implementation "com.squareup.okhttp3:okhttp-urlconnection:$urlConnectionVersion" 103 | 104 | 105 | //Coroutine 106 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" 107 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" 108 | implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$coroutineAdapterVersion" 109 | 110 | //Calligraphy 111 | implementation "io.github.inflationx:calligraphy3:$calligrapyVersion" 112 | implementation "io.github.inflationx:viewpump:$viewpumpVersion" 113 | 114 | // Timber for Logging 115 | implementation "com.jakewharton.timber:timber:$timberVersion" 116 | 117 | // Lifecycle 118 | implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion" 119 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" 120 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" 121 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" 122 | 123 | 124 | // Navigation Jetpack 125 | implementation "androidx.navigation:navigation-fragment-ktx:$navVersion" 126 | implementation "androidx.navigation:navigation-ui-ktx:$navVersion" 127 | } 128 | repositories { 129 | maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' } 130 | mavenCentral() 131 | } -------------------------------------------------------------------------------- /core/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lajesh/clean-architecture-kotlin/af1fe7b55f7585e1406d93a02b7d336a0b81ad45/core/consumer-rules.pro -------------------------------------------------------------------------------- /core/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /core/src/androidTest/java/com/template/core/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.template.core 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.template.common.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/arc/SingleLiveEvent.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.arc 2 | 3 | import android.util.Log 4 | import androidx.annotation.MainThread 5 | import androidx.lifecycle.LifecycleOwner 6 | import androidx.lifecycle.MutableLiveData 7 | import androidx.lifecycle.Observer 8 | import timber.log.Timber 9 | import java.util.concurrent.atomic.AtomicBoolean 10 | 11 | /**** 12 | * A lifecycle-aware observable that sends only new updates after subscription, used for events like 13 | * navigation and SnackBar messages. 14 | * This avoids a common problem with events: on configuration change (like rotation) an update 15 | * can be emitted if the observer is active. This LiveData only calls the observable if there's an 16 | * explicit call to setValue() or call(). 17 | * Note that only one observer is going to be notified of changes. 18 | * Author: Lajesh Dineshkumar 19 | * Company: 20 | * Created on: 2020-03-03 21 | * Modified on: 2020-03-03 22 | *****/ 23 | open class SingleLiveEvent : MutableLiveData() { 24 | 25 | private val mPending = AtomicBoolean(false) 26 | 27 | @MainThread 28 | override fun observe(owner: LifecycleOwner, observer: Observer) { 29 | 30 | if (hasActiveObservers()) { 31 | Timber.log(Log.ASSERT, "Multiple observers registered but only one will be notified of changes.") 32 | } 33 | 34 | // Observe the internal MutableLiveData 35 | super.observe( 36 | owner, 37 | Observer { t -> 38 | if (mPending.compareAndSet(true, false)) { 39 | observer.onChanged(t) 40 | } 41 | } 42 | ) 43 | } 44 | 45 | @MainThread 46 | override fun setValue(t: T?) { 47 | mPending.set(true) 48 | super.setValue(t) 49 | } 50 | 51 | /** 52 | * Used for cases where T is Void, to make calls cleaner. 53 | */ 54 | @MainThread 55 | fun call() { 56 | value = null 57 | } 58 | 59 | companion object { 60 | 61 | private val TAG = "SingleLiveEvent" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/common/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.common 2 | 3 | /**** 4 | * Keep all the common constants here 5 | * Author: Lajesh Dineshkumar 6 | * Company: 7 | * Created on: 2/2/21 8 | * Modified on: 2/2/21 9 | *****/ 10 | object Constants { 11 | const val LANG_CODE_ENGLISH = "en" 12 | const val LANG_CODE_ARABIC = "ar" 13 | const val LANGUAGE_SWITCH = "language_switch" 14 | } -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/common/SharedPrefConstants.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.common 2 | 3 | /**** 4 | * Keep all shared preference constants here 5 | * Author: Lajesh Dineshkumar 6 | * Company: 7 | * Created on: 2020-03-02 8 | * Modified on: 2020-03-02 9 | *****/ 10 | object SharedPrefConstants { 11 | const val APP_PREFS = "app_preferences" 12 | const val TOKEN = "session_token" 13 | const val PREFERENCE_LANGUAGE = "pref_language" 14 | const val PREF_FCM_TOKEN = "prefFcmToken" 15 | const val SELECTED_LANGUAGE = "key_language" 16 | const val USER_ID: String = "user_session" 17 | const val PREF_USER_HASH = "pref_user_hash" 18 | const val PREF_USER_NAME = "pref_user_name" 19 | const val IS_USER_LOGGED = "IS_USER_LOGGED" 20 | const val KEY_CONTAINER_SESSION = "kcs" 21 | const val KEY_SESSION_START_TIME = "ksst" 22 | const val WALLET_ONBOARDING_COMPLETED = "wallet_onboarding_completed" 23 | const val DID_FAVORITE_OFFER = "did_favorite_offer" 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/config/Configuration.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.config 2 | 3 | import com.template.core.BuildConfig 4 | 5 | /**** 6 | * Keep all the common configurations here 7 | * Author: Lajesh Dineshkumar 8 | * Company: 9 | * Created on: 2020-03-29 10 | * Modified on: 2020-03-29 11 | *****/ 12 | object Configuration { 13 | // Deployment Types 14 | private const val DEV = "dev" 15 | private const val SIT = "sit" 16 | private const val UAT = "uat" 17 | private const val PROD = "prod" 18 | private const val PILOT = "pilot" 19 | 20 | // Host URLs 21 | private const val DEV_URL = "http://demo4597733.mockable.io/" 22 | private const val SIT_URL = "http://demo4597733.mockable.io/sit/" 23 | private const val UAT_URL = "http://demo4597733.mockable.io/uat/" 24 | private const val PROD_URL = "http://demo4597733.mockable.io/prod/" 25 | private const val PILOT_URL = "http://demo4597733.mockable.io/pilot/" 26 | 27 | const val CONNECT_TIMEOUT: Long = 60 28 | const val READ_TIMEOUT: Long = 60 29 | const val CALL_TIMEOUT: Long = 60 30 | 31 | 32 | val baseURL: String 33 | get() { 34 | 35 | return when (BuildConfig.FLAVOR) { 36 | 37 | DEV -> DEV_URL 38 | 39 | SIT -> SIT_URL 40 | 41 | UAT -> UAT_URL 42 | 43 | PROD -> PROD_URL 44 | 45 | PILOT -> PILOT_URL 46 | 47 | else -> DEV_URL 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/contract/SubscriptionContract.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.contract 2 | 3 | /**** 4 | * Subscription contract. Wherever you want to subcribe for the network response 5 | * or navigation event, implement this interface 6 | * Author: Lajesh Dineshkumar 7 | * Company: 8 | * Created on: 2020-03-03 9 | * Modified on: 2020-03-03 10 | *****/ 11 | interface SubscriptionContract { 12 | fun subscribeNetworkResponse() { 13 | // Implementation goes in the owner 14 | } 15 | fun subscribeNavigationEvent() { 16 | // Implementation goes in the owner 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/di/Injectable.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.di 2 | 3 | /** 4 | * Makes as activity/fragmenty injectable 5 | * Created by Lajesh Dineshkumar on 10/30/2019. 6 | * Company: 7 | * Email: lajeshds2007@gmail.com 8 | */ 9 | interface Injectable 10 | -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/di/module/application/OkhttpModule.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.di.module.application 2 | 3 | import com.template.core.config.Configuration 4 | import com.template.core.interceptors.HeaderInterceptor 5 | import okhttp3.OkHttpClient 6 | import okhttp3.* 7 | import okhttp3.logging.HttpLoggingInterceptor 8 | import org.koin.core.context.loadKoinModules 9 | import org.koin.dsl.module 10 | import java.net.CookieManager 11 | import java.util.concurrent.TimeUnit 12 | 13 | import com.template.core.BuildConfig 14 | 15 | /**** 16 | * OKhttp Module 17 | * Author: Lajesh Dineshkumar 18 | * Company: 19 | * Created on: 2/2/21 20 | * Modified on: 2/2/21 21 | *****/ 22 | object OkhttpModule { 23 | fun load() { 24 | loadKoinModules(module { 25 | single { 26 | val cookieHandler = CookieManager() 27 | val okHttpBuilder = OkHttpClient() 28 | .newBuilder() 29 | .addInterceptor(get()) 30 | .addInterceptor(get()) 31 | .cookieJar(JavaNetCookieJar(cookieHandler)) 32 | .connectTimeout(Configuration.CONNECT_TIMEOUT, TimeUnit.SECONDS) 33 | .callTimeout(Configuration.CALL_TIMEOUT, TimeUnit.SECONDS) 34 | .readTimeout(Configuration.READ_TIMEOUT, TimeUnit.SECONDS) 35 | 36 | okHttpBuilder.build() 37 | } 38 | 39 | single { 40 | HeaderInterceptor() 41 | } 42 | 43 | single { 44 | val httpLoggingInterceptor = HttpLoggingInterceptor() 45 | if (BuildConfig.DEBUG) 46 | httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY 47 | else 48 | httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.NONE 49 | 50 | httpLoggingInterceptor 51 | } 52 | }) 53 | } 54 | } -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/di/module/application/RetrofitModule.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.di.module.application 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.GsonBuilder 5 | import com.template.core.config.Configuration 6 | import org.koin.core.context.loadKoinModules 7 | import org.koin.dsl.module 8 | import retrofit2.Retrofit 9 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 10 | import retrofit2.converter.gson.GsonConverterFactory 11 | 12 | /**** 13 | * Retrofit Module 14 | * Author: Lajesh Dineshkumar 15 | * Company: 16 | * Created on: 2/2/21 17 | * Modified on: 2/2/21 18 | *****/ 19 | object RetrofitModule { 20 | fun load() { 21 | loadKoinModules(retrofitModules) 22 | } 23 | 24 | private val retrofitModules = module { 25 | single { 26 | Retrofit.Builder() 27 | .client(get()) 28 | .addConverterFactory(getGsonConverterFactory()) 29 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()).baseUrl(get()) 30 | .build() as Retrofit 31 | } 32 | 33 | single { 34 | Configuration.baseURL 35 | } 36 | } 37 | 38 | private fun getGsonConverterFactory(): GsonConverterFactory { 39 | return GsonConverterFactory.create(getGson()) 40 | } 41 | 42 | private fun getGson(): Gson { 43 | val gsonBuilder = GsonBuilder() 44 | gsonBuilder.setDateFormat("yyyy-MM-dd'T'HH:mm:ss") 45 | return gsonBuilder.create() 46 | } 47 | } -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/interceptors/HeaderInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.interceptors 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import android.util.Base64 6 | import okhttp3.Interceptor 7 | import okhttp3.Request 8 | import okhttp3.Response 9 | import java.io.IOException 10 | import java.net.SocketTimeoutException 11 | 12 | /**** 13 | * File Description 14 | * Author: Lajesh Dineshkumar 15 | * Company: 16 | * Created on: 2/2/21 17 | * Modified on: 2/2/21 18 | *****/ 19 | class HeaderInterceptor : okhttp3.Interceptor { 20 | 21 | override fun intercept(chain: Interceptor.Chain): Response { 22 | var response: Response? 23 | var requestBuilder: Request.Builder = chain.request().newBuilder() 24 | requestBuilder = addCommonHeaders(requestBuilder) 25 | val request = requestBuilder.build() 26 | try { 27 | response = chain.proceed(request) 28 | response.takeIf { isHtmlResponse(it) }?.apply { 29 | // throw custom exception 30 | } 31 | } catch (e: Exception) { 32 | throw e 33 | } 34 | return response 35 | } 36 | 37 | /** 38 | * Add the common set of headers 39 | */ 40 | private fun addCommonHeaders(requestBuilder: Request.Builder): Request.Builder { 41 | // Add all the common headers here 42 | 43 | return requestBuilder 44 | } 45 | 46 | private fun isHtmlResponse(response: Response): Boolean { 47 | return response.headers.names().contains("Content-Type") 48 | && response.headers["Content-Type"]?.contains("text/html") == true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/listeners/BackButtonHandlerListener.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.listeners 2 | 3 | /** 4 | * Back button handler interface. Add/remove listener functionality 5 | * Created by Lajesh Dineshkumar on 10/31/2019. 6 | * Company: 7 | * Email: lajeshds2007@gmail.com 8 | */ 9 | interface BackButtonHandlerListener { 10 | fun addBackpressListener(listner: BackPressListener) 11 | fun removeBackpressListener(listner: BackPressListener) 12 | } -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/listeners/BackPressListner.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.listeners 2 | 3 | /** 4 | * Back press listener for handling back navigation in activity/fragments 5 | * Author: Lajesh Dineshkumar 6 | * Company: 7 | * Created on: 7/7/20 8 | * Modified on: 7/7/20 9 | */ 10 | interface BackPressListener { 11 | fun onBackPress(): Boolean 12 | } -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/ui/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.ui.base 2 | 3 | import android.os.Bundle 4 | import android.view.WindowManager 5 | import androidx.annotation.LayoutRes 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.databinding.ViewDataBinding 9 | import androidx.lifecycle.ViewModel 10 | import com.template.core.listeners.BackButtonHandlerListener 11 | import com.template.core.listeners.BackPressListener 12 | import com.template.core.utils.LocaleManager 13 | import com.template.core.viewmodel.ToolbarPropertyViewModel 14 | import com.template.core.viewmodel.base.BaseViewModel 15 | import org.koin.androidx.viewmodel.ext.android.viewModel 16 | import java.lang.ref.WeakReference 17 | import java.util.ArrayList 18 | import kotlin.reflect.KClass 19 | 20 | /**** 21 | * All the activity should be extended from this parent class. 22 | * All the common functionalities across activities should be kept here 23 | * Author: Lajesh Dineshkumar 24 | * Company: 25 | * Created on: 2020-03-03 26 | * Modified on: 2020-03-03 27 | *****/ 28 | abstract class BaseActivity(clazz: KClass) : 29 | AppCompatActivity(), BackButtonHandlerListener { 30 | 31 | val viewModel: V by viewModel(clazz) 32 | 33 | private val toolbarModel: ToolbarPropertyViewModel by viewModel() 34 | 35 | lateinit var dataBinding: D 36 | 37 | @get:LayoutRes 38 | protected abstract val layoutRes: Int 39 | 40 | abstract val bindingVariable: Int 41 | 42 | protected abstract fun getViewModel(): Class 43 | 44 | private val backClickListenersList = ArrayList>() 45 | 46 | override fun onCreate(savedInstanceState: Bundle?) { 47 | super.onCreate(savedInstanceState) 48 | 49 | initializeDataBinding() 50 | (viewModel as? BaseViewModel)?.toolbarPropertyViewModel = toolbarModel 51 | 52 | } 53 | 54 | private fun initializeDataBinding(){ 55 | dataBinding = DataBindingUtil.setContentView(this, layoutRes) 56 | dataBinding.lifecycleOwner = this 57 | 58 | (viewModel as? BaseViewModel)?.toolbarPropertyViewModel = toolbarModel 59 | 60 | dataBinding.setVariable(bindingVariable, viewModel) 61 | dataBinding.executePendingBindings() 62 | } 63 | 64 | fun logout(showConfirm: Boolean? = false) { 65 | (viewModel as BaseViewModel).logoutClickEvent.value = showConfirm ?: false 66 | } 67 | 68 | /** 69 | * Showing progress bar over screen 70 | */ 71 | fun showLoading(it: Boolean?) { 72 | it?.let { disableTouch -> 73 | (viewModel as BaseViewModel).showLoading(it) 74 | if (disableTouch) { 75 | window.setFlags( 76 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, 77 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE 78 | ) 79 | } else { 80 | window.clearFlags( 81 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE 82 | ) 83 | } 84 | } 85 | } 86 | 87 | fun setUpLocale(lngCode: String, isSelected: Boolean = false) { 88 | val currentLanguage = LocaleManager.getCurrentLanguage(this) 89 | if (!currentLanguage.equals(lngCode, true)) { 90 | LocaleManager.setNewLocale(this, lngCode) 91 | if (currentLanguage != null || isSelected) { 92 | LocaleManager.notifyLanguageChange(this) 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * Methods which handles the hardware back button / navigation back view 99 | */ 100 | override fun onBackPressed() { 101 | if (!fragmentsBackKeyIntercept()) { 102 | super.onBackPressed() 103 | } 104 | } 105 | 106 | /** 107 | * Add the back navigation listener here. 108 | * Call this method from onAttach of your fragment 109 | * @param listner - back navigation listener 110 | */ 111 | override fun addBackpressListener(listner: BackPressListener) { 112 | backClickListenersList.add(WeakReference(listner)) 113 | } 114 | 115 | /** 116 | * remove the back navigation listener here. 117 | * Call this method from onDetach of your fragment 118 | * @param listner - back navigation listener 119 | */ 120 | override fun removeBackpressListener(listner: BackPressListener) { 121 | val iterator = backClickListenersList.iterator() 122 | while (iterator.hasNext()) { 123 | val weakRef = iterator.next() 124 | if (weakRef.get() === listner) { 125 | iterator.remove() 126 | } 127 | } 128 | } 129 | 130 | /** 131 | * This method checks if any frgament is overriding the back button behavior or not 132 | * @return true/false 133 | */ 134 | private fun fragmentsBackKeyIntercept(): Boolean { 135 | var isIntercept = false 136 | for (weakRef in backClickListenersList) { 137 | val backpressListner = weakRef.get() 138 | if (backpressListner != null) { 139 | val isFragmIntercept: Boolean = backpressListner.onBackPress() 140 | if (!isIntercept) 141 | isIntercept = isFragmIntercept 142 | } 143 | } 144 | return isIntercept 145 | } 146 | 147 | } -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/utils/NavigationCommand.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.utils 2 | 3 | import android.net.Uri 4 | import androidx.navigation.NavDirections 5 | 6 | /**** 7 | * The app will be using command pattern to handle navigation within the app 8 | * Author: Lajesh Dineshkumar 9 | * Company: 10 | * Created on: 2/3/21 11 | * Modified on: 2/3/21 12 | *****/ 13 | sealed class NavigationCommand { 14 | data class To(val directions: NavDirections): NavigationCommand() 15 | object Back: NavigationCommand() 16 | data class BackTo(val destinationId: Int): NavigationCommand() 17 | data class ToByDeepLink(val deepLink: Uri): NavigationCommand() 18 | } -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/utils/PreferenceUtil.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.utils 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | 6 | /**** 7 | * Shared Preference util class 8 | * ---------------------------- 9 | * How to use ? 10 | * val prefs = PreferenceUtil.defaultPrefs(this) 11 | * set any type of value in prefs 12 | * prefs[PREFUSERID] = "name" 13 | * ---------------------------- 14 | * Author: Lajesh Dineshkumar 15 | * Company: 16 | * Created on: 2020-03-02 17 | * Modified on: 2020-03-02 18 | *****/ 19 | object PreferenceUtil { 20 | 21 | fun customPrefs(context: Context, name: String): SharedPreferences = 22 | context.getSharedPreferences(name, Context.MODE_PRIVATE) 23 | 24 | private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) { 25 | val editor = this.edit() 26 | operation(editor) 27 | editor.apply() 28 | } 29 | 30 | /** 31 | * puts a key value pair in shared prefs if doesn't exists, otherwise updates value on given [key] 32 | */ 33 | operator fun SharedPreferences.set(key: String, value: Any?) { 34 | when (value) { 35 | is String? -> edit { it.putString(key, value) } 36 | is Int -> edit { it.putInt(key, value) } 37 | is Boolean -> edit { it.putBoolean(key, value) } 38 | is Float -> edit { it.putFloat(key, value) } 39 | is Long -> edit { it.putLong(key, value) } 40 | else -> throw UnsupportedOperationException("Not yet implemented") 41 | } 42 | } 43 | 44 | /** 45 | * finds value on given key. 46 | * [T] is the type of value 47 | * @param defaultValue optional default value - will take null for strings, false for bool and -1 for numeric values if [defaultValue] is not specified 48 | */ 49 | inline operator fun SharedPreferences.get(key: String, defaultValue: T? = null): T? { 50 | return when (T::class) { 51 | String::class -> getString(key, defaultValue as? String) as T? 52 | Int::class -> getInt(key, defaultValue as? Int ?: -1) as T? 53 | Boolean::class -> getBoolean(key, defaultValue as? Boolean ?: false) as T? 54 | Float::class -> getFloat(key, defaultValue as? Float ?: -1f) as T? 55 | Long::class -> getLong(key, defaultValue as? Long ?: -1) as T? 56 | else -> throw UnsupportedOperationException("Not yet implemented") 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/utils/SharedPrefHelper.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.utils 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import com.template.core.utils.PreferenceUtil.get 6 | import com.template.core.utils.PreferenceUtil.set 7 | import com.template.core.common.SharedPrefConstants 8 | import com.template.core.common.SharedPrefConstants.DID_FAVORITE_OFFER 9 | import com.template.core.common.SharedPrefConstants.WALLET_ONBOARDING_COMPLETED 10 | 11 | /**** 12 | * Keep all shared preference related methods here 13 | * Author: Lajesh Dineshkumar 14 | * Company: 15 | * Created on: 2020-03-02 16 | * Modified on: 2020-03-02 17 | *****/ 18 | class SharedPrefHelper constructor(context: Context) { 19 | 20 | private var sharedPreferences: SharedPreferences = 21 | PreferenceUtil.customPrefs(context, SharedPrefConstants.APP_PREFS) 22 | 23 | fun getToken(): String? { 24 | return sharedPreferences[SharedPrefConstants.TOKEN] 25 | } 26 | 27 | fun setToken(token: String?) { 28 | sharedPreferences[SharedPrefConstants.TOKEN] = token 29 | } 30 | 31 | fun saveFcmToken(token: String?) { 32 | sharedPreferences[SharedPrefConstants.PREF_FCM_TOKEN] = token 33 | } 34 | 35 | fun getFCMToken(): String? { 36 | return sharedPreferences[SharedPrefConstants.PREF_FCM_TOKEN] 37 | } 38 | 39 | fun isUserLogged(): Boolean { 40 | return getUserID() != 0 41 | } 42 | 43 | fun setWalletOnBoardingCompleted(completed: Boolean) { 44 | sharedPreferences[WALLET_ONBOARDING_COMPLETED] = completed 45 | } 46 | 47 | fun doFavoriteOffer() { 48 | sharedPreferences[DID_FAVORITE_OFFER] = true 49 | } 50 | 51 | fun didFavoriteOffer(): Boolean = sharedPreferences[DID_FAVORITE_OFFER] ?: false 52 | 53 | fun isWalletOnBoardingCompleted(): Boolean { 54 | return sharedPreferences[WALLET_ONBOARDING_COMPLETED] ?: false 55 | } 56 | 57 | private fun getUserID(): Int { 58 | return (sharedPreferences[SharedPrefConstants.USER_ID, "0"] ?: "0").toInt() 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/viewmodel/SharedViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | 5 | /**** 6 | * Shared view model for data sharing between fragments 7 | * Author: Lajesh Dineshkumar 8 | * Company: 9 | * Created on: 2/2/21 10 | * Modified on: 2/2/21 11 | *****/ 12 | open class SharedViewModel : ViewModel() { 13 | 14 | 15 | } -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/viewmodel/ToolbarPropertyViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.viewmodel 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import com.template.core.arc.SingleLiveEvent 6 | 7 | /**** 8 | * Tool bar Property ViewModel 9 | * Author: Lajesh Dineshkumar 10 | * Company: 11 | * Created on: 5/17/20 12 | * Modified on: 5/17/20 13 | *****/ 14 | class ToolbarPropertyViewModel : ViewModel() { 15 | var showBack = MutableLiveData(true) 16 | var showClose = MutableLiveData(false) 17 | var toolbarTitle = MutableLiveData() 18 | val closeButtonAction: SingleLiveEvent = SingleLiveEvent() 19 | val backButtonAction: SingleLiveEvent = SingleLiveEvent() 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/template/core/viewmodel/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.template.core.viewmodel.base 2 | 3 | import androidx.databinding.Observable 4 | import androidx.databinding.PropertyChangeRegistry 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import androidx.navigation.NavDirections 8 | import com.template.core.arc.SingleLiveEvent 9 | import com.template.core.utils.NavigationCommand 10 | import com.template.core.viewmodel.SharedViewModel 11 | import com.template.core.viewmodel.ToolbarPropertyViewModel 12 | import com.template.domain.entity.response.common.ErrorEntity 13 | 14 | /**** 15 | * Base view model. All the common implementation of viewmodel goes here 16 | * Author: Lajesh Dineshkumar 17 | * Company: 18 | * Created on: 2/2/21 19 | * Modified on: 2/2/21 20 | *****/ 21 | abstract class BaseViewModel : ViewModel(), Observable { 22 | 23 | /** 24 | * Live data to handle loading 25 | */ 26 | val loadingEvent = SingleLiveEvent() 27 | 28 | /** 29 | * Live data to handle error 30 | */ 31 | val errorEvent = SingleLiveEvent() 32 | 33 | /** 34 | * Set the Logout Click to true for showing confirmation before logout 35 | */ 36 | var logoutClickEvent = SingleLiveEvent() 37 | 38 | val navigationCommands = SingleLiveEvent() 39 | 40 | val hideKeyboard = MutableLiveData() 41 | 42 | 43 | lateinit var sharedViewModel: SharedViewModel 44 | 45 | 46 | private val callbacks = PropertyChangeRegistry() 47 | 48 | 49 | var toolbarPropertyViewModel: ToolbarPropertyViewModel = ToolbarPropertyViewModel() 50 | 51 | 52 | val isSharedViewModelInitialized: Boolean 53 | get() = this::sharedViewModel.isInitialized 54 | 55 | 56 | override fun addOnPropertyChangedCallback( 57 | callback: Observable.OnPropertyChangedCallback 58 | ) { 59 | callbacks.add(callback) 60 | } 61 | 62 | override fun removeOnPropertyChangedCallback( 63 | callback: Observable.OnPropertyChangedCallback 64 | ) { 65 | callbacks.remove(callback) 66 | } 67 | 68 | /** 69 | * Notifies observers that all properties of this instance have changed. 70 | */ 71 | internal fun notifyChange() { 72 | callbacks.notifyCallbacks(this, 0, null) 73 | } 74 | 75 | /** 76 | * Notifies observers that a specific property has changed. The getter for the 77 | * property that changes should be marked with the @Bindable annotation to 78 | * generate a field in the BR class to be used as the fieldId parameter. 79 | * 80 | * @param fieldId The generated BR id for the Bindable field. 81 | */ 82 | internal fun notifyPropertyChanged(fieldId: Int) { 83 | callbacks.notifyCallbacks(this, fieldId, null) 84 | } 85 | 86 | fun showLoading(show: Boolean) { 87 | loadingEvent.value = show 88 | } 89 | 90 | fun postLoading(show: Boolean) { 91 | loadingEvent.postValue(show) 92 | } 93 | 94 | fun getLoading(): SingleLiveEvent { 95 | return loadingEvent 96 | } 97 | 98 | /** 99 | * Method call to handle error 100 | */ 101 | fun setError(error: ErrorEntity.Error?) { 102 | errorEvent.value = error 103 | } 104 | 105 | fun navigate(directions: NavDirections) { 106 | navigationCommands.postValue(NavigationCommand.To(directions)) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /core/src/main/res/anim/anim_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /core/src/main/res/anim/anim_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /core/src/main/res/anim/anim_frag_fade_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /core/src/main/res/anim/anim_frag_fade_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /core/src/main/res/anim/anim_pop_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /core/src/main/res/anim/anim_pop_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /core/src/main/res/anim/anim_slide_in_up.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /core/src/main/res/anim/anim_slide_out_up.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /core/src/main/res/anim/pop_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /core/src/main/res/anim/pop_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /core/src/main/res/anim/right_in.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /core/src/main/res/anim/right_out.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /core/src/main/res/anim/slide_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /core/src/main/res/anim/slide_down_fade_out_anim.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 14 | -------------------------------------------------------------------------------- /core/src/main/res/anim/slide_down_half.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /core/src/main/res/anim/slide_from_top_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /core/src/main/res/anim/slide_in_from_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 25 | 26 | -------------------------------------------------------------------------------- /core/src/main/res/anim/slide_in_from_rigth.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 19 | 23 | 24 | -------------------------------------------------------------------------------- /core/src/main/res/anim/slide_left_fade_out_anim.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 14 | -------------------------------------------------------------------------------- /core/src/main/res/anim/slide_out_to_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 25 | 26 | -------------------------------------------------------------------------------- /core/src/main/res/anim/slide_out_to_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 25 | 26 | -------------------------------------------------------------------------------- /core/src/main/res/anim/slide_up.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /core/src/main/res/values/do_not_translate.xml: -------------------------------------------------------------------------------- 1 | 2 | CleanArchApp 3 | English 4 | عربى 5 | -------------------------------------------------------------------------------- /core/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Error 3 | OK 4 | 5 | Login 6 | Profile 7 | Splash 8 | Edit Profile 9 | -------------------------------------------------------------------------------- /core/src/test/java/com/template/core/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.template.core 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | } 6 | 7 | apply from: "$rootProject.projectDir/commons.gradle" 8 | 9 | android { 10 | compileSdkVersion rootProject.compileSdkVersion 11 | buildToolsVersion rootProject.buildToolsVersion 12 | 13 | defaultConfig { 14 | minSdkVersion rootProject.minSdkVersion 15 | targetSdkVersion rootProject.targetSdkVersion 16 | versionCode 1 17 | versionName "1.0" 18 | 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | consumerProguardFiles "consumer-rules.pro" 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | kotlinOptions { 34 | jvmTarget = '1.8' 35 | } 36 | } 37 | 38 | dependencies { 39 | 40 | implementation project(':domain') 41 | 42 | //Coroutine 43 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" 44 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" 45 | 46 | /* Android Architecture Component - Room Persistance Lib */ 47 | implementation "androidx.room:room-runtime:$roomVersion" 48 | kapt "androidx.room:room-compiler:$roomVersion" 49 | implementation "androidx.room:room-common:$roomVersion" 50 | implementation "androidx.room:room-ktx:$roomVersion" 51 | 52 | //Retrofit 53 | implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" 54 | implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" 55 | 56 | implementation "org.koin:koin-core:$koinVersion" 57 | implementation "org.koin:koin-android:$koinVersion" 58 | 59 | // Timber for Logging 60 | implementation "com.jakewharton.timber:timber:$timberVersion" 61 | } -------------------------------------------------------------------------------- /data/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lajesh/clean-architecture-kotlin/af1fe7b55f7585e1406d93a02b7d336a0b81ad45/data/consumer-rules.pro -------------------------------------------------------------------------------- /data/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /data/src/androidTest/java/com/template/data/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.template.data 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.template.data.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /data/src/main/java/com/template/data/constants/NetworkConstants.kt: -------------------------------------------------------------------------------- 1 | package com.template.data.constants 2 | 3 | import androidx.annotation.IntDef 4 | import androidx.annotation.StringDef 5 | 6 | /** 7 | * Keep all the network related constants here 8 | * Created by Lajesh Dineshkumar on 2020-03-09. 9 | * Company: 10 | * Email: lajeshds2007@gmail.com 11 | */ 12 | object NetworkConstants { 13 | @IntDef( 14 | NETWORK_ERROR_CODES.SERVICE_UNAVAILABLE, NETWORK_ERROR_CODES.MALFORMED_JSON, 15 | NETWORK_ERROR_CODES.NO_INTERNET, NETWORK_ERROR_CODES.UNEXPECTED_ERROR, 16 | NETWORK_ERROR_CODES.HTML_RESPONSE_ERROR 17 | ) 18 | @Retention(AnnotationRetention.SOURCE) 19 | annotation class NETWORK_ERROR_CODES { 20 | companion object { 21 | const val SERVICE_UNAVAILABLE = 500 22 | const val MALFORMED_JSON = 501 23 | const val NO_INTERNET = 502 24 | const val UNEXPECTED_ERROR = 503 25 | const val HTML_RESPONSE_ERROR = 504 26 | } 27 | } 28 | 29 | @StringDef( 30 | NETWORK_ERROR_MESSAGES.SERVICE_UNAVAILABLE, NETWORK_ERROR_MESSAGES.MALFORMED_JSON, 31 | NETWORK_ERROR_MESSAGES.NO_INTERNET, NETWORK_ERROR_MESSAGES.UNEXPECTED_ERROR 32 | ) 33 | @Retention(AnnotationRetention.SOURCE) 34 | annotation class NETWORK_ERROR_MESSAGES { 35 | companion object { 36 | const val SERVICE_UNAVAILABLE = "System temporarily unavailable, please try again later" 37 | const val MALFORMED_JSON = "Oops! We hit an error. Try again later." 38 | const val NO_INTERNET = "Oh! You are not connected to a wifi or cellular data network. Please connect and try again" 39 | const val UNEXPECTED_ERROR = "Something went wrong" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /data/src/main/java/com/template/data/datasource/local/dao/BaseDao.kt: -------------------------------------------------------------------------------- 1 | package com.template.data.datasource.local.dao 2 | 3 | import androidx.room.Insert 4 | import androidx.room.Query 5 | import androidx.room.Update 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | /**** 9 | * All the DAO should be extended from this base class. 10 | * Author: Lajesh Dineshkumar 11 | * Company: 12 | * Created on: 3/14/21 13 | * Modified on: 3/14/21 14 | *****/ 15 | interface BaseDao { 16 | 17 | /** 18 | * Insert an object in the database. 19 | * 20 | * @param obj the object to be inserted. 21 | */ 22 | @Insert 23 | suspend fun insert(obj: T): Long 24 | 25 | /** 26 | * Update an object from the database. 27 | * 28 | * @param obj the object to be updated 29 | */ 30 | @Update 31 | suspend fun update(obj: T): Void 32 | 33 | /** 34 | * Delete an object from the database 35 | * 36 | * @param obj the object to be deleted 37 | */ 38 | suspend fun delete(): Int 39 | 40 | } -------------------------------------------------------------------------------- /data/src/main/java/com/template/data/datasource/local/dao/BranchDao.kt: -------------------------------------------------------------------------------- 1 | package com.template.data.datasource.local.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.template.data.datasource.local.entity.Branch 8 | 9 | /**** 10 | * DAO for bank branches 11 | * Author: Lajesh Dineshkumar 12 | * Company: 13 | * Created on: 3/14/21 14 | * Modified on: 3/14/21 15 | *****/ 16 | @Dao 17 | interface BranchDao : BaseDao { 18 | 19 | @Query("SELECT * FROM branches") 20 | suspend fun getBranches(): List 21 | 22 | @Insert(onConflict = OnConflictStrategy.REPLACE) 23 | override suspend fun insert(obj: Branch): Long 24 | 25 | @Query("DELETE FROM branches") 26 | override suspend fun delete(): Int 27 | } -------------------------------------------------------------------------------- /data/src/main/java/com/template/data/datasource/local/database/AppDb.kt: -------------------------------------------------------------------------------- 1 | package com.template.data.datasource.local.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.template.data.datasource.local.dao.BranchDao 6 | import com.template.data.datasource.local.entity.Branch 7 | 8 | /**** 9 | * Application Database 10 | * Author: Lajesh Dineshkumar 11 | * Company: 12 | * Created on: 3/14/21 13 | * Modified on: 3/14/21 14 | *****/ 15 | @Database( 16 | entities = [Branch::class], 17 | version = 1, 18 | exportSchema = false 19 | ) 20 | abstract class AppDb : RoomDatabase() { 21 | abstract fun branchDao() : BranchDao 22 | } -------------------------------------------------------------------------------- /data/src/main/java/com/template/data/datasource/local/entity/Branch.kt: -------------------------------------------------------------------------------- 1 | package com.template.data.datasource.local.entity 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | /**** 7 | * Branch model 8 | * Author: Lajesh Dineshkumar 9 | * Company: 10 | * Created on: 3/14/21 11 | * Modified on: 3/14/21 12 | *****/ 13 | @Entity(tableName = "branches") 14 | data class Branch( 15 | @PrimaryKey(autoGenerate = true) 16 | val id: Int = 0, 17 | 18 | val branchName: String? = "", 19 | 20 | val branchAddress: String? = "" 21 | ) 22 | -------------------------------------------------------------------------------- /data/src/main/java/com/template/data/datasource/remote/api/IAuthApi.kt: -------------------------------------------------------------------------------- 1 | package com.template.data.datasource.remote.api 2 | 3 | import com.template.data.datasource.remote.dto.AuthDto 4 | import com.template.data.datasource.remote.dto.CommonDto 5 | import com.template.domain.entity.request.AuthRequest 6 | import kotlinx.coroutines.flow.Flow 7 | import retrofit2.Response 8 | import retrofit2.http.Body 9 | import retrofit2.http.POST 10 | 11 | /**** 12 | * API endpoint of Authentication related service calls 13 | * Author: Lajesh Dineshkumar 14 | * Company: 15 | * Created on: 2/1/21 16 | * Modified on: 2/1/21 17 | *****/ 18 | interface IAuthApi { 19 | 20 | @POST("login") 21 | suspend fun signIn(@Body signinRequest: AuthRequest.SigninRequest) : AuthDto.LoginResponse 22 | 23 | @POST("signup") 24 | suspend fun signUp(@Body signupRequest: AuthRequest.SignupRequest) : CommonDto.CommonResponse 25 | } -------------------------------------------------------------------------------- /data/src/main/java/com/template/data/datasource/remote/dto/AuthDto.kt: -------------------------------------------------------------------------------- 1 | package com.template.data.datasource.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /**** 6 | * Keep all the AUTH related DTOs here 7 | * Author: Lajesh Dineshkumar 8 | * Company: Farabi Technologies 9 | * Created on: 4/25/21 10 | * Modified on: 4/25/21 11 | *****/ 12 | sealed class AuthDto { 13 | 14 | data class LoginResponse( 15 | @SerializedName("userDetails") val userDetails: UserDetails? = null 16 | ) : AuthDto() 17 | 18 | data class UserDetails( 19 | @SerializedName("firstName") val firstName: String? = "", 20 | @SerializedName("lastName") val lastName: String? = "", 21 | @SerializedName("phoneNumber") val phoneNumber: String?= "", 22 | @SerializedName("countryCode") val countryCode: String? = "" 23 | ) : AuthDto() 24 | } -------------------------------------------------------------------------------- /data/src/main/java/com/template/data/datasource/remote/dto/CommonDto.kt: -------------------------------------------------------------------------------- 1 | package com.template.data.datasource.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /**** 6 | * File Description 7 | * Author: Lajesh Dineshkumar 8 | * Company: 9 | * Created on: 2/1/21 10 | * Modified on: 2/1/21 11 | *****/ 12 | sealed class CommonDto { 13 | 14 | data class CommonResponse(val response: Any?, val data: T?) : CommonDto() 15 | 16 | data class Error( 17 | @SerializedName("code")val errorCode: String, 18 | @SerializedName("message")val errorMessage: String 19 | ) : CommonDto() 20 | 21 | data class ServerDate( 22 | @SerializedName("serverDateTime")val dateTime: String? = "" 23 | ) : CommonDto() 24 | 25 | data class Location( 26 | @SerializedName("latitude") val latitude: Double, 27 | @SerializedName("longitude") val longitude: Double 28 | ) 29 | } -------------------------------------------------------------------------------- /data/src/main/java/com/template/data/datasource/remote/dto/ErrorDto.kt: -------------------------------------------------------------------------------- 1 | package com.template.data.datasource.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /** 6 | * ERROR DTO Class 7 | * Created by Lajesh Dineshkumar on 2020-03-09. 8 | * Company: 9 | * Email: lajeshds2007@gmail.com 10 | */ 11 | sealed class ErrorDto { 12 | 13 | data class ErrorResponse(@SerializedName("error") val error: Error? = null) 14 | 15 | data class Error( 16 | @SerializedName("type") val type: String? = "", 17 | @SerializedName("code") val code: Any?, 18 | @SerializedName("error_user_msg") var errorUserMsg: String? = "", 19 | @SerializedName("field_errors") val fieldErrors: List? = emptyList() 20 | ) 21 | 22 | data class FieldErrors( 23 | @SerializedName("code") val code: String? = "", 24 | @SerializedName("message") val message: String? = "" 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /data/src/main/java/com/template/data/di/ApiModule.kt: -------------------------------------------------------------------------------- 1 | package com.template.data.di 2 | 3 | import com.template.data.datasource.remote.api.IAuthApi 4 | import org.koin.core.context.loadKoinModules 5 | import org.koin.dsl.module 6 | import retrofit2.Retrofit 7 | 8 | /**** 9 | * API Module ehich provides the instance of API Endpoints 10 | * Author: Lajesh Dineshkumar 11 | * Company: 12 | * Created on: 2/2/21 13 | * Modified on: 2/2/21 14 | *****/ 15 | object ApiModule { 16 | fun load(){ 17 | loadKoinModules(apiModules) 18 | } 19 | 20 | val apiModules = module { 21 | single { 22 | get().create(IAuthApi::class.java) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /data/src/main/java/com/template/data/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.template.data.di 2 | 3 | import com.template.data.repository.AuthRepositoryImpl 4 | import com.template.domain.repository.IAuthRepository 5 | import org.koin.core.context.loadKoinModules 6 | import org.koin.dsl.module 7 | 8 | /**** 9 | * DI module which provides the factory repository instances 10 | * Author: Lajesh Dineshkumar 11 | * Company: 12 | * Created on: 2/1/21 13 | * Modified on: 2/1/21 14 | *****/ 15 | object RepositoryModule { 16 | fun load() { 17 | loadKoinModules(repositoryModules) 18 | } 19 | 20 | val repositoryModules = module { 21 | factory{ AuthRepositoryImpl(authApi = get(), branchDao = get()) } 22 | } 23 | } -------------------------------------------------------------------------------- /data/src/main/java/com/template/data/mapper/dtotoentity/AuthDtoToEntity.kt: -------------------------------------------------------------------------------- 1 | package com.template.data.mapper.dtotoentity 2 | 3 | import com.template.data.datasource.remote.dto.AuthDto 4 | import com.template.domain.entity.response.auth.AuthEntity 5 | 6 | /**** 7 | * File Description 8 | * Author: Lajesh Dineshkumar 9 | * Company: Farabi Technologies 10 | * Created on: 4/25/21 11 | * Modified on: 4/25/21 12 | *****/ 13 | fun AuthDto.UserDetails.map() = AuthEntity.UserDetails( 14 | firstName = firstName, 15 | lastName = lastName 16 | ) 17 | 18 | fun AuthDto.LoginResponse.map() = AuthEntity.LoginResponse( 19 | userDetails = userDetails?.map() 20 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/template/data/mapper/dtotoentity/CommonDtoToEntity.kt: -------------------------------------------------------------------------------- 1 | package com.template.data.mapper.dtotoentity 2 | 3 | import com.template.data.datasource.remote.dto.AuthDto 4 | import com.template.data.datasource.remote.dto.CommonDto 5 | import com.template.domain.entity.common.CommonEntity 6 | import com.template.domain.entity.response.auth.AuthEntity 7 | 8 | /** 9 | * Keep all the DTO to Entity Mapping here 10 | * Created by Lajesh Dineshkumar on 2020-03-09. 11 | * Company: 12 | * Email: lajeshds2007@gmail.com 13 | */ 14 | 15 | fun CommonDto.CommonResponse.map() = CommonEntity.CommonResponse( 16 | response = response, 17 | data = data 18 | ) 19 | 20 | fun CommonDto.ServerDate.map() = CommonEntity.ServerDate( 21 | dateTime = dateTime 22 | ) 23 | 24 | fun CommonDto.Location.map() = CommonEntity.Location(latitude, longitude) 25 | -------------------------------------------------------------------------------- /data/src/main/java/com/template/data/repository/AuthRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.template.data.repository 2 | 3 | import com.template.data.datasource.local.dao.BranchDao 4 | import com.template.data.datasource.remote.api.IAuthApi 5 | import com.template.data.mapper.dtotoentity.map 6 | import com.template.domain.common.ResultState 7 | import com.template.domain.entity.common.CommonEntity 8 | import com.template.domain.entity.request.AuthRequest 9 | import com.template.domain.entity.response.auth.AuthEntity 10 | import com.template.domain.repository.IAuthRepository 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.flow 14 | import kotlinx.coroutines.flow.flowOn 15 | 16 | /**** 17 | * AuthRepository Implementation 18 | * Author: Lajesh Dineshkumar 19 | * Company: 20 | * Created on: 2/1/21 21 | * Modified on: 2/1/21 22 | *****/ 23 | class AuthRepositoryImpl(private val authApi: IAuthApi, private val branchDao: BranchDao) : BaseRepositoryImpl(), IAuthRepository { 24 | override fun signIn( 25 | signinRequest: AuthRequest.SigninRequest 26 | ): Flow> = flow { 27 | emit(apiCall { authApi.signIn(signinRequest).map() }) 28 | }.flowOn(Dispatchers.IO) 29 | 30 | override fun signUp( 31 | signupRequest: AuthRequest.SignupRequest 32 | ): Flow>> = flow { 33 | emit(apiCall { authApi.signUp(signupRequest).map()}) 34 | }.flowOn(Dispatchers.IO) 35 | } -------------------------------------------------------------------------------- /data/src/test/java/com/template/data/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.template.data 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /domain/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'kotlin' 4 | } 5 | 6 | java { 7 | sourceCompatibility = JavaVersion.VERSION_1_8 8 | targetCompatibility = JavaVersion.VERSION_1_8 9 | } 10 | 11 | dependencies { 12 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 13 | 14 | //Coroutine 15 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" 16 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" 17 | 18 | // Koin 19 | implementation "org.koin:koin-core:$koinVersion" 20 | implementation "org.koin:koin-android:$koinVersion" 21 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/template/domain/common/ResultState.kt: -------------------------------------------------------------------------------- 1 | package com.template.domain.common 2 | 3 | import com.template.domain.entity.response.common.ErrorEntity 4 | 5 | /** 6 | * A wrapper for database and network states. 7 | * Created by Lajesh Dineshkumar on 2020-03-09. 8 | * Company: 9 | * Email: lajeshds2007@gmail.com 10 | */ 11 | sealed class ResultState { 12 | 13 | /** 14 | * A state that shows the [data] is the last known update. 15 | */ 16 | data class Success(val data: T) : ResultState() 17 | 18 | /** 19 | * A state to show an error 20 | */ 21 | data class Error(val error: ErrorEntity.Error?) : ResultState() 22 | } 23 | -------------------------------------------------------------------------------- /domain/src/main/java/com/template/domain/di/UseCaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.template.domain.di 2 | 3 | import com.template.domain.usecases.auth.AuthUseCaseImpl 4 | import com.template.domain.usecases.auth.IAuthUseCase 5 | import org.koin.core.context.loadKoinModules 6 | import org.koin.dsl.module 7 | 8 | /**** 9 | * Module which provides the factory instance of Usecase 10 | * Author: Lajesh Dineshkumar 11 | * Company: 12 | * Created on: 2/1/21 13 | * Modified on: 2/1/21 14 | *****/ 15 | object UseCaseModule { 16 | fun load(){ 17 | loadKoinModules(authUSeCaseModules) 18 | } 19 | 20 | val authUSeCaseModules = module { 21 | factory { AuthUseCaseImpl(authRepository = get()) } 22 | } 23 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/template/domain/entity/request/AuthRequest.kt: -------------------------------------------------------------------------------- 1 | package com.template.domain.entity.request 2 | 3 | /**** 4 | * File Description 5 | * Author: Lajesh Dineshkumar 6 | * Company: 7 | * Created on: 2/1/21 8 | * Modified on: 2/1/21 9 | *****/ 10 | sealed class AuthRequest { 11 | 12 | abstract val userId: String 13 | abstract val password: String 14 | 15 | data class SignupRequest( 16 | override val userId: String, 17 | override val password: String, 18 | val tnc: Boolean 19 | ) : AuthRequest() 20 | 21 | data class SigninRequest( 22 | override val userId: String, 23 | override val password: String, 24 | val loginType: Int, 25 | val enableRememberMe: Boolean, 26 | val encryptionToken: String? = "", 27 | val notificationToken: String? = "" 28 | ) : AuthRequest() 29 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/template/domain/entity/response/auth/AuthEntity.kt: -------------------------------------------------------------------------------- 1 | package com.template.domain.entity.response.auth 2 | 3 | /**** 4 | * Keep all the authentication use case related entities over here. 5 | * Entity model contains only the fields needed for UI 6 | * Author: Lajesh Dineshkumar 7 | * Company: Farabi Technologies 8 | * Created on: 4/25/21 9 | * Modified on: 4/25/21 10 | *****/ 11 | sealed class AuthEntity { 12 | 13 | data class LoginResponse( 14 | val userDetails: UserDetails? = null 15 | ) : AuthEntity() 16 | 17 | data class UserDetails( 18 | val firstName: String? = "", 19 | val lastName: String? = "" 20 | ) : AuthEntity() 21 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/template/domain/entity/response/common/CommonEntity.kt: -------------------------------------------------------------------------------- 1 | package com.template.domain.entity.common 2 | 3 | /**** 4 | * Keep all the common entity class here 5 | * Author: Lajesh Dineshkumar 6 | * Company: 7 | * Created on: 1/31/21 8 | * Modified on: 1/31/21 9 | *****/ 10 | sealed class CommonEntity { 11 | 12 | data class CommonResponse( 13 | val response: Any?, 14 | val data: T? 15 | ) : CommonEntity() 16 | 17 | data class ServerDate( 18 | val dateTime: String? = "" 19 | ) : CommonEntity() 20 | 21 | data class Location( 22 | val latitude: Double, 23 | val longitude: Double 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /domain/src/main/java/com/template/domain/entity/response/common/ErrorEntity.kt: -------------------------------------------------------------------------------- 1 | package com.template.domain.entity.response.common 2 | 3 | /**** 4 | * Keep all the error related model class here 5 | * Author: Lajesh Dineshkumar 6 | * Company: 7 | * Created on: 1/31/21 8 | * Modified on: 1/31/21 9 | *****/ 10 | sealed class ErrorEntity { 11 | data class Error(val errorCode: Any?, val errorMessage: String? = "") : ErrorEntity() 12 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/template/domain/repository/IAuthRepository.kt: -------------------------------------------------------------------------------- 1 | package com.template.domain.repository 2 | 3 | import com.template.domain.common.ResultState 4 | import com.template.domain.entity.common.CommonEntity 5 | import com.template.domain.entity.request.AuthRequest 6 | import com.template.domain.entity.response.auth.AuthEntity 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | /**** 10 | * The abstraction of Auth repositories goes here 11 | * Author: Lajesh Dineshkumar 12 | * Company: 13 | * Created on: 2/1/21 14 | * Modified on: 2/1/21 15 | *****/ 16 | interface IAuthRepository { 17 | 18 | fun signIn(signinRequest: AuthRequest.SigninRequest) : Flow> 19 | 20 | fun signUp(signupRequest: AuthRequest.SignupRequest) : Flow>> 21 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/template/domain/usecases/auth/AuthUseCaseImpl.kt: -------------------------------------------------------------------------------- 1 | package com.template.domain.usecases.auth 2 | 3 | import com.template.domain.common.ResultState 4 | import com.template.domain.entity.common.CommonEntity 5 | import com.template.domain.entity.request.AuthRequest 6 | import com.template.domain.entity.response.auth.AuthEntity 7 | import com.template.domain.repository.IAuthRepository 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | /**** 11 | * Implementation of auth usecase goes here 12 | * Author: Lajesh Dineshkumar 13 | * Company: 14 | * Created on: 1/31/21 15 | * Modified on: 1/31/21 16 | *****/ 17 | class AuthUseCaseImpl(private val authRepository: IAuthRepository) : IAuthUseCase { 18 | override fun signIn( 19 | signinRequest: AuthRequest.SigninRequest 20 | ): Flow> = authRepository.signIn( 21 | signinRequest 22 | ) 23 | 24 | override fun signUp( 25 | signupRequest: AuthRequest.SignupRequest 26 | ): Flow>> = authRepository.signUp( 27 | signupRequest 28 | ) 29 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/template/domain/usecases/auth/IAuthUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.template.domain.usecases.auth 2 | 3 | import com.template.domain.common.ResultState 4 | import com.template.domain.entity.common.CommonEntity 5 | import com.template.domain.entity.request.AuthRequest 6 | import com.template.domain.entity.response.auth.AuthEntity 7 | import com.template.domain.usecases.base.BaseUseCase 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | /**** 11 | * Keep all the authentication related business use cases here 12 | * Author: Lajesh Dineshkumar 13 | * Company: 14 | * Created on: 1/31/21 15 | * Modified on: 1/31/21 16 | *****/ 17 | interface IAuthUseCase : BaseUseCase { 18 | 19 | fun signIn(signinRequest: AuthRequest.SigninRequest) : Flow> 20 | 21 | fun signUp(signupRequest: AuthRequest.SignupRequest) : Flow>> 22 | 23 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/template/domain/usecases/base/BaseUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.template.domain.usecases.base 2 | 3 | /**** 4 | * All the use cases should be extended from this interface 5 | * Author: Lajesh Dineshkumar 6 | * Company: 7 | * Created on: 1/31/21 8 | * Modified on: 1/31/21 9 | *****/ 10 | interface BaseUseCase { 11 | } -------------------------------------------------------------------------------- /features/onboarding/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /features/onboarding/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | id 'androidx.navigation.safeargs.kotlin' 6 | } 7 | apply from: "$rootProject.projectDir/commons.gradle" 8 | apply from: "$rootProject.projectDir/commons.features.gradle" 9 | 10 | android { 11 | compileSdkVersion 30 12 | buildToolsVersion "30.0.2" 13 | 14 | defaultConfig { 15 | minSdkVersion 25 16 | targetSdkVersion 30 17 | versionCode 1 18 | versionName "1.0" 19 | 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | consumerProguardFiles "consumer-rules.pro" 22 | } 23 | 24 | buildTypes { 25 | release { 26 | minifyEnabled false 27 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 28 | } 29 | } 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | 40 | 41 | flavorDimensions "server" 42 | 43 | productFlavors { 44 | 45 | dev { 46 | dimension "server" 47 | } 48 | 49 | uat { 50 | dimension "server" 51 | } 52 | 53 | pilot { 54 | dimension "server" 55 | } 56 | 57 | prod { 58 | dimension "server" 59 | } 60 | } 61 | 62 | publishNonDefault true 63 | 64 | buildFeatures { 65 | dataBinding true 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /features/onboarding/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lajesh/clean-architecture-kotlin/af1fe7b55f7585e1406d93a02b7d336a0b81ad45/features/onboarding/consumer-rules.pro -------------------------------------------------------------------------------- /features/onboarding/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /features/onboarding/src/androidTest/java/com/template/feature_onboarding/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.template.feature_onboarding 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.template.feature_onboarding.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /features/onboarding/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /features/onboarding/src/main/java/com/template/feature_onboarding/di/OnboardingViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package com.template.feature_onboarding.di 2 | 3 | import com.template.feature_onboarding.view.fragment.signin.LoginViewModel 4 | import com.template.feature_onboarding.view.fragment.splash.SplashViewModel 5 | import kotlinx.coroutines.Dispatchers 6 | import org.koin.androidx.viewmodel.dsl.viewModel 7 | import org.koin.core.context.loadKoinModules 8 | import org.koin.dsl.module 9 | 10 | /**** 11 | * File Description 12 | * Author: Lajesh Dineshkumar 13 | * Company: 14 | * Created on: 2/8/21 15 | * Modified on: 2/8/21 16 | *****/ 17 | object OnboardingViewModelModule { 18 | fun load() { 19 | loadKoinModules(module { 20 | 21 | viewModel { 22 | LoginViewModel(get()) 23 | } 24 | 25 | 26 | viewModel { 27 | SplashViewModel() 28 | } 29 | 30 | }) 31 | } 32 | } -------------------------------------------------------------------------------- /features/onboarding/src/main/java/com/template/feature_onboarding/view/fragment/signin/LoginFragment.kt: -------------------------------------------------------------------------------- 1 | package com.template.feature_onboarding.view.fragment.signin 2 | 3 | import com.template.core.ui.base.BaseFragment 4 | import com.template.feature_onboarding.R 5 | import com.template.feature_onboarding.BR 6 | import com.template.feature_onboarding.databinding.FragmentLoginBinding 7 | 8 | /**** 9 | * File Description 10 | * Author: Lajesh Dineshkumar 11 | * Company: 12 | * Created on: 2/3/21 13 | * Modified on: 2/3/21 14 | *****/ 15 | class LoginFragment : BaseFragment(LoginViewModel::class) { 16 | override val layoutRes: Int 17 | get() = R.layout.fragment_login 18 | override val bindingVariable: Int 19 | get() = BR.viewModel 20 | } -------------------------------------------------------------------------------- /features/onboarding/src/main/java/com/template/feature_onboarding/view/fragment/signin/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.template.feature_onboarding.view.fragment.signin 2 | 3 | import android.net.Uri 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import com.template.core.utils.NavigationCommand 7 | import com.template.core.viewmodel.base.BaseViewModel 8 | import com.template.domain.common.ResultState 9 | import com.template.domain.entity.request.AuthRequest 10 | import com.template.domain.entity.response.auth.AuthEntity 11 | import com.template.domain.usecases.auth.IAuthUseCase 12 | import kotlinx.coroutines.CoroutineDispatcher 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.flow.collect 15 | import kotlinx.coroutines.launch 16 | 17 | /**** 18 | * File Description 19 | * Author: Lajesh Dineshkumar 20 | * Company: 21 | * Created on: 2/3/21 22 | * Modified on: 2/3/21 23 | *****/ 24 | class LoginViewModel constructor( 25 | private val authUseCase: IAuthUseCase 26 | ) : BaseViewModel() { 27 | 28 | val data = MutableLiveData(false) 29 | val userDetails = MutableLiveData() 30 | 31 | private var likeCount = 0 32 | 33 | fun getLikeCount() = likeCount 34 | 35 | fun addLikeCount() { 36 | viewModelScope.launch { 37 | likeCount += 1 38 | likeCount 39 | } 40 | } 41 | 42 | fun signup() { 43 | showLoading(true) 44 | viewModelScope.launch { 45 | authUseCase.signUp(AuthRequest.SignupRequest("", "dasdasd", true)) 46 | .collect { state -> 47 | when (state) { 48 | is ResultState.Success -> { 49 | login() 50 | } 51 | 52 | is ResultState.Error -> { 53 | setError(error = state.error) 54 | showLoading(false) 55 | } 56 | } 57 | 58 | } 59 | 60 | } 61 | } 62 | 63 | fun login() { 64 | showLoading(true) 65 | viewModelScope.launch { 66 | authUseCase.signIn(AuthRequest.SigninRequest("test@test.com", "dasdasd", 0, false)) 67 | .collect { state -> 68 | when (state) { 69 | is ResultState.Success -> { 70 | userDetails.value = state.data.userDetails 71 | data.value = true 72 | showLoading(false) 73 | navigationCommands.value = 74 | NavigationCommand.To(LoginFragmentDirections.actionLoginToProfile()) 75 | } 76 | 77 | is ResultState.Error -> { 78 | setError(error = state.error) 79 | showLoading(false) 80 | } 81 | } 82 | 83 | } 84 | 85 | } 86 | } 87 | 88 | fun ediProfile() { 89 | navigationCommands.value = NavigationCommand.ToByDeepLink(Uri.parse("dpl://editprofile")) 90 | } 91 | } -------------------------------------------------------------------------------- /features/onboarding/src/main/java/com/template/feature_onboarding/view/fragment/splash/SplashFragment.kt: -------------------------------------------------------------------------------- 1 | package com.template.feature_onboarding.view.fragment.splash 2 | 3 | import com.template.core.ui.base.BaseFragment 4 | import com.template.feature_onboarding.R 5 | import com.template.feature_onboarding.BR 6 | import com.template.feature_onboarding.databinding.FragmentSplashBinding 7 | 8 | /**** 9 | * SplashFragment 10 | * Author: Lajesh Dineshkumar 11 | * Company: 12 | * Created on: 2/3/21 13 | * Modified on: 2/3/21 14 | *****/ 15 | class SplashFragment : BaseFragment(SplashViewModel::class){ 16 | override val layoutRes: Int 17 | get() = R.layout.fragment_splash 18 | override val bindingVariable: Int 19 | get() = BR.viewModel 20 | } -------------------------------------------------------------------------------- /features/onboarding/src/main/java/com/template/feature_onboarding/view/fragment/splash/SplashViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.template.feature_onboarding.view.fragment.splash 2 | 3 | import com.template.core.utils.NavigationCommand 4 | import com.template.core.viewmodel.base.BaseViewModel 5 | 6 | /**** 7 | * SplashViewModel 8 | * Author: Lajesh Dineshkumar 9 | * Company: 10 | * Created on: 2/3/21 11 | * Modified on: 2/3/21 12 | *****/ 13 | class SplashViewModel : BaseViewModel() { 14 | 15 | init { 16 | android.os.Handler().postDelayed({ 17 | navigationCommands.value = NavigationCommand.To(SplashFragmentDirections.actionSplashToLogin()) 18 | }, 5000) 19 | } 20 | } -------------------------------------------------------------------------------- /features/onboarding/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /features/onboarding/src/main/res/layout/fragment_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 17 | 18 | 27 | 28 | 29 |