compose-extension

Maven CentralAndroid Min SDK

This is a dependency for Jetpack Compose related functional extensions and supports multi-platform.

Currently supported platforms: Android, iOS, Desktop (JVM).

Configure Dependency

You can add this module to your project using the following method.

This is a Kotlin Multiplatform dependency, you need the org.jetbrains.kotlin.multiplatform plugin to apply the relevant dependencies.

Add dependencies to your project SweetDependency configuration file.

libraries:
  com.highcapable.betterandroid:
    # commonMain
    compose-extension:
      version: +
    # androidMain
    compose-extension-android:
      version-ref: <this>::compose-extension
    # iosArm64Main
    compose-extension-iosarm64:
      version-ref: <this>::compose-extension
    # iosX64Main
    compose-extension-iosx64:
      version-ref: <this>::compose-extension
    # iosSimulatorArm64Main
    compose-extension-iossimulatorarm64:
      version-ref: <this>::compose-extension
    # desktopMain
    compose-extension-desktop:
      version-ref: <this>::compose-extension

Configure dependencies in your project build.gradle.kts.

If you use multi-platform dependencies in a regular project, you only need to deploy the corresponding platform suffix dependencies as needed.

implementation(com.highcapable.betterandroid.compose.extension.android)
implementation(com.highcapable.betterandroid.compose.extension.iosarm64)
implementation(com.highcapable.betterandroid.compose.extension.iosx64)
implementation(com.highcapable.betterandroid.compose.extension.iossimulatorarm64)
implementation(com.highcapable.betterandroid.compose.extension.desktop)

If you use multi-platform dependencies in a multi-platform project, you need to add the compose-extension dependency in commonMain.

kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(com.highcapable.betterandroid.compose.extension)
            }
        }
    }
}

Traditional Method

Configure dependencies in your project build.gradle.kts.

If you use multi-platform dependencies in a regular project, you only need to deploy the corresponding platform suffix dependencies as needed.

implementation("com.highcapable.betterandroid:compose-extension-android:<version>")
implementation("com.highcapable.betterandroid:compose-extension-iosarm64:<version>")
implementation("com.highcapable.betterandroid:compose-extension-iosx64:<version>")
implementation("com.highcapable.betterandroid:compose-extension-iossimulatorarm64:<version>")
implementation("com.highcapable.betterandroid:compose-extension-desktop:<version>")

If you use multi-platform dependencies in a multi-platform project, you need to add the compose-extension dependency in commonMain.

kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("com.highcapable.betterandroid:compose-extension:<version>")
            }
        }
    }
}

Please change <version> to the version displayed at the top of this document.

Function Introduction

You can view the KDoc click hereopen in new window.

Color Extension

Color in Jetpack Compose encapsulates color-related implementations, which solves the problem of using the Integer type to pass colors in native Android.

BetterAndroid also provides extensions for Color similar to those in ui-extension → Color Extension, making it easier use.

Since Color provides the alpha property, the toAlphaColor extension provided natively is no longer needed in Color.

Here are some relevant example uses of color extensions.

Determine how bright the color is.

This is useful when you need to decide whether to use dark text based on how bright the color is.

The following example

// Assume we have the following colors.
val color = Color.White
// To determine how bright it is, you just need to use the following method.
// You will definitely get a true because this is a white color.
val isBright = color.isBrightColor

Convert color to HEX string.

The following example

// Assume we have the following colors.
val color = Color.White
// To convert it to a HEX string you just need to use the following method.
// You will get a "#FFFFFFFF" with transparency.
val hexString = color.toHexColor()

Mix two colors.

The following example

// Assume we have the following colors.
val color1 = Color.White
val color2 = Color.Black
// You can mix them very easily using.
val mixColor = mixColorOf(color1, color2)
// You can also set the mixing ratio, the default is 0.5f.
val mixColor = mixColorOf(color1, color2, 0.2f)

