├── app ├── .gitignore ├── src │ ├── staging │ │ ├── res │ │ │ ├── values │ │ │ │ └── strings.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 │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── drawable │ │ │ │ └── splash_background.xml │ │ │ └── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ └── google-services.json │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── styles.xml │ │ │ │ ├── colors.xml │ │ │ │ └── themes.xml │ │ │ ├── drawable │ │ │ │ ├── ic_celerik.png │ │ │ │ ├── splash_background.xml │ │ │ │ └── ic_launcher_background.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 │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ ├── activity_main.xml │ │ │ │ └── activity_splash.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ └── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── celerik │ │ │ │ └── app │ │ │ │ ├── di │ │ │ │ ├── modules │ │ │ │ │ ├── FragmentModule.kt │ │ │ │ │ ├── CelerikUseCasesModule.kt │ │ │ │ │ ├── IntentsModule.kt │ │ │ │ │ ├── MainBindsModule.kt │ │ │ │ │ ├── AppUseCasesModule.kt │ │ │ │ │ ├── ActivityModule.kt │ │ │ │ │ ├── SplashModule.kt │ │ │ │ │ ├── LoggerModule.kt │ │ │ │ │ ├── InterceptorsModule.kt │ │ │ │ │ ├── BaseModule.kt │ │ │ │ │ ├── CelerikAppModule.kt │ │ │ │ │ └── NetworkModule.kt │ │ │ │ ├── ViewModelFactory.kt │ │ │ │ └── components │ │ │ │ │ └── AppComponent.kt │ │ │ │ ├── data │ │ │ │ ├── AppVersion.kt │ │ │ │ ├── SplashNews.kt │ │ │ │ ├── ResolveIntentImpl.kt │ │ │ │ └── CelerikResources.kt │ │ │ │ ├── viewModels │ │ │ │ ├── MainViewModel.kt │ │ │ │ ├── CelerikAppViewModel.kt │ │ │ │ └── SplashViewModel.kt │ │ │ │ ├── CelerikDebugTree.kt │ │ │ │ ├── network │ │ │ │ └── NetworkConnectivityInterceptor.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── CelerikLogger.kt │ │ │ │ ├── useCases │ │ │ │ └── VerifyInternetConnectivityUseCase.kt │ │ │ │ ├── CelerikApp.kt │ │ │ │ └── SplashActivity.kt │ │ └── AndroidManifest.xml │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── celerik │ │ │ └── app │ │ │ └── ExampleInstrumentedTest.kt │ └── test │ │ └── java │ │ └── com │ │ └── celerik │ │ └── app │ │ └── viewModels │ │ └── SplashViewModelTest.kt ├── debug-keystore.jks ├── stagingInternal │ └── debug │ │ └── output-metadata.json ├── proguard-rules.pro └── build.gradle.kts ├── base ├── .gitignore ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── app │ │ │ └── base │ │ │ ├── others │ │ │ └── Constants.kt │ │ │ ├── data │ │ │ ├── DensityEnum.kt │ │ │ ├── DateObject.kt │ │ │ └── HttpObject.kt │ │ │ ├── interfaces │ │ │ ├── Cache.kt │ │ │ ├── Logger.kt │ │ │ └── UseCases.kt │ │ │ ├── utils │ │ │ ├── Tuples.kt │ │ │ └── Extensions.kt │ │ │ ├── Optional.kt │ │ │ └── Either.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── app │ │ └── base │ │ └── utils │ │ └── ExtensionsTest.kt └── build.gradle.kts ├── core-test ├── .gitignore ├── consumer-rules.pro ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── app │ │ └── core │ │ └── test │ │ ├── data │ │ └── TestData.kt │ │ ├── interfaces │ │ └── TestApi.kt │ │ └── utils │ │ ├── RxSchedulerExtension.kt │ │ └── InstantExecutorExtension.kt └── build.gradle.kts ├── core ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ └── values │ │ │ ├── styles.xml │ │ │ └── strings.xml │ │ └── java │ │ └── com │ │ └── app │ │ └── core │ │ ├── CoreComponent.kt │ │ ├── CoreApp.kt │ │ ├── di │ │ ├── GeneralBindsModule.kt │ │ ├── NetworkQualifiers.kt │ │ ├── ViewModelKey.kt │ │ ├── GeneralProvidesModule.kt │ │ └── CommonBindings.kt │ │ ├── entities │ │ └── FileData.kt │ │ ├── exceptions │ │ └── NoConnectionException.kt │ │ ├── interfaces │ │ ├── IntentResolver.kt │ │ └── AppResources.kt │ │ ├── qualifiers │ │ └── GeneralQualifiers.kt │ │ ├── BaseViewModel.kt │ │ ├── data │ │ ├── SharedPreferencesHelper.kt │ │ └── SharedPreferencesCache.kt │ │ ├── Event.kt │ │ └── network │ │ └── ServerInterceptor.kt └── build.gradle.kts ├── components ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ └── values │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ ├── styles.xml │ │ │ ├── attrs.xml │ │ │ └── dimens.xml │ │ └── java │ │ └── com │ │ └── celerik │ │ └── components │ │ └── utils │ │ ├── ActivityViewBinding.kt │ │ ├── Extensions.kt │ │ └── FragmentViewBingingProperty.kt └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle.kts ├── fastlane ├── Pluginfile ├── Appfile ├── README.md └── Fastfile ├── Gemfile ├── .github ├── workflows │ ├── release-drafter.yml │ ├── pr-pipeline-workflow.yml │ └── app-distribution-pipeline-workflow.yml ├── pull_request_template.md └── release-drafter.yml ├── .sonarcloud.properties ├── .editorconfig ├── docs ├── cicd.md ├── utilities.md └── codestyle.md ├── .azuredevops ├── pull_request_template.md └── pipelines │ └── pr-pipeline-workflow.yml ├── gradle.properties ├── .gitignore ├── gradlew.bat ├── README.md ├── gradlew └── Gemfile.lock /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /base/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /core-test/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core-test/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /components/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/src/staging/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /core-test/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /core/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/CoreComponent.kt: -------------------------------------------------------------------------------- 1 | package com.app.core 2 | 3 | interface CoreComponent 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | app 3 | 4 | -------------------------------------------------------------------------------- /app/debug-keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/debug-keystore.jks -------------------------------------------------------------------------------- /base/src/main/java/com/app/base/others/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.app.base.others 2 | 3 | const val ONE_SECOND_IN_MILLISECONDS = 1_000L 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_celerik.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/main/res/drawable/ic_celerik.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/staging/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/staging/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/staging/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/staging/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/staging/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/staging/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/staging/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/staging/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /base/src/main/java/com/app/base/data/DensityEnum.kt: -------------------------------------------------------------------------------- 1 | package com.app.base.data 2 | 3 | enum class DensityEnum { 4 | HIGH_OR_LOWER, XHIGH, XXHIGH, XXXHIGH 5 | } 6 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "app" 2 | 3 | include(":app") 4 | include(":base") 5 | include(":components") 6 | include(":core") 7 | include(":core-test") 8 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/staging/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/staging/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/CoreApp.kt: -------------------------------------------------------------------------------- 1 | package com.app.core 2 | 3 | import androidx.multidex.MultiDexApplication 4 | 5 | open class CoreApp : MultiDexApplication() 6 | -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/di/GeneralBindsModule.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.di 2 | 3 | import dagger.Module 4 | 5 | @Module 6 | abstract class GeneralBindsModule 7 | -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/entities/FileData.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.entities 2 | 3 | data class FileData( 4 | val folder: String, 5 | val name: String 6 | ) 7 | -------------------------------------------------------------------------------- /fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | gem 'fastlane-plugin-firebase_app_distribution' 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/staging/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/staging/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/staging/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/staging/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/staging/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/staging/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /core/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Error de conexión 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/di/modules/FragmentModule.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.di.modules 2 | 3 | import dagger.Module 4 | 5 | @Module 6 | abstract class FragmentModule 7 | -------------------------------------------------------------------------------- /app/src/staging/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/staging/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/staging/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celerik/android-kotlin-boilerplate/HEAD/app/src/staging/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/data/AppVersion.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.data 2 | 3 | data class AppVersion( 4 | val forceUpdate: Boolean, 5 | val minimumVersion: Int 6 | ) 7 | -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/exceptions/NoConnectionException.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.exceptions 2 | 3 | import java.io.IOException 4 | 5 | object NoConnectionException : IOException() 6 | -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one 2 | package_name("com.celerik.app") # e.g. com.krausefx.app 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 5 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /base/src/main/java/com/app/base/data/DateObject.kt: -------------------------------------------------------------------------------- 1 | package com.app.base.data 2 | 3 | import java.io.Serializable 4 | 5 | data class DateObject( 6 | val date: Int, 7 | val timeZone: String 8 | ) : Serializable 9 | -------------------------------------------------------------------------------- /base/src/main/java/com/app/base/data/HttpObject.kt: -------------------------------------------------------------------------------- 1 | package com.app.base.data 2 | 3 | data class HttpObject( 4 | val method: String, 5 | val request: String, 6 | val response: String, 7 | val httpCode: Int 8 | ) 9 | -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/interfaces/IntentResolver.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.interfaces 2 | 3 | import android.content.Intent 4 | 5 | interface IntentResolver { 6 | fun resolveIntent(host: String): Intent 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/qualifiers/GeneralQualifiers.kt: -------------------------------------------------------------------------------- 1 | // ktlint-disable filename 2 | package com.app.core.qualifiers 3 | 4 | import javax.inject.Qualifier 5 | 6 | @Qualifier 7 | annotation class VerifyInternet 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/di/modules/CelerikUseCasesModule.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.di.modules 2 | 3 | import dagger.Module 4 | 5 | @Module(includes = [AppUseCasesModule::class]) 6 | abstract class CelerikUseCasesModule 7 | -------------------------------------------------------------------------------- /core-test/src/main/java/com/app/core/test/data/TestData.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.test.data 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class TestData(@Json(name = "name") val name: String) 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jul 12 13:30:03 COT 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /core-test/src/main/java/com/app/core/test/interfaces/TestApi.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.test.interfaces 2 | 3 | import com.app.core.test.data.TestData 4 | import retrofit2.Call 5 | import retrofit2.http.GET 6 | 7 | interface TestApi { 8 | @GET("/test") 9 | fun test(): Call 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/interfaces/AppResources.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.interfaces 2 | 3 | interface AppResources { 4 | fun getString(resId: Int): String 5 | fun getString(resId: Int, vararg others: String): String 6 | fun getColor(resId: Int): Int 7 | fun parseColor(color: String): Int 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/data/SplashNews.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.data 2 | 3 | sealed class SplashNews { 4 | object AppInitialized : SplashNews() 5 | object ShowNoConnectivityView : SplashNews() 6 | object FinishSplashNews : SplashNews() 7 | data class ShowErrorNews(val errorMessage: String) : SplashNews() 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /base/src/main/java/com/app/base/interfaces/Cache.kt: -------------------------------------------------------------------------------- 1 | package com.app.base.interfaces 2 | 3 | interface Cache { 4 | fun saveString(key: String, value: String) 5 | 6 | fun readString(key: String): String? 7 | 8 | fun removeValue(key: String) 9 | 10 | fun containsValues(): Boolean 11 | 12 | fun clearAll() 13 | } 14 | -------------------------------------------------------------------------------- /app/src/staging/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/staging/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/staging/res/drawable/splash_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /components/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ¡Lo sentimos, no tienes conexión! 4 | Por favor verifica tu conexión a internet, o intenta conectarte a una red Wi-Fi. 5 | Reintentar 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: release-drafter/release-drafter@v5 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/di/NetworkQualifiers.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.di 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Qualifier 6 | annotation class RetrofitCelerik 7 | 8 | @Qualifier 9 | annotation class RetrofitBasic 10 | 11 | @Qualifier 12 | annotation class RetrofitNullSerializationEnabled 13 | 14 | @Qualifier 15 | annotation class OkHttpClientBasic 16 | 17 | @Qualifier 18 | annotation class BasePath 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/di/modules/IntentsModule.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.di.modules 2 | 3 | import com.app.core.interfaces.IntentResolver 4 | import com.celerik.app.data.ResolveIntentImpl 5 | import dagger.Module 6 | import dagger.Provides 7 | 8 | @Module 9 | class IntentsModule { 10 | 11 | @Provides 12 | fun providesResolveIntentImplementation(): IntentResolver { 13 | return ResolveIntentImpl() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/di/ViewModelKey.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.di 2 | 3 | import androidx.lifecycle.ViewModel 4 | import dagger.MapKey 5 | import kotlin.reflect.KClass 6 | 7 | @MustBeDocumented 8 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) 9 | @Retention(AnnotationRetention.RUNTIME) 10 | @MapKey 11 | annotation class ViewModelKey(val value: KClass) 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/di/GeneralProvidesModule.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.di 2 | 3 | import com.app.core.data.SharedPreferencesHelper 4 | import dagger.Module 5 | import dagger.Provides 6 | import javax.inject.Singleton 7 | 8 | @Module 9 | class GeneralProvidesModule { 10 | @Provides 11 | @Singleton 12 | fun providesSharedPreferencesHelper(): SharedPreferencesHelper { 13 | return SharedPreferencesHelper() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | sonar.organization=celerik 2 | sonar.projectKey=celerik_android-kotlin-boilerplate 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | sonar.projectName=Celerik-Android 6 | 7 | # Code configuration 8 | sonar.language=kotlin 9 | sonar.sourceEncoding=UTF-8 10 | 11 | sonar.sources=. 12 | sonar.exclusions=**/build/**, **/*.xml, **/test/** 13 | sonar.coverage.exclusions=**/test/**, **/androidTest/** 14 | -------------------------------------------------------------------------------- /base/src/main/java/com/app/base/utils/Tuples.kt: -------------------------------------------------------------------------------- 1 | package com.app.base.utils 2 | 3 | import java.io.Serializable 4 | 5 | data class Quadruple( 6 | val first: A, 7 | val second: B, 8 | val third: C, 9 | val fourth: D 10 | ) : Serializable { 11 | override fun toString(): String = "($first, $second, $third, $fourth)" 12 | } 13 | 14 | fun Quadruple.toList(): List = listOf(first, second, third, fourth) 15 | -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.app.core 2 | 3 | import androidx.lifecycle.LifecycleObserver 4 | import androidx.lifecycle.ViewModel 5 | import io.reactivex.rxjava3.disposables.CompositeDisposable 6 | 7 | open class BaseViewModel : ViewModel(), LifecycleObserver { 8 | 9 | protected val disposables = CompositeDisposable() 10 | 11 | override fun onCleared() { 12 | disposables.clear() 13 | super.onCleared() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*.{kt,kts}] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{java,xml,gradle}] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 2 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/data/ResolveIntentImpl.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.data 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import com.app.core.interfaces.IntentResolver 6 | import com.celerik.app.BuildConfig 7 | 8 | class ResolveIntentImpl : IntentResolver { 9 | override fun resolveIntent(host: String): Intent { 10 | val url = "${BuildConfig.SCHEME}://$host" 11 | return Intent(Intent.ACTION_VIEW, Uri.parse(url)) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/viewModels/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.viewModels 2 | 3 | import androidx.lifecycle.LifecycleObserver 4 | import com.app.base.interfaces.Logger 5 | import com.app.core.BaseViewModel 6 | import com.app.core.interfaces.AppResources 7 | import javax.inject.Inject 8 | 9 | class MainViewModel @Inject constructor( 10 | private val logger: Logger, 11 | private val appResources: AppResources, 12 | ) : BaseViewModel(), LifecycleObserver 13 | -------------------------------------------------------------------------------- /app/stagingInternal/debug/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "com.celerik.app.staging", 8 | "variantName": "stagingInternalDebug", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "versionCode": 32, 14 | "versionName": "v1.0-Dirty-Staging", 15 | "outputFile": "app-staging-internal-debug.apk" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/viewModels/CelerikAppViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.viewModels 2 | 3 | import androidx.lifecycle.LifecycleObserver 4 | import com.app.base.interfaces.Logger 5 | import com.app.core.BaseViewModel 6 | import com.app.core.interfaces.AppResources 7 | import javax.inject.Inject 8 | 9 | class CelerikAppViewModel @Inject constructor( 10 | private val logger: Logger, 11 | private val appResources: AppResources, 12 | ) : BaseViewModel(), LifecycleObserver 13 | -------------------------------------------------------------------------------- /docs/cicd.md: -------------------------------------------------------------------------------- 1 | # Celerik CI/CD 2 | 3 | ## Description 4 | Continuous integration and deployment is based on [Fastlane](https://fastlane.tools/), which allows to specify multiple sequences of tasks in a file named Fastfile. They can be executed in any computer without the need of an external platform. 5 | 6 | ## Available Lines 7 | - run_checks: 8 | Params: None 9 | Description: It executes Kotlin linter, unitary tests and code coverage analysis from all project's modules. 10 | Command: `fastlane run_checks` -------------------------------------------------------------------------------- /base/src/main/java/com/app/base/interfaces/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.app.base.interfaces 2 | 3 | interface Logger { 4 | fun v(message: String, throwable: Throwable? = null) 5 | fun d(message: String, throwable: Throwable? = null) 6 | fun i(message: String, throwable: Throwable? = null) 7 | fun w(message: String, throwable: Throwable? = null) 8 | fun e(message: String, throwable: Throwable? = null) 9 | fun http(url: String, method: String, request: String? = null, response: String? = null, statusCode: Int? = null) 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/di/modules/MainBindsModule.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.di.modules 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.app.core.di.ViewModelKey 5 | import com.celerik.app.viewModels.MainViewModel 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.multibindings.IntoMap 9 | 10 | @Module 11 | abstract class MainBindsModule { 12 | @Binds 13 | @IntoMap 14 | @ViewModelKey(MainViewModel::class) 15 | abstract fun bindsMainViewModel(mainViewModel: MainViewModel): ViewModel 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/di/modules/AppUseCasesModule.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.di.modules 2 | 3 | import com.app.base.interfaces.SingleUseCase 4 | import com.app.core.qualifiers.VerifyInternet 5 | import com.celerik.app.useCases.VerifyInternetConnectivityUseCase 6 | import dagger.Binds 7 | import dagger.Module 8 | 9 | @Module 10 | abstract class AppUseCasesModule { 11 | 12 | @Binds 13 | @VerifyInternet 14 | abstract fun bindVerifyInternetConnectivityUseCase(useCase: VerifyInternetConnectivityUseCase): SingleUseCase 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/di/modules/ActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.di.modules 2 | 3 | import com.celerik.app.MainActivity 4 | import com.celerik.app.SplashActivity 5 | import dagger.Module 6 | import dagger.android.ContributesAndroidInjector 7 | 8 | @Module 9 | abstract class ActivityModule { 10 | 11 | @ContributesAndroidInjector(modules = [SplashModule::class]) 12 | abstract fun bindSplashActivity(): SplashActivity 13 | 14 | @ContributesAndroidInjector(modules = [MainBindsModule::class]) 15 | abstract fun bindMainActivity(): MainActivity 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/di/modules/SplashModule.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.di.modules 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.app.core.di.ViewModelKey 5 | import com.celerik.app.viewModels.SplashViewModel 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.multibindings.IntoMap 9 | 10 | @Module(includes = [AppUseCasesModule::class]) 11 | abstract class SplashModule { 12 | @Binds 13 | @IntoMap 14 | @ViewModelKey(SplashViewModel::class) 15 | abstract fun bindsSplashViewModel(splashViewModel: SplashViewModel): ViewModel 16 | } 17 | -------------------------------------------------------------------------------- /components/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @color/grey_pale_light 4 | @color/grey_blackish 5 | @color/blue_dark 6 | 7 | #000000 8 | #ee417f 9 | #00d4c8 10 | #f8f8f9 11 | #8a000000 12 | #012C3D 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/di/modules/LoggerModule.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.di.modules 2 | 3 | import com.app.base.interfaces.Logger 4 | import com.celerik.app.CelerikDebugTree 5 | import com.celerik.app.CelerikLogger 6 | import dagger.Module 7 | import dagger.Provides 8 | import javax.inject.Singleton 9 | 10 | @Module 11 | object LoggerModule { 12 | @Provides 13 | @Singleton 14 | fun providesLoggerImplementation(): Logger { 15 | val tree = CelerikDebugTree() // The logger could be changed according to current environment 16 | return CelerikLogger(tree) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /components/src/main/java/com/celerik/components/utils/ActivityViewBinding.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.components.utils 2 | 3 | import android.view.LayoutInflater 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.viewbinding.ViewBinding 6 | 7 | /* 8 | * From: https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c 9 | */ 10 | inline fun AppCompatActivity.viewBinding( 11 | crossinline bindingInflater: (LayoutInflater) -> T 12 | ) = 13 | lazy(LazyThreadSafetyMode.NONE) { 14 | bindingInflater(layoutInflater) 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/CelerikDebugTree.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app 2 | 3 | import timber.log.Timber 4 | 5 | class CelerikDebugTree : Timber.DebugTree() { 6 | override fun createStackElementTag(element: StackTraceElement): String { 7 | return getCleanClassName(newStackTraceElement()) 8 | } 9 | 10 | private fun newStackTraceElement(): StackTraceElement { 11 | val elements = Throwable().stackTrace 12 | return elements[9] 13 | } 14 | 15 | private fun getCleanClassName(element: StackTraceElement): String { 16 | return String.format("C:%s:%s", element.className, element.lineNumber) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /base/src/main/java/com/app/base/interfaces/UseCases.kt: -------------------------------------------------------------------------------- 1 | package com.app.base.interfaces 2 | 3 | import io.reactivex.rxjava3.core.Completable 4 | import io.reactivex.rxjava3.core.Observable 5 | import io.reactivex.rxjava3.core.Single 6 | 7 | abstract class ObservableUseCase { 8 | abstract fun execute(input: T): Observable 9 | } 10 | 11 | abstract class SingleUseCase { 12 | abstract fun execute(input: T): Single 13 | } 14 | 15 | abstract class CompletableUseCase { 16 | abstract fun execute(input: T): Completable 17 | } 18 | 19 | abstract class UseCase { 20 | abstract fun execute(input: T): R 21 | } 22 | -------------------------------------------------------------------------------- /docs/utilities.md: -------------------------------------------------------------------------------- 1 | # Android Coding Utilities 2 | 3 | ## Description 4 | This document shows multiple commands executions and utilities for coding in the Kotlin Programming Language. 5 | ## Table of Contents 6 | 7 | - [ktlint](#ktlint) 8 | - [JaCoCo](#jacoco) 9 | - [Dokka](#dokka) 10 | 11 | ## ktlint 12 | Executing _ktlint_ command for Kotlin linter with built-in formatter: 13 | ```./gradlew ktlintFormat ``` 14 | 15 | ## JaCoCo 16 | Executing _JaCoCo_ command for measuring code coverage: 17 | ```./gradlew checkCoverage ``` 18 | 19 | ## Dokka 20 | Execution _Dokka_ command for generating Kotlin documentation in HTML format: 21 | ```./gradlew dokkaHtml ``` 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Justification / Approach 2 | 3 | In this section we provide a summary of the decisions made and the outcome of our changes in this PR. This can be as simple as detailing where we are making changes or as detailed as the Architectural decisions behind all the changes. 4 | 5 | ## Output 6 | 7 | Here we provided outputs associated to the end result in case this is a FE application a Screenshot is required. 8 | 9 | ## Checklist 10 | 11 | PRs must include the following: 12 | 13 | - [ ] Technical description of the implementation. 14 | - [ ] Screenshots of the implementation if the story includes UI changes. 15 | - [ ] Unit test cases. -------------------------------------------------------------------------------- /.azuredevops/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Justification / Approach 2 | 3 | In this section we provide a summary of the decisions made and the outcome of our changes in this PR. This can be as simple as detailing where we are making changes or as detailed as the Architectural decisions behind all the changes. 4 | 5 | ## Output 6 | 7 | Here we provided outputs associated to the end result in case this is a FE application a Screenshot is required. 8 | 9 | ## Checklist 10 | 11 | PRs must include the following: 12 | 13 | - [ ] Technical description of the implementation. 14 | - [ ] Screenshots of the implementation if the story includes UI changes. 15 | - [ ] Unit test cases. -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/di/modules/InterceptorsModule.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.di.modules 2 | 3 | import com.app.core.network.ServerInterceptor 4 | import com.celerik.app.network.NetworkConnectivityInterceptor 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.multibindings.IntoSet 8 | import okhttp3.Interceptor 9 | 10 | @Module 11 | abstract class InterceptorsModule { 12 | 13 | @Binds 14 | @IntoSet 15 | abstract fun bindsServerInterceptor(serverInterceptor: ServerInterceptor): Interceptor 16 | 17 | @Binds 18 | @IntoSet 19 | abstract fun bindsNetworkConnectivityInterceptor(networkConnectivityInterceptor: NetworkConnectivityInterceptor): Interceptor 20 | } 21 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION 🌈' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | categories: 4 | - title: '🚀 Features' 5 | labels: 6 | - 'feature' 7 | - 'task' 8 | - title: '🐛 Bug Fixes' 9 | labels: 10 | - 'bugfix' 11 | - 'hotfix' 12 | - title: '🧰 Maintenance' 13 | labels: 14 | - 'maintenance' 15 | - 'documentation' 16 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 17 | version-resolver: 18 | major: 19 | labels: 20 | - 'major' 21 | minor: 22 | labels: 23 | - 'minor' 24 | patch: 25 | labels: 26 | - 'patch' 27 | default: patch 28 | template: | 29 | ## Changes 30 | 31 | $CHANGES 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/di/modules/BaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.di.modules 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.lifecycle.ViewModelProvider 6 | import com.app.core.interfaces.AppResources 7 | import com.celerik.app.data.CelerikResources 8 | import com.celerik.app.di.ViewModelFactory 9 | import dagger.Binds 10 | import dagger.Module 11 | 12 | @Module 13 | abstract class BaseModule { 14 | @Binds 15 | abstract fun bindViewModelFactory(viewModelFactory: ViewModelFactory): ViewModelProvider.Factory 16 | 17 | @Binds 18 | abstract fun bindContext(celerikApp: Application): Context 19 | 20 | @Binds 21 | abstract fun bindResources(celerikResources: CelerikResources): AppResources 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/network/NetworkConnectivityInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.network 2 | 3 | import com.app.core.exceptions.NoConnectionException 4 | import com.app.core.network.ServerException 5 | import okhttp3.Interceptor 6 | import okhttp3.Response 7 | import java.io.IOException 8 | import javax.inject.Inject 9 | 10 | class NetworkConnectivityInterceptor @Inject constructor() : Interceptor { 11 | 12 | override fun intercept(chain: Interceptor.Chain): Response { 13 | try { 14 | val request = chain.request() 15 | return chain.proceed(request) 16 | } catch (serverException: ServerException) { 17 | throw serverException 18 | } catch (_: IOException) { 19 | throw NoConnectionException 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/celerik/app/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.celerik.app", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/data/SharedPreferencesHelper.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.data 2 | 3 | import com.app.base.interfaces.Cache 4 | import com.squareup.moshi.JsonAdapter 5 | import com.squareup.moshi.Moshi 6 | 7 | class SharedPreferencesHelper { 8 | 9 | inline fun getObject(key: String, cache: Cache): T? { 10 | val moshi = Moshi.Builder().build() 11 | val adapter: JsonAdapter = moshi.adapter(T::class.java) 12 | return adapter.fromJson(cache.readString(key).orEmpty()) 13 | } 14 | 15 | inline fun saveObject(obj: T, key: String, cache: Cache) { 16 | val moshi = Moshi.Builder().build() 17 | val jsonAdapter: JsonAdapter = moshi.adapter(T::class.java) 18 | val json: String = jsonAdapter.toJson(obj) 19 | cache.saveString(key, json) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /base/src/main/java/com/app/base/utils/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.app.base.utils 2 | 3 | import io.reactivex.rxjava3.disposables.Disposable 4 | import java.lang.Enum.valueOf 5 | 6 | fun Disposable.discard() { 7 | Unit 8 | } 9 | 10 | fun Boolean.toInt() = if (this) 1 else 0 11 | 12 | fun Double?.isGreaterThanOrEqualTo(value: Double) = this != null && this >= value 13 | 14 | inline fun > safeValueOf(type: String?): T? { 15 | return try { 16 | type?.let { valueOf(T::class.java, it.replace("-", "_")) } 17 | } catch (e: IllegalArgumentException) { 18 | null 19 | } 20 | } 21 | 22 | inline fun multiLet(vararg elements: T?, closure: (List) -> Unit): Unit? { 23 | return if (elements.all { it != null }) { 24 | closure(elements.filterNotNull()) 25 | } else { 26 | null 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/data/CelerikResources.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.data 2 | 3 | import android.content.Context 4 | import android.graphics.Color 5 | import androidx.core.content.ContextCompat 6 | import com.app.core.interfaces.AppResources 7 | import javax.inject.Inject 8 | 9 | class CelerikResources @Inject constructor(private val context: Context) : AppResources { 10 | 11 | override fun getString(resId: Int): String { 12 | return context.getString(resId) 13 | } 14 | 15 | override fun getString(resId: Int, vararg others: String): String { 16 | return context.getString(resId, *others) 17 | } 18 | 19 | override fun getColor(resId: Int): Int { 20 | return ContextCompat.getColor(context, resId) 21 | } 22 | 23 | override fun parseColor(color: String): Int { 24 | return Color.parseColor(color) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/di/modules/CelerikAppModule.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.di.modules 2 | 3 | import android.content.ContentResolver 4 | import android.content.Context 5 | import com.app.base.interfaces.Logger 6 | import com.app.core.interfaces.AppResources 7 | import com.celerik.app.viewModels.CelerikAppViewModel 8 | import dagger.Module 9 | import dagger.Provides 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | object CelerikAppModule { 14 | @Provides 15 | fun postCelerikAppViewModel( 16 | logger: Logger, 17 | appResources: AppResources, 18 | ): CelerikAppViewModel { 19 | return CelerikAppViewModel( 20 | logger, 21 | appResources, 22 | ) 23 | } 24 | 25 | @Provides 26 | @Singleton 27 | fun providesContentResolver(context: Context): ContentResolver { 28 | return context.contentResolver 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | -------------------------------------------------------------------------------- /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.kts. 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 | -keep class androidx.lifecycle.DefaultLifecycleObserver 23 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.app.base.interfaces.Logger 6 | import com.celerik.app.databinding.ActivityMainBinding 7 | import com.celerik.components.utils.viewBinding 8 | import dagger.android.AndroidInjection 9 | import javax.inject.Inject 10 | 11 | /** 12 | * Represents main activity. 13 | * 14 | * This is the orchestrator of app's views. 15 | */ 16 | class MainActivity : AppCompatActivity() { 17 | 18 | @Inject 19 | lateinit var logger: Logger 20 | 21 | private val binding by viewBinding(ActivityMainBinding::inflate) 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | AndroidInjection.inject(this) 25 | 26 | super.onCreate(savedInstanceState) 27 | setContentView(binding.root) 28 | 29 | logger.d("MainActivity started") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /core-test/src/main/java/com/app/core/test/utils/RxSchedulerExtension.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.test.utils 2 | 3 | import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins 4 | import io.reactivex.rxjava3.plugins.RxJavaPlugins 5 | import io.reactivex.rxjava3.schedulers.Schedulers 6 | import org.junit.jupiter.api.extension.AfterEachCallback 7 | import org.junit.jupiter.api.extension.BeforeEachCallback 8 | import org.junit.jupiter.api.extension.ExtensionContext 9 | 10 | class RxSchedulerExtension : AfterEachCallback, BeforeEachCallback { 11 | 12 | private val scheduler by lazy { Schedulers.trampoline() } 13 | 14 | override fun beforeEach(context: ExtensionContext) { 15 | RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler } 16 | RxJavaPlugins.setIoSchedulerHandler { scheduler } 17 | } 18 | 19 | override fun afterEach(context: ExtensionContext) { 20 | RxAndroidPlugins.reset() 21 | RxJavaPlugins.reset() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /base/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | id("kotlin") 4 | id("jacoco") 5 | id("plugins.jacoco-report") 6 | kotlin("kapt") 7 | } 8 | 9 | dependencies { 10 | 11 | implementation(Libraries.kotlinJDK) 12 | 13 | implementation(Libraries.javaInject) 14 | implementation(Libraries.rxJava) 15 | 16 | implementation(Libraries.moshi) 17 | kapt(AnnotationProcessors.moshiCodegen) 18 | 19 | Libraries.suiteTest.forEach { testImplementation(it) } 20 | } 21 | 22 | afterEvaluate { 23 | val function = extra.get("generateCheckCoverageTasks") as (File, String, Coverage, List, List) -> Unit 24 | function.invoke( 25 | buildDir, 26 | "test", 27 | Coverage( 28 | instructions = 8.24, 29 | lines = 12.31, 30 | complexity = 12.82, 31 | methods = 9.68, 32 | classes = 9.09 33 | ), 34 | emptyList(), 35 | listOf("**/base/data/**", "**/base/Either*", "**/base/utils/Tuples*") 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew install fastlane` 16 | 17 | # Available Actions 18 | ## Android 19 | ### android run_checks 20 | ``` 21 | fastlane android run_checks 22 | ``` 23 | Run Checks for the app 24 | ### android distribute_staging 25 | ``` 26 | fastlane android distribute_staging 27 | ``` 28 | Distribute the App using Firebase App Distribution 29 | 30 | ---- 31 | 32 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 33 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 34 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 35 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | 8 | android.useAndroidX=true 9 | android.enableJetifier=true 10 | #android.enableR8.fullMode=true // Produces an error for the moshi objects with default values 11 | android.bundle.enableUncompressedNativeLibs=false 12 | androidx.databinding.enableV2=true 13 | 14 | kotlin.code.style=official 15 | kotlin.incremental=true 16 | 17 | org.gradle.jvmargs=-XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -Xmx1536m --illegal-access=permit 18 | org.gradle.parallel=true 19 | org.gradle.daemon=true 20 | org.gradle.configureondemand=true 21 | org.gradle.caching=true 22 | 23 | kapt.verbose=true 24 | kapt.use.worker.api=true 25 | kapt.incremental.apt=true 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /components/src/main/java/com/celerik/components/utils/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.components.utils 2 | 3 | import android.os.Parcelable 4 | import androidx.fragment.app.Fragment 5 | import java.io.Serializable 6 | 7 | inline fun Fragment.getSerializableArgument(key: String, default: T? = null): T { 8 | val isNullable = null is T 9 | var value = arguments?.getSerializable(key) 10 | if (value == null || value !is T) { 11 | value = default 12 | } 13 | if (value == null && !isNullable) { 14 | throw Exception("Unable to get serializable $key from bundle argument") 15 | } 16 | return value as T 17 | } 18 | 19 | inline fun Fragment.getParcelableArgument(key: String, default: T? = null): T { 20 | val isNullable = null is T 21 | var value = arguments?.getParcelable(key) 22 | if (value == null) { 23 | value = default 24 | } 25 | if (value == null && !isNullable) { 26 | throw Exception("Unable to get parcelable $key from bundle argument") 27 | } 28 | return value as T 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/CelerikLogger.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app 2 | 3 | import com.app.base.interfaces.Logger 4 | import timber.log.Timber 5 | import javax.inject.Inject 6 | 7 | class CelerikLogger @Inject constructor(tree: Timber.Tree) : Logger { 8 | 9 | init { 10 | Timber.plant(tree) 11 | } 12 | 13 | override fun v(message: String, throwable: Throwable?) { 14 | Timber.v(throwable, message) 15 | } 16 | 17 | override fun d(message: String, throwable: Throwable?) { 18 | Timber.d(throwable, message) 19 | } 20 | 21 | override fun i(message: String, throwable: Throwable?) { 22 | Timber.i(throwable, message) 23 | } 24 | 25 | override fun w(message: String, throwable: Throwable?) { 26 | Timber.w(throwable, message) 27 | } 28 | 29 | override fun e(message: String, throwable: Throwable?) { 30 | Timber.e(throwable, message) 31 | } 32 | 33 | override fun http(url: String, method: String, request: String?, response: String?, statusCode: Int?) { 34 | Timber.d("$method: $url, $request\n$response\n$statusCode") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/di/ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.di 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import javax.inject.Inject 6 | import javax.inject.Provider 7 | 8 | @Suppress("UNCHECKED_CAST") 9 | class ViewModelFactory @Inject 10 | constructor( 11 | private val creators: Map, @JvmSuppressWildcards Provider> 12 | ) : ViewModelProvider.Factory { 13 | 14 | override fun create(modelClass: Class): T { 15 | var creator: Provider? = creators[modelClass] 16 | if (creator == null) { 17 | for ((key, value) in creators) { 18 | if (modelClass.isAssignableFrom(key)) { 19 | creator = value 20 | break 21 | } 22 | } 23 | } 24 | if (creator == null) { 25 | throw IllegalArgumentException("unknown model class $modelClass") 26 | } 27 | try { 28 | return creator.get() as T 29 | } catch (e: Exception) { 30 | throw RuntimeException(e) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/data/SharedPreferencesCache.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.data 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import com.app.base.interfaces.Cache 6 | 7 | class SharedPreferencesCache(name: String, context: Context) : Cache { 8 | 9 | private var sharedPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE) 10 | private val editor: SharedPreferences.Editor 11 | 12 | init { 13 | editor = sharedPreferences.edit() 14 | } 15 | 16 | override fun saveString(key: String, value: String) { 17 | editor.putString(key, value) 18 | editor.commit() 19 | } 20 | 21 | override fun readString(key: String): String? { 22 | return sharedPreferences.getString(key, null) 23 | } 24 | 25 | override fun removeValue(key: String) { 26 | editor.remove(key) 27 | editor.apply() 28 | } 29 | 30 | override fun containsValues(): Boolean { 31 | return sharedPreferences.all.isNotEmpty() 32 | } 33 | 34 | override fun clearAll() { 35 | editor.clear() 36 | editor.apply() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /components/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | kotlin("android") 4 | kotlin("kapt") 5 | } 6 | 7 | android { 8 | compileSdkVersion(Api.compileSDK) 9 | defaultConfig { 10 | minSdkVersion(Api.minSDK) 11 | targetSdkVersion(Api.targetSDK) 12 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 13 | } 14 | lintOptions { 15 | isAbortOnError = false 16 | } 17 | 18 | buildFeatures { 19 | dataBinding = true 20 | viewBinding = true 21 | } 22 | } 23 | 24 | dependencies { 25 | implementation(Libraries.multidex) 26 | 27 | implementation(Libraries.kotlinJDK) 28 | implementation(Libraries.appcompat) 29 | implementation(Libraries.activityKtx) 30 | implementation(Libraries.androidXCore) 31 | implementation(Libraries.lifeCycleCommonJava8) 32 | implementation(Libraries.constraintLayout) 33 | implementation(Libraries.material) 34 | 35 | Libraries.suiteTest.forEach { testImplementation(it) } 36 | 37 | androidTestImplementation(Libraries.jUnitExtKtx) 38 | androidTestImplementation(Libraries.espressoCore) 39 | } 40 | -------------------------------------------------------------------------------- /core-test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | kotlin("android") 4 | kotlin("kapt") 5 | } 6 | 7 | android { 8 | compileSdkVersion(Api.compileSDK) 9 | defaultConfig { 10 | minSdkVersion(Api.minSDK) 11 | targetSdkVersion(Api.targetSDK) 12 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 13 | } 14 | lintOptions { 15 | isAbortOnError = false 16 | } 17 | } 18 | 19 | dependencies { 20 | implementation(Libraries.multidex) 21 | implementation(Libraries.jUnit5) 22 | implementation(Libraries.kotlinJDK) 23 | implementation(Libraries.appcompat) 24 | implementation(Libraries.androidXCore) 25 | 26 | implementation(Libraries.rxJava) 27 | implementation(Libraries.rxAndroid) 28 | 29 | implementation(Libraries.moshi) 30 | kapt(AnnotationProcessors.moshiCodegen) 31 | 32 | implementation(Libraries.retrofit) 33 | implementation(Libraries.retrofitMoshi) 34 | implementation(Libraries.retrofitRxJava) 35 | 36 | androidTestImplementation(Libraries.jUnitExtKtx) 37 | androidTestImplementation(Libraries.espressoCore) 38 | } 39 | -------------------------------------------------------------------------------- /core-test/src/main/java/com/app/core/test/utils/InstantExecutorExtension.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.test.utils 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.arch.core.executor.ArchTaskExecutor 5 | import androidx.arch.core.executor.TaskExecutor 6 | import org.junit.jupiter.api.extension.AfterAllCallback 7 | import org.junit.jupiter.api.extension.BeforeAllCallback 8 | import org.junit.jupiter.api.extension.ExtensionContext 9 | 10 | class InstantExecutorExtension : BeforeAllCallback, AfterAllCallback { 11 | 12 | @SuppressLint("RestrictedApi") 13 | override fun beforeAll(context: ExtensionContext?) { 14 | ArchTaskExecutor.getInstance() 15 | .setDelegate(object : TaskExecutor() { 16 | override fun executeOnDiskIO(runnable: Runnable) = runnable.run() 17 | 18 | override fun postToMainThread(runnable: Runnable) = runnable.run() 19 | 20 | override fun isMainThread(): Boolean = true 21 | }) 22 | } 23 | 24 | @SuppressLint("RestrictedApi") 25 | override fun afterAll(context: ExtensionContext?) { 26 | ArchTaskExecutor.getInstance().setDelegate(null) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/pr-pipeline-workflow.yml: -------------------------------------------------------------------------------- 1 | name: PR Pipeline Workflow 2 | 3 | # Execute PR Pipeline Workflow on any pull requests 4 | on: pull_request 5 | 6 | jobs: 7 | ############################################################## 8 | # Unit Test Job: 9 | # Install dependencies, run Kotlin linter and execute unit tests 10 | ############################################################## 11 | run_checks: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Set up JDK 18 | uses: actions/setup-java@v2 19 | with: 20 | distribution: 'adopt' 21 | java-version: '11' 22 | 23 | - name: Set up Ruby 2.7 24 | uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: 2.7.2 27 | 28 | - name: Install Dependencies 29 | run: gem install bundler:2.2.11 && bundle install 30 | 31 | - name: Run Fastlane Unit Test Lane 32 | run: bundle exec fastlane run_checks 33 | 34 | - name: JUnit Report Action 35 | uses: mikepenz/action-junit-report@v2 36 | if: ${{ always() }} 37 | with: 38 | report_paths: '**/test-results/**/TEST-*.xml' 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/useCases/VerifyInternetConnectivityUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.useCases 2 | 3 | import com.app.base.interfaces.SingleUseCase 4 | import com.app.core.di.OkHttpClientBasic 5 | import io.reactivex.rxjava3.core.Single 6 | import okhttp3.OkHttpClient 7 | import okhttp3.Request 8 | import javax.inject.Inject 9 | 10 | private const val SETTINGS_HOST = "https://www.google.com" 11 | 12 | class VerifyInternetConnectivityUseCase @Inject constructor( 13 | @OkHttpClientBasic private val okHttpClient: OkHttpClient 14 | ) : SingleUseCase() { 15 | override fun execute(input: Unit): Single { 16 | return checkInternetConnectivity() 17 | } 18 | 19 | private fun checkInternetConnectivity(): Single { 20 | return Single.create { emitter -> 21 | var hasInternetConnectivity = true 22 | 23 | try { 24 | val request = Request.Builder() 25 | .url(SETTINGS_HOST) 26 | .build() 27 | 28 | okHttpClient.newCall(request).execute().close() 29 | } catch (e: Exception) { 30 | e.printStackTrace() 31 | hasInternetConnectivity = false 32 | } 33 | 34 | emitter.onSuccess(hasInternetConnectivity) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # OSL files 30 | .DS_Store 31 | 32 | # Android Studio Navigation editor temp files 33 | .navigation/ 34 | 35 | # Android Studio captures folder 36 | captures/ 37 | 38 | # IntelliJ 39 | *.iml 40 | .idea/ 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/dictionaries 46 | .idea/libraries 47 | .idea/caches 48 | 49 | # Keystore files 50 | # Uncomment the following line if you do not want to check your keystore files in. 51 | #*.jks 52 | 53 | # External native build folder generated in Android Studio 2.2 and later 54 | .externalNativeBuild 55 | 56 | # fastlane 57 | fastlane/report.xml 58 | fastlane/Preview.html 59 | fastlane/screenshots 60 | fastlane/test_output 61 | fastlane/readme.md 62 | fastlane/.env.default 63 | /app/staging/release/output.json 64 | publish.json 65 | -------------------------------------------------------------------------------- /components/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 19 | 20 | 23 | 24 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/Event.kt: -------------------------------------------------------------------------------- 1 | package com.app.core 2 | 3 | import androidx.lifecycle.Observer 4 | 5 | // Source: https://github.com/google/iosched licensed under the Apache License, Version 2.0 (the "License") 6 | 7 | /** 8 | * Used as a wrapper for data that is exposed via a LiveData that represents an event. 9 | */ 10 | open class Event(private val content: T) { 11 | 12 | var hasBeenHandled = false 13 | private set // Allow external read but not write 14 | 15 | /** 16 | * Returns the content and prevents its use again. 17 | */ 18 | fun getContentIfNotHandled(): T? { 19 | return if (hasBeenHandled) { 20 | null 21 | } else { 22 | hasBeenHandled = true 23 | content 24 | } 25 | } 26 | 27 | /** 28 | * Returns the content, even if it's already been handled. 29 | */ 30 | fun peekContent(): T = content 31 | } 32 | 33 | /** 34 | * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has 35 | * already been handled. 36 | * 37 | * [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled. 38 | */ 39 | class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> { 40 | override fun onChanged(event: Event?) { 41 | event?.getContentIfNotHandled()?.let { value -> 42 | onEventUnhandledContent(value) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/di/components/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.di.components 2 | 3 | import android.app.Application 4 | import com.app.core.CoreComponent 5 | import com.celerik.app.CelerikApp 6 | import com.celerik.app.di.modules.ActivityModule 7 | import com.celerik.app.di.modules.BaseModule 8 | import com.celerik.app.di.modules.CelerikAppModule 9 | import com.celerik.app.di.modules.CelerikUseCasesModule 10 | import com.celerik.app.di.modules.FragmentModule 11 | import com.celerik.app.di.modules.IntentsModule 12 | import com.celerik.app.di.modules.LoggerModule 13 | import com.celerik.app.di.modules.NetworkModule 14 | import dagger.BindsInstance 15 | import dagger.Component 16 | import dagger.android.AndroidInjectionModule 17 | import dagger.android.AndroidInjector 18 | import dagger.android.support.AndroidSupportInjectionModule 19 | import javax.inject.Singleton 20 | 21 | @Singleton 22 | @Component( 23 | modules = [ 24 | IntentsModule::class, 25 | AndroidInjectionModule::class, 26 | AndroidSupportInjectionModule::class, 27 | BaseModule::class, 28 | NetworkModule::class, 29 | ActivityModule::class, 30 | FragmentModule::class, 31 | LoggerModule::class, 32 | CelerikAppModule::class, 33 | CelerikUseCasesModule::class, 34 | ] 35 | ) 36 | interface AppComponent : CoreComponent, AndroidInjector { 37 | 38 | @Component.Builder 39 | interface Builder { 40 | @BindsInstance 41 | fun application(application: Application): Builder 42 | 43 | fun build(): AppComponent 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("de.mannodermaus.android-junit5") 4 | kotlin("android") 5 | kotlin("kapt") 6 | } 7 | 8 | android { 9 | compileSdkVersion(Api.compileSDK) 10 | defaultConfig { 11 | minSdkVersion(Api.minSDK) 12 | targetSdkVersion(Api.targetSDK) 13 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | lintOptions { 16 | isAbortOnError = false 17 | } 18 | 19 | buildFeatures { 20 | dataBinding = true 21 | viewBinding = true 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation(project(":base")) 27 | 28 | implementation(Libraries.multidex) 29 | 30 | implementation(Libraries.kotlinJDK) 31 | implementation(Libraries.appcompat) 32 | implementation(Libraries.androidXCore) 33 | implementation(Libraries.constraintLayout) 34 | implementation(Libraries.glide) 35 | implementation(Libraries.recyclerView) 36 | 37 | implementation(Libraries.retrofit) 38 | implementation(Libraries.retrofitMoshi) 39 | implementation(Libraries.retrofitRxJava) 40 | implementation(platform(Libraries.okHttpBoM)) 41 | implementation(Libraries.okHttpInterceptor) 42 | implementation(Libraries.moshi) 43 | kapt(AnnotationProcessors.moshiCodegen) 44 | 45 | implementation(Libraries.dagger) 46 | kapt(AnnotationProcessors.dagger) 47 | 48 | Libraries.suiteTest.forEach { testImplementation(it) } 49 | testImplementation(project(":core-test")) 50 | 51 | androidTestImplementation(Libraries.jUnitExtKtx) 52 | androidTestImplementation(Libraries.espressoCore) 53 | } 54 | -------------------------------------------------------------------------------- /components/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/test/java/com/celerik/app/viewModels/SplashViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.viewModels 2 | 3 | import com.app.base.interfaces.Logger 4 | import com.app.base.interfaces.SingleUseCase 5 | import com.app.core.Event 6 | import com.app.core.interfaces.AppResources 7 | import com.app.core.test.utils.InstantExecutorExtension 8 | import com.app.core.test.utils.RxSchedulerExtension 9 | import com.celerik.app.data.SplashNews 10 | import io.mockk.every 11 | import io.mockk.impl.annotations.MockK 12 | import io.mockk.junit5.MockKExtension 13 | import io.reactivex.rxjava3.core.Single 14 | import org.junit.jupiter.api.BeforeEach 15 | import org.junit.jupiter.api.Test 16 | import org.junit.jupiter.api.extension.ExtendWith 17 | import kotlin.test.assertEquals 18 | 19 | @ExtendWith(RxSchedulerExtension::class, InstantExecutorExtension::class, MockKExtension::class) 20 | class SplashViewModelTest { 21 | 22 | @MockK(relaxed = true) 23 | private lateinit var logger: Logger 24 | 25 | @MockK 26 | private lateinit var resources: AppResources 27 | 28 | @MockK(relaxed = true) 29 | private lateinit var verifyInternetConnectivityUseCase: SingleUseCase 30 | 31 | private lateinit var viewModel: SplashViewModel 32 | 33 | @BeforeEach 34 | fun setUp() { 35 | viewModel = SplashViewModel( 36 | logger, 37 | resources, 38 | verifyInternetConnectivityUseCase, 39 | ) 40 | } 41 | 42 | @Test 43 | fun `Should send network connectivity error event when getting no connection exception`() { 44 | // given 45 | every { verifyInternetConnectivityUseCase.execute(Unit) } returns Single.just(false) 46 | 47 | // when 48 | viewModel.onViewActive() 49 | 50 | // then 51 | assertEquals((viewModel.news.value as Event).peekContent(), SplashNews.ShowNoConnectivityView) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/staging/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /.github/workflows/app-distribution-pipeline-workflow.yml: -------------------------------------------------------------------------------- 1 | name: App Distribution Tag Workflow 2 | 3 | # Execute Pipeline Workflow on pushing tags with the regex specified 4 | on: 5 | push: 6 | tags: 7 | - 'v[0-9]+.[0-9]+.[0-9]+-Beta' 8 | 9 | jobs: 10 | ########################################################## 11 | # Distribute Job: 12 | # Generate APk and upload it to Firebase App Distribution 13 | ########################################################## 14 | distribute-to-testers: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout Repo 18 | uses: actions/checkout@v2 19 | 20 | - name: Set up JDK 21 | uses: actions/setup-java@v2 22 | with: 23 | distribution: 'adopt' 24 | java-version: '11' 25 | 26 | - name: Save Tag as Variable 27 | id: build_data 28 | run: | 29 | echo ::set-output name=build_name::${GITHUB_REF#refs/*/} 30 | echo ::set-output name=build_number::${GITHUB_RUN_NUMBER} 31 | 32 | - name: Set up Ruby 2.7 33 | uses: ruby/setup-ruby@v1 34 | with: 35 | ruby-version: 2.7.2 36 | 37 | - name: Install Dependencies 38 | run: gem install bundler:2.2.11 && bundle install 39 | 40 | - name: Upload to App distribution Staging 41 | run: bundle exec fastlane distribute_staging build_number:${{ steps.build_data.outputs.build_number }} version_name:${{ steps.build_data.outputs.build_name }} 42 | env: 43 | ENCODED_KEY: ${{ secrets.SIGNING_KEY_STAGING}} 44 | STORE_FILE: ${{ github.workspace }}/debug-keystore.jks 45 | KEY_ALIAS: ${{ secrets.ALIAS_STAGING }} 46 | STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD_STAGING }} 47 | KEY_PASSWORD: ${{ secrets.KEY_PASSWORD_STAGING }} 48 | FIREBASE_TOKEN: ${{ secrets.FIREBASE_REFRESH_TOKEN}} -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/CelerikApp.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app 2 | 3 | import android.os.StrictMode 4 | import android.os.StrictMode.ThreadPolicy 5 | import android.os.StrictMode.VmPolicy 6 | import androidx.lifecycle.ProcessLifecycleOwner 7 | import com.app.core.CoreApp 8 | import com.celerik.app.di.components.AppComponent 9 | import com.celerik.app.di.components.DaggerAppComponent 10 | import com.celerik.app.viewModels.CelerikAppViewModel 11 | import dagger.android.AndroidInjector 12 | import dagger.android.DispatchingAndroidInjector 13 | import dagger.android.HasAndroidInjector 14 | import javax.inject.Inject 15 | 16 | open class CelerikApp : CoreApp(), HasAndroidInjector { 17 | 18 | private lateinit var appComponent: AppComponent 19 | 20 | @Inject 21 | lateinit var dispatchingActivityInjector: DispatchingAndroidInjector 22 | 23 | @Inject 24 | lateinit var viewModel: CelerikAppViewModel 25 | 26 | override fun onCreate() { 27 | initializeStrictMode() 28 | super.onCreate() 29 | 30 | initializeComponent() 31 | ProcessLifecycleOwner.get().lifecycle.addObserver(viewModel) 32 | } 33 | 34 | private fun initializeComponent() { 35 | appComponent = DaggerAppComponent.builder() 36 | .application(this) 37 | .build() 38 | 39 | appComponent.inject(this) 40 | } 41 | 42 | override fun androidInjector(): AndroidInjector { 43 | return dispatchingActivityInjector 44 | } 45 | 46 | private fun initializeStrictMode() { 47 | if (BuildConfig.DEBUG) { 48 | StrictMode.setThreadPolicy( 49 | ThreadPolicy.Builder() 50 | .detectAll() 51 | .permitDiskReads() 52 | .penaltyLog() 53 | .build() 54 | ) 55 | StrictMode.setVmPolicy( 56 | VmPolicy.Builder() 57 | .detectLeakedSqlLiteObjects() 58 | .detectLeakedClosableObjects() 59 | .penaltyLog() 60 | .penaltyDeath() 61 | .build() 62 | ) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/di/CommonBindings.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.di 2 | 3 | import android.graphics.drawable.Drawable 4 | import android.text.method.LinkMovementMethod 5 | import android.text.style.ClickableSpan 6 | import android.view.View 7 | import android.widget.ImageView 8 | import android.widget.TextView 9 | import androidx.core.text.set 10 | import androidx.core.text.toSpannable 11 | import androidx.core.view.isVisible 12 | import androidx.databinding.BindingAdapter 13 | import androidx.recyclerview.widget.RecyclerView 14 | import com.bumptech.glide.Glide 15 | 16 | @BindingAdapter("loadImagePath", "errorImage", requireAll = false) 17 | fun bindingUriIntoImageView(imageView: ImageView, imagePath: String?, errorImage: Drawable?) { 18 | Glide.with(imageView.context) 19 | .load(imagePath) 20 | .error(errorImage) 21 | .into(imageView) 22 | } 23 | 24 | interface BindableAdapter { 25 | fun setData(data: T) 26 | } 27 | 28 | @BindingAdapter("data") 29 | @Suppress("UNCHECKED_CAST") 30 | fun setRecyclerViewProperties(recyclerView: RecyclerView, data: T) { 31 | (recyclerView.adapter as? BindableAdapter)?.setData(data) 32 | } 33 | 34 | @BindingAdapter("goneUnless") 35 | fun goneUnless(view: View, visible: Boolean) { 36 | view.isVisible = visible 37 | } 38 | 39 | @BindingAdapter("linkText", "onLinkClick", "text", requireAll = false) 40 | fun setLinkToTextView( 41 | view: TextView, 42 | linkText: String, 43 | onLinkClick: View.OnClickListener, 44 | text: CharSequence 45 | ) { 46 | val spannableString = text.toSpannable() 47 | 48 | spannableString.findAnyOf(listOf(linkText))?.let { 49 | val linkStart = it.first 50 | val linkEnd = it.first + it.second.length 51 | spannableString[linkStart..linkEnd] = object : ClickableSpan() { 52 | override fun onClick(widget: View) { 53 | onLinkClick.onClick(view) 54 | } 55 | } 56 | } 57 | view.movementMethod = LinkMovementMethod.getInstance() 58 | view.text = spannableString 59 | } 60 | -------------------------------------------------------------------------------- /.azuredevops/pipelines/pr-pipeline-workflow.yml: -------------------------------------------------------------------------------- 1 | name: PR Pipeline Workflow 2 | 3 | # Execute PR Pipeline Workflow on any pull requests. 4 | # This configuration was performed as a Azure's Build Policy 5 | 6 | resources: 7 | - repo: self 8 | 9 | jobs: 10 | ############################################################## 11 | # Unit Test Job: 12 | # Install dependencies, run Kotlin linter and execute unit tests 13 | ############################################################## 14 | - job: run_checks 15 | 16 | pool: 17 | vmImage: 'ubuntu-latest' 18 | 19 | steps: 20 | - task: JavaToolInstaller@0 21 | displayName: 'Set up JDK' 22 | inputs: 23 | versionSpec: '11' 24 | jdkArchitectureOption: 'x64' 25 | jdkSourceOption: 'PreInstalled' 26 | 27 | - task: SonarCloudPrepare@1 28 | displayName: 'Set up SonarCloud' 29 | inputs: 30 | SonarCloud: 'sc-sonar' 31 | organization: 'celerikdevops' 32 | scannerMode: 'CLI' 33 | configMode: 'file' 34 | configFile: '.sonarcloud.properties' 35 | 36 | - task: SonarCloudAnalyze@1 37 | displayName: 'Analyze source code' 38 | 39 | 40 | - task: SonarCloudPublish@1 41 | displayName: 'Publish SonarCloud Results' 42 | inputs: 43 | pollingTimeoutSec: '3000' 44 | 45 | - task: UseRubyVersion@0 46 | displayName: 'Set up Ruby 2.7' 47 | inputs: 48 | versionSpec: '2.7' 49 | 50 | - task: CmdLine@2 51 | displayName: 'Install Dependencies' 52 | inputs: 53 | script: 'gem install bundler:2.2.11 && bundle install' 54 | 55 | - task: CmdLine@2 56 | displayName: 'Run Fastlane run_checks Lane' 57 | inputs: 58 | script: 'bundle exec fastlane run_checks' 59 | 60 | - task: PublishTestResults@2 61 | displayName: 'Publish JUnit Test Results' 62 | inputs: 63 | testResultsFormat: 'JUnit' 64 | testResultsFiles: '**/test-results/**/TEST-*.xml' 65 | failTaskOnFailedTests: true -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /base/src/main/java/com/app/base/Optional.kt: -------------------------------------------------------------------------------- 1 | package com.app.base 2 | // From: https://github.com/gojuno/koptional/blob/master/koptional/src/main/kotlin/com/gojuno/koptional/Optional.kt 3 | 4 | sealed class Optional { 5 | 6 | /** 7 | * Converts [Optional] to either its non-`null` value if it’s [Some] or `null` if it’s [None]. 8 | */ 9 | abstract fun toNullable(): T? 10 | 11 | /** 12 | * Unwraps this [Optional] to either its non-`null` value if it’s [Some] or `null` if it’s [None]. 13 | */ 14 | @JvmSynthetic 15 | abstract operator fun component1(): T? 16 | 17 | companion object { 18 | 19 | /** 20 | * Wraps an instance of `T` (or `null`) into an [Optional]: 21 | * 22 | * ```java 23 | * Optional some = Optional.toOptional("value"); // Some("value") 24 | * Optional none = Optional.toOptional(null); // None 25 | * ``` 26 | * 27 | * This is the preferred method of obtaining an instance of [Optional] in Java. In Kotlin, 28 | * prefer using the [toOptional][com.gojuno.koptional.toOptional] extension function. 29 | */ 30 | @JvmStatic 31 | fun toOptional(value: T?): Optional = if (value == null) None else Some(value) 32 | } 33 | } 34 | 35 | data class Some(val value: T) : Optional() { 36 | override fun toNullable(): T = value 37 | override fun toString() = "Some($value)" 38 | } 39 | 40 | object None : Optional() { 41 | override fun component1(): Nothing? = null 42 | override fun toNullable(): Nothing? = null 43 | override fun toString() = "None" 44 | } 45 | 46 | /** 47 | * Wraps an instance of `T` (or `null`) into an [Optional]: 48 | * 49 | * ```kotlin 50 | * val someValue: String? = "value" 51 | * val noneValue: String? = null 52 | * 53 | * val some = someValue.toOptional() // Some("value") 54 | * val none = noneValue.toOptional() // None 55 | * ``` 56 | * 57 | * This is the preferred method of obtaining an instance of [Optional] in Kotlin. In Java, prefer 58 | * using the static [Optional.toOptional] method. 59 | */ 60 | fun T?.toOptional(): Optional = if (this == null) None else Some(this) 61 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | fastlane_version "2.168.0" 2 | 3 | default_platform(:android) 4 | 5 | platform :android do 6 | 7 | desc "Run Checks for the app" 8 | lane :run_checks do 9 | gradle(task: "ktlintcheck") 10 | unit_tests 11 | end 12 | 13 | desc "Distribute the App using Firebase App Distribution" 14 | lane :distribute_staging do |options| 15 | run_checks 16 | commit = last_git_commit 17 | version_name_hash = "#{options[:version_name]} - #{commit[:abbreviated_commit_hash]}" 18 | assemble(build_number:options[:build_number], version_name:version_name_hash, flavor:"stagingInternal") 19 | upload_to_firebase_staging 20 | end 21 | 22 | ###### Private lanes ###### 23 | 24 | desc "Execute unit tests" 25 | private_lane :unit_tests do 26 | gradle(task: "app:checkCoverage") 27 | gradle(task: "base:checkCoverage") 28 | gradle(task: "core:testDebugUnitTest") 29 | end 30 | 31 | desc "Assemble app" 32 | private_lane :assemble do |options| 33 | sh("echo #{ENV['ENCODED_KEY']} | base64 --decode > #{ENV['STORE_FILE']}") 34 | version_code = options[:build_number] 35 | version_name = options[:version_name] 36 | flavor = options[:flavor] 37 | build_android_app( 38 | task: "assemble", 39 | flavor: flavor, 40 | build_type: "release", 41 | flags: "-Pversion_code=#{version_code} -Pversion_name='#{version_name}'", 42 | properties: { 43 | "android.injected.signing.store.file" => "#{ENV['STORE_FILE']}", 44 | "android.injected.signing.store.password" => ENV['STORE_PASSWORD'], 45 | "android.injected.signing.key.alias" => ENV['KEY_ALIAS'], 46 | "android.injected.signing.key.password" => ENV['KEY_PASSWORD'], 47 | } 48 | ) 49 | end 50 | 51 | private_lane :upload_to_firebase_staging do 52 | sh("curl -sL https://firebase.tools | bash") 53 | firebase_app_distribution( 54 | app: "1:625987584363:android:5f8734ddeb49834bbdcc2e", 55 | groups: "testers", 56 | release_notes: "", 57 | firebase_cli_token: "#{ENV['FIREBASE_REFRESH_TOKEN']}", 58 | firebase_cli_path: "/usr/local/bin/firebase" 59 | ) 60 | end 61 | 62 | end 63 | -------------------------------------------------------------------------------- /components/src/main/java/com/celerik/components/utils/FragmentViewBingingProperty.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.components.utils 2 | 3 | import android.view.View 4 | import androidx.fragment.app.Fragment 5 | import androidx.lifecycle.DefaultLifecycleObserver 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.lifecycle.LifecycleOwner 8 | import androidx.lifecycle.Observer 9 | import androidx.viewbinding.ViewBinding 10 | import kotlin.properties.ReadOnlyProperty 11 | import kotlin.reflect.KProperty 12 | 13 | /* 14 | * From: https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c 15 | * and fix: https://itnext.io/an-update-to-the-fragmentviewbindingdelegate-the-bug-weve-inherited-from-autoclearedvalue-7fc0a89fcae1 16 | */ 17 | class FragmentViewBindingDelegate( 18 | val fragment: Fragment, 19 | val viewBindingFactory: (View) -> T 20 | ) : ReadOnlyProperty { 21 | private var _binding: T? = null 22 | 23 | init { 24 | fragment.lifecycle.addObserver(object : DefaultLifecycleObserver { 25 | val viewLifecycleOwnerLiveDataObserver = 26 | Observer { 27 | val viewLifecycleOwner = it ?: return@Observer 28 | 29 | viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { 30 | override fun onDestroy(owner: LifecycleOwner) { 31 | _binding = null 32 | } 33 | }) 34 | } 35 | 36 | override fun onCreate(owner: LifecycleOwner) { 37 | fragment.viewLifecycleOwnerLiveData.observeForever(viewLifecycleOwnerLiveDataObserver) 38 | } 39 | 40 | override fun onDestroy(owner: LifecycleOwner) { 41 | fragment.viewLifecycleOwnerLiveData.removeObserver(viewLifecycleOwnerLiveDataObserver) 42 | } 43 | }) 44 | } 45 | 46 | override fun getValue(thisRef: Fragment, property: KProperty<*>): T { 47 | val binding = _binding 48 | if (binding != null) { 49 | return binding 50 | } 51 | 52 | val lifecycle = fragment.viewLifecycleOwner.lifecycle 53 | if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { 54 | throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.") 55 | } 56 | 57 | return viewBindingFactory(thisRef.requireView()).also { _binding = it } 58 | } 59 | } 60 | 61 | fun Fragment.viewBinding(viewBindingFactory: (View) -> T) = 62 | FragmentViewBindingDelegate(this, viewBindingFactory) 63 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /core/src/main/java/com/app/core/network/ServerInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.app.core.network 2 | 3 | import com.app.base.data.HttpObject 4 | import com.app.base.interfaces.Logger 5 | import com.app.core.exceptions.NoConnectionException 6 | import okhttp3.Interceptor 7 | import okhttp3.Response 8 | import org.json.JSONObject 9 | import java.io.IOException 10 | import java.net.SocketTimeoutException 11 | import java.net.UnknownHostException 12 | import javax.inject.Inject 13 | 14 | class ServerInterceptor @Inject constructor(private val logger: Logger) : Interceptor { 15 | 16 | @Throws(ServerException::class, NoConnectionException::class) 17 | override fun intercept(chain: Interceptor.Chain): Response { 18 | val request = chain.request() 19 | try { 20 | val response = chain.proceed(request) 21 | val httpCode = response.code 22 | 23 | val method = request.method 24 | val endpoint = request.url.toString() 25 | 26 | if (httpCode in 400..500) { 27 | var responseBody: JSONObject? = null 28 | val serverException = try { 29 | responseBody = JSONObject(response.body?.string().orEmpty()) 30 | val code = responseBody.optString("code") 31 | val message = responseBody.optString("message") 32 | ServerException(code, message, httpCode) 33 | } catch (e: Exception) { 34 | ServerException("UNKNOWN", "GENERIC ERROR", httpCode, "$request $response") 35 | } 36 | 37 | val infoRequest = HttpObject( 38 | method, endpoint, responseBody.toString(), httpCode 39 | ) 40 | 41 | logger.http( 42 | endpoint, 43 | method, 44 | infoRequest.toString(), 45 | response = response.toString(), 46 | statusCode = httpCode 47 | ) 48 | 49 | logger.e("Server Exception ${serverException.message}", serverException) 50 | 51 | throw serverException 52 | } else { 53 | return response 54 | } 55 | } catch (serverException: ServerException) { 56 | throw serverException 57 | } catch (_: UnknownHostException) { 58 | throw NoConnectionException 59 | } catch (_: SocketTimeoutException) { 60 | throw NoConnectionException 61 | } catch (_: NoConnectionException) { 62 | throw NoConnectionException 63 | } catch (_: IOException) { 64 | throw NoConnectionException 65 | } catch (e: Exception) { 66 | throw ServerException("Network Issue", e.message.orEmpty()) 67 | } 68 | } 69 | } 70 | 71 | data class ServerException( 72 | val code: String, 73 | override val message: String = "", 74 | val httpCode: Int = 0, 75 | val extra: String = "" 76 | ) : IOException(message) 77 | -------------------------------------------------------------------------------- /app/src/staging/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "625987584363", 4 | "project_id": "android-kotlin-boilerpla-5284d", 5 | "storage_bucket": "android-kotlin-boilerpla-5284d.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:625987584363:android:7e68259c8f75f636bdcc2e", 11 | "android_client_info": { 12 | "package_name": "com.celerik.app" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "625987584363-0qj7506gdm715quiqfe983qbh76qffc9.apps.googleusercontent.com", 18 | "client_type": 1, 19 | "android_info": { 20 | "package_name": "com.celerik.app", 21 | "certificate_hash": "45ce1fa0132591f067a433f5e4c07ee3b0660ae7" 22 | } 23 | }, 24 | { 25 | "client_id": "625987584363-lss6tjbbu5vqbej5h3vsh74suclac806.apps.googleusercontent.com", 26 | "client_type": 3 27 | } 28 | ], 29 | "api_key": [ 30 | { 31 | "current_key": "AIzaSyCaI_Ja5j0_B2GHnJWhyF8JBU_oorY0Ttk" 32 | } 33 | ], 34 | "services": { 35 | "appinvite_service": { 36 | "other_platform_oauth_client": [ 37 | { 38 | "client_id": "625987584363-lss6tjbbu5vqbej5h3vsh74suclac806.apps.googleusercontent.com", 39 | "client_type": 3 40 | } 41 | ] 42 | } 43 | } 44 | }, 45 | { 46 | "client_info": { 47 | "mobilesdk_app_id": "1:625987584363:android:5f8734ddeb49834bbdcc2e", 48 | "android_client_info": { 49 | "package_name": "com.celerik.app.staging" 50 | } 51 | }, 52 | "oauth_client": [ 53 | { 54 | "client_id": "625987584363-njmsrhegh1e4h3keokfdfk0jdrjpapaj.apps.googleusercontent.com", 55 | "client_type": 1, 56 | "android_info": { 57 | "package_name": "com.celerik.app.staging", 58 | "certificate_hash": "22c2d9ca0c19471ffd58018e9001e2c62345492a" 59 | } 60 | }, 61 | { 62 | "client_id": "625987584363-lss6tjbbu5vqbej5h3vsh74suclac806.apps.googleusercontent.com", 63 | "client_type": 3 64 | } 65 | ], 66 | "api_key": [ 67 | { 68 | "current_key": "AIzaSyCaI_Ja5j0_B2GHnJWhyF8JBU_oorY0Ttk" 69 | } 70 | ], 71 | "services": { 72 | "appinvite_service": { 73 | "other_platform_oauth_client": [ 74 | { 75 | "client_id": "625987584363-lss6tjbbu5vqbej5h3vsh74suclac806.apps.googleusercontent.com", 76 | "client_type": 3 77 | } 78 | ] 79 | } 80 | } 81 | } 82 | ], 83 | "configuration_version": "1" 84 | } -------------------------------------------------------------------------------- /components/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1dp 4 | 4dp 5 | 6dp 6 | 2sp 7 | 8 | 32dp 9 | 50dp 10 | 11 | 23dp 12 | 140dp 13 | 14 | 25dp 15 | 28dp 16 | 40dp 17 | 49dp 18 | 60dp 19 | 70dp 20 | 40dp 21 | 50dp 22 | 40dp 23 | 24 | 100dp 25 | 26 | 0.5dp 27 | 1dp 28 | 2dp 29 | 4dp 30 | 6dp 31 | 8dp 32 | 10dp 33 | 12dp 34 | 14dp 35 | 16dp 36 | 18dp 37 | 20dp 38 | 24dp 39 | 27dp 40 | 30dp 41 | 32dp 42 | 35dp 43 | 38dp 44 | 40dp 45 | 42dp 46 | 48dp 47 | 48 | 8sp 49 | 9sp 50 | 10sp 51 | 13sp 52 | 20sp 53 | 26sp 54 | 55 | 56 | 24sp 57 | 18sp 58 | 16sp 59 | 14sp 60 | 12sp 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/viewModels/SplashViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.viewModels 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import com.app.base.interfaces.Logger 6 | import com.app.base.interfaces.SingleUseCase 7 | import com.app.core.BaseViewModel 8 | import com.app.core.Event 9 | import com.app.core.exceptions.NoConnectionException 10 | import com.app.core.interfaces.AppResources 11 | import com.app.core.network.ServerException 12 | import com.app.core.qualifiers.VerifyInternet 13 | import com.celerik.app.R 14 | import com.celerik.app.data.SplashNews 15 | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers 16 | import io.reactivex.rxjava3.core.Completable 17 | import io.reactivex.rxjava3.schedulers.Schedulers 18 | import retrofit2.HttpException 19 | import java.net.ConnectException 20 | import javax.inject.Inject 21 | 22 | class SplashViewModel @Inject constructor( 23 | private val logger: Logger, 24 | private val resources: AppResources, 25 | @VerifyInternet private val verifyInternetConnectivityUseCase: SingleUseCase, 26 | ) : BaseViewModel() { 27 | 28 | private val _news = MutableLiveData>() 29 | val news: LiveData> = _news 30 | 31 | fun onViewActive() { 32 | disposables.add( 33 | validateConnectivity() 34 | .subscribeOn(Schedulers.io()) 35 | .observeOn(AndroidSchedulers.mainThread()) 36 | .subscribe({ 37 | _news.value = Event(SplashNews.AppInitialized) 38 | }) { 39 | handleError(it) 40 | } 41 | ) 42 | } 43 | 44 | fun retryInitialize() { 45 | onViewActive() 46 | } 47 | 48 | private fun validateConnectivity(): Completable { 49 | return Completable.defer { 50 | verifyInternetConnectivityUseCase.execute(Unit) 51 | .flatMapCompletable { isConnected -> 52 | if (isConnected) { 53 | Completable.complete() 54 | } else { 55 | Completable.error(NoConnectionException) 56 | } 57 | } 58 | } 59 | } 60 | 61 | private fun handleError(throwable: Throwable) { 62 | when (throwable) { 63 | is ConnectException -> { 64 | _news.value = 65 | Event(SplashNews.ShowErrorNews(resources.getString(R.string.connection_error))) 66 | } 67 | is ServerException -> { 68 | _news.value = Event(SplashNews.ShowErrorNews(throwable.message)) 69 | } 70 | is HttpException -> { 71 | val message = throwable.response()?.errorBody()?.string() 72 | _news.value = Event(SplashNews.ShowErrorNews(message.toString())) 73 | } 74 | is NoConnectionException -> { 75 | _news.value = Event(SplashNews.ShowNoConnectivityView) 76 | } 77 | is UpdateRequiredException -> { 78 | _news.value = Event(SplashNews.FinishSplashNews) 79 | logger.d("SplashViewModel InitializeException", throwable) 80 | } 81 | else -> { 82 | _news.value = Event(SplashNews.ShowErrorNews(throwable.message.toString())) 83 | logger.e("SplashViewModel handle error", throwable) 84 | } 85 | } 86 | } 87 | 88 | private class UpdateRequiredException : Exception() 89 | } 90 | -------------------------------------------------------------------------------- /base/src/test/kotlin/com/app/base/utils/ExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | package com.app.base.utils 2 | 3 | import io.mockk.junit5.MockKExtension 4 | import io.reactivex.rxjava3.disposables.Disposable 5 | import org.junit.jupiter.api.Test 6 | import org.junit.jupiter.api.extension.ExtendWith 7 | import org.junit.jupiter.params.ParameterizedTest 8 | import org.junit.jupiter.params.provider.Arguments 9 | import org.junit.jupiter.params.provider.MethodSource 10 | import java.util.stream.Stream 11 | 12 | @ExtendWith(MockKExtension::class) 13 | class ExtensionsTest { 14 | 15 | companion object { 16 | @JvmStatic 17 | private fun `Should return an Int value when boolean toInt is invoked`() = Stream.of( 18 | Arguments.of(true, 1), 19 | Arguments.of(false, 0) 20 | ) 21 | 22 | @JvmStatic 23 | private fun `Should return Unit or null when multiLet is either activated or not respectively`() = Stream.of( 24 | Arguments.of(null, null), 25 | Arguments.of("value3", Unit) 26 | ) 27 | 28 | @JvmStatic 29 | private fun `Should return enum element when a string one is sent`() = Stream.of( 30 | Arguments.of(null, null), 31 | Arguments.of("CAR", Vehicles.CAR) 32 | ) 33 | 34 | @JvmStatic 35 | private fun `Should return true when value1 is greater or equal than value2`() = Stream.of( 36 | Arguments.of(0, 1, false), 37 | Arguments.of(5, 3, true), 38 | Arguments.of(5, 5, true) 39 | ) 40 | } 41 | 42 | @Test 43 | fun `Should return Unit when disposable discard is invoked`() { 44 | // given 45 | val testDisposable = Disposable.fromAction { } 46 | 47 | // when 48 | val returnedValue = testDisposable.discard() 49 | 50 | // then 51 | assert(returnedValue == Unit) 52 | } 53 | 54 | @ParameterizedTest 55 | @MethodSource 56 | fun `Should return enum element when a string one is sent`(enumElementInString: String?, expectedValue: Vehicles?) { 57 | // when 58 | val enumElement = safeValueOf(enumElementInString) 59 | 60 | // then 61 | assert(enumElement == expectedValue) 62 | } 63 | 64 | @ParameterizedTest 65 | @MethodSource 66 | fun `Should return an Int value when boolean toInt is invoked`(booleanValue: Boolean, expectedIntValue: Int) { 67 | // when 68 | val intValue = booleanValue.toInt() 69 | 70 | // then 71 | assert(intValue == expectedIntValue) 72 | } 73 | 74 | @ParameterizedTest 75 | @MethodSource 76 | fun `Should return Unit or null when multiLet is either activated or not respectively`(val3: String?, expectedResult: Unit?) { 77 | // given 78 | val val1 = "asd" 79 | val val2 = 512 80 | 81 | // when 82 | val multiLetResult = multiLet(val1, val2, val3) { 83 | // no-op by default 84 | } 85 | 86 | // then 87 | assert(multiLetResult == expectedResult) 88 | } 89 | 90 | @ParameterizedTest 91 | @MethodSource 92 | fun `Should return true when value1 is greater or equal than value2`(value1: Double, value2: Double, expectedResult: Boolean) { 93 | // when 94 | val result = value1.isGreaterThanOrEqualTo(value2) 95 | 96 | // then 97 | assert(result == expectedResult) 98 | } 99 | 100 | enum class Vehicles { 101 | CAR 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.os.Handler 6 | import android.os.Looper 7 | import androidx.activity.viewModels 8 | import androidx.appcompat.app.AlertDialog 9 | import androidx.appcompat.app.AppCompatActivity 10 | import androidx.lifecycle.ViewModelProvider 11 | import com.app.base.interfaces.Logger 12 | import com.app.base.others.ONE_SECOND_IN_MILLISECONDS 13 | import com.app.core.EventObserver 14 | import com.celerik.app.data.SplashNews 15 | import com.celerik.app.databinding.ActivitySplashBinding 16 | import com.celerik.app.viewModels.SplashViewModel 17 | import com.celerik.components.utils.viewBinding 18 | import com.google.android.material.snackbar.Snackbar 19 | import dagger.android.AndroidInjection 20 | import javax.inject.Inject 21 | 22 | /** 23 | * Represents splash activity. 24 | * 25 | * This is the first screen the user will watch. 26 | */ 27 | class SplashActivity : AppCompatActivity() { 28 | 29 | @Inject 30 | lateinit var logger: Logger 31 | 32 | @Inject 33 | lateinit var viewModelFactory: ViewModelProvider.Factory 34 | 35 | private val viewModel: SplashViewModel by viewModels { viewModelFactory } 36 | 37 | private val binding by viewBinding(ActivitySplashBinding::inflate) 38 | 39 | override fun onCreate(savedInstanceState: Bundle?) { 40 | AndroidInjection.inject(this) 41 | 42 | super.onCreate(savedInstanceState) 43 | 44 | setContentView(binding.root) 45 | 46 | showVersionName() 47 | 48 | initializeSubscription() 49 | initializeApp() 50 | 51 | logger.d("SplashActivity started") 52 | } 53 | 54 | private fun initializeApp() { 55 | Handler(Looper.getMainLooper()).postDelayed( 56 | { 57 | viewModel.onViewActive() 58 | }, 59 | ONE_SECOND_IN_MILLISECONDS 60 | ) 61 | } 62 | 63 | private fun initializeSubscription() { 64 | viewModel.news.observe(this, EventObserver { handleNews(it) }) 65 | } 66 | 67 | private fun showVersionName() { 68 | binding.textViewVersion.text = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" 69 | } 70 | 71 | private fun handleAppInitialized() { 72 | val mainIntent = Intent(this, MainActivity::class.java) 73 | startActivity(mainIntent) 74 | finish() 75 | } 76 | 77 | private fun handleNews(news: SplashNews) { 78 | when (news) { 79 | is SplashNews.AppInitialized -> { 80 | handleAppInitialized() 81 | } 82 | is SplashNews.ShowErrorNews -> { 83 | Snackbar.make(binding.root, news.errorMessage, Snackbar.LENGTH_INDEFINITE).show() 84 | } 85 | is SplashNews.ShowNoConnectivityView -> { 86 | showNoConnectionAlert() 87 | } 88 | is SplashNews.FinishSplashNews -> { 89 | finish() 90 | } 91 | } 92 | } 93 | 94 | private fun showNoConnectionAlert() { 95 | val title = resources.getString(R.string.no_internet_title) 96 | val message = resources.getString(R.string.no_internet_description) 97 | val positiveButtonText = resources.getString(R.string.no_internet_retry_button) 98 | 99 | val dialog: AlertDialog.Builder = AlertDialog.Builder(this, R.style.CustomAlertDialog) 100 | dialog.apply { 101 | setTitle(title) 102 | setMessage(message) 103 | setCancelable(false) 104 | setPositiveButton(positiveButtonText) { dialog, _ -> 105 | viewModel.retryInitialize() 106 | dialog.dismiss() 107 | } 108 | show() 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Celerik 3 | 4 | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=celerik_android-kotlin-boilerplate&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=celerik_android-kotlin-boilerplate) [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=celerik_android-kotlin-boilerplate&metric=code_smells)](https://sonarcloud.io/dashboard?id=celerik_android-kotlin-boilerplate) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=celerik_android-kotlin-boilerplate&metric=ncloc)](https://sonarcloud.io/dashboard?id=celerik_android-kotlin-boilerplate) 5 | 6 | # Welcome to Android Kotlin Boilerplate! 7 | 8 | ## Getting Started 9 | Android Kotlin Boilerplate refers to standardized methods, procedures and files that may be used over again for efficiency developing new Android mobile applications. 10 | 11 | ## What's included 12 | * An Android app with _modular architecture_ and _MVVM_ architectural pattern. 13 | * _Splash screen_ with app version and empty _MainActivity_. 14 | * _Network connectivity interceptor_ for HTTP requests. 15 | * _Dagger2_ for dependencies injection. 16 | * _ViewBinding_ for activities and fragments. 17 | * _Timber_ for logging purposes. 18 | * Android Studio _EditorConfig_ file to maintain consistent coding styles. 19 | * Gradle’s Kotlin _DSL_. 20 | * _SonarQube_ configuration files. 21 | * _JaCoCo_ maven plugin to generate test coverage reports. 22 | * _ktlint_ for static code analysis. 23 | * _LeakCanary_ for memory leaks detection. 24 | * _Fastlane_ for CI/CD tasks. 25 | * _SonarCloud_ for static code analysis. 26 | * _Github_ workflows for automated PR actions and Firebase app distribution. 27 | * _Azure DevOps Pipelines_ workflows for automated PR actions. 28 | * _dokka_ for Kotlin's documentation generation. 29 | 30 | ## Installation 31 | Clone this repository and import it into **Android Studio** 32 | ```bash 33 | git clone https://github.com/celerik/android-kotlin-boilerplate.git 34 | ``` 35 | 36 | ## Build variants 37 | Herein you can find multiple targets that the app takes into account: 38 | 39 | | |Staging |Production | 40 | |----------|-----------|------------| 41 | |`Internal`|Debug |Debug | 42 | |`External`|Release |Release | 43 | 44 | Where the following formed variants are built for staging purposes: 45 | - stagingInternalDebug 46 | - stagingInternalRelease 47 | 48 | And these ones for production purposes: 49 | - productionInternalDebug 50 | - productionInternalRelease 51 | - productionExternalDebug 52 | - productionExternalRelease 53 | 54 | **_Sidenote:_** environments with _Internal_ keyword, for example, could set a specific timeout for debug servers, whereas environments with _External_ keyword could have another timeout according to production servers' features. In the other hand, environments with _Debug_ keyword, could keep a debug logger activated; whereas environments with _Release_ keyword don't. 55 | 56 | ## Debug app signing 57 | In order to sign your debug app build using _debug-keystore.jks_ keystore, these are the credentials you will have to take in mind: 58 | 59 | `STORE_FILE = ./app/debug-keystore.jks` 60 | 61 | `STORE_PASSWORD = android` 62 | 63 | `KEY_ALIAS = android_celerik` 64 | 65 | `KEY_PASSWORD = android` 66 | 67 | ## Others 68 | 1. Project's CodeStyle can be found [here](docs/codestyle.md). 69 | 2. Project utilities file can be found [here](docs/utilities.md). 70 | 3. CI/CD documentation can be found [here](docs/cicd.md). 71 | 72 | ## Screenshots 73 | 74 | ![Screenshot_1626474458](https://user-images.githubusercontent.com/25390317/126014560-dbd18cf5-75f9-4e0a-a72e-9b63e6db0bf4.png) 75 | ![Screenshot_1626474925](https://user-images.githubusercontent.com/25390317/126014713-1c25cf42-7307-4d05-b121-5be96abdf1a4.png) 76 | -------------------------------------------------------------------------------- /base/src/main/java/com/app/base/Either.kt: -------------------------------------------------------------------------------- 1 | package com.app.base 2 | 3 | /** 4 | * From: https://github.com/android10/Android-CleanArchitecture-Kotlin 5 | * 6 | * Represents a value of one of two possible types (a disjoint union). 7 | * Instances of [Either] are either an instance of [Left] or [Right]. 8 | * FP Convention dictates that [Left] is used for "failure" 9 | * and [Right] is used for "success". 10 | * 11 | * @see Left 12 | * @see Right 13 | */ 14 | sealed class Either { 15 | /** * Represents the left side of [Either] class which by convention is a "Failure". */ 16 | data class Left(val a: L) : Either() 17 | 18 | /** * Represents the right side of [Either] class which by convention is a "Success". */ 19 | data class Right(val b: R) : Either() 20 | 21 | /** 22 | * Returns true if this is a Right, false otherwise. 23 | * @see Right 24 | */ 25 | val isRight get() = this is Right 26 | 27 | /** 28 | * Returns true if this is a Left, false otherwise. 29 | * @see Left 30 | */ 31 | val isLeft get() = this is Left 32 | 33 | /** 34 | * Creates a Left type. 35 | * @see Left 36 | */ 37 | fun left(a: L) = Either.Left(a) 38 | 39 | /** 40 | * Creates a Left type. 41 | * @see Right 42 | */ 43 | fun right(b: R) = Either.Right(b) 44 | 45 | /** 46 | * Applies fnL if this is a Left or fnR if this is a Right. 47 | * @see Left 48 | * @see Right 49 | */ 50 | fun fold(fnL: (L) -> Any, fnR: (R) -> Any): Any = 51 | when (this) { 52 | is Left -> fnL(a) 53 | is Right -> fnR(b) 54 | } 55 | 56 | /** 57 | * Returns the value from `Left` or null if Either is a `Right`. 58 | * @see Left 59 | */ 60 | fun l(): L? = 61 | when (this) { 62 | is Left -> a 63 | else -> null 64 | } 65 | 66 | /** 67 | * Returns the value from `Right` or null if Either is a `Left`. 68 | * @see Right 69 | */ 70 | fun r(): R? = 71 | when (this) { 72 | is Right -> b 73 | else -> null 74 | } 75 | } 76 | 77 | /** 78 | * Composes 2 functions 79 | * See Credits to Alex Hart. 80 | */ 81 | fun ((A) -> B).c(f: (B) -> C): (A) -> C = { 82 | f(this(it)) 83 | } 84 | 85 | /** 86 | * Right-biased flatMap() FP convention which means that Right is assumed to be the default case 87 | * to operate on. If it is Left, operations like map, flatMap, ... return the Left value unchanged. 88 | */ 89 | fun Either.flatMap(fn: (R) -> Either): Either = 90 | when (this) { 91 | is Either.Left -> Either.Left(a) 92 | is Either.Right -> fn(b) 93 | } 94 | 95 | /** 96 | * Right-biased map() FP convention which means that Right is assumed to be the default case 97 | * to operate on. If it is Left, operations like map, flatMap, ... return the Left value unchanged. 98 | */ 99 | fun Either.map(fn: (R) -> (T)): Either = this.flatMap(fn.c(::right)) 100 | 101 | /** Returns the value from this `Right` or the given argument if this is a `Left`. 102 | * Right(12).getOrElse(17) RETURNS 12 and Left(12).getOrElse(17) RETURNS 17 103 | */ 104 | fun Either.getOrElse(value: R): R = 105 | when (this) { 106 | is Either.Left -> value 107 | is Either.Right -> b 108 | } 109 | 110 | /** 111 | * Left-biased onFailure() FP convention dictates that when this class is Left, it'll perform 112 | * the onFailure functionality passed as a parameter, but, overall will still return an either 113 | * object so you chain calls. 114 | */ 115 | fun Either.onFailure(fn: (failure: L) -> Unit): Either = 116 | this.apply { if (this is Either.Left) fn(a) } 117 | 118 | /** 119 | * Right-biased onSuccess() FP convention dictates that when this class is Right, it'll perform 120 | * the onSuccess functionality passed as a parameter, but, overall will still return an either 121 | * object so you chain calls. 122 | */ 123 | fun Either.onSuccess(fn: (success: R) -> Unit): Either = 124 | this.apply { if (this is Either.Right) fn(b) } 125 | -------------------------------------------------------------------------------- /app/src/main/java/com/celerik/app/di/modules/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.celerik.app.di.modules 2 | 3 | import androidx.annotation.VisibleForTesting 4 | import com.app.base.interfaces.Logger 5 | import com.app.core.di.BasePath 6 | import com.app.core.di.OkHttpClientBasic 7 | import com.app.core.di.RetrofitBasic 8 | import com.app.core.di.RetrofitCelerik 9 | import com.app.core.di.RetrofitNullSerializationEnabled 10 | import com.app.core.network.ServerInterceptor 11 | import com.celerik.app.BuildConfig 12 | import com.squareup.moshi.Moshi 13 | import dagger.Module 14 | import dagger.Provides 15 | import okhttp3.Interceptor 16 | import okhttp3.OkHttpClient 17 | import okhttp3.logging.HttpLoggingInterceptor 18 | import okreplay.OkReplayInterceptor 19 | import retrofit2.Retrofit 20 | import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory 21 | import retrofit2.converter.moshi.MoshiConverterFactory 22 | import java.util.concurrent.TimeUnit 23 | import javax.inject.Singleton 24 | 25 | private const val FLAVOR_TARGET_INTERNAL = "internal" 26 | private const val EXTERNAL_REQUEST_TIMEOUT_IN_SECONDS = 60L 27 | private const val INTERNAL_REQUEST_TIMEOUT_IN_SECONDS = 300L 28 | 29 | @Module(includes = [InterceptorsModule::class]) 30 | object NetworkModule { 31 | 32 | @VisibleForTesting 33 | val okReplayInterceptor = OkReplayInterceptor() 34 | 35 | @Provides 36 | @Singleton 37 | fun providesOkReplayInterceptor(): OkReplayInterceptor { 38 | return okReplayInterceptor 39 | } 40 | 41 | @Provides 42 | fun providesOkHttpClientBuilder(): OkHttpClient.Builder { 43 | val timeout = 44 | if (BuildConfig.FLAVOR_target == FLAVOR_TARGET_INTERNAL) INTERNAL_REQUEST_TIMEOUT_IN_SECONDS else EXTERNAL_REQUEST_TIMEOUT_IN_SECONDS 45 | return OkHttpClient.Builder() 46 | .connectTimeout(timeout, TimeUnit.SECONDS) 47 | .readTimeout(timeout, TimeUnit.SECONDS) 48 | .writeTimeout(timeout, TimeUnit.SECONDS) 49 | } 50 | 51 | @Provides 52 | @OkHttpClientBasic 53 | fun providesBasicOkHttpClient( 54 | builder: OkHttpClient.Builder, 55 | ): OkHttpClient { 56 | return builder.build() 57 | } 58 | 59 | @Provides 60 | @Singleton 61 | fun providesMoshi(): Moshi { 62 | return Moshi.Builder().build() 63 | } 64 | 65 | @Provides 66 | @Singleton 67 | fun providesLoggingInterceptor(): Interceptor { 68 | val logging = HttpLoggingInterceptor() 69 | val level = if (BuildConfig.DEBUG) { 70 | HttpLoggingInterceptor.Level.BODY 71 | } else { 72 | HttpLoggingInterceptor.Level.NONE 73 | } 74 | logging.setLevel(level) 75 | return logging 76 | } 77 | 78 | @Provides 79 | @Singleton 80 | fun providesBaseOkHttpClient( 81 | builder: OkHttpClient.Builder, 82 | interceptor: Interceptor, 83 | okReplayInterceptor: OkReplayInterceptor, 84 | logger: Logger 85 | ): OkHttpClient { 86 | val serverInterceptor = ServerInterceptor(logger) 87 | return builder 88 | .addInterceptor(okReplayInterceptor) 89 | .addInterceptor(serverInterceptor) 90 | .addInterceptor(interceptor) 91 | .build() 92 | } 93 | 94 | @Provides 95 | @Singleton 96 | @RetrofitCelerik 97 | @JvmSuppressWildcards 98 | fun providesOkHttpClient( 99 | builder: OkHttpClient.Builder, 100 | interceptor: Interceptor, 101 | interceptorList: Set, 102 | okReplayInterceptor: OkReplayInterceptor, 103 | ): OkHttpClient { 104 | interceptorList.forEach { 105 | builder.addInterceptor(it) 106 | } 107 | return builder 108 | .addInterceptor(okReplayInterceptor) 109 | .addInterceptor(interceptor) 110 | .build() 111 | } 112 | 113 | @Provides 114 | @Singleton 115 | @BasePath 116 | fun providesBasePath(): String { 117 | return BuildConfig.BASE_URL 118 | } 119 | 120 | @Provides 121 | @Singleton 122 | @RetrofitBasic 123 | fun providesRetrofitBasic( 124 | okHttpClient: OkHttpClient, 125 | @BasePath basePath: String 126 | ): Retrofit { 127 | return Retrofit.Builder() 128 | .client(okHttpClient) 129 | .baseUrl(basePath) 130 | .addConverterFactory(MoshiConverterFactory.create()) 131 | .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) 132 | .build() 133 | } 134 | 135 | @Provides 136 | @Singleton 137 | @RetrofitNullSerializationEnabled 138 | fun providesRetrofitNullSerializationEnabled( 139 | @RetrofitCelerik okHttpClient: OkHttpClient, 140 | @BasePath basePath: String 141 | ): Retrofit { 142 | return Retrofit.Builder() 143 | .client(okHttpClient) 144 | .baseUrl(basePath) 145 | .addConverterFactory(MoshiConverterFactory.create().withNullSerialization()) 146 | .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) 147 | .build() 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.3) 5 | addressable (2.8.0) 6 | public_suffix (>= 2.0.2, < 5.0) 7 | artifactory (3.0.15) 8 | atomos (0.1.3) 9 | aws-eventstream (1.1.1) 10 | aws-partitions (1.480.0) 11 | aws-sdk-core (3.117.0) 12 | aws-eventstream (~> 1, >= 1.0.2) 13 | aws-partitions (~> 1, >= 1.239.0) 14 | aws-sigv4 (~> 1.1) 15 | jmespath (~> 1.0) 16 | aws-sdk-kms (1.44.0) 17 | aws-sdk-core (~> 3, >= 3.112.0) 18 | aws-sigv4 (~> 1.1) 19 | aws-sdk-s3 (1.96.2) 20 | aws-sdk-core (~> 3, >= 3.112.0) 21 | aws-sdk-kms (~> 1) 22 | aws-sigv4 (~> 1.1) 23 | aws-sigv4 (1.2.4) 24 | aws-eventstream (~> 1, >= 1.0.2) 25 | babosa (1.0.4) 26 | claide (1.0.3) 27 | colored (1.2) 28 | colored2 (3.1.2) 29 | commander (4.6.0) 30 | highline (~> 2.0.0) 31 | declarative (0.0.20) 32 | digest-crc (0.6.4) 33 | rake (>= 12.0.0, < 14.0.0) 34 | domain_name (0.5.20190701) 35 | unf (>= 0.0.5, < 1.0.0) 36 | dotenv (2.7.6) 37 | emoji_regex (3.2.2) 38 | excon (0.85.0) 39 | faraday (1.5.1) 40 | faraday-em_http (~> 1.0) 41 | faraday-em_synchrony (~> 1.0) 42 | faraday-excon (~> 1.1) 43 | faraday-httpclient (~> 1.0.1) 44 | faraday-net_http (~> 1.0) 45 | faraday-net_http_persistent (~> 1.1) 46 | faraday-patron (~> 1.0) 47 | multipart-post (>= 1.2, < 3) 48 | ruby2_keywords (>= 0.0.4) 49 | faraday-cookie_jar (0.0.7) 50 | faraday (>= 0.8.0) 51 | http-cookie (~> 1.0.0) 52 | faraday-em_http (1.0.0) 53 | faraday-em_synchrony (1.0.0) 54 | faraday-excon (1.1.0) 55 | faraday-httpclient (1.0.1) 56 | faraday-net_http (1.0.1) 57 | faraday-net_http_persistent (1.2.0) 58 | faraday-patron (1.0.0) 59 | faraday_middleware (1.0.0) 60 | faraday (~> 1.0) 61 | fastimage (2.2.4) 62 | fastlane (2.189.0) 63 | CFPropertyList (>= 2.3, < 4.0.0) 64 | addressable (>= 2.8, < 3.0.0) 65 | artifactory (~> 3.0) 66 | aws-sdk-s3 (~> 1.0) 67 | babosa (>= 1.0.3, < 2.0.0) 68 | bundler (>= 1.12.0, < 3.0.0) 69 | colored 70 | commander (~> 4.6) 71 | dotenv (>= 2.1.1, < 3.0.0) 72 | emoji_regex (>= 0.1, < 4.0) 73 | excon (>= 0.71.0, < 1.0.0) 74 | faraday (~> 1.0) 75 | faraday-cookie_jar (~> 0.0.6) 76 | faraday_middleware (~> 1.0) 77 | fastimage (>= 2.1.0, < 3.0.0) 78 | gh_inspector (>= 1.1.2, < 2.0.0) 79 | google-apis-androidpublisher_v3 (~> 0.3) 80 | google-apis-playcustomapp_v1 (~> 0.1) 81 | google-cloud-storage (~> 1.31) 82 | highline (~> 2.0) 83 | json (< 3.0.0) 84 | jwt (>= 2.1.0, < 3) 85 | mini_magick (>= 4.9.4, < 5.0.0) 86 | multipart-post (~> 2.0.0) 87 | naturally (~> 2.2) 88 | plist (>= 3.1.0, < 4.0.0) 89 | rubyzip (>= 2.0.0, < 3.0.0) 90 | security (= 0.1.3) 91 | simctl (~> 1.6.3) 92 | terminal-notifier (>= 2.0.0, < 3.0.0) 93 | terminal-table (>= 1.4.5, < 2.0.0) 94 | tty-screen (>= 0.6.3, < 1.0.0) 95 | tty-spinner (>= 0.8.0, < 1.0.0) 96 | word_wrap (~> 1.0.0) 97 | xcodeproj (>= 1.13.0, < 2.0.0) 98 | xcpretty (~> 0.3.0) 99 | xcpretty-travis-formatter (>= 0.0.3) 100 | fastlane-plugin-firebase_app_distribution (0.2.9) 101 | gh_inspector (1.1.3) 102 | google-apis-androidpublisher_v3 (0.9.0) 103 | google-apis-core (>= 0.4, < 2.a) 104 | google-apis-core (0.4.1) 105 | addressable (~> 2.5, >= 2.5.1) 106 | googleauth (>= 0.16.2, < 2.a) 107 | httpclient (>= 2.8.1, < 3.a) 108 | mini_mime (~> 1.0) 109 | representable (~> 3.0) 110 | retriable (>= 2.0, < 4.a) 111 | rexml 112 | webrick 113 | google-apis-iamcredentials_v1 (0.6.0) 114 | google-apis-core (>= 0.4, < 2.a) 115 | google-apis-playcustomapp_v1 (0.5.0) 116 | google-apis-core (>= 0.4, < 2.a) 117 | google-apis-storage_v1 (0.6.0) 118 | google-apis-core (>= 0.4, < 2.a) 119 | google-cloud-core (1.6.0) 120 | google-cloud-env (~> 1.0) 121 | google-cloud-errors (~> 1.0) 122 | google-cloud-env (1.5.0) 123 | faraday (>= 0.17.3, < 2.0) 124 | google-cloud-errors (1.1.0) 125 | google-cloud-storage (1.34.1) 126 | addressable (~> 2.5) 127 | digest-crc (~> 0.4) 128 | google-apis-iamcredentials_v1 (~> 0.1) 129 | google-apis-storage_v1 (~> 0.1) 130 | google-cloud-core (~> 1.6) 131 | googleauth (>= 0.16.2, < 2.a) 132 | mini_mime (~> 1.0) 133 | googleauth (0.16.2) 134 | faraday (>= 0.17.3, < 2.0) 135 | jwt (>= 1.4, < 3.0) 136 | memoist (~> 0.16) 137 | multi_json (~> 1.11) 138 | os (>= 0.9, < 2.0) 139 | signet (~> 0.14) 140 | highline (2.0.3) 141 | http-cookie (1.0.4) 142 | domain_name (~> 0.5) 143 | httpclient (2.8.3) 144 | jmespath (1.4.0) 145 | json (2.5.1) 146 | jwt (2.2.3) 147 | memoist (0.16.2) 148 | mini_magick (4.11.0) 149 | mini_mime (1.1.0) 150 | multi_json (1.15.0) 151 | multipart-post (2.0.0) 152 | nanaimo (0.3.0) 153 | naturally (2.2.1) 154 | os (1.1.1) 155 | plist (3.6.0) 156 | public_suffix (4.0.6) 157 | rake (13.0.6) 158 | representable (3.1.1) 159 | declarative (< 0.1.0) 160 | trailblazer-option (>= 0.1.1, < 0.2.0) 161 | uber (< 0.2.0) 162 | retriable (3.1.2) 163 | rexml (3.2.5) 164 | rouge (2.0.7) 165 | ruby2_keywords (0.0.5) 166 | rubyzip (2.3.2) 167 | security (0.1.3) 168 | signet (0.15.0) 169 | addressable (~> 2.3) 170 | faraday (>= 0.17.3, < 2.0) 171 | jwt (>= 1.5, < 3.0) 172 | multi_json (~> 1.10) 173 | simctl (1.6.8) 174 | CFPropertyList 175 | naturally 176 | terminal-notifier (2.0.0) 177 | terminal-table (1.8.0) 178 | unicode-display_width (~> 1.1, >= 1.1.1) 179 | trailblazer-option (0.1.1) 180 | tty-cursor (0.7.1) 181 | tty-screen (0.8.1) 182 | tty-spinner (0.9.3) 183 | tty-cursor (~> 0.7) 184 | uber (0.1.0) 185 | unf (0.1.4) 186 | unf_ext 187 | unf_ext (0.0.7.7) 188 | unicode-display_width (1.7.0) 189 | webrick (1.7.0) 190 | word_wrap (1.0.0) 191 | xcodeproj (1.20.0) 192 | CFPropertyList (>= 2.3.3, < 4.0) 193 | atomos (~> 0.1.3) 194 | claide (>= 1.0.2, < 2.0) 195 | colored2 (~> 3.1) 196 | nanaimo (~> 0.3.0) 197 | rexml (~> 3.2.4) 198 | xcpretty (0.3.0) 199 | rouge (~> 2.0.7) 200 | xcpretty-travis-formatter (1.0.1) 201 | xcpretty (~> 0.2, >= 0.0.7) 202 | 203 | PLATFORMS 204 | x64-mingw32 205 | 206 | DEPENDENCIES 207 | fastlane 208 | fastlane-plugin-firebase_app_distribution 209 | 210 | BUNDLED WITH 211 | 2.2.24 212 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("de.mannodermaus.android-junit5") 4 | id("jacoco") 5 | id("plugins.jacoco-report") 6 | id("com.google.gms.google-services") 7 | kotlin("android") 8 | kotlin("kapt") 9 | } 10 | 11 | android { 12 | compileSdkVersion(Api.compileSDK) 13 | 14 | defaultConfig { 15 | applicationId = "com.celerik.app" 16 | minSdkVersion(Api.minSDK) 17 | targetSdkVersion(Api.targetSDK) 18 | versionCode = getNewVersionCode() 19 | versionName = getNewVersionName() 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | testInstrumentationRunnerArguments += mapOf( 22 | "disableAnalytics" to "true", 23 | "clearPackageData" to "true" 24 | ) 25 | multiDexEnabled = true 26 | } 27 | signingConfigs { 28 | getByName("debug") { 29 | val defaultPassword = "android" 30 | keyAlias = "android_celerik" 31 | keyPassword = defaultPassword 32 | storeFile = file("debug-keystore.jks") 33 | storePassword = defaultPassword 34 | } 35 | } 36 | buildTypes { 37 | getByName("release") { 38 | isMinifyEnabled = true 39 | isShrinkResources = true 40 | proguardFiles( 41 | getDefaultProguardFile("proguard-android-optimize.txt"), 42 | "proguard-rules.pro", 43 | "multidex-config.pro" 44 | ) 45 | } 46 | getByName("debug") { 47 | isTestCoverageEnabled = true 48 | isPseudoLocalesEnabled = true 49 | signingConfig = signingConfigs.getByName("debug") 50 | } 51 | } 52 | 53 | flavorDimensions("version", "target") 54 | productFlavors { 55 | create("staging") { 56 | dimension("version") 57 | applicationIdSuffix = ".staging" 58 | versionNameSuffix = "-Staging" 59 | manifestPlaceholders["scheme"] = "celerik.staging" 60 | buildConfigField("String", "SCHEME", "\"${manifestPlaceholders["scheme"]}\"") 61 | buildConfigField("String", "BASE_URL", "\"https://staging.base.url.com\"") 62 | } 63 | create("production") { 64 | dimension("version") 65 | manifestPlaceholders["scheme"] = "celerik.production" 66 | buildConfigField("String", "SCHEME", "\"${manifestPlaceholders["scheme"]}\"") 67 | buildConfigField("String", "BASE_URL", "\"https://production.base.url.com\"") 68 | } 69 | create("internal") { 70 | dimension("target") 71 | } 72 | create("external") { 73 | dimension("target") 74 | } 75 | 76 | variantFilter { 77 | val names = flavors.map { it.name } 78 | if (names.contains("external") && names.contains("staging")) { 79 | ignore = true 80 | } 81 | } 82 | } 83 | 84 | lintOptions { 85 | isAbortOnError = false 86 | } 87 | 88 | buildFeatures { 89 | dataBinding = true 90 | viewBinding = true 91 | } 92 | 93 | compileOptions { 94 | isCoreLibraryDesugaringEnabled = true 95 | sourceCompatibility = JavaVersion.VERSION_1_8 96 | targetCompatibility = JavaVersion.VERSION_1_8 97 | } 98 | 99 | testOptions { 100 | unitTests.isIncludeAndroidResources = true 101 | unitTests.isReturnDefaultValues = true 102 | animationsDisabled = true 103 | execution = "ANDROIDX_TEST_ORCHESTRATOR" 104 | } 105 | } 106 | 107 | dependencies { 108 | implementation(project(":base")) 109 | implementation(project(":components")) 110 | implementation(project(":core")) 111 | implementation(project(":core-test")) 112 | 113 | implementation(Libraries.multidex) 114 | 115 | kapt(AnnotationProcessors.dagger) 116 | kapt(AnnotationProcessors.daggerAndroid) 117 | kapt(AnnotationProcessors.moshiCodegen) 118 | 119 | implementation(platform(Libraries.firebaseBoM)) 120 | implementation(Libraries.firebaseAnalytics) 121 | 122 | implementation(Libraries.kotlinJDK) 123 | implementation(Libraries.appcompat) 124 | implementation(Libraries.androidXCore) 125 | implementation(Libraries.constraintLayout) 126 | implementation(Libraries.material) 127 | implementation(Libraries.preferences) 128 | coreLibraryDesugaring(Libraries.desugarJdkLibs) 129 | 130 | implementation(Libraries.dagger) 131 | implementation(Libraries.daggerAndroid) 132 | implementation(Libraries.daggerAndroidSupport) 133 | 134 | implementation(Libraries.glide) 135 | 136 | implementation(Libraries.rxJava) 137 | implementation(Libraries.rxAndroid) 138 | 139 | implementation(Libraries.moshi) 140 | 141 | implementation(Libraries.retrofit) 142 | implementation(Libraries.retrofitMoshi) 143 | implementation(Libraries.retrofitRxJava) 144 | implementation(platform(Libraries.okHttpBoM)) 145 | implementation(Libraries.okHttpInterceptor) 146 | implementation(Libraries.okHttp) 147 | 148 | implementation(Libraries.lifeCycleProcess) 149 | implementation(Libraries.lifeCycleCommonJava8) 150 | 151 | implementation(Libraries.timber) 152 | 153 | debugImplementation(Libraries.leakCanary) 154 | 155 | kaptTest(AnnotationProcessors.moshiCodegen) 156 | testImplementation(Libraries.mockWebServer) 157 | Libraries.suiteTest.forEach { testImplementation(it) } 158 | testRuntimeOnly(Libraries.jUnit5Engine) 159 | 160 | debugImplementation(Libraries.okReplay) 161 | releaseImplementation(Libraries.okReplayNoop) 162 | androidTestImplementation(Libraries.okReplayEspresso) 163 | 164 | androidTestImplementation(Libraries.jUnitExtKtx) 165 | androidTestImplementation(Libraries.testCoreKtx) 166 | androidTestImplementation(Libraries.androidXRunner) 167 | androidTestImplementation(Libraries.espressoCore) 168 | androidTestImplementation(Libraries.androidXRules) 169 | androidTestImplementation(Libraries.barista) { 170 | exclude(group = "org.jetbrains.kotlin") 171 | } 172 | androidTestUtil(Libraries.orchestrator) 173 | } 174 | 175 | fun getNewVersionCode(): Int { 176 | val versionCode = if (project.hasProperty("version_code")) { 177 | project.properties["version_code"].toString().toIntOrNull() 178 | } else { 179 | null 180 | } 181 | return versionCode ?: 32 182 | } 183 | 184 | fun getNewVersionName(): String { 185 | return if (project.hasProperty("version_name")) { 186 | project.properties["version_name"].toString() 187 | } else { 188 | "v1.0-Dirty" 189 | } 190 | } 191 | 192 | tasks.withType { 193 | configure { 194 | isIncludeNoLocationClasses = true 195 | excludes = listOf("jdk.internal.*") 196 | } 197 | } 198 | 199 | afterEvaluate { 200 | val function = 201 | extra.get("generateCheckCoverageTasks") as (File, String, Coverage, List, List) -> Unit 202 | function.invoke( 203 | buildDir, 204 | "testStagingInternalDebugUnitTest", 205 | Coverage( 206 | instructions = 19.23, 207 | lines = 19.89, 208 | complexity = 15.15, 209 | methods = 17.54, 210 | classes = 23.81 211 | ), 212 | listOf("**/tmp/kotlin-classes/stagingInternalDebug/**"), 213 | emptyList() 214 | ) 215 | } 216 | -------------------------------------------------------------------------------- /docs/codestyle.md: -------------------------------------------------------------------------------- 1 | # Android CodeStyle 2 | 3 | ## Description 4 | This document serves as the definition of Celerik’s Android coding standards for source code in the Kotlin Programming Language. The code style described below is inspired by Google's and Kotlin's style guides, as well as our own formed standard according to the experiences we have had and will have in our team (i.e. code reviews). 5 | 6 | The issues covered span not only aesthetic issues of formatting, but other types of conventions or coding standards as well. However, this document focuses primarily on the hard-and-fast rules that we follow, and avoids giving advice that isn’t clearly enforceable (whether by human or tool). 7 | 8 | ## Inspiration 9 | Let's see this code style like an extension of the language guidance drawn from: 10 | 11 | - [Android Kotlin style guide](https://android.github.io/kotlin-guides/style.html). 12 | - [Kotlin Coding Conventions](https://kotlinlang.org/docs/reference/coding-conventions.html). 13 | - [Android contributors style guide](https://source.android.com/source/code-style.html). 14 | 15 | ## Notions 16 | In below, you will find the notions: 17 | 18 | (1). Strongly recommended. 19 | 20 | (2). Recommended. 21 | 22 | (3). Preferred. 23 | 24 | (4). Suggested. 25 | 26 | Where at (1) refers that it's mandatory taking these advices into account, and the rest ones, are also important but at a lower level. This stuff could depend on your context. 27 | 28 | ## Table of Contents 29 | 30 | - [Nomenclature](#nomenclature) 31 | + [Packages](#packages) 32 | + [Classes & Interfaces](#classes--interfaces) 33 | + [Methods](#methods) 34 | + [Fields](#fields) 35 | + [Variables & Parameters](#variables--parameters) 36 | - [Declarations](#declarations) 37 | + [Visibility Modifiers](#visibility-modifiers) 38 | + [Classes](#classes) 39 | + [Data Type Objects](#data-type-objects) 40 | + [Enum Classes](#enum-classes) 41 | - [Spacing](#spacing) 42 | + [Indentation](#indentation) 43 | + [Enum Classes](#enum-classes) 44 | + [Line Length](#line-length-open-to-discussion) 45 | + [Enum Classes](#enum-classes) 46 | + [Vertical Spacing](#vertical-spacing) 47 | - [Comments](#comments) 48 | - [Semicolons](#semicolons) 49 | - [Getters & Setters](#getters--setters) 50 | - [Brace Style](#brace-style) 51 | - [When Statements](#when-statements-open-to-discussion) 52 | + [Type Inference](#type-inference) 53 | + [Constants vs. Variables](#constants-vs-variables) 54 | + [Companion Objects](#companion-objects) 55 | + [Nullable Types](#nullable-types) 56 | + [Optional Types](#optional-types) 57 | - [Language](#language-open-to-discussion) 58 | - [Trailing Commas](#trailing-commas) 59 | 60 | ## Nomenclature 61 | 62 | On the whole, naming should follow Java standards, as Kotlin is a JVM-compatible language. 63 | 64 | ### Packages 65 | ###### Strongly recommended 66 | Package names are similar to Java: all __lower-case__, multiple words concatenated together, without hypens or underscores: 67 | 68 | **WRONG:** 69 | 70 | ```kotlin 71 | com.celerik.auth.use_cases 72 | ``` 73 | 74 | **RIGHT:** 75 | 76 | ```kotlin 77 | com.celerik.auth.usecases 78 | ``` 79 | 80 | ### Classes & Interfaces 81 | ###### Strongly recommended 82 | 83 | Written in __UpperCamelCase__. For example `TermsAndConditionsDialogFragment`. 84 | 85 | ### Methods 86 | ###### Strongly recommended 87 | 88 | Written in __lowerCamelCase__. For example `onViewActive`. 89 | 90 | ### Fields 91 | ###### Strongly recommended 92 | 93 | Written in __lowerCamelCase__. 94 | 95 | Example field names: 96 | 97 | ```kotlin 98 | data class MyDataClass ( 99 | var publicField: Int = 0, 100 | val person: Person = Person(), 101 | private var privateField: Int? 102 | ) 103 | ``` 104 | 105 | Constant values in the companion object should be written in __uppercase__, with an underscore separating words: 106 | 107 | ```kotlin 108 | companion object { 109 | const val MAX_ALLOWABLE_RETRY_ATTEMPTS = 3 110 | } 111 | ``` 112 | 113 | ### Variables & Parameters 114 | ###### Strongly recommended 115 | 116 | Written in __lowerCamelCase__. 117 | 118 | Single character values and abbreviations must be avoided. 119 | 120 | In code, acronyms should be treated as words. 121 | 122 | **WRONG:** 123 | 124 | ```kotlin 125 | val phoneUIModel: PhoneUIModel 126 | 127 | fun method(XMLHTTPRequest: Request) 128 | 129 | URL: String? 130 | 131 | findItemByID 132 | 133 | initView() 134 | ``` 135 | **RIGHT:** 136 | 137 | ```kotlin 138 | val phoneUiModel: PhoneUiModel 139 | 140 | fun method(xmlHttpRequest: Request) 141 | 142 | url: String? 143 | 144 | findItemById 145 | 146 | initializeView() 147 | ``` 148 | 149 | ## Declarations 150 | 151 | ### Visibility Modifiers 152 | ###### Recommended 153 | 154 | Only include visibility modifiers if you need something other than the default of public. 155 | 156 | **WRONG:** 157 | 158 | ```kotlin 159 | public val wideOpenProperty = "I am public" 160 | ``` 161 | 162 | **RIGHT:** 163 | 164 | ```kotlin 165 | val wideOpenProperty = "I am public too" 166 | ``` 167 | 168 | ### Classes 169 | ###### Strongly recommended 170 | 171 | Exactly one class per source file, although inner classes are encouraged where scoping appropriate. 172 | 173 | ### Data Type Objects 174 | ###### Strongly recommended 175 | 176 | Prefer data classes for simple data holding objects. 177 | 178 | **WRONG:** 179 | 180 | ```kotlin 181 | class Person(private val name: String) { 182 | override fun toString() : String { 183 | return "Person(name=$name)" 184 | } 185 | } 186 | ``` 187 | 188 | **RIGHT:** 189 | 190 | ```kotlin 191 | data class Person(val name: String) 192 | ``` 193 | 194 | ### Enum Classes 195 | ###### Suggested 196 | 197 | Enum classes without methods may have its values formatted without line-breaks, as follows: 198 | 199 | ```kotlin 200 | enum class CompassDirection { 201 | EAST, NORTH, WEST, SOUTH 202 | } 203 | ``` 204 | 205 | ## Spacing 206 | ###### Preferred 207 | 208 | Spacing is especially important in our code, as code needs to be easily readable. Even, we can separate code into blocks related by its context. 209 | 210 | **WRONG:** 211 | 212 | ```kotlin 213 | binding.apply { 214 | textViewClientName.text = orderInfo.clientName 215 | textViewDeliveryDate.isGone = orderInfo.deliveryDate.isBlank() 216 | textViewDeliveryDate.text = "some text" 217 | textViewEarningsValue.text = orderInfo.earnings 218 | textViewOnDeliveryValue.text = orderInfo.orderOnDelivery 219 | textViewClientPayValue.text = orderInfo.orderTotal 220 | textViewStatus.text = orderInfo.status.getOrderStatusLabel(context) 221 | textViewStatus.setTextColor(orderInfo.status.getColor(context)) 222 | } 223 | ``` 224 | 225 | **RIGHT:** 226 | 227 | ```kotlin 228 | binding.apply { 229 | textViewClientName.text = orderInfo.clientName 230 | 231 | textViewDeliveryDate.isGone = orderInfo.deliveryDate.isBlank() 232 | textViewDeliveryDate.text = "some text" 233 | 234 | textViewEarningsValue.text = orderInfo.earnings 235 | textViewOnDeliveryValue.text = orderInfo.orderOnDelivery 236 | textViewClientPayValue.text = orderInfo.orderTotal 237 | 238 | textViewStatus.text = orderInfo.status.getOrderStatusLabel(context) 239 | textViewStatus.setTextColor(orderInfo.status.getColor(context)) 240 | } 241 | ``` 242 | 243 | ### Indentation 244 | ###### Preferred 245 | 246 | Indentation is using spaces - never tabs. 247 | 248 | #### Blocks 249 | ###### Strongly recommended 250 | 251 | Indentation for blocks uses 2 spaces (not the default 4): 252 | 253 | **WRONG:** 254 | 255 | ```kotlin 256 | for (i in 0..9) { 257 | Log.i(TAG, "index=" + i) 258 | } 259 | ``` 260 | 261 | **RIGHT:** 262 | 263 | ```kotlin 264 | for (i in 0..9) { 265 | Log.i(TAG, "index=" + i) 266 | } 267 | ``` 268 | 269 | #### Line Wraps 270 | ###### Recommended 271 | 272 | Indentation for line wraps should use 2 spaces (not the default 8): 273 | 274 | **WRONG:** 275 | 276 | ```kotlin 277 | val widget: CoolUiWidget = 278 | someIncrediblyLongExpression(that, reallyWouldNotFit, on, aSingle, line) 279 | ``` 280 | 281 | **RIGHT:** 282 | 283 | ```kotlin 284 | val widget: CoolUiWidget = 285 | someIncrediblyLongExpression(that, reallyWouldNotFit, on, aSingle, line) 286 | ``` 287 | 288 | ### Line Length (open to discussion) 289 | ###### Strongly recommended 290 | 291 | Lines should be no longer than 140 characters long. 292 | 293 | ### Vertical Spacing 294 | ###### Preferred 295 | 296 | There should be exactly one blank line between and within methods to aid in visual clarity and organization. 297 | 298 | ## Comments 299 | ###### Preferred 300 | 301 | Comments are only allowed in test files methods, i.e. 302 | 303 | ```kotlin 304 | @Test 305 | fun `Should add comments when you are implementing a test method`() { 306 | // given 307 | . . . 308 | 309 | // when 310 | . . . 311 | 312 | // then 313 | . . . 314 | } 315 | ``` 316 | 317 | Or whenever the method needs to be declared but without content: 318 | 319 | ```kotlin 320 | private val onItemSelectedListener = object : AdapterView.OnItemSelectedListener { 321 | override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { 322 | onChangeListener?.invoke(getQueryString()) 323 | } 324 | 325 | override fun onNothingSelected(parent: AdapterView<*>?) { 326 | // no-op by default 327 | } 328 | } 329 | ``` 330 | 331 | The code should be as self-documenting as possible. 332 | 333 | ## Semicolons 334 | ###### Strongly recommended 335 | 336 | Semicolons should be avoided wherever possible in Kotlin. 337 | 338 | ## Getters & Setters 339 | ###### Strongly recommended 340 | 341 | Unlike Java, direct access to fields in Kotlin is preferred. 342 | 343 | If custom getters and setters are required, they should be declared [following Kotlin conventions](https://kotlinlang.org/docs/reference/properties.html) rather than as separate methods. 344 | 345 | ## Brace Style 346 | ###### Strongly recommended 347 | 348 | Only trailing closing-braces are awarded their own line. All others appear the same line as preceding code: 349 | 350 | **WRONG:** 351 | 352 | ```kotlin 353 | class MyClass 354 | { 355 | fun doSomething() 356 | { 357 | if (someTest) 358 | { 359 | // ... 360 | } 361 | else 362 | { 363 | // ... 364 | } 365 | } 366 | } 367 | ``` 368 | 369 | **RIGHT:** 370 | 371 | ```kotlin 372 | class MyClass { 373 | fun doSomething() { 374 | if (condition) { 375 | . . . 376 | } else { 377 | . . . 378 | } 379 | } 380 | } 381 | ``` 382 | 383 | Conditional statements are _always_ required to be enclosed with braces, irrespective of the number of lines required. 384 | 385 | **WRONG:** 386 | 387 | ```kotlin 388 | if (condition1) 389 | doSomething() 390 | if (condition2) doSomethingElse() 391 | ``` 392 | 393 | **RIGHT:** 394 | 395 | ```kotlin 396 | if (condition1) { 397 | doSomething() 398 | } 399 | 400 | if (condition2) { 401 | doSomethingElse() 402 | } 403 | ``` 404 | 405 | ## When Statements (open to discussion) 406 | ###### Recommended 407 | 408 | 409 | 410 | Separate cases using commas if they should be handled the same way. 411 | 412 | Always include the else case. 413 | 414 | **WRONG:** 415 | 416 | ```kotlin 417 | when (input) { 418 | 1 -> doSomethingForCaseOneOrTwo() 419 | 2 -> doSomethingForCaseOneOrTwo() 420 | 3 -> doSomethingForCaseThree() 421 | } 422 | ``` 423 | 424 | **RIGHT:** 425 | 426 | ```kotlin 427 | when (input) { 428 | 1, 2 -> doSomethingForCaseOneOrTwo() 429 | 3 -> doSomethingForCaseThree() 430 | else -> logger.w("No case satisfied") 431 | } 432 | ``` 433 | 434 | ### Type Inference 435 | ###### Preferred 436 | 437 | Type inference should be preferred where possible to explicitly declared types. 438 | 439 | **WRONG:** 440 | 441 | ```kotlin 442 | val something: MyType = MyType() 443 | val meaningOfLife: Int = 42 444 | ``` 445 | 446 | **RIGHT:** 447 | 448 | ```kotlin 449 | val something = MyType() 450 | val meaningOfLife = 42 451 | ``` 452 | 453 | ### Constants vs. Variables 454 | ###### Strongly recommended 455 | 456 | Constants are defined using the `val` keyword, and variables with the `var` keyword. Always use `val` instead of `var` if the value of the variable will not change. 457 | 458 | *Tip*: A good technique is to define everything using `val` and only change it to `var` if the compiler complains! 459 | 460 | 461 | ### Companion Objects 462 | ###### Strongly recommended 463 | 464 | Companion objects should be declared at the _top_ of the class: 465 | 466 | ```kotlin 467 | class CelerikAuthenticator { 468 | 469 | companion object { 470 | const val MAX_ALLOWABLE_RETRY_ATTEMPTS = 3 471 | } 472 | 473 | . . . 474 | 475 | } 476 | ``` 477 | 478 | ### Nullable Types 479 | ###### Strongly recommended 480 | 481 | Declare variables and function return types as nullable with `?` where a `null` value is acceptable. 482 | 483 | Usage of the `!!` is only allowed in tests files. 484 | 485 | When naming nullable variables and parameters, avoid naming them like `nullableString` / `maybeView` / `optValue` since their nullability is already in the type declaration. 486 | 487 | When accessing a nullable value, use the safe call operator if the value is only accessed once or if there are many nullables in the chain: 488 | 489 | ```kotlin 490 | editText?.setText("foo") 491 | ``` 492 | 493 | If there are several usages of this nullable value, consider the use of `?.apply` instead: 494 | 495 | ```kotlin 496 | binding?.apply { 497 | viewModel = feedDetailViewModel 498 | lifecycleOwner = this@FeedDetailFragment 499 | } 500 | ``` 501 | 502 | ### Optional Types 503 | ###### Recommended 504 | 505 | When naming optional variables and parameters, avoid naming them like `maybeView` / `optValue` since their optional type is already in its declaration. 506 | 507 | ## Language (open to discussion) 508 | ###### Preferred 509 | 510 | Use `en-US` English spelling. 511 | 512 | **WRONG:** 513 | 514 | ```kotlin 515 | val colourName = "red" 516 | ``` 517 | 518 | **RIGHT:** 519 | 520 | ```kotlin 521 | val colorName = "red" 522 | ``` 523 | 524 | ## Trailing Commas 525 | ###### Recommended 526 | 527 | The compiler now allows leaving a dangling comma after function, constructor, lambda parameters, and many other places where it was previously forbidden. 528 | 529 | This is useful to make clearer the GIT diffs. Let's look at an example: 530 | 531 | - Before: 532 | ```kotlin 533 | class NoCommas( 534 | val foo: Int 535 | ) 536 | ``` 537 | 538 | - After adding `bar` property: 539 | ```kotlin 540 | class NoCommas( 541 | - val foo: Int, 542 | + val foo: Int, 543 | + val bar: Int 544 | ) 545 | ``` 546 | 547 | Now, we're going to using trailing commas: 548 | 549 | - Before: 550 | ```kotlin 551 | class YesCommas( 552 | val foo: Int, 553 | ) 554 | ``` 555 | 556 | - After adding `bar` property with trailing comma: 557 | ```kotlin 558 | class NoCommas( 559 | val foo: Int, 560 | + val bar: Int, 561 | ) 562 | ``` 563 | 564 | As we can see, using trailing commas we are taking care of our reviewers, since we are avoiding them some eye fatigue in the hour of reviewing our PR. --------------------------------------------------------------------------------