Skip to content

Commit 372f74c

Browse files
committed
add SharedPreferences delegates, unit tests
1 parent 715a597 commit 372f74c

8 files changed

Lines changed: 322 additions & 35 deletions

File tree

androidutils/build.gradle

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ android {
2323
}
2424

2525
dependencies {
26-
implementation fileTree(dir: 'libs', include: ['*.jar'])
26+
testImplementation 'junit:junit:4.13'
27+
testImplementation 'androidx.test:core:1.3.0'
28+
testImplementation 'org.robolectric:robolectric:4.3'
29+
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
30+
2731
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
2832
implementation 'androidx.appcompat:appcompat:1.2.0'
2933
implementation 'androidx.preference:preference-ktx:1.1.1'

androidutils/src/main/java/me/jfenn/androidutils/ColorUtils.kt

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,48 @@ private const val RED_CHANNEL = 16
1111
private const val GREEN_CHANNEL = 8
1212
private const val BLUE_CHANNEL = 0
1313

14-
fun Int.red() = this.shr(RED_CHANNEL).and(0xFF)
15-
fun Int.green(): Int = this.shr(GREEN_CHANNEL).and(0xFF)
16-
fun Int.blue(): Int = this.shr(BLUE_CHANNEL).and(0xFF)
17-
fun Int.alpha(): Float = this.shr(ALPHA_CHANNEL).and(0xFF).div(255f)
18-
19-
fun Int.setColor(red: Int = red(), green: Int = green(), blue: Int = blue(), alpha: Float = alpha()): Int {
14+
val Int.red get() = this.shr(RED_CHANNEL).and(0xFF)
15+
val Int.green get() = this.shr(GREEN_CHANNEL).and(0xFF)
16+
val Int.blue get() = this.shr(BLUE_CHANNEL).and(0xFF)
17+
val Int.alpha get() = this.shr(ALPHA_CHANNEL).and(0xFF).div(255f)
18+
19+
fun Int.setColor(
20+
red: Int = this.red,
21+
green: Int = this.green,
22+
blue: Int = this.blue,
23+
alpha: Float = this.alpha
24+
): Int {
2025
return Color.argb((alpha * 255).toInt(), red, green, blue)
2126
}
2227

2328
fun Int.mixColor(vararg colors: Int): Int {
24-
var red: Int = red()
25-
var green: Int = green()
26-
var blue: Int = blue()
27-
var alpha: Float = alpha()
29+
var r: Int = red
30+
var g: Int = green
31+
var b: Int = blue
32+
var a: Float = alpha
2833

2934
for (color in colors) {
30-
red += (color.red() * color.alpha()).toInt()
31-
green += (color.green() * color.alpha()).toInt()
32-
blue += (color.blue() * color.alpha()).toInt()
33-
alpha += color.alpha()
35+
r += (color.red * color.alpha).toInt()
36+
g += (color.green * color.alpha).toInt()
37+
b += (color.blue * color.alpha).toInt()
38+
a += color.alpha
3439
}
3540

3641
return Color.argb(
37-
((alpha / (colors.size + 1)) * 255).toInt(),
38-
(red / alpha).toInt().coerceIn(0, 255),
39-
(green / alpha).toInt().coerceIn(0, 255),
40-
(blue / alpha).toInt().coerceIn(0, 255)
42+
((a / (colors.size + 1)) * 255).toInt(),
43+
(r / a).toInt().coerceIn(0, 255),
44+
(g / a).toInt().coerceIn(0, 255),
45+
(b / a).toInt().coerceIn(0, 255)
4146
)
4247
}
4348

4449
fun Int.multiplyColor(vararg colors: Int, base: Int = 0): Int {
4550
val mixer = base.mixColor(*colors)
4651

4752
return Color.rgb(
48-
(red() * mixer.red() / 255).coerceIn(0, 255),
49-
(green() * mixer.green() / 255).coerceIn(0, 255),
50-
(blue() * mixer.blue() / 255).coerceIn(0, 255)
53+
(red * mixer.red / 255).coerceIn(0, 255),
54+
(green * mixer.green / 255).coerceIn(0, 255),
55+
(blue * mixer.blue / 255).coerceIn(0, 255)
5156
)
5257
}
5358

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,107 @@
11
package me.jfenn.androidutils.prefs
22

33
import android.content.SharedPreferences
4+
import java.lang.reflect.ParameterizedType
5+
import java.lang.reflect.Type
6+
import java.lang.reflect.WildcardType
7+
8+
/**
9+
* Edits the calling SharedPreferences instance through the provided
10+
* lambda and applies changes after completion
11+
*/
12+
inline fun SharedPreferences.edit(block: SharedPreferences.Editor.() -> Unit) {
13+
val editor = edit()
14+
editor.block()
15+
editor.apply()
16+
}
17+
18+
abstract class TypeReference<T> : Comparable<TypeReference<T>> {
19+
val type = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments.first() as ParameterizedType
20+
val arg = (type.actualTypeArguments.first() as WildcardType).upperBounds.first() as Class<*>
21+
override fun compareTo(other: TypeReference<T>) = 0
22+
}
423

524
inline operator fun <reified T> SharedPreferences.get(key: String) : T? {
25+
return when(T::class) {
26+
List::class, MutableList::class -> {
27+
val itemType = object : TypeReference<T>() {}.arg
28+
val list = ArrayList<Any>()
29+
val size = getInt("${key}-length", -1)
30+
31+
if (size == -1)
32+
return null
33+
34+
for (i in 0 until size) {
35+
val itemKey = "${key}#${i}"
36+
when (itemType) {
37+
Boolean::class.java -> getPrimitive<Boolean>(itemKey)
38+
Float::class.java -> getPrimitive<Float>(itemKey)
39+
Int::class.java -> getPrimitive<Int>(itemKey)
40+
Long::class.java -> getPrimitive<Long>(itemKey)
41+
String::class.java -> getPrimitive<String>(itemKey)
42+
else -> null
43+
}?.let {
44+
list.add(it)
45+
}
46+
}
47+
48+
return list as? T
49+
}
50+
else -> getPrimitive(key)
51+
}
52+
}
53+
54+
inline fun <reified T> SharedPreferences.getPrimitive(key: String) : T? {
655
if (!this.contains(key))
756
return null
857

9-
return when(T::class) {
58+
return when (T::class) {
1059
Boolean::class -> this.getBoolean(key, false) as T
1160
Float::class -> this.getFloat(key, 0f) as T
1261
Int::class -> this.getInt(key, 0) as T
1362
Long::class -> this.getLong(key, 0) as T
1463
String::class -> this.getString(key, "") as T
15-
Set::class -> this.getStringSet(key, HashSet()) as T
64+
Set::class, HashSet::class -> {
65+
this.getStringSet(key, HashSet()) as T
66+
}
1667
else -> null
1768
}
1869
}
1970

20-
inline operator fun <reified T> SharedPreferences.set(key: String, value: T?) {
21-
val editor = this.edit()
71+
inline operator fun <reified T> SharedPreferences.set(key: String, value: T?) = edit {
72+
when (T::class) {
73+
List::class, MutableList::class -> {
74+
val itemType = object : TypeReference<T>() {}.arg
75+
val list = value as List<*>
2276

77+
putInt("${key}-length", list.size)
78+
list.forEachIndexed { index, item ->
79+
val itemKey = "${key}#${index}"
80+
when (itemType) {
81+
Boolean::class.java -> putPrimitive(itemKey, item as? Boolean)
82+
Float::class.java -> putPrimitive(itemKey, item as? Float)
83+
Int::class.java -> putPrimitive(itemKey, item as? Int)
84+
Long::class.java -> putPrimitive(itemKey, item as? Long)
85+
String::class.java -> putPrimitive(itemKey, item as? String)
86+
else -> null
87+
}
88+
}
89+
}
90+
else -> putPrimitive(key, value)
91+
}
92+
}
93+
94+
inline fun <reified T> SharedPreferences.Editor.putPrimitive(key: String, value: T?) {
2395
if (value == null)
24-
editor.remove(key)
96+
remove(key)
2597
else when(T::class) {
26-
Boolean::class -> editor.putBoolean(key, value as Boolean)
27-
Float::class -> editor.putFloat(key, value as Float)
28-
Int::class -> editor.putInt(key, value as Int)
29-
Long::class -> editor.putLong(key, value as Long)
30-
String::class -> editor.putString(key, value as String)
31-
Set::class -> editor.putStringSet(key, value as Set<String>)
98+
Boolean::class -> putBoolean(key, value as Boolean)
99+
Float::class -> putFloat(key, value as Float)
100+
Int::class -> putInt(key, value as Int)
101+
Long::class -> putLong(key, value as Long)
102+
String::class -> putString(key, value as String)
103+
Set::class, HashSet::class -> {
104+
putStringSet(key, value as Set<String>)
105+
}
32106
}
33-
34-
editor.apply()
35107
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package me.jfenn.androidutils
2+
3+
import android.graphics.Color
4+
import org.junit.Test
5+
import org.junit.runner.RunWith
6+
import org.robolectric.RobolectricTestRunner
7+
import org.robolectric.annotation.Config
8+
import kotlin.test.assertEquals
9+
10+
@RunWith(RobolectricTestRunner::class)
11+
@Config(sdk = [28])
12+
class ColorTest {
13+
14+
@Test
15+
fun `color channels should equal the provided values`() {
16+
val red = 210
17+
val green = 150
18+
val blue = 100
19+
val color = 0.setColor(red, green, blue)
20+
21+
println(color.toString(16))
22+
23+
assertEquals(red, color.red)
24+
assertEquals(green, color.green)
25+
assertEquals(blue, color.blue)
26+
}
27+
28+
@Test
29+
fun `colors should stay the same when mixed with nothing`() {
30+
val color = Color.argb(255, 255, 255, 255)
31+
val mixedColor = color.mixColor()
32+
33+
assertEquals(color.red, mixedColor.red)
34+
assertEquals(color.green, mixedColor.green)
35+
assertEquals(color.blue, mixedColor.blue)
36+
}
37+
38+
@Test
39+
fun `colors should stay the same when mixed with a transparent color`() {
40+
val color = Color.argb(255, 255, 255, 255)
41+
val mixedColor = color.mixColor(
42+
Color.argb(0, 148, 0, 211)
43+
)
44+
45+
assertEquals(color.red, mixedColor.red)
46+
assertEquals(color.green, mixedColor.green)
47+
assertEquals(color.blue, mixedColor.blue)
48+
}
49+
50+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package me.jfenn.androidutils.prefs
2+
3+
import android.content.Context
4+
import android.content.SharedPreferences
5+
import androidx.test.core.app.ApplicationProvider
6+
7+
fun Any.getSharedPreferences() : SharedPreferences {
8+
return ApplicationProvider.getApplicationContext<Context>()
9+
.getSharedPreferences(javaClass.name, Context.MODE_PRIVATE)
10+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package me.jfenn.androidutils.prefs
2+
3+
import org.junit.Test
4+
import org.junit.runner.RunWith
5+
import org.robolectric.RobolectricTestRunner
6+
import org.robolectric.annotation.Config
7+
import kotlin.test.assertEquals
8+
import kotlin.test.assertNotNull
9+
10+
@RunWith(RobolectricTestRunner::class)
11+
@Config(sdk = [28])
12+
class PrefsDelegateProviderTest {
13+
14+
private val prefs = getSharedPreferences()
15+
16+
@Test
17+
fun `should store & return int types`() {
18+
val testInt by pref<Int>()
19+
var testIntDelegate by prefs.get(testInt)
20+
21+
val test = 512;
22+
testIntDelegate = test
23+
24+
assertEquals(test, testIntDelegate)
25+
}
26+
27+
@Test
28+
fun `should use default values`() {
29+
val test = listOf("a", "b", "c")
30+
val testList by pref(defaultValue = test)
31+
val result by prefs.get(testList)
32+
33+
assertNotNull(result)
34+
assertEquals(test.toString(), result!!.toString())
35+
}
36+
37+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package me.jfenn.androidutils.prefs
2+
3+
import android.content.Context
4+
import android.content.SharedPreferences
5+
import androidx.preference.PreferenceManager
6+
import androidx.test.core.app.ApplicationProvider
7+
import org.junit.Test
8+
import org.junit.runner.RunWith
9+
import org.robolectric.RobolectricTestRunner
10+
import org.robolectric.annotation.Config
11+
import kotlin.test.assertEquals
12+
import kotlin.test.assertNotNull
13+
14+
@RunWith(RobolectricTestRunner::class)
15+
@Config(sdk = [28])
16+
class PrefsDelegateTest {
17+
18+
private val prefs = getSharedPreferences()
19+
20+
@Test
21+
fun `should store & return int types`() {
22+
var testInt by prefs.get<Int>()
23+
24+
val test = 512;
25+
testInt = test
26+
27+
assertEquals(test, testInt)
28+
}
29+
30+
@Test
31+
fun `should store & return StringSet types`() {
32+
var testStringSet by prefs.get<Set<String>>()
33+
34+
val test = HashSet<String>().apply {
35+
add("abc")
36+
add("xyz")
37+
}
38+
testStringSet = test
39+
40+
val result = testStringSet
41+
assertNotNull(result)
42+
assertEquals(test.toString(), result.toString())
43+
}
44+
45+
@Test
46+
fun `should store lists with string types`() {
47+
var testList by prefs.get<List<String>>()
48+
49+
val test = listOf("a", "b", "c")
50+
testList = test
51+
52+
val result = testList
53+
assertNotNull(result)
54+
assertEquals(test.toString(), result.toString())
55+
}
56+
57+
}

0 commit comments

Comments
 (0)