Skip to content

Commit

Permalink
Create debounce and update searchview (#27)
Browse files Browse the repository at this point in the history
* Prepare version

* Create debounce base

* Add debouncer and fix transition crash

* Add debounce docs

* Update links

* Update searchview docs

* Test without a ref

* Add links to core components

* Update links

* Update to bullet points

* Test core md

* Test slash

* Test slash

* Specify implemented dependencies
  • Loading branch information
AllanWang authored Aug 6, 2017
1 parent caaa565 commit 187d8e6
Show file tree
Hide file tree
Showing 13 changed files with 329 additions and 122 deletions.
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,25 @@ dependencies {

# Submodules
> Linked to their respective docs.<br/>
> Included dependencies are only those with exposed APIs; see [new dependency configurations](https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#new_configurations).
> Included dependencies are only those with exposed APs; see [new dependency configurations](https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#new_configurations).<br/>
> Implemented dependencies are wrapped in parentheses.
## [Core](core#readme)
* Collection of extension functions and small helper methods applicable in almost any application.
* Notable features: KPrefs, Changelog XML, Kotterknife, Ripple Canvas, Delegates, Swipe, Lazy Resettables, Extensions, Email Builder
* Notable features:
* [KPrefs](core#kprefs)
* [Changelog XML](core#changelog-xml)
* [FAQ XML](core#faq-xml)
* [Kotterknife](core#kotterknife)
* [Ripple Canvas](core#ripple-canvas)
* [MeasureSpecDelegate](core#measure-spec-delegate)
* [CollapsibleViewDelegate](core#collapsible-view-delegate)
* [Swipe](core#swipe)
* [Debounce](core#debounce)
* [Timber Logger](core#timber-logger)
* [Email Builder](core#email-builder)
* [Extension Functions](core#extension-functions)
* [Lazy Resettable](core#lazy-resettable)
* Includes
[`AppCompat`](https://developer.android.com/topic/libraries/support-library/index.html),
[`Material Dialogs (core)`](https://github.com/afollestad/material-dialogs),
Expand All @@ -87,7 +101,7 @@ dependencies {
## [Color Picker](colorpicker#readme)
* Implementation of a color picker dialog with subtle transitions and a decoupled callback
* Includes `:core`,
[`Material Dialogs (commons)`](https://github.com/afollestad/material-dialogs)
([`Material Dialogs (commons)`](https://github.com/afollestad/material-dialogs))

## [KPref Activity](kpref-activity#readme)
* Fully programmatic implementation of a Preference Activity, backed by RecyclerViews
Expand All @@ -97,14 +111,11 @@ dependencies {
* Fully functional image and video pickers, both as an overlay and as a requested activity.
* Includes `:core-ui`,
[`Glide`](https://github.com/bumptech/glide),
[`Blurry`](https://github.com/wasabeef/Blurry)
([`Blurry`](https://github.com/wasabeef/Blurry))

## [SearchView](searchview#readme)
* Material searchview with kotlin bindings
* Includes `:core-ui`, `:adapter`,
[`RxAndroid`](https://github.com/ReactiveX/RxAndroid),
[`RxKotlin`](https://github.com/ReactiveX/RxKotlin),
[`RxBinding`](https://github.com/JakeWharton/RxBinding)
* Includes `:core-ui`, `:adapter`

-----------

Expand Down
35 changes: 21 additions & 14 deletions core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@
## Contents

* [KPrefs](#kprefs)
* [Changelog XML](#changelog)
* [Changelog XML](#changelog-xml)
* [FAQ XML](#faq-xml)
* [Kotterknife](#kotterknife)
* [Ripple Canvas](#ripple-canvas)
* [MeasureSpecDelegate](#measure-spec-delegate)
* [CollapsibleViewDelegate](#collapsible-view-delegate)
* [Swipe](#swipe)
* [Debounce](#debounce)
* [Timber Logger](#timber-logger)
* [Email Builder](#email-builder)
* [Extensions](#extensions)
* [Extension Functions](#extension-functions)
* [Lazy Resettable](#lazy-resettable)

<a name="kprefs"></a>
## KPrefs

A typical SharedPreference contains items that look like so:
Expand Down Expand Up @@ -71,7 +71,6 @@ object MyPrefs : KPref() {
Notice that it is a `val` and takes no default. It will return true the first time and false for all subsequent calls.
<a name="changelog"></a>
## Changelog XML
Create an xml resource with the following structure:
Expand Down Expand Up @@ -109,7 +108,6 @@ Here is a template xml changelog file:
</resources>
```
<a name="faq-xml"></a>
## FAQ XML
There is another parser for a FAQ list with the following format:
Expand All @@ -123,7 +121,6 @@ Call `kauParseFaq` and pass a callback taking in a `List<Pair<Spanned, Spanned>`
By default, the questions are numbered, and the content is formatted with HTML.
You may still need to add your own methods to allow interaction with certain elements such as links.
<a name="kotterknife"></a>
## Kotterknife
KAU comes shipped with [Kotterknife](https://github.com/JakeWharton/kotterknife) by Jake Wharton.
Expand All @@ -134,7 +131,6 @@ These variants are weakly held in the private `KotterknifeRegistry` object, and
values through the `Kotterknife.reset` method. This is typically useful for Fragments, as they do not follow
the same lifecycle as Activities and Views.
<a name="ripple-canvas"></a>
## Ripple Canvas
Ripple canvas provides a way to create simultaneous ripples against a background color.
Expand All @@ -145,21 +141,18 @@ They can be used as transitions, or as a toolbar background to replicate the loo
Many ripples can be stacked on top of each other to run at the same time from different locations.
The canvas also supports color fading and direct color setting so it can effectively replace any background.
<a name="measure-spec-delegate"></a>
## Measure Spec Delegate
If you ever have a view needing exact aspect ratios with its parent and/or itself, this delegate is here to help.
Implementing this in any view class unlocks its attributes, giving you three layers of view measuring to ensure exact sizing.
More information can be found in the [klass file](https://github.com/AllanWang/KAU/blob/master/core/src/main/kotlin/ca/allanwang/kau/ui/views/MeasureSpecDelegate.kt)
<a name="collapsible-view-delegate"></a>
## Collapsible View Delegate
A common animation is having a view that can smoothly enter and exit by changing its height.
This delegate will implement everything for you and give you the methods `expand`, `collapse`, etc.
See the [kclass file](https://github.com/AllanWang/KAU/blob/master/core/src/main/kotlin/ca/allanwang/kau/ui/views/CollapsibleViewDelegate.kt) for more details.
<a name="swipe"></a>
# Swipe
A collection of activity extension methods to easily make any activity swipable:
Expand All @@ -182,20 +175,35 @@ Special thanks goes to the original project, [SwipeBackHelper](https://github.co
KAU's swipe is a Kotlin rewrite, along with support for all directions and weakly referenced contexts.

<a name="timber-logger"></a>
# Debounce

Debouncing is a means of throttling a function so that it is called no more than once in a given instance of time.
An example where you'd like this behaviour is the searchview; you want to deliver search results quickly,
but you don't want to update your response with each new character.
Instead, you can wait until a user finishes their query, then search for the results.

Example:

![Debounce 0](https://raw.githubusercontent.com/AllanWang/Storage-Hub/master/kau/kau_search_debounce_0.gif)
![Debounce 500](https://raw.githubusercontent.com/AllanWang/Storage-Hub/master/kau/kau_search_debounce_500.gif)

The first case is an example of no debouncing, whereas the second case is an example with a 500ms debounce.

KAU offers extensions to easily convert or create functions into debouncables.
Simply call `debounce` and specify your interval on an existing function, or with a new function.


## Timber Logger

[Timber](https://github.com/JakeWharton/timber)'s DebugTree uses the tag to specify the current class that is being logged.
To add the tag directly in the message, create an object that extends the TimberLogger class with the tag name as the argument.
Along with the timber methods (`v`, `i`, `d`, `e`), Timber Logger also supports `eThrow` to wrap a String in a throwable

<a name="email-builder"></a>
## Email Builder

Easily send an email through `Context.sendEmail`.
Include your email and subject, along with other optional configurations such as retrieving device info.

<a name="extensions"></a>
## Extension Functions

> "[Extensions](https://kotlinlang.org/docs/reference/extensions.html) provide the ability to extend a class with new functionality without having to inherit from the class"
Expand All @@ -204,7 +212,6 @@ Note that since KAU depends on [ANKO](https://github.com/Kotlin/anko), all of th
KAU's vast collection of extensions is one of its strongest features.
There are too many to explain here, but you may check out the [utils package](https://github.com/AllanWang/KAU/tree/master/core/src/main/kotlin/ca/allanwang/kau/utils)

<a name="lazy-resettable></a>
## Lazy Resettable

In the spirit of Kotlin's Lazy delegate, KAU supports a resettable version. Calling `lazyResettable` produces the same delegate,
Expand Down
124 changes: 124 additions & 0 deletions core/src/main/kotlin/ca/allanwang/kau/kotlin/Debouncer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package ca.allanwang.kau.kotlin

import ca.allanwang.kau.logging.KL
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

/**
* Created by Allan Wang on 2017-08-05.
*
* Thread safe function wrapper to allow for debouncing
* With reference to <a href="https://stackoverflow.com/a/20978973/4407321">Stack Overflow</a>
*/

/**
* The debouncer base
* Implements everything except for the callback,
* as the number of variables is different between implementations
* You may still use this without extending it, but you'll have to pass a callback each time
*/
open class Debouncer(var interval: Long) {
private val sched = Executors.newScheduledThreadPool(1)
private var task: DebounceTask? = null

/**
* Generic invocation to pass a callback to the new task
* Pass a new callback for the task
* If another task is pending, it will be invalidated
*/
operator fun invoke(callback: () -> Unit) {
synchronized(this) {
task?.invalidate()
val newTask = DebounceTask(callback)
KL.v("Debouncer task created: $newTask in $this")
sched.schedule(newTask, interval, TimeUnit.MILLISECONDS)
task = newTask
}
}

/**
* Call to cancel all pending requests and shutdown the thread pool
* The debouncer cannot be used after this
*/
fun terminate() = sched.shutdownNow()

/**
* Invalidate any pending tasks
*/
fun cancel() {
synchronized(this) {
if (task != null) KL.v("Debouncer cancelled for $task in $this")
task?.invalidate()
task = null
}
}

}

/*
* Helper extensions for functions with 0 to 3 arguments
*/

/**
* The debounced task
* Holds a callback to execute if the time has come and it is still valid
* All methods can be viewed as synchronous as the invocation is synchronous
*/
private class DebounceTask(inline val callback: () -> Unit) : Runnable {
private var valid = true

fun invalidate() {
valid = false
}

override fun run() {
if (!valid) return
valid = false
KL.v("Debouncer task executed $this")
try {
callback()
} catch (e: Exception) {
KL.e(e, "DebouncerTask exception")
}
}
}

/**
* A zero input debouncer
*/
class Debouncer0 internal constructor(interval: Long, val callback: () -> Unit) : Debouncer(interval) {
operator fun invoke() = invoke(callback)
}

fun debounce(interval: Long, callback: () -> Unit) = Debouncer0(interval, callback)
fun (() -> Unit).debounce(interval: Long) = debounce(interval, this)

/**
* A one argument input debouncer
*/
class Debouncer1<T> internal constructor(interval: Long, val callback: (T) -> Unit) : Debouncer(interval) {
operator fun invoke(key: T) = invoke { callback(key) }
}

fun <T> debounce(interval: Long, callback: (T) -> Unit) = Debouncer1(interval, callback)
fun <T> ((T) -> Unit).debounce(interval: Long) = debounce(interval, this)

/**
* A two argument input debouncer
*/
class Debouncer2<T, V> internal constructor(interval: Long, val callback: (T, V) -> Unit) : Debouncer(interval) {
operator fun invoke(arg0: T, arg1: V) = invoke { callback(arg0, arg1) }
}

fun <T, V> debounce(interval: Long, callback: (T, V) -> Unit) = Debouncer2(interval, callback)
fun <T, V> ((T, V) -> Unit).debounce(interval: Long) = debounce(interval, this)

/**
* A three argument input debouncer
*/
class Debouncer3<T, U, V> internal constructor(interval: Long, val callback: (T, U, V) -> Unit) : Debouncer(interval) {
operator fun invoke(arg0: T, arg1: U, arg2: V) = invoke { callback(arg0, arg1, arg2) }
}

fun <T, U, V> debounce(interval: Long, callback: ((T, U, V) -> Unit)) = Debouncer3(interval, callback)
fun <T, U, V> ((T, U, V) -> Unit).debounce(interval: Long) = debounce(interval, this)
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ internal object UNINITIALIZED

fun <T : Any> lazyResettable(initializer: () -> T): LazyResettable<T> = LazyResettable<T>(initializer)

open class LazyResettable<T : Any>(private val initializer: () -> T, lock: Any? = null) : ILazyResettable<T>, Serializable {
class LazyResettable<T : Any>(private val initializer: () -> T, lock: Any? = null) : ILazyResettable<T>, Serializable {
@Volatile private var _value: Any = UNINITIALIZED
private val lock = lock ?: this

Expand Down
3 changes: 3 additions & 0 deletions core/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ open class TimberLogger(tag: String) {
inline fun i(s: String) = Timber.i(TAG, s)
inline fun v(s: String) = Timber.v(TAG, s)
inline fun eThrow(s: String) = e(Throwable(s))
// fun plant() {
// Timber.plant(Timber.Tree())
// }
}
53 changes: 53 additions & 0 deletions core/src/test/kotlin/ca/allanwang/kau/kotlin/DebounceTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package ca.allanwang.kau.kotlin

import org.jetbrains.anko.doAsync
import org.junit.Test
import kotlin.test.assertEquals

/**
* Created by Allan Wang on 2017-08-05.
*/
class DebounceTest {

@Test
fun basic() {
var i = 0
val debounce = debounce(20) { i++ }
assertEquals(0, i, "i should start as 0")
(1..5).forEach { debounce() }
Thread.sleep(50)
assertEquals(1, i, "Debouncing did not cancel previous requests")
}

@Test
fun basicExtension() {
var i = 0
val increment: () -> Unit = { i++ }
(1..5).forEach { increment() }
assertEquals(5, i, "i should be 5")
val debounce = increment.debounce(50)
(6..10).forEach { debounce() }
assertEquals(5, i, "i should not have changed")
Thread.sleep(100)
assertEquals(6, i, "i should increment to 6")
}

@Test
fun multipleDebounces() {
var i = 0
val debounce = debounce<Int>(10) { i += it }
debounce(1) //ignore -> i = 0
Thread.sleep(5)
assertEquals(0, i)
debounce(2) //accept -> i = 2
Thread.sleep(15)
assertEquals(2, i)
debounce(4) //ignore -> i = 2
Thread.sleep(5)
assertEquals(2, i)
debounce(8) //accept -> i = 10
Thread.sleep(15)
assertEquals(10, i)
}

}
7 changes: 6 additions & 1 deletion docs/Changelog.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# Changelog

## v3.2.4
## v3.3.0
* :core: Create debounce methods
* :searchview: [Breaking] remove reactive dependencies and stick with basic callbacks

## v3.2.5
* :core: Fix FAQ background
* :core: Create FileUtils
* :core: Create NotificationUtils
* :core: Update swipe to remove most exceptions
* :core: Make logging class functions inline
* :core: Create removeIf for mutableIteratables
* :core-ui: Move reactive libs to :searchview:

## v3.2.3
Expand Down
Loading

0 comments on commit 187d8e6

Please sign in to comment.