Skip to content

Instantly share code, notes, and snippets.

@jakubkinst
Last active January 29, 2020 16:40
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jakubkinst/7c84e5551c026141dc5a4062df5dbde8 to your computer and use it in GitHub Desktop.
Save jakubkinst/7c84e5551c026141dc5a4062df5dbde8 to your computer and use it in GitHub Desktop.
ktools

ktools

Kotlin Tools for Android ViewModel, LiveData, Data Binding, Dependency injection, Async operations, Repository pattern, Retrofit, Form Validation, Cloud Firestore, etc.

package com.strv.ktools
import android.os.Handler
import android.os.Looper
import java.lang.ref.WeakReference
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Future
class AnkoAsyncContext<T>(val weakRef: WeakReference<T>)
fun <T> AnkoAsyncContext<T>.uiThread(f: (T) -> Unit): Boolean {
val ref = weakRef.get() ?: return false
if (ContextHelper.mainThread == Thread.currentThread()) {
f(ref)
} else {
ContextHelper.handler.post { f(ref) }
}
return true
}
fun <T> T.doAsync(
exceptionHandler: ((Throwable) -> Unit)? = null,
task: AnkoAsyncContext<T>.() -> Unit
): Future<Unit> {
val context = AnkoAsyncContext(WeakReference(this))
return BackgroundExecutor.submit {
try {
context.task()
} catch (thr: Throwable) {
exceptionHandler?.invoke(thr) ?: Unit
}
}
}
internal object BackgroundExecutor {
var executor: ExecutorService =
Executors.newScheduledThreadPool(2 * Runtime.getRuntime().availableProcessors())
fun <T> submit(task: () -> T): Future<T> {
return executor.submit(task)
}
}
private object ContextHelper {
val handler = Handler(Looper.getMainLooper())
val mainThread = Looper.getMainLooper().thread
}
package com.strv.ktools
import androidx.lifecycle.LiveData
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
class CurrentUserLiveData(val auth: FirebaseAuth) : LiveData<FirebaseUser?>() {
private val listener = FirebaseAuth.AuthStateListener { firebaseAuth ->
value = firebaseAuth.currentUser
}
init {
value = auth.currentUser
}
override fun onActive() {
super.onActive()
auth.addAuthStateListener(listener)
}
override fun onInactive() {
auth.removeAuthStateListener(listener)
super.onInactive()
}
}
package com.strv.ktools
// To be used on when() expression to make sure compiler checks for exhaustivity
val <T> T.exhaustive: T
get() = this
package com.strv.ktools
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
// Provider functions - use scope param to create DI scopes
// Example: `provideSingleton { Gson() }`
// Inject function
// Example: `val gson by inject<Gson>()`
inline fun <reified T : Any> inject(scope: String = DI_SCOPE_GLOBAL) = object : ReadOnlyProperty<Any?, T> {
var value: T? = null
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
if (value != null) return value!!
val found = DIStorage.get(scope, T::class.java.name)
?: throw IllegalStateException("Dependency for property ${property.name}: ${T::class.java.name} not provided.")
return when (found) {
is SingletonDIProvider -> found.instance as T
else -> found.provider.invoke() as T
}.also { value = it }
}
}
inline fun <reified T : Any> findDependency(): T {
val dep: T by inject()
return dep
}
abstract class DIModule {
private inline fun <reified T : Any> provide(scope: String = DI_SCOPE_GLOBAL, noinline provider: () -> T) = DIStorage.put(scope, T::class.java.name, DIProvider(provider))
protected inline fun <reified T : Any> provideSingleton(scope: String = DI_SCOPE_GLOBAL, noinline provider: () -> T) = DIStorage.put(scope, T::class.java.name, SingletonDIProvider(provider))
abstract fun onProvide()
}
fun setupModule(module: DIModule) {
logD("Setting up DI module ${module.javaClass.name}")
module.onProvide()
}
// -- internal --
const val DI_SCOPE_GLOBAL = "#__global"
open class DIProvider<T>(val provider: () -> T)
class SingletonDIProvider<T>(provider: () -> T) : DIProvider<T>(provider) {
val instance by lazy { provider() }
}
object DIStorage {
private val provided = HashMap<String, HashMap<String, DIProvider<Any>>?>()
fun get(scope: String, className: String) = provided[scope]?.get(className)
fun put(scope: String, className: String, provider: DIProvider<Any>) {
if (!provided.containsKey(scope))
provided[scope] = hashMapOf()
provided[scope]!![className] = provider
}
}
package com.strv.ktools
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import androidx.room.Ignore
import com.firebase.ui.firestore.paging.FirestorePagingAdapter
import com.firebase.ui.firestore.paging.FirestorePagingOptions
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.DocumentReference
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.EventListener
import com.google.firebase.firestore.Exclude
import com.google.firebase.firestore.ListenerRegistration
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.QuerySnapshot
/**
* LiveData wrapper for Firestore database.
* EventListener is automatically attached to the Firestore database when LiveData becomes active. Listener is removed when LiveData becomes inactive.
* Data are parsed to the model, which has to be provided in the constructor.
*/
/**
* Document variant with document reference as a parameter.
*/
class FirestoreDocumentLiveData<T>(private val documentRef: DocumentReference, private val clazz: Class<T>) : LiveData<Resource<T>>() {
// if cached data are up to date with server DB, listener won't get called again with isFromCache=false
private val listener = EventListener<DocumentSnapshot> { value, e ->
logD("Loaded ${documentRef.path}, cache: ${value?.metadata?.isFromCache.toString()} error: ${e?.message}")
try {
val document = value?.toObject(clazz)
if (document != null && document is Document) {
document.docId = value.id
}
setValue(Resource(if (e != null) Resource.Status.ERROR else if (value!!.metadata.isFromCache) Resource.Status.LOADING else Resource.Status.SUCCESS,
document, e?.message))
} catch (e: RuntimeException) {
logE("Error while firestore document deserialization. docId: ${value?.id}")
setValue(Resource(Resource.Status.ERROR, null, e.message))
}
}
private lateinit var listenerRegistration: ListenerRegistration
override fun onActive() {
super.onActive()
logD("Start listening ${documentRef.path}")
listenerRegistration = documentRef.addSnapshotListener(listener)
}
override fun onInactive() {
super.onInactive()
logD("Stop listening ${documentRef.path}")
listenerRegistration.remove()
}
}
class FirestoreDocumentMapLiveData(private val documentRef: DocumentReference) : LiveData<Resource<Map<String, Any>>>() {
// if cached data are up to date with server DB, listener won't get called again with isFromCache=false
private val listener = EventListener<DocumentSnapshot> { value, e ->
logD("Loaded ${documentRef.path}, cache: ${value?.metadata?.isFromCache.toString()} error: ${e?.message}")
val map = value?.data
map?.put("docId", value.id)
setValue(Resource(if (e != null) Resource.Status.ERROR else if (value!!.metadata.isFromCache) Resource.Status.LOADING else Resource.Status.SUCCESS, map, e?.message))
}
private lateinit var listenerRegistration: ListenerRegistration
override fun onActive() {
super.onActive()
logD("Start listening ${documentRef.path}")
listenerRegistration = documentRef.addSnapshotListener(listener)
}
override fun onInactive() {
super.onInactive()
logD("Stop listening ${documentRef.path}")
listenerRegistration.remove()
}
}
/**
* Document variant with Query as a parameter. Query will be limited only for one document.
*/
class FirestoreDocumentQueryLiveData<T>(private val query: Query, private val clazz: Class<T>) : LiveData<Resource<T>>() {
// if cached data are up to date with server DB, listener won't get called again with isFromCache=false
private val listener = EventListener<QuerySnapshot> { value, e ->
logD("Loaded $query, cache: ${value?.metadata?.isFromCache.toString()} error: ${e?.message}")
val list = value?.documents?.map {
try {
val item = it.toObject(clazz)
if (item is Document) {
(item as Document).docId = it.id
}
item!!
} catch (e: RuntimeException) {
logE("Skipping firestore document deserialization. docId: ${it.id}")
e.printStackTrace()
null
}
}?.filterNot { it == null }?.map { it!! } ?: emptyList()
setValue(Resource(if (e != null) Resource.Status.ERROR else if (value!!.metadata.isFromCache) Resource.Status.LOADING else Resource.Status.SUCCESS,
if (!list.isEmpty()) list[0] else null, e?.message))
}
private lateinit var listenerRegistration: ListenerRegistration
override fun onActive() {
super.onActive()
logD("Start listening $query")
listenerRegistration = query.limit(1).addSnapshotListener(listener)
}
override fun onInactive() {
super.onInactive()
logD("Stop listening $query")
listenerRegistration.remove()
}
}
/**
* List of documents variant.
*/
class FirestoreDocumentListLiveData<T>(private val query: Query, private val clazz: Class<T>) : LiveData<Resource<List<T>>>() {
// if cached data are up to date with server DB, listener won't get called again with isFromCache=false
private val listener = EventListener<QuerySnapshot> { value, e ->
logD("Loaded $query; cache: ${value?.metadata?.isFromCache.toString()}; error: ${e?.message}; documents: ${value?.size()}")
val list: List<T> = value?.documents?.map {
try {
val item = it.toObject(clazz)
if (item is Document) {
(item as Document).docId = it.id
}
item!!
} catch (e: RuntimeException) {
logE("Skipping firestore document deserialization. docId: ${it.id}")
e.printStackTrace()
null
}
}?.filterNot { it == null }?.map { it!! } ?: emptyList()
setValue(Resource(if (e != null) Resource.Status.ERROR else if (value!!.metadata.isFromCache) Resource.Status.LOADING else Resource.Status.SUCCESS, list, e?.message))
}
private lateinit var listenerRegistration: ListenerRegistration
override fun onActive() {
super.onActive()
logD("Start listening $query")
listenerRegistration = query.addSnapshotListener(listener)
}
override fun onInactive() {
super.onInactive()
logD("Stop listening $query")
listenerRegistration.remove()
}
}
open class Document(@Ignore @get:Exclude var docId: String? = null)
// Search functionality
fun CollectionReference.whereStartsWith(field: String, prefix: String): Query {
val lastLetter = prefix[prefix.length - 1] + 1
val prefixShift = prefix.dropLast(1).plus(lastLetter)
logD("Searching between $prefix and $prefixShift")
return whereGreaterThanOrEqualTo(field, prefix).whereLessThan(field, prefixShift)
}
// Paged adapter
open class DataBoundFirestorePagingAdapter<T, S>(val query: Query, val pageSize: Int, val prefetchDistance: Int, val lifecycleOwner: LifecycleOwner, @LayoutRes val itemLayoutId: Int, val bindingVariableId: Int, val itemClass: Class<T>, val mappingFunction: (T?) -> S = { it as S })
: FirestorePagingAdapter<S, DataBoundViewHolder>(
FirestorePagingOptions.Builder<S>()
.setLifecycleOwner(lifecycleOwner)
.setQuery(query, PagedList.Config.Builder()
.setPageSize(pageSize)
.setEnablePlaceholders(false)
.setPrefetchDistance(prefetchDistance)
.build()
) {
try {
mappingFunction.invoke(it.toObject(itemClass).apply {
if (this is Document) docId = it.id
})
} catch (e: RuntimeException) {
logE("Skipping firestore document deserialization. docId: ${it.id}")
e.printStackTrace()
mappingFunction(null)
}
}
.build()
) {
private val extras = hashMapOf<Int, Any>()
override fun onBindViewHolder(holder: DataBoundViewHolder, position: Int, model: S) {
holder.binding.setVariable(bindingVariableId, model)
extras.forEach { (varId, extra) -> holder.binding.setVariable(varId, extra) }
holder.binding.executePendingBindings()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataBoundViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = DataBindingUtil.inflate<ViewDataBinding>(layoutInflater, itemLayoutId, parent, false)
binding.setLifecycleOwner(lifecycleOwner)
return DataBoundViewHolder(binding)
}
fun bindExtra(bindingVariableId: Int, extra: Any) = this.also {
extras.put(bindingVariableId, extra)
}
}
// extension methods
fun <T> Query.toLiveData(clazz: Class<T>) = FirestoreDocumentListLiveData(this, clazz)
fun <T> DocumentReference.toLiveData(clazz: Class<T>) = FirestoreDocumentLiveData(this, clazz)
package com.strv.ktools
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
class Form {
val valid = MediatorLiveData<Boolean>().apply { value = true }
private val sources = mutableSetOf<LiveData<Boolean>>()
fun <T> addField(field: FormField<T>) {
sources += field.valid
valid.addSource(field.valid) { valid.value = sources.all { it.value ?: false} }
}
}
class FormField<T>(defaultValue: T?, parentForm: Form? = null) {
private val validators: MutableList<(T?) -> String?> = mutableListOf()
val value: MutableLiveData<T?> = mutableLiveDataOf(defaultValue)
val errorMessage: LiveData<String?> = value.map { v -> validators.map { it.invoke(v) }.filterNotNull().firstOrNull() }
val valid: LiveData<Boolean> = errorMessage.map { it == null }
init {
parentForm?.addField(this)
}
fun validator(validator: (value: T?) -> String?) = this.apply {
validators += validator
value.value = value.value // refresh validation
}
}
package com.strv.ktools
import android.util.Log
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.Transformations
/**
* Live Data variation used for event-based communication from ViewModel to Activity/Fragment
*
* Simply create an instance in ViewModel, observe the instance in Activity/Fragment the same way as any other LiveData and when you need to trigger the event,
* call @see LiveAction.publish(T).
*/
class LiveAction<T> : MutableLiveData<T>() {
private var pending = false
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w("LiveAction", "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer {
if (pending) {
pending = false
observer.onChanged(it)
}
})
}
override fun setValue(t: T?) {
pending = true
super.setValue(t)
}
fun publish(value: T) {
setValue(value)
}
}
/**
* Shorthand for LiveAction where you don't need to pass any value
*/
fun LiveAction<Unit>.publish() {
publish(Unit)
}
/**
* Shorthand for adding source to MediatorLiveData and assigning its value - great for validators, chaining live data etc.
*/
fun <S, T> MediatorLiveData<T>.addValueSource(source: LiveData<S>, resultFunction: (sourceValue: S?) -> T) = this.apply { addSource(source, { value = resultFunction(it) }) }
/**
* Shorthand for mapping LiveData instead of using static methods from Transformations
*/
fun <S, T> LiveData<T>.map(mapFunction: (T) -> S) = Transformations.map(this, mapFunction)
/**
* Shorthand for switch mapping LiveData instead of using static methods from Transformations
*/
fun <S, T> LiveData<T>.switchMap(switchMapFunction: (T) -> LiveData<S>) = Transformations.switchMap(this, switchMapFunction)
fun <S, T> LiveData<T?>.switchMapNotNull(switchMapFunction: (T) -> LiveData<S>) = Transformations.switchMap(this) { if (it != null) switchMapFunction.invoke(it) else MutableLiveData<S>() }
/**
* Shorthand for creating MutableLiveData
*/
fun <T> mutableLiveDataOf(value: T) = MutableLiveData<T>().apply { this.value = value }
fun <T> LiveData<T>.observeOnce(observer: Observer<T>, onlyIf: (T?) -> Boolean = { true }) {
observeForever(object : Observer<T> {
override fun onChanged(value: T?) {
if (onlyIf(value)) {
removeObserver(this)
observer.onChanged(value)
}
}
})
}
fun <T> combineLiveData(vararg input: LiveData<out Any?>, combineFunction: () -> T): LiveData<T> = MediatorLiveData<T>().apply {
input.forEach { addSource(it) { value = combineFunction() } }
}
fun <T> MutableLiveData<T>.refresh() {
value = value
}
package com.strv.ktools
import android.util.Log
private var logTag = "Log"
private var logEnabled = true
private var showCodeLocation = true
private var showCodeLocationThread = false
private var showCodeLocationLine = false
fun setLogEnabled(enabled: Boolean) {
logEnabled = enabled
}
fun setLogTag(tag: String) {
logTag = tag
}
fun setShowCodeLocation(enabled: Boolean) {
showCodeLocation = enabled
}
fun setShowCodeLocationThread(enabled: Boolean) {
showCodeLocationThread = enabled
}
fun setShowCodeLocationLine(enabled: Boolean) {
showCodeLocationLine = enabled
}
fun log(message: String, vararg args: Any?) {
if (logEnabled) Log.d(logTag, getCodeLocation().toString() + message.format(args))
}
fun logD(message: String, vararg args: Any?) {
if (logEnabled) Log.d(logTag, getCodeLocation().toString() + message.format(args))
}
fun logE(message: String, vararg args: Any?) {
if (logEnabled) Log.e(logTag, getCodeLocation().toString() + message.format(args))
}
fun logI(message: String, vararg args: Any?) {
if (logEnabled) Log.i(logTag, getCodeLocation().toString() + message.format(args))
}
fun logW(message: String, vararg args: Any?) {
if (logEnabled) Log.w(logTag, getCodeLocation().toString() + message.format(args))
}
fun Any?.logMe() {
if (logEnabled) Log.d(logTag, getCodeLocation().toString() + this.toString())
}
fun Any?.logMeD() {
if (logEnabled) Log.d(logTag, getCodeLocation().toString() + this.toString())
}
fun Any?.logMeI() {
if (logEnabled) Log.i(logTag, getCodeLocation().toString() + this.toString())
}
private fun getCodeLocation(depth: Int = 3): CodeLocation {
val stackTrace = Throwable().stackTrace
val filteredStackTrace = arrayOfNulls<StackTraceElement>(stackTrace.size - depth)
System.arraycopy(stackTrace, depth, filteredStackTrace, 0, filteredStackTrace.size)
return CodeLocation(filteredStackTrace)
}
private class CodeLocation(stackTrace: Array<StackTraceElement?>) {
private val thread: String
private val fileName: String
private val className: String
private val method: String
private val lineNumber: Int
init {
val root = stackTrace[0]
thread = Thread.currentThread().name
fileName = root!!.fileName
val className = root.className
this.className = className.substring(className.lastIndexOf('.') + 1)
method = root.methodName
lineNumber = root.lineNumber
}
override fun toString(): String {
val builder = StringBuilder()
if (showCodeLocation) {
builder.append('[')
if (showCodeLocationThread) {
builder.append(thread)
builder.append('.')
}
builder.append(className)
builder.append('.')
builder.append(method)
if (showCodeLocationLine) {
builder.append('(')
builder.append(fileName)
builder.append(':')
builder.append(lineNumber)
builder.append(')')
}
builder.append("] ")
}
return builder.toString()
}
}
package com.strv.ktools
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import android.content.Intent
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import java.util.EmptyStackException
import java.util.Stack
class Navigation<S> {
companion object {
const val RESULT_NONE = 0
const val RESULT_OK = -1
}
data class Action<S>(val screen: S, val addToBackStack: Boolean, val rcCode: Int? = null)
private val backStack = Stack<S>()
val changeScreenAction = LiveAction<Action<S>>()
val backAction = LiveAction<Int>()
val exitAction = LiveAction<Unit>()
var currentScreen = mutableLiveDataOf<S?>(null)
var rcCode = 0
val resultCallbacks = mutableMapOf<Int, (Int, Intent?) -> Unit>()
fun navigate(screen: S, addToBackStack: Boolean = true) {
if (addToBackStack) backStack.push(currentScreen.value)
currentScreen.value = screen
changeScreenAction.postValue(Action(screen, addToBackStack))
}
fun navigateForResult(screen: S, resultCallback: (code: Int, data: Intent?) -> Unit) {
backStack.push(currentScreen.value)
currentScreen.value = screen
rcCode++
resultCallbacks[rcCode] = resultCallback
changeScreenAction.postValue(Action(screen, true, rcCode))
}
fun navigateBack(result: Int = RESULT_NONE): Boolean {
try {
val prevScreen = backStack.pop()
currentScreen.value = prevScreen
backAction.publish(result)
logD("Navigation: going back to $prevScreen")
return true
} catch (e: EmptyStackException) {
logD("Could not go back. Nothing on back stack")
currentScreen.value = null
return false
}
}
fun exit() {
backStack.clear()
currentScreen.value = null
exitAction.publish()
}
fun onResult(requestCode: Int, resultCode: Int, data: Intent?) {
resultCallbacks[requestCode]?.invoke(resultCode, data)
resultCallbacks.remove(requestCode)
}
}
abstract class NavigationController<S>(private val lifecycleOwner: LifecycleOwner) {
private var _navigation: Navigation<S>? = null
fun setNavigation(navigation: Navigation<S>) {
_navigation = navigation
navigation.changeScreenAction.observe(lifecycleOwner, Observer { screen ->
screen?.let { showScreen(screen.screen, screen.addToBackStack, screen.rcCode) }
})
navigation.exitAction.observe(lifecycleOwner, Observer {
exit()
})
navigation.backAction.observe(lifecycleOwner, Observer {
goBack(it!!)
})
}
fun onResult(requestCode: Int, resultCode: Int, data: Intent) {
_navigation?.onResult(requestCode, resultCode, data)
}
abstract fun showScreen(screen: S, addToBackStack: Boolean, rcCode: Int? = null)
abstract fun exit()
abstract fun goBack(resultCode: Int)
}
abstract class IdentifiableScreen(val screenId: String)
class FragmentNavigationController<S : IdentifiableScreen>(private val activity: androidx.fragment.app.FragmentActivity, private val containerViewId: Int, private val fragmentFactory: (screen: S) -> androidx.fragment.app.Fragment, val keepFragments: Boolean = false) : NavigationController<S>(activity) {
private val fragmentManager = activity.supportFragmentManager
override fun showScreen(screen: S, addToBackStack: Boolean, rcCode: Int?) {
fragmentManager.beginTransaction().apply {
val previousFragment = fragmentManager.findFragmentById(containerViewId)
if (previousFragment != null) {
if (addToBackStack || keepFragments)
detach(previousFragment)
else
remove(previousFragment)
}
// attach/add new fragment
var fragment = fragmentManager.findFragmentByTag(screen.screenId)
if (fragment == null) {
fragment = fragmentFactory.invoke(screen)
add(containerViewId, fragment, screen.screenId)
} else {
attach(fragment)
}
}.commitNowAllowingStateLoss()
}
override fun goBack(resultCode: Int) {
fragmentManager.popBackStack()
}
override fun exit() {
activity.finish()
}
}
class ActivityNavigationController<S>(private val activity: androidx.fragment.app.FragmentActivity, private val intentFactory: (screen: S) -> Intent) : NavigationController<S>(activity) {
override fun showScreen(screen: S, addToBackStack: Boolean, rcCode: Int?) {
if (rcCode != null)
activity.startActivityForResult(intentFactory.invoke(screen), rcCode)
else
activity.startActivity(intentFactory.invoke(screen))
if (!addToBackStack) activity.finish()
}
override fun goBack(resultCode: Int) {
activity.setResult(resultCode)
activity.finish()
}
override fun exit() {
activity.finish()
}
}
package com.strv.ktools
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.core.content.ContextCompat
abstract class PermissionManager {
private var lastRequestId = 0
private val permissionRequests = HashMap<Int, PermissionRequest>()
fun requestPermission(permissionRequest: PermissionRequest) {
val requestId = ++lastRequestId
permissionRequests[requestId] = permissionRequest
askForPermissions(permissionRequest.permissions, requestId)
}
fun onPermissionResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
val permissionRequest = permissionRequests[requestCode]
permissionRequest?.let {
val granted = ArrayList<String>()
val denied = ArrayList<String>()
permissions.indices.forEach {
if (grantResults[it] == PackageManager.PERMISSION_GRANTED)
granted.add(permissions[it])
else
denied.add(permissions[it])
}
if (granted.isNotEmpty())
permissionRequest.grantedCallback.invoke(granted)
if (denied.isNotEmpty())
permissionRequest.deniedCallback.invoke(denied)
permissionRequests.remove(requestCode)
}
}
fun checkPermission(permission: String) = performPermissionCheck(permission) == PackageManager.PERMISSION_GRANTED
abstract fun performPermissionCheck(permission: String): Int
abstract fun askForPermissions(permissions: List<String>, requestId: Int)
}
class ActivityPermissionManager(val activity: androidx.fragment.app.FragmentActivity) : PermissionManager() {
override fun performPermissionCheck(permission: String) = ContextCompat.checkSelfPermission(activity, permission)
override fun askForPermissions(permissions: List<String>, requestId: Int) {
ActivityCompat.requestPermissions(activity, permissions.toTypedArray(), requestId)
}
}
class FragmentPermissionManager(val fragment: androidx.fragment.app.Fragment) : PermissionManager() {
override fun performPermissionCheck(permission: String) = ContextCompat.checkSelfPermission(fragment.activity!!, permission)
override fun askForPermissions(permissions: List<String>, requestId: Int) {
fragment.requestPermissions(permissions.toTypedArray(), requestId)
}
}
open class PermissionRequest(
val permissions: List<String>,
val grantedCallback: (grantedPermissions: List<String>) -> Unit = {},
val deniedCallback: (deniedPermissions: List<String>) -> Unit = {})
class SinglePermissionRequest(
permission: String,
grantedCallback: (grantedPermission: String) -> Unit = {},
deniedCallback: (deniedPermission: String) -> Unit = {}) : PermissionRequest(listOf(permission), { grantedCallback(it[0]) }, { deniedCallback(it[0]) })
package com.strv.ktools
import android.app.Application
import android.app.Fragment
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import android.content.Context
import android.content.ContextWrapper
import android.content.SharedPreferences
import android.content.SharedPreferences.Editor
import android.preference.PreferenceManager
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
// obtain SharedPreferencesProvider within Activity/Fragment/ViewModel/Service/Context etc.
fun Context.sharedPrefs(name: String? = null, mode: Int = ContextWrapper.MODE_PRIVATE) = SharedPreferencesProvider({
if (name == null)
PreferenceManager.getDefaultSharedPreferences(this)
else
getSharedPreferences(name, mode)
})
fun AndroidViewModel.sharedPrefs(name: String? = null, mode: Int = ContextWrapper.MODE_PRIVATE) = getApplication<Application>().sharedPrefs(name, mode)
fun Fragment.sharedPrefs(name: String? = null, mode: Int = ContextWrapper.MODE_PRIVATE) = activity.sharedPrefs(name, mode)
// Simple SharedPreferences delegates - supports getter/setter
// Note: When not specified explicitly, the name of the property is used as a preference key
// Example: `val userName by sharedPrefs().string()`
fun SharedPreferencesProvider.int(def: Int = 0, key: String? = null): ReadWriteProperty<Any?, Int> = delegatePrimitive(def, key, SharedPreferences::getInt, Editor::putInt)
fun SharedPreferencesProvider.long(def: Long = 0, key: String? = null): ReadWriteProperty<Any?, Long> = delegatePrimitive(def, key, SharedPreferences::getLong, Editor::putLong)
fun SharedPreferencesProvider.float(def: Float = 0f, key: String? = null): ReadWriteProperty<Any?, Float> = delegatePrimitive(def, key, SharedPreferences::getFloat, Editor::putFloat)
fun SharedPreferencesProvider.boolean(def: Boolean = false, key: String? = null): ReadWriteProperty<Any?, Boolean> = delegatePrimitive(def, key, SharedPreferences::getBoolean, Editor::putBoolean)
fun SharedPreferencesProvider.stringSet(def: Set<String> = emptySet(), key: String? = null): ReadWriteProperty<Any?, Set<String>?> = delegate(def, key, SharedPreferences::getStringSet, Editor::putStringSet)
fun SharedPreferencesProvider.string(def: String? = null, key: String? = null): ReadWriteProperty<Any?, String?> = delegate(def, key, SharedPreferences::getString, Editor::putString)
// LiveData SharedPreferences delegates - provides LiveData access to prefs with sync across app on changes
// Note: When not specified explicitly, the name of the property is used as a preference key
// Example: `val userName by sharedPrefs().stringLiveData()`
fun SharedPreferencesProvider.intLiveData(def: Int, key: String? = null): ReadOnlyProperty<Any?, MutableLiveData<Int>> = liveDataDelegatePrimitive(def, key, SharedPreferences::getInt, SharedPreferences.Editor::putInt)
fun SharedPreferencesProvider.longLiveData(def: Long, key: String? = null): ReadOnlyProperty<Any?, MutableLiveData<Long>> = liveDataDelegatePrimitive(def, key, SharedPreferences::getLong, SharedPreferences.Editor::putLong)
fun SharedPreferencesProvider.floatLiveData(def: Float, key: String? = null): ReadOnlyProperty<Any?, MutableLiveData<Float>> = liveDataDelegatePrimitive(def, key, SharedPreferences::getFloat, SharedPreferences.Editor::putFloat)
fun SharedPreferencesProvider.booleanLiveData(def: Boolean, key: String? = null): ReadOnlyProperty<Any?, MutableLiveData<Boolean>> = liveDataDelegatePrimitive(def, key, SharedPreferences::getBoolean, SharedPreferences.Editor::putBoolean)
fun SharedPreferencesProvider.stringLiveData(def: String? = null, key: String? = null): ReadOnlyProperty<Any?, MutableLiveData<String?>> = liveDataDelegate(def, key, SharedPreferences::getString, SharedPreferences.Editor::putString)
fun SharedPreferencesProvider.stringSetLiveData(def: Set<String>? = null, key: String? = null): ReadOnlyProperty<Any?, MutableLiveData<Set<String>?>> = liveDataDelegate(def, key, SharedPreferences::getStringSet, SharedPreferences.Editor::putStringSet)
// -- internal
private inline fun <T> SharedPreferencesProvider.delegate(
defaultValue: T?,
key: String? = null,
crossinline getter: SharedPreferences.(String, T?) -> T?,
crossinline setter: Editor.(String, T?) -> Editor
) =
object : ReadWriteProperty<Any?, T?> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T? =
provide().getter(key
?: property.name, defaultValue)
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) =
provide().edit().setter(key
?: property.name, value).apply()
}
private inline fun <T> SharedPreferencesProvider.delegatePrimitive(
defaultValue: T,
key: String? = null,
crossinline getter: SharedPreferences.(String, T) -> T,
crossinline setter: Editor.(String, T) -> Editor
) =
object : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T =
provide().getter(key
?: property.name, defaultValue)!!
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) =
provide().edit().setter(key
?: property.name, value).apply()
}
private inline fun <T> SharedPreferencesProvider.liveDataDelegate(
defaultValue: T? = null,
key: String? = null,
crossinline getter: SharedPreferences.(String, T?) -> T?,
crossinline setter: Editor.(String, T?) -> Editor
): ReadOnlyProperty<Any?, MutableLiveData<T?>> = object : MutableLiveData<T?>(), ReadOnlyProperty<Any?, MutableLiveData<T?>>, SharedPreferences.OnSharedPreferenceChangeListener {
var originalProperty: KProperty<*>? = null
lateinit var prefKey: String
override fun getValue(thisRef: Any?, property: KProperty<*>): MutableLiveData<T?> {
originalProperty = property
prefKey = key ?: originalProperty!!.name
return this
}
override fun getValue(): T? {
val value = provide().getter(prefKey, defaultValue)
return super.getValue()
?: value
?: defaultValue
}
override fun setValue(value: T?) {
super.setValue(value)
provide().edit().setter(prefKey, value).apply()
}
override fun onActive() {
super.onActive()
value = provide().getter(prefKey, defaultValue)
provide().registerOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, changedKey: String) {
if (changedKey == prefKey) {
value = sharedPreferences.getter(changedKey, defaultValue)
}
}
override fun onInactive() {
super.onInactive()
provide().unregisterOnSharedPreferenceChangeListener(this)
}
}
private inline fun <T> SharedPreferencesProvider.liveDataDelegatePrimitive(
defaultValue: T,
key: String? = null,
crossinline getter: SharedPreferences.(String, T) -> T,
crossinline setter: Editor.(String, T) -> Editor
): ReadOnlyProperty<Any?, MutableLiveData<T>> = object : MutableLiveData<T>(), ReadOnlyProperty<Any?, MutableLiveData<T>>, SharedPreferences.OnSharedPreferenceChangeListener {
var originalProperty: KProperty<*>? = null
lateinit var prefKey: String
override fun getValue(thisRef: Any?, property: KProperty<*>): MutableLiveData<T> {
originalProperty = property
prefKey = key ?: originalProperty!!.name
return this
}
override fun getValue(): T {
val value = provide().getter(prefKey, defaultValue)
return super.getValue()
?: value
?: defaultValue
}
override fun setValue(value: T) {
super.setValue(value)
provide().edit().setter(prefKey, value).apply()
}
override fun onActive() {
super.onActive()
value = provide().getter(prefKey, defaultValue)
provide().registerOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, changedKey: String) {
if (changedKey == prefKey) {
value = sharedPreferences.getter(changedKey, defaultValue)
}
}
override fun onInactive() {
super.onInactive()
provide().unregisterOnSharedPreferenceChangeListener(this)
}
}
class SharedPreferencesProvider(private val provider: () -> SharedPreferences) {
internal fun provide() = provider()
}
package com.strv.ktools
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Transformations
import retrofit2.Response
/**
* Resource wrapper adding status and error to its value
*/
data class Resource<T> constructor(
val status: Status,
val data: T? = null,
val message: String? = null,
val rawResponse: Response<T>? = null,
val throwable: Throwable? = null
) {
enum class Status { SUCCESS, ERROR, FAILURE, NO_CONNECTION, LOADING }
companion object {
fun <T> fromResponse(response: Response<T>?, error: Throwable?, apiErrorResolver: ApiErrorResolver?): Resource<T> {
error?.printStackTrace()
val message = response?.message() ?: error?.message
var status = Resource.Status.SUCCESS
if (response == null || response.isSuccessful.not()) {
status = if (response != null) Resource.Status.ERROR else if (error is NoConnectivityException) Resource.Status.NO_CONNECTION else Resource.Status.FAILURE
}
val newError =
if (response?.isSuccessful == false && apiErrorResolver != null)
apiErrorResolver.resolve(response)
else error
return Resource(status, response?.body(), message, response, newError)
}
fun <T> loading(data: T? = null, message: String? = null) = Resource<T>(Status.LOADING, data, message, null, null)
fun <T> success(data: T?, message: String? = null) = Resource<T>(Status.SUCCESS, data, message, null, null)
fun <T> error(throwable: Throwable?, message: String? = throwable?.message) = Resource<T>(Status.ERROR, null, message, null, throwable)
}
fun <S> map(mapFunction: (T?) -> S?) = Resource(status, mapFunction(data), message, rawResponse?.map(mapFunction), throwable)
}
abstract class RefreshableLiveData<T> : MediatorLiveData<T>() {
abstract fun refresh()
}
/**
* BaseClass for making any resource accessible via LiveData interface with database cache support
*/
open class CachedResourceLiveData<T>(private val resourceCallback: NetworkBoundResource.Callback<T>, initLoad: Boolean = true) : RefreshableLiveData<Resource<T>>() {
private val resource = NetworkBoundResource(this)
init {
if (initLoad) refresh()
}
override fun refresh() {
resource.setupCached(resourceCallback)
}
}
open class ResourceLiveData<T>(initLoad: Boolean = true, private val networkCallLiveDataProvider: () -> LiveData<Resource<T>>) : RefreshableLiveData<Resource<T>>() {
private val resource = NetworkBoundResource(this)
init {
if (initLoad) refresh()
}
override fun refresh() {
resource.setup(networkCallLiveDataProvider.invoke())
}
}
// -- internal --
/**
* NetworkBoundResource based on https://developer.android.com/topic/libraries/architecture/guide.html, but modified
* Note: use Call<T>.map() extension function to map Retrofit response to the entity object - therefore we don't need RequestType and ResponseType separately
*/
class NetworkBoundResource<T>(private val result: MediatorLiveData<Resource<T>>) {
interface Callback<T> {
// Called to save the result of the API response into the database
@WorkerThread
fun saveCallResult(item: T)
// Called with the dataFromCache in the database to decide whether it should be
// fetched from the network.
@MainThread
fun shouldFetch(dataFromCache: T?): Boolean
// Called to get the cached data from the database
@MainThread
fun loadFromDb(): LiveData<T>
// Called to create the API call.
@MainThread
fun createNetworkCall(): LiveData<Resource<T>>
}
private var callback: Callback<T>? = null
private val savedSources = mutableSetOf<LiveData<*>>()
init {
result.value = Resource.loading()
}
fun setup(networkCallLiveData: LiveData<Resource<T>>) {
callback = null
// clear saved sources from previous setup
savedSources.forEach { result.removeSource(it) }
savedSources.clear()
result.value = result.value?.copy(status = result.value?.status ?: Resource.Status.LOADING) ?: Resource.loading()
savedSources.add(networkCallLiveData)
result.addSource(networkCallLiveData) { networkResource ->
result.setValue(networkResource)
}
}
fun setupCached(resourceCallback: Callback<T>) {
callback = resourceCallback
// clear saved sources from previous setup
savedSources.forEach { result.removeSource(it) }
savedSources.clear()
result.value = result.value?.copy(status = result.value?.status ?: Resource.Status.LOADING) ?: Resource.loading()
val dbSource = callback!!.loadFromDb()
savedSources.add(dbSource)
result.addSource(dbSource) { data ->
savedSources.remove(dbSource)
result.removeSource(dbSource)
if (callback!!.shouldFetch(data)) {
fetchFromNetwork(dbSource)
} else {
savedSources.add(dbSource)
result.addSource(dbSource) { newData ->
result.setValue(Resource.success(newData))
}
}
}
}
private fun fetchFromNetwork(dbSource: LiveData<T>) {
val apiResponse = callback!!.createNetworkCall()
// we re-attach dbSource as a new source,
// it will dispatch its latest value quickly
savedSources.add(dbSource)
result.addSource(dbSource) { newData -> result.setValue(Resource.loading(newData)) }
savedSources.add(apiResponse)
result.addSource(apiResponse) { networkResource ->
savedSources.remove(apiResponse)
result.removeSource(apiResponse)
savedSources.remove(dbSource)
result.removeSource(dbSource)
if (networkResource?.status == Resource.Status.SUCCESS) {
saveResultAndReInit(networkResource)
} else {
savedSources.add(dbSource)
result.addSource(dbSource) { newData ->
result.setValue(networkResource?.copy(data = newData))
}
}
}
}
@MainThread
private fun saveResultAndReInit(resource: Resource<T>) {
doAsync {
callback?.let {
resource.data?.let {
try {
callback!!.saveCallResult(it)
} catch (e: Exception) {
uiThread { throw(IllegalStateException(e)) }
}
}
uiThread {
val dbSource = callback!!.loadFromDb()
savedSources.add(dbSource)
result.addSource(dbSource) { newData ->
result.setValue(resource.copy(data = newData))
}
}
}
}
}
}
// shorthand for mapping value directly
fun <T, S> LiveData<Resource<T>>.mapData(mapFunction: (T?) -> S?): LiveData<Resource<S>> = Transformations.map(this) { it.map(mapFunction) }
package com.strv.ktools
import android.content.Context
import android.net.ConnectivityManager
import androidx.lifecycle.LiveData
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.IOException
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
// shorthand for enqueue call
fun <T> Call<T>.then(callback: (response: Response<T>?, error: Throwable?) -> Unit) {
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
callback(response, null)
}
override fun onFailure(call: Call<T>, t: Throwable) {
callback(null, t)
}
})
}
// map response to another response using body map function
fun <T, S> Response<T>.map(mapFunction: (T?) -> S?) = if (isSuccessful) Response.success(mapFunction(body()), raw()) else Response.error(errorBody(), raw())
// map resource data
fun <T, S> LiveData<Resource<T>>.mapResource(mapFunction: (T?) -> S?) = this.map { it.map(mapFunction) }
// get live data from Retrofit call
fun <T> Call<T>.toLiveData(apiErrorResolver: ApiErrorResolver? = null, cancelOnInactive: Boolean = false) = RetrofitCallLiveData(this, apiErrorResolver, cancelOnInactive)
// Retrofit CallAdapter Factory - use with Retrofit builder
class LiveDataCallAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type?, annotations: Array<out Annotation>?, retrofit: Retrofit?): CallAdapter<*, *>? {
if (CallAdapter.Factory.getRawType(returnType) != LiveData::class.java) {
return null
}
if (returnType !is ParameterizedType) {
throw IllegalStateException("Response must be parametrized as " + "LiveData<Resource<T>> or LiveData<? extends Resource>")
}
val responseType = CallAdapter.Factory.getParameterUpperBound(0, CallAdapter.Factory.getParameterUpperBound(0, returnType) as ParameterizedType)
return LiveDataBodyCallAdapter<Any>(responseType)
}
}
// get basic Retrofit setupCached with logger
internal fun getRetrofit(context: Context, url: String, logLevel: HttpLoggingInterceptor.Level, clientBuilderBase: OkHttpClient.Builder? = null, gson: Gson = GsonBuilder().create()): Retrofit {
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = logLevel
}
val client = (clientBuilderBase ?: OkHttpClient.Builder()).addInterceptor(loggingInterceptor).addInterceptor(ConnectivityInterceptor(context)).build()
return Retrofit.Builder()
.client(client)
.baseUrl(url)
.addCallAdapterFactory(LiveDataCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
}
// -- internal --
private class LiveDataBodyCallAdapter<R> internal constructor(private val responseType: Type) : CallAdapter<R, LiveData<Resource<R>>> {
override fun responseType() = responseType
override fun adapt(call: Call<R>) = call.toLiveData()
}
open class RetrofitCallLiveData<T>(val call: Call<T>,val apiErrorResolver: ApiErrorResolver?, val cancelOnInactive: Boolean = false) : LiveData<Resource<T>>() {
override fun onActive() {
super.onActive()
if (call.isExecuted)
return
call.then { response, error ->
postValue(Resource.fromResponse(response, error, apiErrorResolver))
}
}
override fun onInactive() {
super.onInactive()
if (cancelOnInactive)
call.cancel()
}
}
class ConnectivityInterceptor(val context: Context) : Interceptor {
override fun intercept(chain: Interceptor.Chain?): okhttp3.Response {
if (!isOnline(context)) throw NoConnectivityException()
val builder = chain!!.request().newBuilder()
return chain.proceed(builder.build())
}
}
class NoConnectivityException() : IOException("No connectivity exception")
fun isOnline(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return connectivityManager.activeNetworkInfo?.isConnected ?: false
}
interface ApiErrorResolver {
fun <T> resolve(response: Response<T>): Throwable
fun getUserFriendlyMessage(throwable: Throwable): String
}
package com.strv.ktools
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
open class Task<R, E> {
private val successCallbacks = mutableListOf<(R) -> Unit>()
private val errorCallbacks = mutableListOf<(E) -> Unit>()
private val completeCallbacks = mutableListOf<(Task<R, E>) -> Unit>()
var result: R? = null
var error: E? = null
var completed = false
val successful get() = completed && error == null
fun onSuccess(callback: (result: R) -> Unit) = this.apply {
successCallbacks.add(callback)
if (completed && successful && result != null)
callback(result!!)
}
fun onError(callback: (error: E) -> Unit) = this.apply {
errorCallbacks.add(callback)
if (completed && !successful && error != null)
callback(error!!)
}
fun onComplete(callback: (Task<R, E>) -> Unit) = this.apply {
completeCallbacks.add(callback)
if (completed)
callback(this)
}
fun invokeSuccess(result: R) {
completed = true
this.result = result
successCallbacks.forEach { it.invoke(result) }
completeCallbacks.forEach { it.invoke(this) }
}
fun invokeError(error: E) {
completed = true
this.error = error
errorCallbacks.forEach { it.invoke(error) }
completeCallbacks.forEach { it.invoke(this) }
}
fun <S> map(mapFunction: (R) -> S) = Task<S, E>().apply {
this@Task.onSuccess { invokeSuccess(mapFunction(it)) }
this@Task.onError { invokeError(it) }
}
}
class ProgressTask<R, E> : Task<R, E>() {
private var progress: Int = 0
private val progressCallbacks = mutableListOf<(Int) -> Unit>()
fun onProgress(callback: (progress: Int) -> Unit) = this.apply {
progressCallbacks.add(callback)
}
fun invokeProgress(progress: Int) {
this.progress = progress
progressCallbacks.forEach { it.invoke(progress) }
}
}
fun <R, E> task(runnable: Task<R, E>.() -> Unit) = Task<R, E>().apply(runnable)
fun <R, E> progressTask(runnable: ProgressTask<R, E>.() -> Unit) = ProgressTask<R, E>().apply(runnable)
fun <T> com.google.android.gms.tasks.Task<T>.toTask() = task<T, Throwable> {
this@toTask.addOnSuccessListener { invokeSuccess(it) }
this@toTask.addOnFailureListener {
it.printStackTrace()
invokeError(it)
}
}
fun <T> Call<T>.toTask(apiErrorResolver: ApiErrorResolver? = null) = task<T?, Throwable> {
enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
invokeError(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
invokeSuccess(response.body())
} else {
invokeError(apiErrorResolver?.resolve(response) ?: Throwable("Unknown API Error: ${response.message()}"))
}
}
})
}
package com.strv.ktools
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.databinding.BindingAdapter
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
open class DataBoundAdapter<T>(val lifecycleOwner: LifecycleOwner, val itemLayoutIdProvider: (T) -> Int, val bindingVariableId: Int, diffCallback: DiffUtil.ItemCallback<T>) : ListAdapter<T, DataBoundViewHolder>(diffCallback) {
constructor(lifecycleOwner: LifecycleOwner, @LayoutRes itemLayoutId: Int, bindingVariableId: Int, diffCallback: DiffUtil.ItemCallback<T>) : this(lifecycleOwner, { itemLayoutId }, bindingVariableId, diffCallback)
private val extras = hashMapOf<Int, Any>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataBoundViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = DataBindingUtil.inflate<ViewDataBinding>(layoutInflater, viewType, parent, false)
binding.setLifecycleOwner(lifecycleOwner)
return DataBoundViewHolder(binding)
}
override fun getItemViewType(position: Int) = itemLayoutIdProvider.invoke(getItem(position)!!)
override fun onBindViewHolder(holder: DataBoundViewHolder, position: Int) {
holder.binding.setVariable(bindingVariableId, getItem(position))
extras.forEach { (varId, extra) -> holder.binding.setVariable(varId, extra) }
holder.binding.executePendingBindings()
}
fun bindExtra(bindingVariableId: Int, extra: Any) = this.also {
extras.put(bindingVariableId, extra)
}
}
class DataBoundViewHolder constructor(val binding: ViewDataBinding) : ViewHolder(binding.root)
@BindingAdapter("app:adapter", "app:items", requireAll = false)
fun <T> RecyclerView.setDataBoundAdapter(adapter: DataBoundAdapter<T>?, items: List<T>?) {
if (this.adapter == null)
this.adapter = adapter
(this.adapter as DataBoundAdapter<T>).submitList(items)
}
package com.strv.ktools
import androidx.lifecycle.ViewModel
import androidx.databinding.ViewDataBinding
import androidx.annotation.StringRes
import com.google.android.material.snackbar.Snackbar
// Snackbar
fun <VM : ViewModel, B : ViewDataBinding> ViewModelBinding<VM, B>.snackbar(message: String, length: Int = com.google.android.material.snackbar.Snackbar.LENGTH_SHORT) = com.google.android.material.snackbar.Snackbar.make(rootView, message, length).apply { show() }
fun <VM : ViewModel, B : ViewDataBinding> ViewModelBinding<VM, B>.snackbar(@StringRes messageResId: Int, length: Int = com.google.android.material.snackbar.Snackbar.LENGTH_SHORT) = snackbar(rootView.context.getString(messageResId), length)
package com.strv.ktools
import android.app.Activity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.annotation.LayoutRes
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import org.karmaapp.BR
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
// ViewModelBinding extension functions for Fragment and FragmentActivity
// Note: these functions are meant to be used as delegates
// Example: `private val vmb by vmb<MyLibraryViewModel, ActivityMainBinding>(R.layout.activity_main)`
// Example with ViewModel constructor: private val vmb by vmb<MyLibraryViewModel, ActivityMainBinding>(R.layout.activity_main) { MyLibraryViewModel(xxx) }
inline fun <reified VM : ViewModel, B : ViewDataBinding> androidx.fragment.app.FragmentActivity.vmb(@LayoutRes layoutResId: Int, viewModelProvider: ViewModelProvider? = null) = object : ReadOnlyProperty<androidx.fragment.app.FragmentActivity, ViewModelBinding<VM, B>> {
var instance = ViewModelBinding<VM, B>(this@vmb, VM::class.java, layoutResId, viewModelProvider, null)
override fun getValue(thisRef: androidx.fragment.app.FragmentActivity, property: KProperty<*>) = instance
}
inline fun <reified VM : ViewModel, B : ViewDataBinding> androidx.fragment.app.FragmentActivity.vmb(@LayoutRes layoutResId: Int, noinline viewModelFactory: () -> VM) = object : ReadOnlyProperty<androidx.fragment.app.FragmentActivity, ViewModelBinding<VM, B>> {
var instance = ViewModelBinding<VM, B>(this@vmb, VM::class.java, layoutResId, null, viewModelFactory)
override fun getValue(thisRef: androidx.fragment.app.FragmentActivity, property: KProperty<*>) = instance
}
inline fun <reified VM : ViewModel, B : ViewDataBinding> androidx.fragment.app.Fragment.vmb(@LayoutRes layoutResId: Int, viewModelProvider: ViewModelProvider? = null) = object : ReadOnlyProperty<androidx.fragment.app.Fragment, ViewModelBinding<VM, B>> {
var instance = ViewModelBinding<VM, B>(this@vmb, VM::class.java, layoutResId, viewModelProvider, null)
override fun getValue(thisRef: androidx.fragment.app.Fragment, property: KProperty<*>) = instance
}
inline fun <reified VM : ViewModel, B : ViewDataBinding> androidx.fragment.app.Fragment.vmb(@LayoutRes layoutResId: Int, noinline viewModelFactory: () -> VM) = object : ReadOnlyProperty<androidx.fragment.app.Fragment, ViewModelBinding<VM, B>> {
var instance = ViewModelBinding<VM, B>(this@vmb, VM::class.java, layoutResId, null, viewModelFactory)
override fun getValue(thisRef: androidx.fragment.app.Fragment, property: KProperty<*>) = instance
}
// -- internal --
/**
* Main VMB class connecting View (Activity/Fragment) to a Android Architecture ViewModel and Data Binding
*
* Note: Do not use this constructor directly. Use extension functions above instead.
*/
class ViewModelBinding<out VM : ViewModel, out B : ViewDataBinding> constructor(
private val lifecycleOwner: LifecycleOwner,
private val viewModelClass: Class<VM>,
@LayoutRes private val layoutResId: Int,
private var viewModelProvider: ViewModelProvider?,
val viewModelFactory: (() -> VM)?
) {
init {
if (!(lifecycleOwner is androidx.fragment.app.FragmentActivity || lifecycleOwner is androidx.fragment.app.Fragment))
throw IllegalArgumentException("Provided LifecycleOwner must be one of FragmentActivity or Fragment")
}
val binding: B by lazy {
initializeVmb()
DataBindingUtil.inflate<B>(activity.layoutInflater, layoutResId, null, false)!!
}
val rootView by lazy { binding.root }
val viewModel: VM by lazy {
initializeVmb()
viewModelProvider!!.get(viewModelClass)
}
val fragment: androidx.fragment.app.Fragment? = lifecycleOwner as? androidx.fragment.app.Fragment
val activity: androidx.fragment.app.FragmentActivity by lazy {
lifecycleOwner as? androidx.fragment.app.FragmentActivity ?: (lifecycleOwner as androidx.fragment.app.Fragment).activity!!
}
private var initialized = false
init {
lifecycleOwner.lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
// Note: This line will not work for Android Gradle plugin older than 3.1.0-alpha06 - comment it out if using those
binding.setLifecycleOwner(lifecycleOwner)
// setupCached binding variables
// Note: BR.viewModel, BR.view will be auto-generated if you have those variables somewhere in your layout files
// If you're not using both of them you will have to comment out one of the lines
binding.setVariable(BR.viewModel, viewModel)
binding.setVariable(BR.view, fragment ?: activity)
// binding.setVariable(BR.lifecycleOwner, lifecycleOwner)
if (lifecycleOwner is Activity)
activity.setContentView(binding.root)
}
})
}
private fun initializeVmb() {
if (initialized) return
if (viewModelFactory != null) {
val factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>) = viewModelFactory.invoke() as T
}
if (viewModelProvider == null)
viewModelProvider = if (fragment != null) ViewModelProviders.of(fragment, factory) else ViewModelProviders.of(activity, factory)
} else {
if (viewModelProvider == null)
viewModelProvider = if (fragment != null) ViewModelProviders.of(fragment) else ViewModelProviders.of(activity)
}
initialized = true
}
}
inline fun <reified T : ViewModel> androidx.fragment.app.FragmentActivity.findViewModel() = ViewModelProviders.of(this)[T::class.java]
inline fun <reified T : ViewModel> androidx.fragment.app.Fragment.findViewModel() = ViewModelProviders.of(this)[T::class.java]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment