Skip to content

Instantly share code, notes, and snippets.

@PetkevichPavel
Last active September 2, 2020 14:22
Show Gist options
  • Save PetkevichPavel/0c871f59fc825d689d4d4b9259b5988c to your computer and use it in GitHub Desktop.
Save PetkevichPavel/0c871f59fc825d689d4d4b9259b5988c to your computer and use it in GitHub Desktop.
onboarding
<?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>
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()
}
}
}
}
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
}
}
}
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) }
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
),
}
}
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),
}
}
<?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>
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()))
}
}
}
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) }
}
}
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)
}
}
}
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)
}
}
}
}
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)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment