Skip to content

w2sv/Composed

Repository files navigation

Composed

API JitPack Build GitHub License


A collection of utils to facilitate development with Jetpack Compose.


Installation

Make sure you have jitpack added to your dependency resolution repositories by adding the following to your settings.gradle.kts:

dependencyResolutionManagement {
  repositories {
    maven("https://jitpack.io")
  }
}

Then add the dependencies you lust for to your build.gradle.kts files:

dependencies {
    // Core utils
    implementation "com.github.w2sv.Composed:composed:<version>"
    // Permission utils
    implementation "com.github.w2sv.Composed:permissions:<version>"
}

Contents

State Savers

/**
 * Returns a rememberSavable state saver for Color.
 */
fun colorSaver(): Saver<Color, Int>

/**
 * Returns a rememberSavable state saver for an optional Color.
 */
fun nullableColorSaver(): Saver<Color?, Float>

/**
 * listSaver for an optional object, enabling handling of non-null instances only.
 */
fun <Original, Saveable> nullableListSaver(
    saveNonNull: SaverScope.(value: Original) -> List<Saveable>,
    restoreNonNull: (list: List<Saveable>) -> Original?
): Saver<Original?, Any>

/**
 * mapSaver for an optional object, enabling handling of non-null instances only.
 */
fun <T> nullableMapSaver(
    saveNonNull: SaverScope.(value: T) -> Map<String, Any?>,
    restoreNonNull: (Map<String, Any?>) -> T
): Saver<T, Any>

Styled Text

/**
 * Converts a HTML-styled string resource text to a remembered AnnotatedString.
 */
@Composable
fun rememberStyledTextResource(@StringRes id: Int, vararg formatArgs: Any): AnnotatedString

Modifiers

/**
 * Applies modifiers depending on a condition.
 */
inline fun Modifier.thenIf(
    condition: Boolean,
    onFalse: Modifier.() -> Modifier = { this },
    onTrue: Modifier.() -> Modifier = { this },
): Modifier

Layout

/**
 * [Column] whose [elements], rendered through [makeElement], will be divided by [makeDivider]. [makeDivider] will be invoked only in between elements, that is, neither before the first, nor after the last element.
 */
@Composable
fun <T> InterElementDividedColumn(
    elements: List<T>,
    makeElement: @Composable ColumnScope.(T) -> Unit,
    modifier: Modifier = Modifier,
    makeDivider: @Composable ColumnScope.() -> Unit = { HorizontalDivider() },
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
)

/**
 * [Row] whose [elements], rendered through [makeElement], will be divided by [makeDivider]. [makeDivider] will be invoked only in between elements, that is, neither before the first, nor after the last element.
 */
@Composable
fun <T> InterElementDividedRow(
    elements: List<T>,
    makeElement: @Composable RowScope.(T) -> Unit,
    modifier: Modifier = Modifier,
    makeDivider: @Composable RowScope.() -> Unit = { VerticalDivider() },
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
)

Flow Collectors

/**
 * Collects from a flow and emits values into a collector.
 */
@Composable
fun <T> CollectFromFlow(
    flow: Flow<T>,
    key1: Any? = null,
    key2: Any? = null,
    collector: FlowCollector<T>
)

/**
 * Collects latest from a flow with given action.
 */
@Composable
fun <T> CollectLatestFromFlow(
    flow: Flow<T>,
    key1: Any? = null,
    key2: Any? = null,
    action: suspend (value: T) -> Unit
)

Lifecycle Observers

/**
 * Runs a callback whenever the lifecycleOwner reaches the given lifecycleEvent.
 */
@Composable
fun OnLifecycleEvent(
    lifecycleEvent: Lifecycle.Event,
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    key1: Any? = null,
    key2: Any? = null,
    callback: () -> Unit
)

/**
 * Runs a callback when removed from composition.
 */
@Composable
fun OnDispose(callback: () -> Unit)

Orientation

/**
 * Returns true if the landscape mode is active, false otherwise.
 */
val isLandscapeModeActive: Boolean

/**
 * Returns true if the portrait mode is active, false otherwise.
 */
val isPortraitModeActive: Boolean

Dimension Conversion