Determine whether Color is unspecified.

When Color is Color.Unspecified, you can use the following method to get an object that can be null in this state.

The following example

// Assume we have the following colors.
val color = Color.Unspecified
// null will be returned in this state, then you can use ?: to pass the default value in undefined cases.
val myColor = color.orNull() ?: Color.White

On non-Android platforms, you can convert the corresponding Color object to the platform's color object in the following way.

The following example

// Assume we have the following colors.
val color = Color.White
// For example, you can convert it to a color object in JavaFX.
val awtColor = color.toPlatformColor()
// In iOS, you can convert it to a UIColor object.
val uiColor = color.toPlatformColor()

Tips

In Android platform, you can use the Color.toArgb() method to convert a Color object to an Integer type.

Likewise, you can convert the platform's color objects into Compose's Color objects in the following way.

The following example

// Assume this is a color object in JavaFX.
val awtColor = Color.WHITE
// Assume this is a color object in iOS.
val uiColor = UIColor.whiteColor()
// Convert it to a Compose Color object.
val composeColor = awtColor.toComposeColor()
val composeColor = uiColor.toComposeColor()

Tips

In Android platform, you can use a form like Color(0xFFFFFFFF) to convert an Long type color into a Color object.

Border Extension

Contents of This Section

commonMain

Border → borderOrElseopen in new window

BorderStroke → solidColoropen in new window

Extensions for Border and BorderStroke.

You can add a border to a component through the Modifier.border(...) method, but when the border size is 0.dp, the border will still exist.

The origin of this question comes from hereopen in new window, the solution mentioned in setting the border to a transparent color is not friendly because it still performs a drawing operation.

So BetterAndroid provides the borderOrElse extension, which will no longer add borders to components when the border size is 0.dp.

The following example

Box(
    modifier = Modifier
        .size(50.dp)
        // Here, the size of the border is 0.dp, so the component will not have a border.
        .borderOrElse(0.dp, Color.Black, RectangleShape)
) {
    // Some content.
}

Gets the color in brush (SolidColor) from an existing BorderStroke object.

SolidColor is a type of Brush, which can be used to fill color, a border created by BorderStroke(10.dp, Color.White) will not be able to easily obtain the color from brush.

BetterAndroid provides an extension for this, now you can get the color in brush using the following method.

The following example

// Assume this is your BorderStroke object.
val border = BorderStroke(10.dp, Color.White)
// When the brush is determined to be SolidColor, you can use the following method to get the color in it.
// If it cannot be obtained, Color.Unspecified will be returned.
val color = border.solidColor
// You can also set its default value when it cannot be obtained.
val color = border.solidColor(Color.Black)

Padding Extension

Contents of This Section

commonMain

ComponentPaddingopen in new window

Componentized padding, inherited from PaddingValues.

PaddingValues is the padding usage provided in Jetpack Compose, which can be set directly to Modifier.padding(...).

However, PaddingValues does not provide functions such as copy, once set, it cannot be modified, which is very inconvenient.

So BetterAndroid inherited from PaddingValues and rewrote ComponentPadding to make it easier to use.

In terms of usage, ComponentPadding is exactly the same as PaddingValues, and you can also set it directly into Modifier.padding(...).

The following example

// Create a ComponentPadding.
val padding = ComponentPadding(10.dp)
// Create a 0.dp ComponentPadding of all directions.
val padding = ComponentPadding.None
// Set padding for each direction.
val padding = ComponentPadding(
    start = 15.dp,
    top = 10.dp,
    bottom = 12.dp,
    end = 16.dp
)
// Set horizontal and vertical padding.
val padding = ComponentPadding(
    horizontal = 10.dp,
    vertical = 15.dp
)
// Create a new ComponentPadding via copy.
val paddingCopy = padding.copy(top = 15.dp)
// Set to component.
Box(
     modifier = Modifier.padding(padding)
) {
    // Some content.
}

