Kotlin Tools for Android ViewModel, LiveData, Data Binding, Dependency injection, Async operations, Repository pattern, Retrofit, Form Validation, Cloud Firestore, etc.
Last active
January 29, 2020 16:40
-
-
Save jakubkinst/7c84e5551c026141dc5a4062df5dbde8 to your computer and use it in GitHub Desktop.
ktools
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.strv.ktools | |
// To be used on when() expression to make sure compiler checks for exhaustivity | |
val <T> T.exhaustive: T | |
get() = this |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()}")) | |
} | |
} | |
}) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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