/**
 * Converts Dp to pixels.
 */
@Composable
@ReadOnlyComposable
fun Dp.toPx(): Float

/**
 * Converts pixels to Dp.
 */
@Composable
@ReadOnlyComposable
fun Int.toDp(): Dp

/**
 * Converts pixels to Dp.
 */
@Composable
@ReadOnlyComposable
fun Float.toDp(): Dp

Color Conversion

/**
 * Converts a hex color string to Color.
 */
fun String.toComposeColor(): Color

Map Conversion

/**
 * Converts a regular Map to a SnapshotStateMap.
 */
fun <K, V> Map<K, V>.toMutableStateMap(): SnapshotStateMap<K, V>

DrawerState

/**
 * Returns a State<Float> whose value ranges from 0.0 (drawer closed) to 1.0 (drawer fully open).
 */
fun DrawerState.visibilityPercentage(@FloatRange(from = 0.0) maxWidthPx: Float): State<Float>

/**
 * Remembers a visibility percentage for the drawer.
 */
@Composable
fun DrawerState.rememberVisibilityPercentage(@FloatRange(from = 0.0) maxWidthPx: Float = DrawerDefaults.MaximumDrawerWidth.toPx()): State<Float>

SnackbarHostState

/**
 * Dismisses the currently showing snackbar if there is one and shows a new one with the given [snackbarVisuals].
 */
suspend fun SnackbarHostState.dismissCurrentSnackbarAndShow(snackbarVisuals: SnackbarVisuals)

/**
 * Dismisses the currently showing snackbar if there is one and shows a new one with the given parameters.
 */
suspend fun SnackbarHostState.dismissCurrentSnackbarAndShow(
    message: String,
    actionLabel: String? = null,
    withDismissAction: Boolean = false,
    duration: SnackbarDuration = if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite
)

Easing

fun TimeInterpolator.toEasing() = Easing

Permission States

/**
 * Permission state which, as opposed to the accompanist ones,
 * - exposes a [grantedFromRequest] shared flow to allow for distributed subscription and callback invocation, instead of only being able to pass a onPermissionResult callback upon instantiation, which needs to cover all granting reactions, possibly impacting various components
 * - allows for callbacks upon permission requesting being suppressed
 */
@Stable
interface ExtendedPermissionState {
    val granted: Boolean
    val grantedFromRequest: SharedFlow<Boolean>
    fun launchRequest(onSuppressed: (() -> Unit)? = null)
}

// With the implementations:

@Stable
open class ExtendedSinglePermissionState(
    private val requestLaunchedBefore: StateFlow<Boolean>,
    permissionState: PermissionState,
    override val grantedFromRequest: SharedFlow<Boolean>,
    private val defaultOnLaunchingSuppressed: () -> Unit = {}
) : PermissionState by permissionState, ExtendedPermissionState

@Composable
fun rememberExtendedSinglePermissionState(
    permission: String,
    requestLaunchedBefore: StateFlow<Boolean>,
    saveRequestLaunched: () -> Unit,
    defaultOnPermissionResult: (Boolean) -> Unit = {},
    defaultOnLaunchingSuppressed: () -> Unit = {},
    scope: CoroutineScope = rememberCoroutineScope()
): ExtendedSinglePermissionState

// And

@Stable
open class ExtendedMultiplePermissionsState(
    private val requestLaunchedBefore: StateFlow<Boolean>,
    multiplePermissionsState: MultiplePermissionsState,
    override val grantedFromRequest: SharedFlow<Boolean>,
    private val defaultOnLaunchingSuppressed: () -> Unit = {}
) : MultiplePermissionsState by multiplePermissionsState, ExtendedPermissionState

@Composable
fun rememberExtendedMultiplePermissionsState(
    permissions: List<String>,
    requestLaunchedBefore: StateFlow<Boolean>,
    saveRequestLaunched: () -> Unit,
    defaultOnPermissionResult: (Map<String, Boolean>) -> Unit = {},
    defaultOnLaunchingSuppressed: () -> Unit = {},
    scope: CoroutineScope = rememberCoroutineScope()
): ExtendedMultiplePermissionsState

License

Designed and developed by 2024 w2sv (Janek Zangenberg)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.