BetterAndroid also encapsulates calculateLeftPadding and calculateRightPadding into Composeable methods, you no longer need to use LayoutDirection to calculate padding in the LTR and RTL directions.

The following example

@Composable
fun MyComponent() {
    // Assume this is your ComponentPadding.
    val padding: ComponentPadding
    // Get left.
    val left = padding.left
    // Get right.
    val right = padding.right
}

You can also convert it to PaddingValues using the following method.

The following example

// Assume this is your ComponentPadding.
val padding: ComponentPadding
// Convert to PaddingValues.
val paddingValues = padding.toPaddingValues()

Foundation Extension

There is a lack of a "disabled" state in Jetpack Compose components, BetterAndroid provides the following ways to achieve this effect by adjusting the transparency of the component.

Its actual function is actually a visual enabling or disabling transparency effect, and does not set any state on the component.

The following example

Box(
    modifier = Modifier
        .size(50.dp)
        // Set the state of the component through componentState.
        // You can also set disabledAlpha to adjust the transparency of
        // the component in the disabled state.
        .componentState(enabled = false)
) {
    // Some content.
}

In the original clickable, combinedClickable, toggleable, and selectable, you need to manually set the default value for them.

If you only use these functions in a simple scenario, it will be more cumbersome.

BetterAndroid provides extensions of the same name for the above functions, now you can use these functions more conveniently without having to think about using remember and other methods to set their state.

The following example

Box(
     modifier = Modifier
        .size(50.dp)
        // Set the click event of the component through clickable.
        .clickable {
            // Handle click event.
        }
) {
    // Some content.
}

Notice

You need to change the package name referenced by these functions from androidx.compose.foundation to com.highcapable.betterandroid.compose.extension.ui.

In native Android, to implement haptic feedback, you need to use the View.performHapticFeedback method or pass true in the last bit of a method like setOnLongClickListener.

In Jetpack Compose, you need to reference LocalHapticFeedback every time and then use the performHapticFeedback method, which seems unfriendly.

BetterAndroid provides the HapticFeedback method for this purpose, you can now use the following methods to implement haptic feedback more simply.

The following example

Box(
    modifier = Modifier.combinedClickable(
        onLongClick = HapticFeedback {
            // Do something.
        }
    )
) {
    Text("Long Click Me")
}

ImageVector Extension

Contents of This Section

commonMain

ImageVector → ImageVectoropen in new window

Extensions for ImageVector.

Jetpack Compose natively provides an ImageVector that can create vector images, but you need to use ImageVector.Builder to create it.

This method does not seem very friendly, so BetterAndroid provides a method of the same name of ImageVector for this purpose.

The following example

// Create a vector image through ImageVector.
val myVector = ImageVector(
    name = "my_vector",
    defaultWidth = 32.dp,
    defaultHeight = 32.dp,
    viewportWidth = 48f,
    viewportHeight = 48f
) {
    path(
        fillAlpha = 1.0f,
        stroke = SolidColor(Color.White),
        strokeAlpha = 1.0f,
        strokeLineWidth = 1.0f,
        strokeLineCap = StrokeCap.Round,
        strokeLineJoin = StrokeJoin.Round,
        strokeLineMiter = 1.0f,
        pathFillType = PathFillType.NonZero
    ) {
        // ...
    }
    // ...
}

Unit Extension

Contents of This Section

commonMain

Unit → orNullopen in new window

Extensions for Unit.

In all units that can use isSpecified for judgment, Jetpack Compose provides the takeOrElse method, but it is not concise and easy to understand.

So BetterAndroid provides the orNull method for this, you can use it to get an object that can be null in this state.

Below is a comparison of using takeOrElse versus orNull.

The following example

// Assume we have the following units.
val dp: Dp
val sp: TextUnit
// Use takeOrElse.
val dpValue = dp.takeOrElse { 10.dp }
val spValue = sp.takeOrElse { 10.sp }
// Use orNull.
val dpValue = dp.orNull() ?: 10.dp
val spValue = sp.orNull() ?: 10.sp

