├── 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 |
3 |
4 | [](https://sonarcloud.io/dashboard?id=celerik_android-kotlin-boilerplate) [](https://sonarcloud.io/dashboard?id=celerik_android-kotlin-boilerplate) [](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 | 
75 | 
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.
--------------------------------------------------------------------------------