Skip to content

Instantly share code, notes, and snippets.

@PetkevichPavel
Last active September 2, 2020 14:22
Show Gist options
  • Select an option

  • Save PetkevichPavel/0c871f59fc825d689d4d4b9259b5988c to your computer and use it in GitHub Desktop.

Select an option

Save PetkevichPavel/0c871f59fc825d689d4d4b9259b5988c to your computer and use it in GitHub Desktop.

Revisions

  1. PetkevichPavel revised this gist Aug 13, 2020. 1 changed file with 33 additions and 0 deletions.
    33 changes: 33 additions & 0 deletions BaseEpoxyHolder.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,33 @@
    abstract class BaseEpoxyHolder : EpoxyHolder() {
    lateinit var view: View
    val context: Context
    get() = view.context

    @CallSuper
    override fun bindView(itemView: View) {
    view = itemView
    }

    protected fun <V : View> bind(id: Int): ReadOnlyProperty<BaseEpoxyHolder, V> =
    Lazy { holder: BaseEpoxyHolder, prop ->
    holder.view.findViewById(id) as V?
    ?: throw IllegalStateException("View ID $id for '${prop.name}' not found.")
    }

    private class Lazy<V>(private val initializer: (BaseEpoxyHolder, KProperty<*>) -> V) :
    ReadOnlyProperty<BaseEpoxyHolder, V> {
    private object EMPTY

    private var value: Any? = EMPTY

    /**
    * Taken from Kotterknife.
    * https://github.com/JakeWharton/kotterknife
    */
    @Suppress("UNCHECKED_CAST")
    override fun getValue(thisRef: BaseEpoxyHolder, property: KProperty<*>): V {
    if (value == EMPTY) value = initializer(thisRef, property)
    return value as V
    }
    }
    }
  2. PetkevichPavel revised this gist Aug 13, 2020. 3 changed files with 467 additions and 0 deletions.
    176 changes: 176 additions & 0 deletions ActivityExtensions.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,176 @@
    package com.berider.app.common.utils

    import android.app.Activity
    import android.content.Intent
    import android.os.Bundle
    import android.os.Parcelable
    import android.view.inputmethod.InputMethodManager
    import androidx.activity.OnBackPressedCallback
    import androidx.appcompat.app.ActionBar
    import androidx.appcompat.app.AppCompatActivity
    import androidx.core.os.bundleOf
    import com.afollestad.materialdialogs.LayoutMode
    import com.afollestad.materialdialogs.customview.customView
    import com.berider.app.common.R
    import com.berider.app.common.navigation.Navigation
    import com.berider.app.common.sharedpref.Generic
    import com.berider.app.common.sharedpref.credential.CredentialsManager
    import kotlinx.android.synthetic.main.unauthorized_bottom_sheet.*
    import org.jetbrains.anko.contentView

    /**
    * Created by pavel.petkevich@skodaautodigilab.com on 12.March.2020
    */

    /**
    * AppCompatActivity extension function, make from primary colored action bar transparent.
    * @param isTransparent - Boolean, true-is transparent, otherwise primary colored.
    * @param showTile - Boolean, true for show title, otherwise do not show title.
    * @return The Activity's ActionBar, or null if it does not have one.
    */
    fun AppCompatActivity.actionBarTransparent(
    isTransparent: Boolean = true,
    showTile: Boolean = false
    ) = supportActionBar?.apply {
    setBackgroundDrawable(getDrawable(if (isTransparent) R.color.transparent else R.color.colorOnPrimary))
    setDisplayShowTitleEnabled(showTile)
    }

    /**
    * Actionbar extension, for hiding/showing action bar.
    * @param show - Boolean, true for show, otherwise false for hiding.
    */
    fun ActionBar.showActionBar(show: Boolean = true) {
    if (show) show() else hide()
    }

    /**
    * AppCompatActivity extension function on registering onBack pressed callback.
    * @param onBackPressed - lambda function for overriding callback in calling place.
    * @return OnBackPressedCallback - on back pressed callback.
    */
    fun AppCompatActivity.registerOnBackPressedListener(onBackPressed: () -> Unit): OnBackPressedCallback =
    object : OnBackPressedCallback(true) {
    override fun handleOnBackPressed() {
    onBackPressed.invoke()
    }
    }.also {
    onBackPressedDispatcher.addCallback(this, it)
    }

    /**
    * Activity extension function for safely of hiding the soft key board.s
    * @return Boolean - in case of hided true, and false if the keyboard wasn't open.
    */
    fun Activity.hideKeyboard() = contentView?.windowToken?.let { wToken ->
    (getSystemService(Activity.INPUT_METHOD_SERVICE) as? InputMethodManager)?.hideSoftInputFromWindow(wToken, 0)
    }

    /**
    * Activity extension function for getting
    * Generic parcelable object from bundle.
    * @param bundleName - name of bundle.
    * @param argName - argument name.
    * @return T - generic parcelable object, nullable.
    */
    fun <T : Parcelable> Activity.getBundleArg(bundleName: String, argName: String) =
    intent?.getBundleExtra(bundleName)?.getParcelable<T>(argName)

    /**
    * Activity extension function for getting argument with type Any from bundle.
    * @param bundleName - name of bundle.
    * @param argName - argument name.
    * @return Any - [Any] type, nullable.
    */
    fun Activity.getBundleArg(bundleName: String, argName: String) = intent?.getBundleExtra(bundleName)?.get(argName)

    /**
    * Activity extension function for getting bundle name from name of ::class.java.
    * @return bundle name.
    */
    fun Activity.getBundleName() = "${this::class.java.name}-bundle"

    /**
    * Class<T> extension function for getting bundle name from name of Class<T>.
    * @return bundle name.
    */
    fun <T> Class<T>.getBundleName() = "$name-bundle"

    /**
    * Activity extension function, for getting bundle with [bundleName] and [argKeys].
    * @return bundle - Returns a new [Bundle] with the given key/value pairs as elements.
    */
    fun Activity.getBundleArgs(bundleName: String, vararg argKeys: String) = bundleOf().apply {
    argKeys.forEach { argKey ->
    intent?.getBundleExtra(bundleName)?.get(argKey)?.let { arg ->
    putAny(argKey, arg)
    }
    }
    }

    /**
    * Bundle extension function for putting Any [arg] in correct data type.
    * @param argKey - key for bundle element.
    * @param arg - bundle element.
    * @return Boolean - false in case of value is not supported otherwise true.
    */
    fun Bundle.putAny(argKey: String, arg: Any): Boolean {
    when {
    Generic<String>().checkType(arg) -> (arg as? String)?.let {
    putString(argKey, it)
    }
    Generic<Boolean>().checkType(arg) -> (arg as? Boolean)?.let {
    putBoolean(argKey, it)
    }
    Generic<Float>().checkType(arg) -> (arg as? Float)?.let {
    putFloat(argKey, it)
    }
    Generic<Int>().checkType(arg) -> (arg as? Int)?.let {
    putInt(argKey, it)
    }
    Generic<Long>().checkType(arg) -> (arg as? Long)?.let {
    putLong(argKey, it)
    }
    else -> return false
    }
    return true
    }

    /**
    * Activity extension, finish activity with result.
    * @param result - Integer by default [Activity.RESULT_OK], which you can use also you will find here [Activity].
    * @param intent - in case you want to return some data use the intent, by default is null.
    */
    fun Activity.finishWithResult(result: Int = Activity.RESULT_OK, intent: Intent? = null) {
    setResult(result, intent)
    finish()
    }

    /**
    * Activity extension function, for rendering unauthorized state or continue [block] if the user authorized.
    * @param credentials - [CredentialsManager].
    * @param navigation - [Navigation] - for navigating.
    */
    fun Activity.renderUnauthorizedState(credentials: CredentialsManager, navigation: Navigation, block: () -> Unit) {
    if (credentials.hasValidCredentials()) block()
    else unauthorizedBottomSheet(navigation)
    }

    /**
    * Activity extension function, for showing unauthorized bottom sheet.
    * @param navigation - for navigating into specific sections of Auth Flow [Navigation].
    */
    fun Activity.unauthorizedBottomSheet(navigation: Navigation) {
    showBottomSheet(layoutMode = LayoutMode.WRAP_CONTENT) { md ->
    md.customView(R.layout.unauthorized_bottom_sheet).apply {
    btnUnauthorizedLogin?.setOnClickListener {
    navigation.navigateToLogin(this@unauthorizedBottomSheet)
    md.cancel()
    }
    btnUnauthorizedSingUp?.setOnClickListener {
    navigation.navigateToRegistration(this@unauthorizedBottomSheet)
    md.cancel()
    }
    }
    }
    }
    195 changes: 195 additions & 0 deletions GeneralExtensions.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,195 @@
    package com.berider.app.common.utils

    import android.app.Activity
    import android.content.SharedPreferences
    import android.net.Uri
    import com.berider.app.common.BuildConfig
    import com.berider.app.common.base.BaseConstant
    import com.berider.app.common.sharedpref.Generic
    import com.google.crypto.tink.subtle.Hex
    import okhttp3.MediaType.Companion.toMediaTypeOrNull
    import okhttp3.MultipartBody
    import okhttp3.RequestBody
    import okhttp3.RequestBody.Companion.asRequestBody
    import okhttp3.RequestBody.Companion.toRequestBody
    import timber.log.Timber
    import java.io.File
    import java.lang.Enum.valueOf
    import java.util.*
    import javax.crypto.KeyGenerator
    import kotlin.reflect.KVisibility
    import kotlin.reflect.full.memberProperties

    /**
    * Created by pavel.petkevich@skodaautodigilab.com on 18.March.2020
    */

    /**
    * Executes given block and returns it's return value of null on case of some exception.
    */
    fun <T> safe(block: () -> T): T? =
    try {
    block.invoke()
    } catch (e: Exception) {
    Timber.i(e)
    null
    }

    /**
    * Function for generating key according to [algorithm] and with specific [length].
    * @param algorithm - by default is HmacMD5.
    * @param length - by default is 56 chars.
    * @return String - Hex encoded key as a String value.
    */
    fun generateEncKey(algorithm: String = "HmacMD5", length: Int = 56) =
    KeyGenerator.getInstance(algorithm)?.apply {
    init(length)
    }?.let { Hex.encode(it.generateKey().encoded) }.orEmpty()

    /**
    * Activity extension function.
    * Prepare file part for multipart.
    * @param fileName - name of file in multipart.
    * @param uri - uri to the file.
    * @return MultipartBody.Part - returns part can be null.
    */
    fun Activity.prepareFilePart(fileName: String, uri: Uri): MultipartBody.Part? =
    uri.path?.let { path ->
    File(path).let { file ->
    MultipartBody.Part.createFormData(
    fileName,
    file.name,
    file.asRequestBody(contentResolver.getType(uri)?.toMediaTypeOrNull())
    )
    }
    }

    /**
    * String extension function for creating a RequestBody from string.
    * @param mediaTypeStr - media type string which will be transform into toMediaTypeOrNull, by default is [BaseConstants.MediaType.TEXT_PLAIN].
    * @return RequestBody - request body that transmits this string.
    */
    fun String.createPart(mediaTypeStr: String = BaseConstants.MediaType.TEXT_PLAIN): RequestBody =
    this.toRequestBody(mediaTypeStr.toMediaTypeOrNull())

    /**
    * Any extension function which get string from Class<T> parameter via locale.
    * @param paramName - Class<T> parameter name for getting value.
    * @param locale - is optional, by default using [Locale.getDefault].
    * @return string - parameter value as a string.
    */
    fun Any.getStringViaLocale(paramName: String, locale: Locale? = Locale.getDefault()) =
    "${this.getParameter(
    if (listOf(*BuildConfig.APP_LOCALES).contains(locale?.language)) {
    "${paramName}_${locale?.language}"
    } else "${paramName}_${Locale.ENGLISH.language}"
    )}"

    /**
    * Any extension reflection function for return field data of Any object via field name.
    * @param paramName - Class<T> parameter name for getting value.
    * @return Any - field data of Any object via field name, can be null.
    */
    fun Any.getParameter(paramName: String): Any? {
    this::class.memberProperties.forEach { property ->
    property.takeIf { it.visibility == KVisibility.PUBLIC }?.apply {
    if (name == paramName) return getter.call(this@getParameter)
    }
    }
    return null
    }

    /**
    * Int extension function, for checking if the index is the first element of Array/List.
    * @param block - lambda function, invoked in case of the first element.
    */
    fun Int.isFirst(block: () -> Unit) = if (this == 0) block() else null

    /**
    * Map<String, String> extension function, where <[Locale.getLanguage], String>.
    * @param locale - is optional, by default using [Locale.getDefault].
    * @return String - return prepared string or null.
    */
    fun Map<String, String>.getItemViaLocale(locale: Locale? = Locale.getDefault()) =
    if (listOf(*BuildConfig.APP_LOCALES).contains(locale?.language)) {
    this[locale?.language]
    } else this[Locale.ENGLISH.language]

    /**
    * String extension function, where string is a currency code as a String.
    * @return currency symbol or empty string.
    */
    fun String?.getCurrencySymbolOrEmpty() =
    safe {
    this?.takeIf { it.isNotBlank() }?.run { Currency.getInstance(this)?.symbol } ?: defaultCurrencyCode
    } ?: defaultCurrencyCode

    /**
    * Generic Enum valueOf function, which is via [value] safely return value as [T] or null using [safe] block.
    * @param value - string value.
    * @return T? - returns [T] or null.
    */
    inline fun <reified T : Enum<T>> enumValueOf(value: String): T? = safe { valueOf(T::class.java, value) }

    /**
    * Generic T extension Infix function, for returning T or [otherVal] in case of T is null.
    * @param otherVal - otherVal to return.
    * @return T - should be always returns [T].
    */
    infix fun <T> T?.or(otherVal: T) = this ?: otherVal

    /**
    * Generic T extension function, for returning T or [otherVal] according to [isDefVal].
    * @param otherVal - otherVal to return.
    * @param isDefVal - is true returns current value else [otherVal].
    * @return T - should be always returns [T].
    */
    fun <T> T.or(otherVal: T, isDefVal: Boolean) = if (isDefVal) this else otherVal

    /**
    * General Boolean? extension function.
    * @return returns value(true/false) or in case of the value is null returns false.
    */
    fun Boolean?.orFalse() = this ?: false

    /**
    * SharedPreferences.Editor extension function for putting [value] as Any into Shared preferences.
    * @param key - key for the value.
    * @param value - value as Any.
    */
    @Suppress("UNCHECKED_CAST")
    fun SharedPreferences.Editor.putAny(key: String, value: Any) {
    when {
    Generic<String>().checkType(value) -> (value as? String)?.let { putString(key, it) }
    Generic<Boolean>().checkType(value) -> (value as? Boolean)?.let { putBoolean(key, it) }
    Generic<Float>().checkType(value) -> (value as? Float)?.let { putFloat(key, it) }
    Generic<Int>().checkType(value) -> (value as? Int)?.let { putInt(key, it) }
    Generic<Long>().checkType(value) -> (value as? Long)?.let { putLong(key, it) }
    Generic<Set<String>>().checkType(value) -> (value as? Set<String>)?.let {
    putStringSet(key, it)
    }
    }
    }

    /**
    * Boolean extension function with generic Params and returned value, for choosing the value via condition.
    * @param first - first value of DT returns in case the true.
    * @param second - second value of DT returns in case the false.
    */
    fun <DT> Boolean.chooseByCondition(first: DT, second: DT) = takeIf { this }?.run { first } ?: second

    /**
    * T generic extension function, which returns value according to [condition], in case the condition is true will return [value] otherwise [T].
    * @param value - value which has to be returned in case the condition is true.
    * @param condition - condition for processing the result of the function.
    */
    fun <T> T.orWith(value: T, condition: Boolean) = takeIf { condition }?.run { value } ?: this

    /**
    * T generic extension function, which returns T as returned value or returns it in callback [block].
    * @param condition - condition for the if, under which method will returns null & no triggering [block] callback.
    * @param block - returns the T as it is only in case the condition is true.
    * @return T - returning always T if it satisfies the given [condition] otherwise null.
    */
    fun <T> T.continueWithIf(condition: Boolean, block: (T.() -> Unit?)? = null) =
    this.takeIf { condition }?.apply { block?.invoke(this@continueWithIf) }
    96 changes: 96 additions & 0 deletions PermissionExtensions.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,96 @@
    package com.berider.app.common.utils

    import android.Manifest.permission.*
    import android.app.Activity
    import android.content.Context
    import android.content.pm.PackageManager
    import androidx.core.app.ActivityCompat
    import androidx.core.content.ContextCompat
    import androidx.fragment.app.Fragment
    import com.berider.app.analytics.base.AnalyticsService
    import com.berider.app.analytics.base.Event
    import com.berider.app.common.utils.PermissionConstants.REQUEST_LOCATION
    import com.berider.app.common.utils.PermissionConstants.locationPermissions

    /**
    * Created by pavel.petkevich@skodaautodigilab.com on 20.March.2020
    */

    object PermissionConstants {
    const val REQUEST_STORAGE = 1000
    const val REQUEST_CAMERA = 1001
    const val REQUEST_LOCATION = 1002

    val permissionMap = mapOf(
    CAMERA to REQUEST_CAMERA,
    READ_EXTERNAL_STORAGE to REQUEST_STORAGE,
    ACCESS_FINE_LOCATION to REQUEST_LOCATION
    )

    internal val locationPermissions = arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)
    }

    /**
    * Context extension function, check if permission is gived or not.
    * @return Boolean - true in case of gived otherwise false.
    */
    fun Context.isLocationPermissionGranted() =
    ContextCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED

    /**
    * Fragment extension function for running code with following [permission].
    * @param permission - manifest permission.
    * @param granted - lambda function invoked in case of permission already granted.
    * @param showRationale - lambda function invoked in case of needs to show rationale before requesting [permission].
    */
    fun Fragment.runWithPermission(
    permission: String,
    granted: () -> Unit,
    analyticsService: AnalyticsService,
    showRationale: (() -> Unit)? = null
    ) {
    activity?.let { act ->
    when {
    ContextCompat.checkSelfPermission(act, permission) == PackageManager.PERMISSION_GRANTED -> granted.invoke()
    ActivityCompat.shouldShowRequestPermissionRationale(
    act,
    READ_CONTACTS // TODO PermissionRationale: ignoring(set it on READ_CONTACTS) replace with [permission].
    ) -> showRationale?.invoke()
    else -> PermissionConstants.permissionMap[permission]?.let {
    analyticsService.logEvent(Event.Names.GLOBAL_PERMISSION.name, Event.Parameters.TYPE.name to permission)
    requestPermissions(it.getPermissions(permission), it)
    }
    }
    }
    }

    /**
    * Activity extension function for running code with following [permission].
    * @param permission - manifest permission.
    * @param granted - lambda function invoked in case of permission already granted.
    * @param showRationale - lambda function invoked in case of needs to show rationale before requesting [permission].
    */
    fun Activity.runWithPermission(
    permission: String,
    granted: (() -> Unit)? = null,
    analyticsService: AnalyticsService,
    showRationale: (() -> Unit)? = null
    ) {
    when {
    ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED -> granted?.invoke()
    ActivityCompat.shouldShowRequestPermissionRationale(
    this,
    READ_CONTACTS // TODO PermissionRationale: ignoring(set it on READ_CONTACTS) replace with [permission].
    ) -> showRationale?.invoke()
    else -> PermissionConstants.permissionMap[permission]?.let {
    analyticsService.logEvent(Event.Names.GLOBAL_PERMISSION.name, Event.Parameters.TYPE.name to permission)
    requestPermissions(it.getPermissions(permission), it)
    }
    }
    }

    private fun Int.getPermissions(permission: String) =
    when (this) {
    REQUEST_LOCATION -> locationPermissions
    else -> arrayOf(permission)
    }
  3. PetkevichPavel revised this gist Aug 13, 2020. 2 changed files with 150 additions and 0 deletions.
    72 changes: 72 additions & 0 deletions activity_onboarding.xml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,72 @@
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/onbMainConstraint"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.berider.app.onboarding.ui.OnboardingActivity">

    <Button
    android:id="@+id/onbSkipBtn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="@dimen/content_4"
    android:layout_marginEnd="@dimen/content_4"
    android:background="?attr/selectableItemBackgroundBorderless"
    android:text="@string/general_skip"
    android:textAllCaps="false"
    android:textColor="@color/base_black"
    android:visibility="gone"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

    <com.airbnb.epoxy.EpoxyRecyclerView
    android:id="@+id/epoxyRecyclerView"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:overScrollMode="never"
    app:layout_constraintBottom_toTopOf="@+id/guidelineContent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:listitem="@layout/onboarding_page" />

    <androidx.constraintlayout.widget.Guideline
    android:id="@+id/guidelineContent"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    app:layout_constraintGuide_percent="0.8" />

    <me.relex.circleindicator.CircleIndicator2
    android:id="@+id/onbIndicator"
    android:layout_width="wrap_content"
    android:layout_height="@dimen/content_32"
    app:ci_drawable="@drawable/black_ci"
    app:layout_constraintBottom_toTopOf="@+id/onbMainBtn"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="@+id/guidelineContent" />

    <androidx.constraintlayout.widget.Guideline
    android:id="@+id/guidelineBtnWidth"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    app:layout_constraintGuide_percent="0.55" />


    <com.google.android.material.button.MaterialButton
    android:id="@+id/onbMainBtn"
    style="@style/BaseOutlineButton"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_margin="@dimen/content_16"
    android:text="@string/general_next"
    android:visibility="gone"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="@+id/guidelineBtnWidth" />

    </androidx.constraintlayout.widget.ConstraintLayout>
    78 changes: 78 additions & 0 deletions onboarding_page.xml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,78 @@
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/transparent">

    <ImageView
    android:id="@+id/imgOnbPage"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:scaleType="centerCrop"
    app:layout_constraintBottom_toTopOf="@+id/guidelineImg"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:ignore="ContentDescription" />

    <ProgressBar
    android:id="@+id/progressOnb"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="gone"
    app:layout_constraintBottom_toBottomOf="@+id/imgOnbPage"
    app:layout_constraintEnd_toEndOf="@+id/imgOnbPage"
    app:layout_constraintStart_toStartOf="@+id/imgOnbPage"
    app:layout_constraintTop_toTopOf="@+id/imgOnbPage"
    tools:visibility="visible" />

    <androidx.constraintlayout.widget.Guideline
    android:id="@+id/guidelineImg"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    app:layout_constraintGuide_percent="0.6" />

    <TextView
    android:id="@+id/txtOnbTitle"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:fontFamily="sans-serif"
    android:letterSpacing="0.01"
    android:lineSpacingExtra="-4sp"
    android:paddingStart="@dimen/content_16"
    android:paddingTop="@dimen/content_16"
    android:paddingEnd="@dimen/content_16"
    android:textColor="#de000000"
    android:textSize="28sp"
    android:textStyle="bold"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="@+id/guidelineImg"
    tools:text="Jezděte opatrně!" />

    <TextView
    android:id="@+id/txtOnbContent"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:fontFamily="sans-serif"
    android:letterSpacing="0.03"
    android:lineSpacingExtra="8sp"
    android:padding="@dimen/content_16"
    android:textColor="#de000000"
    android:textSize="16sp"
    android:textStyle="normal"
    app:autoSizeMaxTextSize="16sp"
    app:autoSizeMinTextSize="10sp"
    app:autoSizeStepGranularity="1sp"
    app:autoSizeTextType="uniform"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/txtOnbTitle"
    tools:text="Dodržujte dopravní předpisy a buďte při jízdě obzvlášť opatrní. Vaše bezpečí je pro nás důležité." />


    </androidx.constraintlayout.widget.ConstraintLayout>
  4. PetkevichPavel created this gist Aug 13, 2020.
    135 changes: 135 additions & 0 deletions OnboardingActivity
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,135 @@
    package com.berider.app.onboarding.ui

    import android.Manifest
    import android.content.pm.PackageManager
    import android.os.Bundle
    import androidx.core.os.bundleOf
    import androidx.lifecycle.Observer
    import androidx.lifecycle.lifecycleScope
    import androidx.recyclerview.widget.LinearLayoutManager
    import androidx.recyclerview.widget.PagerSnapHelper
    import androidx.recyclerview.widget.RecyclerView
    import com.airbnb.epoxy.EpoxyControllerAdapter
    import com.berider.app.analytics.base.Event
    import com.berider.app.common.base.BaseActivity
    import com.berider.app.common.utils.*
    import com.berider.app.models.domain.onboarding.Onboarding
    import com.berider.app.models.domain.onboarding.OnboardingPage
    import com.berider.app.models.domain.onboarding.OnboardingPage.Companion.getPageType
    import com.berider.app.onboarding.R
    import com.berider.app.onboarding.epoxy.PageController
    import kotlinx.android.synthetic.main.activity_onboarding.*
    import org.koin.androidx.viewmodel.ext.android.viewModel

    class OnboardingActivity : BaseActivity() {

    companion object {
    const val ONBOARDING_TYPE = "onboarding_type"

    fun prepareBundle(onboardingType: Onboarding.Type) = bundleOf(ONBOARDING_TYPE to onboardingType)
    }

    private val viewModel by viewModel<OnboardingViewModel>()
    private var data: List<OnboardingPage>? = null
    private var currentPagePos = 0
    private var currentPage: OnboardingPage? = null
    private var layoutManager: LinearLayoutManager? = null

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_onboarding)

    getBundleArg<Onboarding.Type>(getBundleName(), ONBOARDING_TYPE)?.let {
    analyticsService.logEvent(Event.Names.ONB_VIEW.name, Event.Parameters.TYPE.name to it.name)
    viewModel.fetchOnBoarding(it, isLocationPermissionGranted())
    } ?: finish()

    onbMainBtn?.onClick(lifecycleScope) {
    currentPage?.chooseAction()
    }

    onbSkipBtn?.setOnClickListener {
    analyticsService.logEvent(Event.Names.ONB_PAGE_BTN_SKIP.name, *getData(currentPagePos))
    finish()
    }
    }

    override fun setObservers() {
    super.setObservers()
    viewModel.data.observe(this, Observer { data ->
    this.data = data
    PageController.setData(data).adapter.setRecyclerView()
    onbMainConstraint.makeVisible()
    })
    viewModel.uiState.observe(this, Observer { state ->
    processState(state)
    })
    }

    private fun EpoxyControllerAdapter.setRecyclerView() {
    layoutManager = LinearLayoutManager(this@OnboardingActivity, LinearLayoutManager.HORIZONTAL, false)
    epoxyRecyclerView?.layoutManager = layoutManager
    epoxyRecyclerView?.adapter = this

    PagerSnapHelper().apply {
    epoxyRecyclerView.onFlingListener = null
    attachToRecyclerView(epoxyRecyclerView)
    onbIndicator.attachToRecyclerView(epoxyRecyclerView, this)
    }

    epoxyRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
    super.onScrolled(recyclerView, dx, dy)
    layoutManager?.findLastCompletelyVisibleItemPosition().takeIf { it != -1 }?.apply {
    currentPagePos = this
    data?.get(this)?.run {
    setButton()
    analyticsService.logEvent(Event.Names.ONB_PAGE_VIEW.name, *getData(this@apply))
    }
    }
    }
    })
    }

    private fun OnboardingPage.setButton() {
    currentPage = this@setButton
    getPageType(type)?.apply {
    onbSkipBtn?.makeVisible(isSkippable)
    guidelineBtnWidth?.moveWithAnim(guidelineBtnWidth.currentPercent(), guidelinePosition)
    onbMainBtn?.setWith(stringResId, styleResId, colorResId)?.makeVisible()
    }
    }

    private fun OnboardingPage.chooseAction() {
    when (getPageType(type)) {
    OnboardingPage.Type.BASE -> nextPage()
    OnboardingPage.Type.LOCATION -> runWithPermission(
    Manifest.permission.ACCESS_FINE_LOCATION,
    analyticsService = analyticsService
    )
    OnboardingPage.Type.START_RIDE -> finish()
    OnboardingPage.Type.FINISH_RIDE -> finish()
    }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    when (requestCode) {
    PermissionConstants.REQUEST_LOCATION -> finish()
    }
    }
    }

    private fun getData(position: Int) = data?.get(position)?.run {
    arrayOf(Event.Parameters.TYPE.name to type, Event.Parameters.PAGE_NUMBER.name to position)
    } ?: emptyArray()

    private fun nextPage() {
    layoutManager?.findLastCompletelyVisibleItemPosition()?.plus(1)?.let { pos ->
    if (currentPage == data?.last()) finish()
    else epoxyRecyclerView?.smoothScrollToPosition(pos)
    analyticsService.logEvent(Event.Names.ONB_PAGE_BTN_NEXT.name, *getData(data?.getOrLast(currentPagePos).orZero()))
    }
    }
    }
    50 changes: 50 additions & 0 deletions OnboardingRepository.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,50 @@
    package com.berider.app.onboarding.data

    import com.berider.app.common.base.BaseRepository
    import com.berider.app.common.core.ResponseState
    import com.berider.app.common.utils.SingleLiveEvent
    import com.berider.app.common.utils.emit
    import com.berider.app.models.domain.onboarding.Onboarding
    import com.berider.app.models.domain.onboarding.OnboardingPage
    import com.berider.app.models.domain.onboarding.OnboardingPage.Companion.getPageType
    import com.berider.app.onboarding.R
    import com.berider.app.remoteconfig.RemoteConfigFunctions

    /**
    * Created by pavel.petkevich@skodaautodigilab.com on 06.April.2020
    */
    interface IOnboardingRepository {
    val uiState: SingleLiveEvent<ResponseState>
    val data: SingleLiveEvent<List<OnboardingPage>?>
    fun fetchOnBoarding(onboardingType: Onboarding.Type, isLocationPermissionGranted: Boolean)
    }

    class OnboardingRepository(private val remoteConfigFunctions: RemoteConfigFunctions) : IOnboardingRepository, BaseRepository() {
    override val uiState: SingleLiveEvent<ResponseState>
    get() = responseState

    private val _data = SingleLiveEvent<List<OnboardingPage>?>()
    override val data: SingleLiveEvent<List<OnboardingPage>?>
    get() = _data

    override fun fetchOnBoarding(onboardingType: Onboarding.Type, isLocationPermissionGranted: Boolean) {
    responseState.emit(ResponseState.BlockingLoadingState.Start(bgColorRes = R.color.transparent))
    remoteConfigFunctions.apply {
    Array<OnboardingPage>::class.fetchRemoteConfig(
    onboardingType.defRcFile,
    onboardingType.rcKeyName
    ) { list ->
    _data.emit(list?.removeLocationPermPage(onboardingType, isLocationPermissionGranted)?.filter { !it.isIosOnly })
    responseState.emit(ResponseState.BlockingLoadingState.Stop())
    }
    }
    }

    private fun Array<OnboardingPage>.removeLocationPermPage(
    onboardingType: Onboarding.Type,
    isLocationPermissionGranted: Boolean
    ) = toMutableList().apply {
    this.takeIf { onboardingType == Onboarding.Type.MAIN && isLocationPermissionGranted }
    ?.find { getPageType(it.type) == OnboardingPage.Type.LOCATION }?.let { remove(it) }
    }
    }
    21 changes: 21 additions & 0 deletions OnboardingViewModel
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,21 @@
    package com.berider.app.onboarding.ui

    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import com.berider.app.models.domain.onboarding.Onboarding
    import com.berider.app.onboarding.data.IOnboardingRepository
    import kotlinx.coroutines.launch

    /**
    * Created by pavel.petkevich@skodaautodigilab.com on 06.April.2020
    */
    class OnboardingViewModel(private val onboardingRepository: IOnboardingRepository) : ViewModel() {
    val uiState = onboardingRepository.uiState
    val data = onboardingRepository.data

    fun fetchOnBoarding(onboardingType: Onboarding.Type, isLocationPermissionGranted: Boolean) {
    viewModelScope.launch {
    onboardingRepository.fetchOnBoarding(onboardingType, isLocationPermissionGranted)
    }
    }
    }
    62 changes: 62 additions & 0 deletions PageModel.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,62 @@
    package com.berider.app.onboarding.epoxy

    import android.widget.ImageView
    import android.widget.ProgressBar
    import android.widget.TextView
    import com.airbnb.epoxy.EpoxyAttribute
    import com.airbnb.epoxy.EpoxyModelClass
    import com.airbnb.epoxy.EpoxyModelWithHolder
    import com.airbnb.epoxy.TypedEpoxyController
    import com.berider.app.common.epoxy.BaseEpoxyHolder
    import com.berider.app.common.utils.getStringViaLocale
    import com.berider.app.common.utils.makeVisible
    import com.berider.app.common.utils.setImageToView
    import com.berider.app.models.domain.onboarding.OnboardingPage
    import com.berider.app.onboarding.R

    /**
    * Created by pavel.petkevich@skodaautodigilab.com on 07.April.2020
    */
    @EpoxyModelClass
    abstract class PageModel : EpoxyModelWithHolder<PageModel.Holder?>() {
    @EpoxyAttribute
    lateinit var page: OnboardingPage
    override fun getDefaultLayout(): Int = R.layout.onboarding_page

    override fun bind(holder: Holder) {
    super.bind(holder)
    with(holder) {
    progressOnb.apply {
    makeVisible()
    imgOnbPage.setImageToView(page.imageURL) {
    makeVisible(false)
    }
    }
    txtOnbTitle.text = page.getStringViaLocale(OnboardingPage.TITLE_PARAM)
    txtOnbContent.text = page.getStringViaLocale(OnboardingPage.CONTENT_PARAM)
    }
    }

    class Holder : BaseEpoxyHolder() {
    val progressOnb: ProgressBar by bind(R.id.progressOnb)
    val imgOnbPage: ImageView by bind(R.id.imgOnbPage)
    val txtOnbTitle: TextView by bind(R.id.txtOnbTitle)
    val txtOnbContent: TextView by bind(R.id.txtOnbContent)
    }
    }

    class PageController : TypedEpoxyController<List<OnboardingPage>>() {

    companion object {
    fun setData(data: List<OnboardingPage>?) = PageController().apply { setData(data) }
    }

    override fun buildModels(pages: List<OnboardingPage>) {
    pages.forEach {
    page {
    id(it.id)
    page(it)
    }
    }
    }
    }
    33 changes: 33 additions & 0 deletions models.Onboarding
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,33 @@
    package com.berider.app.models.domain.onboarding

    import android.os.Parcelable
    import androidx.annotation.StringRes
    import com.berider.app.models.R
    import com.berider.app.models.domain.utils.ModelsConstants
    import kotlinx.android.parcel.Parcelize

    /**
    * Created by pavel.petkevich@skodaautodigilab.com on 09.April.2020
    */
    object Onboarding {
    val onboardings = listOf(Type.MAIN, Type.PRE_RIDE, Type.POST_RIDE)

    @Parcelize
    enum class Type(val defRcFile: String, val rcKeyName: String, @StringRes val stringRes: Int) : Parcelable {
    MAIN(
    "OnboardingMain.json",
    if (ModelsConstants.isStagingOrDev) "onb_main_staging" else "onb_main_prod",
    R.string.onboarding_main_item
    ),
    PRE_RIDE(
    "OnboardingPostRide.json",
    if (ModelsConstants.isStagingOrDev) "onb_before_ride_staging" else "onb_before_ride_prod",
    R.string.onboarding_pre_ride_item
    ),
    POST_RIDE(
    "OnboardingPreRide.json",
    if (ModelsConstants.isStagingOrDev) "onb_after_ride_staging" else "onb_after_ride_prod",
    R.string.onboarding_post_ride_item
    ),
    }
    }
    44 changes: 44 additions & 0 deletions models.OnboardingPage
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,44 @@
    package com.berider.app.models.domain.onboarding

    import androidx.annotation.ColorRes
    import androidx.annotation.StringRes
    import androidx.annotation.StyleRes
    import com.berider.app.models.R
    import com.squareup.moshi.JsonClass

    /**
    * Created by pavel.petkevich@skodaautodigilab.com on 06.April.2020
    */
    @JsonClass(generateAdapter = true)
    data class OnboardingPage(
    val id: Int,
    val imageURL: String,
    val title_cs: String,
    val title_en: String,
    val content_cs: String,
    val content_en: String,
    val isSkippable: Boolean,
    val isIosOnly: Boolean,
    val type: String
    ) {
    companion object {
    const val TITLE_PARAM = "title"
    const val CONTENT_PARAM = "content"
    const val SMALL_GUIDELINE = 0.55f
    const val FULL_GUIDELINE = 0f

    fun getPageType(str: String) = Type.values().find { it.name == str }
    }

    enum class Type(
    @StringRes val stringResId: Int,
    @StyleRes val styleResId: Int,
    @ColorRes val colorResId: Int,
    val guidelinePosition: Float
    ) {
    BASE(R.string.general_next, R.style.BaseOutlineButton, R.color.transparent, SMALL_GUIDELINE),
    LOCATION(R.string.general_allow, R.style.MaterialPrimaryButtonBlack, R.color.base_black, FULL_GUIDELINE),
    START_RIDE(R.string.general_ride, R.style.MaterialPrimaryButtonBlack, R.color.base_black, SMALL_GUIDELINE),
    FINISH_RIDE(R.string.general_continue, R.style.MaterialPrimaryButtonBlack, R.color.base_black, SMALL_GUIDELINE),
    }
    }