You will find that orNull is more concise and easier to understand.

Adaptive Layout

Adaptive layout provides a solution to measure each child component and distribute it evenly with the size of the parent layout horizontally or vertically without knowing the size of the child component.

For example, you can use the following method to arrange two buttons horizontally and evenly distribute their widths based on the size of the parent component.

The following example

// Create an AdaptiveRow.
AdaptiveRow(
    modifier = Modifier.width(150.dp),
    // You can set the spacing of each component.
    spacingBetween = 10.dp
) {
    Button(onClick = { /* ... */ }) {
        Text("Button 1")
    }
    Button(onClick = { /* ... */ }) {
        Text("Button 2")
    }
}

You don't need to set any width for the Button at this point, they will be automatically measured and evenly distributed.

Likewise, you can use AdaptiveColumn to arrange components vertically.

The following example

// Create an AdaptiveColumn.
AdaptiveColumn(
    modifier = Modifier.height(150.dp),
    // You can set the spacing of each component.
    spacingBetween = 10.dp
) {
    Button(onClick = { /* ... */ }) {
        Text("Button 1")
    }
    Button(onClick = { /* ... */ }) {
        Text("Button 2")
    }
}

Dialog, Popup Component Extension

Contents of This Section

commonMain

DialogPropertiesWrapperopen in new window

A wrapper for DialogProperties in the Android platform.

DialogPropertiesWrapper.AndroidPropertiesopen in new window

Limited properties for Android platform in DialogProperties.

SecureFlagPolicyWrapperopen in new window

A wrapper for SecureFlagPolicy in the Android platform.

Dialog → Dialogopen in new window

Extensions for Dialog.

Popup → Popupopen in new window

Extensions for Popup.

The extensions described in this section are available for use in Kotlin Multiplatform.

When creating Dialog and Popup, commonMain does not handle the special functions in the Android platform well.

BetterAndroid provides functions related to the Android platform for this purpose, you can use them directly in commonMain without having to adapt them separately for Android.

DialogPropertiesWrapper mirrors all properties from DialogProperties and encapsulates Android platform-specific properties into DialogPropertiesWrapper.AndroidProperties.

In the Dialog method provided by BetterAndroid, you can directly pass in the DialogPropertiesWrapper object.

The following example

// Create a dialog.
Dialog(
    onDismissRequest = {
       // Handle related events.
    },
    properties = DialogPropertiesWrapper(
        dismissOnBackPress = true,
        dismissOnClickOutside = true,
        usePlatformDefaultWidth = true,
        usePlatformInsets = true,
        scrimColor = Color.Black.copy(alpha = 0.6f),
        // Set special properties for the Android platform.
        androidProperties = DialogPropertiesWrapper.AndroidProperties(
            secureFlagPolicy = SecureFlagPolicyWrapper.Inherit,
            decorFitsSystemWindows = true
        )
    )
) {
    // Some content.
}

Notice

Currently, only alpha in scrimColor in DialogPropertiesWrapper can take effect on the Android platform.

For Popup, the onPreviewKeyEvent and onKeyEvent parameters do not exist on the Android platform, which will causes commonMain to fail to compile when distributed to Android.

To fix this problem, BetterAndroid simulates onPreviewKeyEvent and onKeyEvent for the Android platform, now you don't need to worry about compatibility issues.

The following example

// Create a popup.
Popup(
    onDismissRequest = {
       // Handle related events.
    },
    properties = PopupProperties(),
    onPreviewKeyEvent = {
       // Handle related events.
       false
    },
    onKeyEvent = {
       // Handle related events.
       false
    }
) {
    // Some content.
}

Notice

You need to change the package name referenced by Dialog and Popup above from androidx.compose.ui.window to com.highcapable.betterandroid.compose.extension.ui.window.