Skip to content

Commit

Permalink
SelectionStateController
Browse files Browse the repository at this point in the history
  • Loading branch information
GuhDoy committed Jul 20, 2024
1 parent e118c08 commit ca0026d
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 26 deletions.
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
val androidGradlePluginVersion = "8.4.1"
val androidGradlePluginVersion = "8.5.1"
id("com.android.application") version androidGradlePluginVersion apply false
id("com.android.library") version androidGradlePluginVersion apply false
kotlin("android") version "1.9.23" apply false
kotlin("android") version "2.0.0" apply false
id("com.vanniktech.maven.publish") version "0.27.0" apply false
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Sun Jan 07 15:17:48 CST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
6 changes: 3 additions & 3 deletions library/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id("com.android.library")
kotlin("android")
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0"
id("com.vanniktech.maven.publish")
}

Expand All @@ -18,7 +19,6 @@ android {
}
kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() }
buildFeatures { compose = true }
composeOptions { kotlinCompilerExtensionVersion = "1.5.13" }
buildTypes {
release {
isMinifyEnabled = false
Expand All @@ -27,8 +27,8 @@ android {
}

dependencies {
implementation(platform("androidx.compose:compose-bom:2024.05.00"))
implementation(platform("androidx.compose:compose-bom:2024.06.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3")
}
74 changes: 57 additions & 17 deletions library/src/main/java/me/gm/selection/SelectionState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,54 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.runtime.toMutableStateMap

/**
* An interface used to control whether the [SelectionState] can be modified.
*
* @return `true` to allow modification; otherwise, do not allow it.
*/
interface SelectionStateController<K, V> {

fun canSelect(e: K, item: V): Boolean

fun canDeselect(e: K): Boolean

fun canClearSelection(): Boolean
}

/**
* The default [SelectionStateController] always allows modification.
*/
private class DefaultSelectionStateController<K, V> : SelectionStateController<K, V> {

override fun canSelect(e: K, item: V): Boolean = true

override fun canDeselect(e: K): Boolean = true

override fun canClearSelection(): Boolean = true
}

/**
* Creates a [KeySelectionState] that is remembered across compositions.
*
* Changes to the provided initial values will **not** result in the state being recreated or
* changed in any way if it has already been created.
*
* @param saver is used to save the data of selected items. You can use the built-in
* [noOpSaver] or [viewModelSaver] to keep the implementation simple. Alternatively, you can use
* [noOpSaver] or [danglingSaver] to keep the implementation simple. Alternatively, you can use
* [autoSaver] or a custom [Saver] to ensure data is not lost
* @param initialSelection the initial value for [SelectionSupport.selection]
* @param mutable control whether the [SelectionState] can be modified
*/
@Composable
fun <V> rememberKeySelectionState(
saver: Saver<Pair<List<Any>, List<V>>, Any> = noOpSaver(),
initialSelection: Iterable<Pair<Any, V>> = emptyList(),
mutable: SelectionStateController<Any, V> = DefaultSelectionStateController(),
): KeySelectionState<V> {
return rememberSaveable(
saver = KeySelectionState.Saver(saver)
) {
KeySelectionState(initialSelection)
KeySelectionState(initialSelection, mutable)
}
}

Expand All @@ -53,19 +81,21 @@ fun <V> rememberKeySelectionState(
* changed in any way if it has already been created.
*
* @param saver is used to save the data of selected items. You can use the built-in
* [noOpSaver] or [viewModelSaver] to keep the implementation simple. Alternatively, you can use
* [noOpSaver] or [danglingSaver] to keep the implementation simple. Alternatively, you can use
* [autoSaver] or a custom [Saver] to ensure data is not lost
* @param initialSelection the initial value for [SelectionSupport.selection]
* @param mutable control whether the [SelectionState] can be modified
*/
@Composable
fun <V> rememberIndexSelectionState(
saver: Saver<Pair<List<Int>, List<V>>, Any> = noOpSaver(),
initialSelection: Iterable<Pair<Int, V>> = emptyList(),
mutable: SelectionStateController<Int, V> = DefaultSelectionStateController(),
): IndexSelectionState<V> {
return rememberSaveable(
saver = IndexSelectionState.Saver(saver)
) {
IndexSelectionState(initialSelection)
IndexSelectionState(initialSelection, mutable)
}
}

Expand Down Expand Up @@ -104,19 +134,26 @@ sealed interface SelectionState<K, V> {
}

abstract class SelectionSupport<K, V>(
initialSelection: Iterable<Pair<K, V>>
initialSelection: Iterable<Pair<K, V>>,
var mutable: SelectionStateController<K, V>,
) : SelectionState<K, V> {
private val selection: SnapshotStateMap<K, V> = initialSelection.toMutableStateMap()

final override fun select(e: K, item: V): Boolean = selection.put(e, item) == null
final override fun select(e: K, item: V): Boolean =
mutable.canSelect(e, item) && selection.put(e, item) == null

final override fun deselect(e: K): Boolean = selection.remove(e) != null
final override fun deselect(e: K): Boolean =
mutable.canDeselect(e) && selection.remove(e) != null

final override fun isSelected(e: K): Boolean = selection.containsKey(e)

final override fun selectedKeys(): List<K> = selection.keys.toList()

final override fun clearSelection() = selection.clear()
final override fun clearSelection() {
if (mutable.canClearSelection()) {
selection.clear()
}
}

final override fun selectedItemCount(): Int = selection.size

Expand All @@ -125,13 +162,15 @@ abstract class SelectionSupport<K, V>(

abstract class DanglingKeysSupport<K, V>(
initialSelection: Iterable<Pair<K, V>>,
mutable: SelectionStateController<K, V>,
internal val danglingKeys: MutableList<K>
) : SelectionSupport<K, V>(initialSelection)
) : SelectionSupport<K, V>(initialSelection, mutable)

class KeySelectionState<V>(
initialSelection: Iterable<Pair<Any, V>> = emptyList(),
mutable: SelectionStateController<Any, V> = DefaultSelectionStateController(),
danglingKeys: MutableList<Any> = mutableListOf()
) : DanglingKeysSupport<Any, V>(initialSelection, danglingKeys) {
) : DanglingKeysSupport<Any, V>(initialSelection, mutable, danglingKeys) {

companion object {
/** The default [Saver] for [KeySelectionState]. */
Expand All @@ -141,21 +180,22 @@ class KeySelectionState<V>(
restore = {
val (selectedKeys, selectedItems) = saver.restore(it)!!
if (saver === DanglingSaver) {
KeySelectionState(emptyList(), selectedKeys.toMutableList())
KeySelectionState(danglingKeys = selectedKeys.toMutableList())
} else if (selectedKeys.size != selectedItems.size) {
// Data cleared or corrupted, drop it.
KeySelectionState(emptyList())
KeySelectionState()
} else {
KeySelectionState(selectedKeys.zip(selectedItems))
KeySelectionState(initialSelection = selectedKeys.zip(selectedItems))
}
}
)
}
}

class IndexSelectionState<V>(
initialSelection: Iterable<Pair<Int, V>> = emptyList()
) : SelectionSupport<Int, V>(initialSelection) {
initialSelection: Iterable<Pair<Int, V>> = emptyList(),
mutable: SelectionStateController<Int, V> = DefaultSelectionStateController(),
) : SelectionSupport<Int, V>(initialSelection, mutable) {

companion object {
/** The default [Saver] for [IndexSelectionState]. */
Expand All @@ -166,9 +206,9 @@ class IndexSelectionState<V>(
val (selectedKeys, selectedItems) = saver.restore(it)!!
if (selectedKeys.size != selectedItems.size) {
// Data cleared or corrupted, drop it.
IndexSelectionState(emptyList())
IndexSelectionState()
} else {
IndexSelectionState(selectedKeys.zip(selectedItems))
IndexSelectionState(initialSelection = selectedKeys.zip(selectedItems))
}
}
)
Expand Down
6 changes: 3 additions & 3 deletions sample/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id("com.android.application")
kotlin("android")
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0"
}

android {
Expand All @@ -20,7 +21,6 @@ android {
}
kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() }
buildFeatures { compose = true }
composeOptions { kotlinCompilerExtensionVersion = "1.5.13" }
signingConfigs {
create("release") {
storeFile = File(System.getenv("STORE_FILE") ?: "/dev/null")
Expand All @@ -45,8 +45,8 @@ android {
dependencies {
implementation(project(":library"))

implementation("androidx.activity:activity-compose:1.8.2")
implementation(platform("androidx.compose:compose-bom:2024.05.00"))
implementation("androidx.activity:activity-compose:1.9.0")
implementation(platform("androidx.compose:compose-bom:2024.06.00"))
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.animation:animation")
implementation("androidx.compose.material:material-icons-core")
Expand Down

0 comments on commit ca0026d

Please sign in to comment.