ui-extension

Maven CentralMaven metadata URLAndroid Min SDK

This is a dependency for UI (user interface) related extensions.

Configure Dependency

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

If you are using multiple Android modules at the same time, we recommend that you first refer to android-bom to use BOM for unified version management.

Add dependency in your project's gradle/libs.versions.toml.

[versions]
betterandroid-ui-extension = "<version>"

[libraries]
betterandroid-ui-extension = { module = "com.highcapable.betterandroid:ui-extension", version.ref = "betterandroid-ui-extension" }

Configure dependency in your project's build.gradle.kts.

implementation(libs.betterandroid.ui.extension)

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

Traditional Method

Configure dependency in your project's build.gradle.kts.

implementation("com.highcapable.betterandroid:ui-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.

Looking for SystemColors?

SystemColors related features have been removed from ui-extension.

If you are migrating old usages, you can refer to Config → Migration Guide → Migrate SystemColors.

Activity Extension

When we need to start another Activity, we need to use Intent to create an Intent(this, AnotherActivity::class.java), and then call startActivity(intent) to start it.

This may not be very friendly to write, so BetterAndroid provides an extension for Activity, now you can directly use the following method to start another Activity.

The following example

// Assume this is your context.
val context: Context
// Assume AnotherActivity is your target activity.
context.startActivity<AnotherActivity>()
// You can create an intent object using the following method.
context.startActivity<AnotherActivity> {
    // Add some extra parameters here.
    putExtra("key", "value")
}
// If you need to use Intent.FLAG_ACTIVITY_NEW_TASK to start another activity,
// you can use it directly like this.
context.startActivity<AnotherActivity>(newTask = true)
// If you need to add launch options, you can add the "options" parameter.
// For example, we need a shared element transition animation.
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity).toBundle()
context.startActivity<AnotherActivity>(options = options)
// Similarly, you can use it directly in a Fragment.
// Assume this is your Fragment.
val fragment: Fragment
// Assume AnotherActivity is your target Activity.
fragment.startActivity<AnotherActivity>()

If you need to start an Activity from an external app, you can use the following method.

The following example

// Assume this is your context.
val context: Context
// Assume that the app package you need to start is named com.example.app.
// Assume that the activity class you need to start is named com.example.app.MainActivity.
context.startActivity("com.example.app", "com.example.app.MainActivity")
// You can still create an intent object using the following method.
context.startActivity("com.example.app", "com.example.app.MainActivity") {
    // Add some extra parameters here.
    putExtra("key", "value")
}
// Intent.FLAG_ACTIVITY_NEW_TASK will be set by default in this method,
// this is to avoid stack overlap after starting a new app.
// If you don't need to consider this issue, you can set the newTask parameter to false.
context.startActivity("com.example.app", "com.example.app.MainActivity", newTask = false)

If you don't know the entry Activity name of the app you want to start, you can use the package name to start it directly.

This method will use getLaunchIntentForPackage internally to obtain the entry Activity.

The following example

// Assume this is your context.
val context: Context
// Assume that the app package you need to start is named com.example.app.
context.startActivity("com.example.app")

Notice

This operation requires the QUERY_ALL_PACKAGES permission or explicitly configuring a queries property list on Android 11 and later.

Please refer to Package visibility filtering on Androidopen in new window.

Tips

You can use startActivityOrElse instead of startActivity to determine whether the Activity can be started successfully.

If the startup fails, this method will not throw an exception but return false.

For the new multi-window mode in Android 7.0 and later versions, BetterAndroid provides a compatible extension for it.

For isInMultiWindowMode, you do not need to consider version compatibility issues, you only need to add a Compat at the end.

The following example

// Assume this is your activity.
val activity: Activity
// Get whether it is currently in multi-window mode.
val isInMultiWindowMode = activity.isInMultiWindowModeCompat

Context Extension

In some cases, you may only have a Context, but still need to turn it back into an Activity before continuing.

If you keep writing as? Activity manually, cases such as ContextThemeWrapper or multiple layers of ContextWrapper can quickly become awkward.

BetterAndroid provides a simpler way for this. It will keep unwrapping the current ContextWrapper chain for you until it finds the final Activity.

The following example

// Assume this is your Context.
val context: Context
// Get the Activity attached to this Context.
val activity = context.hostActivity
// Get the non-null Activity (throws exception if failed).
val activity = context.requireHostActivity()
// Get the Activity of the specified type.
val componentActivity = context.hostActivity<ComponentActivity>()
// Get the non-null Activity of the specified type (throws exception if failed).
val componentActivity = context.requireHostActivity<ComponentActivity>()

Fragment Extension

Fragment is an officially provided efficient fragment for Activity, but its usage is not very user-friendly.

In order to simplify Fragment related operations, BetterAndroid provides some practical extension functions for Fragment.

BetterAndroid will automatically help you introduce the androidx.fragment:fragment-ktx dependency, you can refer hereopen in new window to get started.

Notice

The commitTransaction method in 1.0.2 and previous versions has been deprecated.

In line with the principle of "not reinventing the wheel", please migrate to the commit and commitNow methods in the fragment-ktx dependency.

Starting from version 1.0.5, we have merged the ...ToActivity and ...ToFragment methods and improved their usage, please migrate accordingly.

Get the existing FragmentManager.

BetterAndroid provides a more friendly way for FragmentActivity and Fragment to obtain the existing FragmentManager.

The following example

// Assume this is your FragmentActivity.
val activity: FragmentActivity
// Assume this is your Fragment.
val fragment: Fragment
// Get FragmentManager from FragmentActivity.
val fragmentManager = activity.fragmentManager()
// Get FragmentManager from Fragment.
val fragmentManager = fragment.fragmentManager()
// For Fragment, you can set the parameter parent to true to get the parent FragmentManager.
val parentFragmentManager = fragment.fragmentManager(parent = true)

Using generics to get the parent Fragment.

BetterAndroid provides a more friendly way to get the parent Fragment.

It can automatically help you convert the found Fragment to the current type without using the as form for coercion.

You don't need to worry about not being found or type errors at all, in which case null will be returned.

The following example

// Assume this is your Fragment.
val fragment: Fragment
// Get the parent Fragment using generics.
val parentFragment = fragment.parentFragment<YourParentFragment>()

Use generics to find an existing Fragment.

Similarly, it can automatically convert the found Fragment to the current type without using the as form for coercion.

You don't need to worry about not being found or type errors at all, in which case null will be returned.

The following example

// Assume this is your FragmentActivity.
val activity: FragmentActivity
// Find a fragment by ID.
val fragment = activity.fragmentManager().findFragment<YourFragment>(R.id.container)
// Find a fragment by TAG.
val fragment = activity.fragmentManager().findFragment<YourFragment>("your_fragment_tag")

You can attach a Fragment to a host (FragmentActivity or Fragment) without using FragmentManager.beginTransaction...commit.

This operation is now much simpler.

The following example

// Assume this is your host.
val activity: FragmentActivity
// Assume this is your Fragment.
val fragment = YourFragment()
// Attach Fragment.
fragment.attach(activity)

Yes, that's all you need to do to complete the attachment.

By default, if the host is an Activity, it will attach the Fragment to the layout set by setContentView in the Activity.

If it is a Fragment, it will attach the Fragment to the layout of Fragment.getView.

If you need to attach it to a custom layout, you can use the following method.

The following example

// Assume this is your host.
val activity: FragmentActivity
// Assume this is your Fragment.
val fragment = YourFragment()
// Attach Fragment to a layout with ID R.id.container.
fragment.attach(activity, R.id.container)

If this custom layout is not obtained by ID but is a View, you can use the following method.

Notice

This View must already be added to the currently displayed layout, and it is recommended to set an ID for it.

A View without an ID will use View.generateViewId to generate an ID.

This is the default behavior of attach and replace. If you do not want BetterAndroid to generate an ID for a container view automatically, set generateViewId = false and make sure the container already has a valid ID.

The following example

// Assume this is your host.
val activity: FragmentActivity
// Assume this is your View.
val container: View
// Assume this is your Fragment.
val fragment = YourFragment()
// Attach Fragment to container.
fragment.attach(activity, container)
// Do not generate an ID automatically, the container must already have one.
fragment.attach(activity, container, generateViewId = false)

You can also easily set an entry animation for the Fragment when attaching.

The following example

// Assume this is your host.
val activity: FragmentActivity
// Assume this is your Fragment.
val fragment = YourFragment()
// Attach Fragment and set entry animation.
fragment.attach(
    host = activity,
    container = R.id.container,
    customAnimId = R.anim.slide_in_right // Entry animation.
)

You can also easily detach a Fragment from the host.

The following example

// Assume this is your host.
val activity: FragmentActivity
// Assume this is your Fragment.
val fragment = YourFragment()
// Detach Fragment from host.
fragment.detach(activity)
// If you do not provide a parameter, it will default to the current host held by the Fragment.
fragment.detach()

In addition to attaching, you can also replace a Fragment in the same attached layout.

The following example

// Assume this is your host.
val activity: FragmentActivity
// Assume this is your Fragment.
val fragment = YourFragment()
// Replace Fragment in a layout with ID R.id.container.
fragment.replace(activity, R.id.container)

Show or hide a Fragment.

The following example

// Assume this is your host.
val activity: FragmentActivity
// Assume this is your Fragment.
val fragment = YourFragment()
// Show/Hide Fragment from host.
fragment.show(activity)
fragment.hide(activity)
// If you do not provide a parameter, it will default to the current host held by the Fragment.
fragment.show()
fragment.hide()

Tips

Any attach, detach, replace, show, or hide operation can be set with transition animations.

You can find customAnimId, customEnterAnimId, and customExitAnimId parameters in these methods, which by default will not set any animation effects.

In all transaction events, these methods retain the body parameter, allowing you to continue executing your custom transactions.

BetterAndroid also provides FragmentTransaction, which you can use to create a template and apply it in your body.

The following example

// Assume this is your host.
val activity: FragmentActivity
// Assume this is your Fragment.
val fragment = YourFragment()
// Attach Fragment.
fragment.attach(activity) {
    // Add some extra transactions here.
    addSharedElement(view, "shared_element")
}
// Create a transaction template.
val myTransaction = FragmentTransaction {
    // Add some extra transactions here.
    addSharedElement(view, "shared_element")
}
// Attach Fragment.
fragment.attach(activity, body = myTransaction)

Notice

Starting from version 1.0.4, BetterAndroid has removed the default transition animation and related resource files.

We believe that transition animation should be something that each developer decides for themselves, not for tool libraries.

Insets Extension

Contents of This Section

WindowInsetsWrapperopen in new window

A wrapper for WindowInsets.

WindowInsetsWrapper.Absoluteopen in new window

An absolute insets for WindowInsetsWrapper.

InsetsWrapperopen in new window

A wrapper for Insets.

WindowInsetsLayoutopen in new window

An automatic layout container for handling window insets.

Insetsopen in new window

Extension methods for Insets, WindowInsets.

Notice

Among the library ui-component of 1.0.3 and previous versions, BetterAndroid encapsulates insest, window insets and System Bars (Status Bars, Navigation Bars, etc), this was once incorrect, and now insets and window insets have been decoupled into separate functions, as you can see now.

Insets and window insets are a very important concept in Android.

Although this API has existed as early as Android 5.0, it was only officially recommended in Android 10. (Since Android 9, the system has added related APIs for cutout displays processing)

Insets is a special space, which represents the placeholder area "attached" around the view, insets held by the system such as the part blocked by the cutout displays (notch screens), status bars, navigation bars, and input method are called window insets.

What BetterAndroid mainly does is wrapped this set of APIs to make them easier to use.

Next, you can create a WindowInsetsWrapper from an existing WindowInsets object.

Tips

WindowInsetsWrapper is designed with reference to the Window Insets APIopen in new window officially provided by Jetpack Compose.

You can better use this set at the native level API.

For backward compatibility reasons, the object wrapped by WindowInsetsWrapper is WindowInsetsCompat and it is recommended to use it instead of WindowInsets.

WindowInsetsWrapper wrapped WindowInsetsCompat.getInsets, WindowInsetsCompat.getInsetsIgnoringVisibility, WindowInsetsCompat.isVisible and other methods, you no longer need to write super long code such as WindowInsetsCompat.getInsets(WindowInsetsCompat.Type.systemBars()) to get an insets.

The following example

// Assume this is your WindowInsets.
val windowInsets: WindowInsetsCompat
// Create a WindowInsetsWrapper.
val insetsWrapper = windowInsets.createWrapper()
// You can also create it through the from method.
val insetsWrapper = WindowInsetsWrapper.from(windowInsets)
// Get the insets of the system bars.
val systemBars = insetsWrapper.systemBars
// Normally, the obtained insets will include its visibility,
// when invisible, the values of insets are all 0.
// You can ignore visibility by passing parameter ignoringVisibility.
val systemBars = insetsWrapper.systemBars(ignoreVisibility = true)
// After obtaining the insets, you can use isVisible to determine whether it is visible.
// Note: The value of insets is provided by the system, isVisible is just a state,
// regardless of whether its value is 0,
// you can use it to determine whether the current insets are visible.
val insetsIsVisible = systemBars.isVisible

BetterAndroid has made a compatibility process for the respective private solutions of manufacturers of mainstream brands of cutout display devices below Android 9.

If you need to be compatible with older devices, you can pass in an optional Window object in the method parameter.

If your app only needs to adapt to Android 9 and above devices, you can ignore this parameter.

The following example

// Assume this is your activity.
val activity: Activity
// Under normal circumstances, you can get the window through the current activity.
val window = activity.window
// Create a WindowInsetsWrapper.
val insetsWrapper = windowInsets.createWrapper(window)
// You can also create it through the from method.
val insetsWrapper = WindowInsetsWrapper.from(windowInsets, window)
// Get the insets of the cutout displays.
val displayCutout = insetsWrapper.displayCutout

Notice

If your app needs to run on Android 10 or below devices, we recommend always passing in a Window object to ensure that BetterAndroid can correctly handle compatibility issues for you.

Currently known compatibility issues are that the compatibility processing method provided by androidx cannot give correct values ​​to the isVisible and their contents of statusBars, navigationBars, systemBars of devices below Android 11, for this reason, BetterAndroid repairs were made.

For early devices below Android 9, some manufacturers need to add meta-data in AndroidManifest.xml to enable compatibility processing. To prevent configuration pollution, starting from version 1.1.1, BetterAndroid has removed these default configuration options. If needed, you can refer to the following configuration scheme to manually add them.

The following example

<application>
    <!-- Declared to expand the layout to the maximum width (full screen). -->
    <meta-data
        android:name="android.max_aspect"
        android:value="2.4" />
    <!-- Declared to be compatible with Huawei devices (EMUI) notch screen. -->
    <meta-data
        android:name="android.notch_support"
        android:value="true" />
    <!-- Declared to be compatible with Xiaomi devices (MIUI) notch screen. -->
    <meta-data
        android:name="notch.config"
        android:value="portrait|landscape" />
</application>

Any insets you get from WindowInsetsWrapper is InsetsWrapper, which wrapped Insets and implements controllable isVisible state.

InsetsWrapper can be easily converted to an original Insets object, and can also be converted back to an InsetsWrapper.

The following example

// Get the insets of the system bars.
val systemBars = insetsWrapper.systemBars
// Convert to Insets.
val insets = systemBars.toInsets()
// Convert to InsetsWrapper.
val wrapper = insets.toWrapper(systemBars.isVisible)
// You can also create it through the of method.
val wrapper = InsetsWrapper.of(insets, systemBars.isVisible)

Unlike Insets, InsetsWrapper has overloaded operators, and you can use +, - and or, and to operate or compare on it.

The following example

val insets1 = InsetsWrapper.of(10, 10, 10, 10)
val insets2 = InsetsWrapper.of(20, 20, 20, 20)
// Use "+" operator, equivalent to Insets.add(insets1, insets2).
val insets3 = insets1 + insets2
// Use "-" operator, equivalent to Insets.subtract(insets2, insets1).
val insets3 = insets2 - insets1
// Use "or" operator, equivalent to Insets.max(insets1, insets2).
val insets3 = insets1 or insets2
// Use "and" operator, equivalent to Insets.min(insets1, insets2).
val insets3 = insets1 and insets2
// Use the ">" operator to compare
val isUpperTo = insets1 > insets2
// Use the "<" operator to compare
val isLowerTo = insets1 < insets2

After obtaining the insets, the general approach is to set it to the padding of the View so that it "makes way" for the system to occupy the position.

Whether it is InsetsWrapper or Insets, you do not need to use a form such as View.setPadding(insets.left, insets.top, insets.right, insets.bottom), which seems extremely unfriendly.

You can easily set it directly as the padding of a View using the following method.

The following example

// Assume this is your current view.
val view: View
// Get the insets of the system bars.
val systemBars = insetsWrapper.systemBars
// Use insets to set the padding of the view.
view.setInsetsPadding(systemBars)
// Since the object demonstrated here is the system bars,
// you can only update the vertical (top and bottom) padding.
// Using the updateInsetsPadding method has the same effect as updatePadding.
view.updateInsetsPadding(systemBars, vertical = true)

Notice

Since 1.1.0, setInsetsPadding and updateInsetsPadding will store the current View's padding as a baseline after being called, preventing the previously set padding from being overwritten when setting the window insets padding.

If you manually modify the View's padding at runtime, you need to call syncInsetsPadding(...) to synchronize the new baseline.

The following example

view.updatePadding(left = 20)
view.syncInsetsPadding()

As we mentioned above, to create a WindowInsetsWrapper, you need an existing WindowInsetsCompat.

For backward compatibility reasons, you can use ViewCompat.setOnApplyWindowInsetsListener to set a change listener for View.

Its essential function is to control the transfer of window insets, window insets are transferred from the root view to the sub view through the View.onApplyWindowInsets method.

Delivery will not stop until you explicitly consume it using WindowInsetsCompat.CONSUMED.

The following example

// Assume this is your current view.
val view: View
// Set view's window insets change listener.
ViewCompat.setOnApplyWindowInsetsListener(view) { view, insets ->
    // insets is the current WindowInsetsCompat.
    // You can create WindowInsetsWrapper through it.
    val insetsWrapper = insets.createWrapper()
    // Consume the window insets at the last bit and stop passing them down.
    WindowInsetsCompat.CONSUMED // Or fill in the current insets and continue passing down.
}

This approach seems cumbersome, so BetterAndroid also provides you with a simpler method.

For example, we need to know the space occupied by the input method and set the padding from window insets for the input method layout.

At this point you can use View.handleOnWindowInsetsChanged to directly get a WindowInsetsWrapper.

The following example

// Assume this is your input method layout.
val imeSpaceLayout: FrameLayout
// Handle view's window insets change listener.
imeSpaceLayout.handleOnWindowInsetsChanged { imeSpaceLayout, insetsWrapper ->
    // Set the padding provided by ime.
    imeSpaceLayout.setInsetsPadding(insetsWrapper.ime)
    // Or use ime to update the padding at the bottom.
    imeSpaceLayout.updateInsetsPadding(insetsWrapper.ime, bottom = true)
}

If you want to consume window insets from the subview so that they are no longer passed down, you just need to set consumed = true in the method parameters.

The following example

// Handle view's window insets change listener.
imeSpaceLayout.handleOnWindowInsetsChanged(consumed = true) { imeSpaceLayout, insetsWrapper ->
    // The content is the same as above.
}

Tips

If you find that handleOnWindowInsetsChanged is not triggered immediately after being set, it may be because the current View is not attached to the window. If you want to View triggers a callback when executing onLayout, you can set requestApplyOnLayout = true in the method parameters.

If you want to animate window insets when they change as well, you don't need to reset a View.setWindowInsetsAnimationCallback.

You just need to set animated = true in the method parameters so that the callback will be triggered every time window insets change.

The following example

// Handle view's window insets change listener.
imeSpaceLayout.handleOnWindowInsetsChanged(animated = true) { imeSpaceLayout, insetsWrapper ->
    // The content is the same as above.
}

Notice

This feature was introduced starting with Android 11, in previous systems, callbacks were still triggered immediately, so no animation effects would be produced.

In addition, when you set up window insets change listeners, you don't need to care when the listeners were set, you can remove them at any time.

This operation will remove all View.setOnApplyWindowInsetsListener and View.setWindowInsetsAnimationCallback.

The following example

// Assume this is your current view.
val view: View
// Remove view's window insets change listener.
view.removeWindowInsetsListener()

Notice

You can only set one window insets listener for a View, repeatedly set listeners will be overwritten by the last one.

handleOnWindowInsetsChanged also calls removeWindowInsetsListener before setting its own listener, so it will replace the current listener and animation callback on the target View.

If you want to get window insets directly from the current View, then you can also create a WindowInsetsWrapper using the following method.

The following example

// Assume this is your current view.
val view: View
// Create a WindowInsetsWrapper.
val insetsWrapper = view.createRootWindowInsetsWrapper()
// You can also create it through the from method.
val insetsWrapper = WindowInsetsWrapper.from(view)
// Get the insets of the system bars.
// If window insets cannot be obtained through view, null will be returned.
val systemBars = insetsWrapper?.systemBars

In addition to the above approach, WindowInsetsWrapper also provides a WindowInsetsWrapper.Absolute, which you can directly pass without any listener and use Window.getDecorView to gets an absolute insets.

The following example

// Assume this is your activity.
Val activity: Activity
// Under normal circumstances, you can get the window through the current activity.
val window = activity.window
// Create a WindowInsetsWrapper.Absolute.
val absoluteWrapper = WindowInsetsWrapper.Absolute.from(window)
// Get the insets of the status bars.
val statusBar = absoluteWrapper.statusBar
// Get the insets of the navigation bars.
val navigationBar = absoluteWrapper.navigationBar
// Get the insets of the system bars.
val systemBars = absoluteWrapper.systemBars

Notice

The values obtained in this way are for reference only.

We do not recommend obtaining insets in this way, when the current device has a cutout display, these values may be inaccurate.

Below are all the insets provided in WindowInsetsWrapper.

InsetsDescription
statusBarsStatus bars.
navigationBarsNavigation bars.
captionBarCaption bar.
systemBarsSystem bars. (captionBar + statusBars + navigationBars)
imeInput method.
tappableElementTappable element.
systemGesturesSystem gestures.
mandatorySystemGesturesMandatory system gestures.
displayCutoutcutout display. (notch screen)
waterFallWaterfall screen. (curved screen)
safeGesturesSafe gestures. (systemGestures + mandatorySystemGestures + waterFall + tappableElement)
safeDrawingSafe drawing. (displayCutout + systemBars + ime)
safeDrawingIgnoringImeSafe drawing. (ignoring ime) (displayCutout + systemBars)
safeContentSafe content. (safeDrawing + safeGestures)

Below are all the insets provided in WindowInsetsWrapper.Absolute.

InsetsDescription
statusBarsStatus bars.
navigationBarsNavigation bars.
systemBarsSystem bars. (statusBars + navigationBars)

If your scenario only needs a layout to automatically adjust its padding according to window insets, and you do not want to manually handle handleOnWindowInsetsChanged every time, then you can also directly use WindowInsetsLayout.

It is essentially a FrameLayout that automatically listens for window insets changes after being attached to the window, and applies the target insets to its own padding according to the current configuration.

By default, it uses safeDrawingIgnoringIme as the insets type and applies it to all four directions.

The following example

<com.highcapable.betterandroid.ui.extension.insets.widget.WindowInsetsLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:windowInsetsType="safeDrawingIgnoringIme">

    <!-- Your views here. -->

</com.highcapable.betterandroid.ui.extension.insets.widget.WindowInsetsLayout>

If you only want it to handle specific directions, you can also control it with fitsTopInsets, fitsLeftInsets, fitsRightInsets, and fitsBottomInsets.

The following example

<com.highcapable.betterandroid.ui.extension.insets.widget.WindowInsetsLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:fitsLeftInsets="false"
    app:fitsRightInsets="false"
    app:fitsBottomInsets="false"
    app:windowInsetsType="statusBars">

    <!-- Your views here. -->

</com.highcapable.betterandroid.ui.extension.insets.widget.WindowInsetsLayout>

The supported windowInsetsType values are aligned with the insets types in WindowInsetsWrapper.

In addition, you can adjust its behavior through the following attributes:

Attribute NameDefault ValueDescription
consumedfalseWhether to consume window insets in the current layout. Once consumed, they will no longer continue to child views.
animatedfalseWhether to enable animation callbacks when window insets change.
fitsTopInsetstrueWhether to apply top insets.
fitsLeftInsetstrueWhether to apply left insets.
fitsRightInsetstrueWhether to apply right insets.
fitsBottomInsetstrueWhether to apply bottom insets.
windowInsetsTypesafeDrawingIgnoringImeThe insets type currently applied by this layout.

Tips

WindowInsetsLayout is more suitable for static layout scenarios.

If you still need to execute additional logic when insets change, such as synchronizing child view states, participating in animations, or dynamically combining multiple insets, it is still more recommended to use handleOnWindowInsetsChanged directly.

Lifecycle Extension

In the androidx usage, if you want to observe a lifecycle object, you usually need to manually create an anonymous observer object and then register it to Lifecycle.

BetterAndroid provides a more direct way for this. Now you can use Kotlin lambda to quickly create DefaultLifecycleObserver and LifecycleEventObserver, and you can also add them directly to Lifecycle.

The following example

// Assume this is your Lifecycle.
val lifecycle: Lifecycle
// Create a DefaultLifecycleObserver.
val observer = DefaultLifecycleObserver(
    onCreate = {
        // Your code here.
    },
    onDestroy = {
        // Your code here.
    }
)
// Register the observer.
lifecycle.addObserver(observer)
// You can also register directly without creating the object separately.
val observer = lifecycle.addObserver(
    onStart = {
        // Your code here.
    },
    onStop = {
        // Your code here.
    }
)

If you need to directly observe the full lifecycle event dispatch process, you can also use LifecycleEventObserver.

The following example

// Assume this is your Lifecycle.
val lifecycle: Lifecycle
// Create a LifecycleEventObserver.
val observer = LifecycleEventObserver { _, event ->
    // Your code here.
}
// Register the observer.
lifecycle.addObserver(observer)
// Or register directly.
val observer = lifecycle.addObserver { _, event ->
    // Your code here.
}

LifecycleOwner Extension

LifecycleOwner is an important component in Android Jetpack, providing lifecycle management for Activity, Fragment, etc.

BetterAndroid provides extensions for LifecycleOwner to obtain context, which you can use in commonly inherited instances of LifecycleOwner.

The following example

// Assume this is your LifecycleOwner.
val lcOwner: LifecycleOwner
// Get context.
val context = lcOwner.context
// Get Activity.
val activity = lcOwner.activity
// Get non-null context (throws exception if failed).
val context = lcOwner.requireContext()
// Get non-null Activity (throws exception if failed).
val activity = lcOwner.requireActivity()
// You can also convert the obtained Activity to a specified type.
val yourActivity = lcOwner.activity<YourActivity>()
// Or.
val yourActivity = lcOwner.requireActivity<YourActivity>()

androidx provides View.findViewTreeLifecycleOwner to get the LifecycleOwner from a View, but its usage is not very friendly.

BetterAndroid provides a more friendly way for View to get the LifecycleOwner it is in, which integrates View.findViewTreeLifecycleOwner and adds a fallback mechanism that can handle cases where it may fail during preloading, continuing to use View.getContext to get the context and obtain the LifecycleOwner from it.

The following example

// Assume this is your View.
val view: View
// Get the LifecycleOwner of the View.
val lcOwner = view.lifecycleOwner
// Get the non-null LifecycleOwner (throws exception if failed).
val lcOwner = view.requireLifecycleOwner()

As mentioned above, BetterAndroid also provides an extension method for Context to obtain the LifecycleOwner.

The following example

// Assume this is your Context.
val context: Context
// Get the LifecycleOwner of the Context.
val lcOwner = context.lifecycleOwner
// Get the non-null LifecycleOwner (throws exception if failed).
val lcOwner = context.requireLifecycleOwner()

BackPressed Extension

androidx already provides OnBackPressedDispatcher for handling system back pressed events, but in actual use, you still need to frequently deal with the callback object itself and the process of continuing the current back dispatch.

BetterAndroid provides a more direct set of extensions for this, allowing you to continue using the official capability with a more concise Kotlin call style.

BetterAndroid will automatically help you introduce the androidx.activity:activity dependency, you can refer hereopen in new window to get started.

The following example

// Assume this is your ComponentActivity.
val activity: ComponentActivity
// Create a back pressed callback.
val callback = OnBackPressedCallback {
    // Your code here.
}
// Register it with the official dispatcher.
activity.onBackPressedDispatcher.addCallback(activity, callback)
// You can still directly use the official capabilities.
callback.isEnabled = false
callback.remove()

For Fragment, you can directly obtain the current onBackPressedDispatcher, then bind it to viewLifecycleOwner or the Fragment itself as needed.

The following example

// Assume this is your Fragment.
val fragment: Fragment
// Bind to viewLifecycleOwner.
fragment.onBackPressedDispatcher.addCallback(fragment.viewLifecycleOwner, OnBackPressedCallback {
    // Your code here.
})
// Or bind to the Fragment itself.
fragment.onBackPressedDispatcher.addCallback(fragment, OnBackPressedCallback {
    // Your code here.
})

For View, you can directly obtain the dispatcher from its current LifecycleOwner and host ComponentActivity.

The following example

// Assume this is your View.
val view: View
// Bind to the current LifecycleOwner of this View.
view.onBackPressedDispatcher.addCallback(view.requireLifecycleOwner(), OnBackPressedCallback {
    // Your code here.
})

If you want to continue dispatching this back event in the current callback, you can directly call trigger(dispatcher).

The following example

activity.onBackPressedDispatcher.addCallback(activity, OnBackPressedCallback {
    // Ignore the current callback and continue dispatching the back event.
    trigger(activity.onBackPressedDispatcher)
    // Or remove the current callback after continuing the dispatch.
    trigger(activity.onBackPressedDispatcher, removed = true)
})

If you prefer to directly use the new DSL style provided by androidx, you can also write it like this.

The following example

activity.onBackPressedDispatcher.addCallback(activity) {
    // Your code here.
}

Coroutines Extension

Coroutines are an important feature in Kotlin, providing a more elegant solution for asynchronous programming.

Since coroutines themselves are a standard library in Kotlin and do not directly bind to the Android lifecycle, BetterAndroid provides some practical extension functions to bridge important UI interactions.

Notice

These extension functions only support Kotlin. If you are using Java, you will not be able to use these extensions.

Using coroutines in the Android lifecycle typically requires lifecycleScope, which can be cumbersome.

Now, you no longer need to use lifecycleScope.launch to start a coroutine. You can directly use it in any instance that inherits from LifecycleOwner.

The following example

// Assume this is your LifecycleOwner.
val lcOwner: LifecycleOwner
// Start a coroutine.
lcOwner.launch {
    // Your code here.
}
// Start an async coroutine.
val deferred = lcOwner.async {
    // Your code here.
}

Additionally, BetterAndroid provides more extensions for coroutines to switch between the main thread and non-main threads in Android.

The following example

// Assume this is your LifecycleOwner.
val lcOwner: LifecycleOwner
// Execute after a 1s delay.
lcOwner.runDelayed(1000) {
    // Your code here.
}
// Repeat 10 times, with a default delay of 1s each time.
lcOwner.repeatWithDelay(10) { index ->
    Log.d("Repeat", "Index: $index")
}

Dimension Extension

Contents of This Section

DisplayDensityopen in new window

Friendly interface to density.

Dimension → toPxopen in new window

Dimension → toDpopen in new window

Extension for Dimension.

There are size issues everywhere in the Android UI, for example, you need to set the padding of a View to 10dp, which can be easily set in the XML layout.

But in the code you need to use TypedValue.applyDimension to convert, such code does not look very friendly.

As a result, everyone began to encapsulate a method in the form of dp2px, but this approach was still not very elegant and had problems.

BetterAndroid provides a more elegant solution for this.

Normally, you only need to pass in an existing Context or Resources to complete the conversion.

The following example

// Assume this is your context.
val context: Context
// Assume this is your resources.
val resources: Resources
// Convert 10dp to px.
valpx = 10.toPx(context)
// Convert 10dp to px.
valpx = 10.toPx(resources)
// Convert 36px to dp.
val dp = 36.toDp(context)
// Convert 36px to dp.
val dp = 36.toDp(resources)

If you are already in an environment where a Context or Resources exists, you can write it more simply as follows.

At this time, you only need to implement a DisplayDensity interface for the current instance, and you do not need to override any methods.

The following example

class YourActivity : AppCompatActivity(), DisplayDensity {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Convert 10dp to px.
        valpx = 10.dp
        // Convert 10dp to px.
        valpx = 10.toPx()
        // Convert 36px to dp.
        valdp = 36.px
        // Convert 36px to dp.
        val dp = 36.toDp()
    }
}

Notice

The DisplayDensity interface only supports being implemented into the following instances or instances inherited from them:

Context, Window, View, Resources, Fragment, Dialog

An exception will be thrown if used in an unsupported instance, if you want, you can also override the methods in the DisplayDensity interface to support it manually.

The naming method of dp, px, toPx, toDp may conflict with the naming method in Jetpack Compose, so it is not recommended to use it in such a project.

Resources Extension

Contents of This Section

Resources → themeResIdopen in new window

Resources → isUiInNightModeopen in new window

Resources → toHexResourceIdopen in new window

Resources → getDrawableCompatopen in new window

Resources → getColorCompatopen in new window

Resources → getColorStateListCompatopen in new window

Resources → getFloatCompatopen in new window

Resources → getFontCompatopen in new window

Resources → getThemeAttrsBooleanopen in new window

Resources → getThemeAttrsFloatopen in new window

Resources → getThemeAttrsIntegeropen in new window

Resources → getThemeAttrsIntArrayopen in new window

Resources → getThemeAttrsStringArrayopen in new window

Resources → getThemeAttrsStringopen in new window

Resources → getThemeAttrsDimensionopen in new window

Resources → getThemeAttrsDrawableopen in new window

Resources → getThemeAttrsColorStateListopen in new window

Resources → getThemeAttrsColoropen in new window

Resources → areThemeAttrsIdsValueEqualsopen in new window

Resources → hasThemeAttrsIdopen in new window

Resources → getThemeAttrsIdopen in new window

Resources → getMenuFromResourceopen in new window

Resources → getStringArrayopen in new window

Resources → getIntArrayopen in new window

Resources → getColorOrNullopen in new window

Resources → getColorStateListOrNullopen in new window

Resources → getIntegerOrNullopen in new window

Resources → getIntOrNullopen in new window

Resources → getTextOrNullopen in new window

Resources → getStringOrNullopen in new window

Resources → getBooleanOrNullopen in new window

Resources → getFloatOrNullopen in new window

Resources → getDimensionOrNullopen in new window

Resources → getDimensionPixelSizeOrNullopen in new window

Resources → getDimensionPixelOffsetOrNullopen in new window

Resources → getLayoutDimensionOrNullopen in new window

Resources → getDrawableOrNullopen in new window

Resources → getFontOrNullopen in new window

Extensions for Resources.

Resources are a very important part of Android, they contain layouts, images, strings, etc, needed in the apps.

In order to use Resources more conveniently, BetterAndroid provides some practical extension functions for it.

Get the theme resource ID in ContextThemeWrapper.

Normally, the theme resource ID set using setTheme cannot be obtained directly. For this reason, BetterAndroid provides you with a way to obtain it through reflection.

Notice

This method may not work on all devices or be compatible with all Android versions, please test it yourself.

The following example

// Assume this is your ContextThemeWrapper.
val context: ContextThemeWrapper
// Get the set theme resource ID.
val themeResId = context.themeResId

For the attr in Resources, usually we need to create a TypedValue object, and then use Context.getTheme().resolveAttribute to fill it with the corresponding ID to obtain its own value.

For example, if we need to get the value of android.R.attr.windowBackground, we need to do this.

The following example

// Assume this is your context.
val context: Context
// Create a TypedValue object.
val typedValue = TypedValue()
// Use Context.getTheme().resolveAttribute to fill in the corresponding ID.
context.theme.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
// Get the resource ID.
val windowBackgroundId = typedValue.resourceId
// Get its own value.
val windowBackground = context.getDrawable(windowBackgroundId)

The whole process can be said to be very cumbersome, so BetterAndroid provides a simpler way for this.

Now, you just need to use the following method to get the value of itself.

The following example

// Assume this is your context.
val context: Context
// Get its own value.
val windowBackground = context.getThemeAttrsDrawable(android.R.attr.windowBackground)

If you only need to get the resource ID corresponding to attr, you can use the following method.

The following example

// Assume this is your context.
val context: Context
// Get the resource ID.
val windowBackgroundId = context.getThemeAttrsId(android.R.attr.windowBackground)

You can also only determine whether the resource ID corresponding to attr exists.

The following example

// Assume this is your context.
val context: Context
// Determine whether the resource ID corresponding to attr exists.
val hasWindowBackgroundId = context.hasThemeAttrsId(android.R.attr.windowBackground)

You can also compare two attr values to see if they are equals.

The following example

// Assume this is your context.
val context: Context
// Compare the values ​​of the two attr to see if they are equals.
val isEquals = context.areThemeAttrsIdsValueEquals(R.attr.first_attr, R.attr.second_attr)

Tips

The Context.getThemeAttrs* series of methods support common instances in Android that can be converted to actual resource objects, such as getThemeAttrsColor, getThemeAttrsInteger, getThemeAttrsString, etc.

Currently Context.getThemeAttrs* and Context.areThemeAttrsIdsValueEquals support the value contents of the following common resource IDs:

Color, ColorStateList, Drawable, Dimension, String, StringArray, IntArray, Float, Boolean

There is no way to directly obtain the Menu resource ID and parse it into a Menu object in Android.

For this reason, BetterAndroid provides a possible way to obtain it.

BetterAndroid encapsulates the method of using PopupMenu and converting the obtained content into a List<MenuItem> object through MenuInflater.

Now, you can easily use the following method to get the value of the Menu resource ID.

The following example

// Assume this is your context.
val context: Context
// Get the value of Menu resource ID.
// The obtained content is a List<MenuItem> object.
val menuItems = context.getMenuFromResource(R.menu.my_menu)

For compatibility processing of historical version systems, BetterAndroid encapsulates the methods provided by ResourcesCompat, now, you do not need to consider the issue of some methods being deprecated.

You only need to add Compat after each method to automatically make it compatible and call it like the original method, with exactly the same function.

Below is an example of compatibility handling for Drawable.

The following example

// Assume this is your context.
val context: Context
// Assume this is your resources.
val resources: Resources
// Get the drawable through context.
val drawable = context.getDrawableCompat(R.drawable.my_drawable)
// Get the drawable through resources.
val drawable = resources.getDrawableCompat(R.drawable.my_drawable, context.theme)
// You can also use generics to convert it to the corresponding type.
val drawable = context.getDrawableCompat<ColorDrawable>(R.drawable.my_background)

The following is a comparison table of the original method and the compatibility method.

Original MethodCompatibility Method
Context.getDrawableContext.getDrawableCompat
Context.getColorContext.getColorCompat
Context.getColorStateListContext.getColorStateListCompat
Context.getFloatContext.getFloatCompat
Resources.getDrawableResources.getDrawableCompat
Resources.getColorResources.getColorCompat
Resources.getColorStateListResources.getColorStateListCompat
Resources.getFloatResources.getFloatCompat
Resources.getFontContext.getFontCompat

Tips

BetterAndroid also provides getStringArray, getIntArray and getColorOrNull utility methods for TypedArray, which you can find them in Contents of This Section above.

The TypedArray.get...OrNull method differs from the original method in that it first checks if the attribute resource exists before returning the relevant result.

If it does not exist, it returns the default defValue, which is null by default.

For example, with a color attribute resource, sometimes we need to determine whether the user has set this attribute.

The original method can only set a non-null default value, but any color value is valid. In this case, the need to first check if the attribute resource exists arises.

The specific implementation is val myType = if (value.hasValue(index)) value.get...(..., ...) else ....

In practice, we need to encapsulate such an approach ourselves to call the corresponding attribute resource, which is cumbersome.

Therefore, BetterAndroid provides this encapsulation, allowing you to operate directly using the following method.

The following example

<declare-styleable name="MyView">
    <attr name="myType" format="color" />
</declare-styleable>
<com.example.MyView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:myType="#FF000000" />
context.withStyledAttributes(attrs, R.styleable.MyView) {
    // If "app:myType" is declared in XML, the value of myType is 0xFF000000, otherwise null.
    val myType = getColorOrNull(R.styleable.MyView_myType)
    // If "app:myType" is declared in XML, the value of myType is 0xFF000000, otherwise 0xFF232323.
    // The value of myType can be null in any case,
    // so it is not recommended to use !! to assert non-null.
    val myType = getColorOrNull(R.styleable.MyView_myType, 0xFF232323.toInt())
    // (Recommended) You can ensure that the value of myType is not null
    // by keeping defValue as null.
    val myType = getColorOrNull(R.styleable.MyView_myType) ?: 0xFF232323.toInt()
    // Or, you can handle the case where myType is null while keeping defValue as null.
    // If not null, the user has set the "app:myType" attribute.
    if (myType != null) {
        // Your code here.
    }
}

Function expansion of system night mode (dark mode).

The system's night mode is determined using Configuration.UI_MODE_NIGHT_MASK, in order to make this function more understandable, you do not need to use bit operations to determine.

Because usually we don't need to care about which hosting state the current system's night mode is in, we only need to know whether the current system appearance is dark.

So BetterAndroid provides an extension in Configuration that directly uses the Boolean type for judgment.

The following example

// Assume this is your context.
val context: Context
// Determine whether the current system is in dark mode.
val isDarkMode = context.resources.configuration.isUiInNightMode

Color Extension

Color exists in the form of Integer in Android.

Although there is a class named Color to encapsulate it, many methods are added in higher versions of the system and androidx is no specific compatible implementation for it, objects passed in code context are also usually passed directly using Integer.

BetterAndroid has no reason and no need to redesign a wrapper class to manage colors, so BetterAndroid only provides relevant extensions for the Integer type.

All color objects passed in the method will be marked with the @ColorInt annotation. please also comply with the specifications provided by androidx.

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()

Set the transparency of the current color.

The following example

// Assume we have the following colors.
val color = Color.WHITE
// You can use a floating point number from 0f-1f to set transparency.
val alphaColor = color.toAlphaColor(0.5f)
// You can also use an integer from 0-255 to set transparency.
val alphaColor = color.toAlphaColor(127)

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)

BetterAndroid also provides some extended functions for ColorStateList.

You can quickly convert existing colors to a ColorStateList with default color using the following method.

The following example

// Assume we have the following colors.
val color = Color.WHITE
// Convert it to a ColorStateList with default colors using the following method.
val colorStateList = color.toNormalColorStateList()
// You can also convert to a ColorStateList that returns null when the color is transparent.
val colorStateList = color.toNullableColorStateList()

You can also manually create a ColorStateList via AttrState.

The following example

val colorStateList = ColorStateList(
    AttrState.CHECKED to Color.WHITE,
    AttrState.PRESSED to Color.BLACK,
    AttrState.NORMAL to Color.TRANSPARENT
)

Bitmap Extension

In Android, bitmaps can be used in various places, and they are an important object used to display images.

BetterAndroid provides a series of extended functions for bitmaps, from loading to transforming, scaling, compressing and blurring them.

When loading bitmaps, you no longer need to use BitmapFactory, now there are the following methods to help you complete this operation more conveniently.

Load bitmap via a File or an InputStream.

The following example

// Assume your image is located at this path.
// Note that this path is only for demonstration, in actual situations, 
// you need to access files in the external storage.
// You need to apply for various different permissions and should use the
// Environment.getExternalStorageDirectory() method to get the path.
val imageFile = File("/storage/emulated/0/DCIM/Camera/IMG_20210901_000000.jpg")
// Create a file input stream through the file path.
val inputStream = FileInputStream("/storage/emulated/0/DCIM/Camera/IMG_20210901_000000.jpg")
// Load bitmap through file object.
val bitmap = imageFile.decodeToBitmap()
// You can configure it by passing the BitmapFactory.Options object in the method parameter.
val bitmap = imageFile.decodeToBitmap(BitmapFactory.Options().apply {
    // ...
})
// Load the bitmap through the input stream.
val bitmap = inputStream.decodeToBitmap()

Load bitmap via ByteArray.

If you have an image encoded via Base64, you can convert it to a byte array and load the bitmap.

The following example

// Assume this is your Base64 string.
val base64String = "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAA..."
// Convert Base64 string to byte array.
val byteArray = Base64.decode(base64String, Base64.DEFAULT)
// Load bitmap via byte array.
val bitmap = byteArray.decodeToBitmap()

Load bitmap via Resources.

This operation actually creates a new Bitmap object with a known Drawable resource ID.

The following example

// Assume this is your resources.
val resources: Resources
// Load bitmap via resource ID.
val bitmap = resources.createBitmap(R.drawable.my_image)

Tips

When you are not sure whether the bitmap can be loaded successfully, you can replace the loading method with decodeToBitmapOrNull and createBitmapOrNull, so that if the loading fails, it will return null instead of throwing an exception.

After a bitmap is loaded into memory or a bitmap object exists in memory, you can resave it to a file.

Kotlin's stdlib has already provided a File.writeText method for the File object, so BetterAndroid follows its example and provides a File.writeBitmap method.

The following example

// Assume this is your bitmap object.
val bitmap: Bitmap
// Assume this is the file you want to save to.
// Note that this path is only for demonstration, in actual situations,
// you need to access files in the external storage.
// You need to apply for various different permissions and should use the
// Environment.getExternalStorageDirectory() method to get the path.
val imageFile = File("/storage/emulated/0/DCIM/Camera/IMG_20210901_000000_modified.jpg")
// Save bitmap to file.
imageFile.writeBitmap(bitmap)
// You can configure it by adjusting the format and quality parameters.
// Default is JPEG format, quality is 100.
imageFile.writeBitmap(bitmap, format = Bitmap.CompressFormat.PNG, quality = 100)

If you can actually get an OutputStream object, you can write the bitmap to the output stream using the following.

The following example

// Assume this is your bitmap object.
val bitmap: Bitmap
// Assume this is your output stream object.
val outputStream: OutputStream
// Use the use method to automatically close the output stream.
outputStream.use {
    // Write the bitmap to the output stream.
    it.compressBitmap(bitmap)
    // You can configure it by adjusting the format and quality parameters.
    // Default is JPEG format, quality is 100.
    it.compressBitmap(bitmap, format = Bitmap.CompressFormat.PNG, quality = 100)
}

The following are extended functions for bitmap scaling, shrinking, and blur.

Scale bitmap.

The following example

// Assume this is your bitmap object.
val bitmap: Bitmap
// Scale by multiple, default is 2 times.
val scaledBitmap = bitmap.reduce(3)

Shrink bitmap.

The following example

// Assume this is your bitmap object.
val bitmap: Bitmap
// Shrink bitmap to 100 KB size.
val shrinkedBitmap = bitmap.shrink(maxSize = 100f)
// You can configure it by adjusting the format and quality parameters.
// Default is JPEG format, quality is 100.
val shrinkedBitmap = bitmap.shrink(maxSize = 100f, format = Bitmap.CompressFormat.PNG, quality = 100)

Rounded bitmap.

The following example

// Assume this is your bitmap object.
val bitmap: Bitmap
// Set the bitmap to a rounded corner with a radius of 10dp.
val roundBitmap = bitmap.round(10.toPx(context))
// You can also process the bitmap background color, default is pure white.
val roundBitmap = bitmap.round(10.toPx(context), backgroundColor = Color.BLACK)
// You can also set the fillet radius for each corner.
val roundBitmap = bitmap.round(
    topLeft = 12.toPx(context),
    topRight = 15.toPx(context),
    bottomLeft = 10.toPx(context),
    bottomRight = 9.toPx(context)
)

To blur bitmaps, you can also use BitmapBlurFactory to accomplish this.

The following example

// Assume this is your bitmap object.
val bitmap: Bitmap
// Blur the bitmap with a blur level of 25.
val blurBitmap = bitmap.blur(25)

Notice

The blur provided here is just a general algorithm, which can quickly achieve blur effects, but may have problems with speed and performance, and cannot be used for motion blur effects.

For the bitmap blur effect in Android, you can refer to and use other possible third-party libraries.

Currently, there is no universal and complete solution, this is a historical problem in Android.

There is no reason and no need for BetterAndroid to be special, encapsulates related functions for bitmap blur.

If your app targets Android 12 and above, we recommend using the officially provided RenderEffect for blur operations, and using the RenderScript replacement renderscript-intrinsics-replacement-toolkitopen in new window (Archived).

Drawable Extension

In some cases, we may need to use scenarios that directly manipulate Drawable objects.

BetterAndroid provides some extended functions that may be used for this purpose.

Sets padding for supported Drawable.

You can set padding on any type of Drawable using the following method, but it only works on the following types of Drawable:

RippleDrawable, LayerDrawable, ShapeDrawable, GradientDrawable

The following example

// Assume this is your drawable.
val drawable: Drawable
// Set padding for supported drawables.
drawable.setPadding(10.toPx(context))
// Or update the padding of the specified edge.
drawable.updatePadding(horizontal = 10.toPx(context))

Set padding for GradientDrawable and handle it for compatibility.

GradientDrawable that supports setting padding at the code level is only available in Android 10 and above.

You can set padding for GradientDrawable using the following compatible methods.

The following example

// Assume this is your GradientDrawable object.
val drawable: GradientDrawable
// Set padding.
drawable.setPaddingCompat(10.toPx(context), 10.toPx(context), 10.toPx(context), 10.toPx(context))
// Or use the setting method mentioned above to set the padding of all sides at once.
// This method will automatically call to setPaddingCompat for you.
drawable.setPadding(10.toPx(context))

But please note that in Android 10 and below, you need to use GradientDrawableCompat to create GradientDrawable for padding to take effect.

You can also inherit GradientDrawableCompat to implement your own GradientDrawable.

Toast Extension

Contents of This Section

Toast → toastopen in new window

Extensions for Toast.

Toast in Android has a wide range of usage scenarios, but there are various usage problems.

Earlier, ktx of androidx provided an extension method of Context.toast for Toast, but it was later removed due to notification permission issues.

Discussions about this issue can be found in Toast extensions on Contextopen in new window.

It is necessary to simplify the use of Toast in Kotlin, because sometimes the fastest way to display information is to show a Toast.

So BetterAndroid provides a toast extension method for this purpose, you can use it in the following instances or instances inherited from them:

Context, Window, Fragment, View, Dialog

You can show a Toast very simply using the following method.

The following example

// Assume this is your context.
val context: Context
// Show a toast
context.toast("Hello World!")
// You can pass in Toast.LENGTH_SHORT or Toast.LENGTH_LONG in the method parameters to set the duration.
// Default is Toast.LENGTH_SHORT.
context.toast("Hello World!", Toast.LENGTH_SHORT)

The above are all the ways to use it, BetterAndroid did not continue to customize it because the custom functions were also restricted by Android in the later period.

Please refer to Custom toasts from the background are blockedopen in new window.

Toast can only be used in the main thread, if you want to show a Toast in any thread, you only need to configure it simply like below.

The following example

// Assume this is your context.
val context: Context
// Create a new thread.
thread {
    // Delay 1 second.
    Thread.sleep(1000)
    // Show a toast and set allowBackground to true.
    context.toast("Hello World!", allowBackground = true)
}

In this way, you can show a Toast in any thread, it should be noted that this parameter is false by default and you need to set it manually.

When this parameter is enabled, BetterAndroid posts the actual Toast operation back to the main thread and does not create a background Looper.

You should still try to avoid requesting Toast from non-main threads unless necessary, because it may make message timing harder to predict.

Notice

As mentioned above, in Android 13 and above, you need to define and add runtime permissions for Toast just like notifications.

Some third-party ROMs like MIUI may allow Toast to show inside the app without permission.

If there is no need for floating windows, a good suggestion is to use some other similar behavior instead of Toast, such as using the Snackbar provided by the material component.

Please refer to Notification runtime permissionopen in new window.

Window Extension

BetterAndroid provides some possible extension functions for Window.

You can modify Window.attributes in the same way as View.updateLayoutParams in androidx.

The following example

// Assume this is your window.
val window: Window
// Modify parameters in Window.attributes.
window.updateLayoutParams {
    gravity = Gravity.CENTER
    // ...
}

BetterAndroid also encapsulates the method of setting the screen brightness separately for Window.

You can modify Window.attributes.screenBrightness more conveniently using the following methods.

The following example

// Assume this is your window.
val window: Window
// Set screen brightness (0-100) for the current window.
window.updateScreenBrightness(50)
// You can also use Float type parameters to set the screen brightness.
window.updateScreenBrightness(0.5f)

If you need to restore the default screen brightness provided by the system, you can use the following method.

The following example

// Assume this is your window.
val window: Window
// Clear the set screen brightness.
window.clearScreenBrightness()

View Extension

Contents of This Section

View → locationopen in new window

View → tagopen in new window

View → getTagopen in new window

View → parentopen in new window

View → parentOrNullopen in new window

View → childopen in new window

View → childOrNullopen in new window

View → firstChildopen in new window

View → lastChildopen in new window

View → firstChildOrNullopen in new window

View → lastChildOrNullopen in new window

View → removeSelfopen in new window

View → removeSelfInLayoutopen in new window

View → tooltipTextCompatopen in new window

View → animateopen in new window

View → showImeopen in new window

View → hideImeopen in new window

View → performKeyPressedopen in new window

View → performTouchopen in new window

View → setIntervalOnClickListeneropen in new window

View → paddingopen in new window

View → setPaddingopen in new window

View → updatePaddingopen in new window

View → updateMarginsopen in new window

View → updateMarginsRelativeopen in new window

View → setMarginsopen in new window

View → walkToRootopen in new window

View → walkThroughChildrenopen in new window

View → indexOfInParentopen in new window

View → outlineProvideropen in new window

View → ViewLayoutParamsopen in new window

View → LayoutParamsMatchParentopen in new window

View → LayoutParamsWrapContentopen in new window

Extensions for View.

ViewPaddingopen in new window

A wrapper for View padding.

AbsolutePaddingopen in new window

An absolute-direction padding values object.

RelativePaddingopen in new window

A relative-direction padding values object.

PaddingValuesopen in new window

A value interface for padding.

View is an important part of the user interface, when using Kotlin, androidx provides us with extended functions for View, but it is still not perfect enough.

BetterAndroid has been improved and enriched based on the related extension functions of androidx, here are some extension functions you can use.

Get the location of View on the screen.

You can get a Point object in the following way, which contains the location of the View on the screen.

The following example

// Assume this is your view.
val view: View
// Get the location of the View on the screen.
val location = view.location
// X coordinate.
val x = location.x
// Y coordinate.
val y = location.y

Get the tag of the current View.

In traditional writing, we need to use View.getTag to get the tag object, and then use as to convert it to the type we need.

This way of writing seems very cumbersome, so BetterAndroid provides a simpler way for this.

The following example

// Assume this is your View object.
val view: View
// Specify the type of the tag (if the type is known and determined).
// The return result can be null.
val tag = view.tag<String>()
// Specify the type and ID of the tag (if the type is known and determined).
// The return result can be null.
val tag = view.getTag<String>(R.id.my_tag)
// Specify the type and ID of the tag and set a default value (if the type is known and determined).
// The return result will not be null,
// if the type is incorrect or the tag does not exist, the default value will be returned.
val tag = view.getTag<String>(R.id.my_tag, "Hello World!")

Get the parent layout of the current View.

In traditional writing, we need to use View.parent to get the ViewParent object, and then use as to convert to ViewGroup to get the parent layout object.

This way of writing seems very cumbersome, so BetterAndroid provides a simpler way for this.

The following example

// Assume this is your View object.
val view: View
// Get the parent layout of the current View.
val parent: ViewGroup = view.parent()
// Specify the type of the parent layout (if the type is known and determined).
val parent = view.parent<LinearLayout>()
// When you are not sure whether the parent layout exists, you can also use the following method.
val parent = view.parentOrNull()

Get the child layout of the current ViewGroup.

In traditional writing, we need to use ViewGroup.getChildAt to get the View object, and then use as to convert to View to get the child layout object.

This way of writing also seems very troublesome, so BetterAndroid also provides a simpler way.

The following example

// Assume this is your ViewGroup object.
val viewGroup: ViewGroup
// Get the sublayout of the current ViewGroup.
val child: View = viewGroup.child(index = 0)
// Specify the type of sublayout (if the type is known and determined).
val child = viewGroup.child<Button>(index = 0)
// Get the first sublayout of the current ViewGroup.
val firstChild: View = viewGroup.firstChild
// Get the last sublayout of the current ViewGroup.
val lastChild: View = viewGroup.lastChild
// When you are not sure whether the sublayout exists, you can also use the following method.
val child = viewGroup.childOrNull(index = 0)
val firstChild = viewGroup.firstChildOrNull
val lastChild = viewGroup.lastChildOrNull

Removes itself from the parent layout (container).

Normally, when we remove View from the parent layout, we need to use View.getParent to obtain the parent layout object, then convert it to ViewGroup, and finally call the ViewGroup.removeView method.

Now, you can more easily remove itself from the parent layout using the following method, if the parent layout does not exist or the parent layout is not a ViewGroup (which usually does not exist), this method will have no effect and no need to worry about any negative effects.

The following example

// Assume this is your view.
val view: View
// Remove itself from parent layout.
view.removeSelf()
// Use the ViewGroup.removeViewInLayout method to remove itself.
view.removeSelfInLayout()

Set tooltip text (backward compatible).

In Android 8.0 (26) and above, you can use View.tooltipText to set the tooltip text. Although androidx provides ViewCompat.setTooltipText to support usage below version 26, it will be ineffective below version 26, and TooltipTextCompat only provides a compatible simulation scheme without providing a method to get the tooltip text.

To make the usage of this feature consistent across all versions, BetterAndroid provides a View.tooltipTextCompat method, which uses Toast to simulate tooltip text on lower versions of the system and allows you to retrieve the text you set.

The following example

// Assume this is your view.
val view: View
// Set tooltip text.
view.tooltipTextCompat = "Hello World!"
// Get tooltip text.
val tooltipText = view.tooltipTextCompat

Create animations.

BetterAndroid provides a lambda implementation of the animate method for View, which will automatically call the start method.

You can use it to create some simple animation effects.

The following example

// Assume this is your View.
val view: View
// Create animation.
view.animate {
    // Your code here.
}

Show or hide the input method (IME).

Normally, controlling the showing and hiding of the input method can be done automatically through the focus of the input box, but in some cases we want to be able to manually control the showing and hiding of the input method.

If the current View is in Activity, you can use the following methods to show or hide the input method.

The following example

// Assume this is your view.
val view: View
// Show input method.
view.showIme()
// Hide input method.
view.hideIme()

In Android 11 and above, the above method will use WindowInsetsController to control the showing and hiding of the input method.

When View is not in Activity or the current Android version is lower than Android 11, InputMethodManager will be used to control the showing and hiding of the input method.

Tips

If you need to use drag and drop to control the showing or hiding of input methods (window insets animation), currently BetterAndroid does not provide related extension functions.

You can refer to WindowInsetsAnimationControlleropen in new window to implement it yourself.

Notice

In Android 11 and above, it is recommended to set the android:windowSoftInputMode parameter of Activity to adjustResize to better control the showing and hiding of the input method.

If your View is not in Activity or the current Android version is lower than 11, the above solution may not be effective under certain conditions.

When Activity is first started, it is recommended to delay processing of show or hide input method events, otherwise it may be ineffective, and the interval between show and hide events should not be too short.

Simulate touch events.

The performClick method provided by View can only simulate click events, if you need to simulate touch events, you can use the View.performTouch method provided by BetterAndroid.

The parameters accepted by this method are shown in the table below.

Parameter NameParameter TypeParameter Description
downXFloatSimulates the down X coordinate of a touch event.
downYFloatSimulates the down Y coordinate of a touch event.
upXFloatSimulate the lift X coordinate of a touch event.
upYFloatSimulate the lift Y coordinate of a touch event.
durationLongThe duration of the simulated touch event. (from press to lift), in milliseconds

The following example

// Assume this is your view.
val view: View
// Simulate touch events from (15, 30) to (105, 130) for 500 milliseconds.
view.performTouch(15f, 30f, 105f, 130f, 500)

Simulate key events.

You can simulate physical keyboard or input method (IME) key events sent to the current View, for example in EditText you can simulate pressing the backspace key to delete text.

The following example

// Assume this is your view.
val view: EditText
// Simulate pressing the backspace key.
view.performKeyPressed(KeyEvent.KEYCODE_DEL)
// You can add the duration parameter at the end to set the duration (from pressing to lifting),
// the unit is milliseconds.
// Default is 150 milliseconds.
view.performKeyPressed(KeyEvent.KEYCODE_DEL, duration = 500)

Set interval click event.

The setOnClickListener method provided by View may cause misoperations if the click event is triggered multiple times in a short period.

To address this, BetterAndroid provides the setIntervalOnClickListener method.

Within the specified interval, repeated click events will be ignored.

The following example

// Assume this is your view.
val view: View
// Set interval click event.
view.setIntervalOnClickListener {
    // Your code here.
}
// You can pass the timeMillis parameter to set the interval time for the click event, in milliseconds.
// The default is 300 milliseconds.
view.setIntervalOnClickListener(1000) {
    // Your code here.
}

Update View’s padding and margin.

androidx provides a View.updatePadding method, and BetterAndroid provides a method that can update the horizontal and vertical directions, if you only need to update the padding in these two directions, you don't need to write two repeat the value.

The following example

// Assume this is your view.
val view: View
// Update the padding in the horizontal direction.
view.updatePadding(horizontal = 10.toPx(context))
// Update the padding in the vertical direction.
view.updatePadding(vertical = 10.toPx(context))

But this still does not solve another problem.

With the current extension capabilities in androidx, properties such as paddingLeft, paddingRight, paddingStart, and paddingEnd can be read directly, but they still cannot be written like normal mutable properties, so in the end you still have to go back to forms such as updatePadding or setPadding.

For this reason, BetterAndroid provides Jetpack Compose style PaddingValues, so that padding gets a more natural read-write experience in Kotlin.

The following example

// Assume this is your view.
val view: View
// Read the current padding directly.
val left = view.padding.left
val start = view.padding.start
// Modify padding in a specific direction directly.
view.padding.left = 10.toPx(context)
view.padding.top = 12.toPx(context)
view.padding.start = 16.toPx(context)
view.padding.end = 16.toPx(context)

When you want to set a group of padding values at once, you can directly use AbsolutePadding or RelativePadding.

The following example

// Set padding using absolute directions.
view.setPadding(AbsolutePadding(
    left = 16.toPx(context),
    top = 12.toPx(context),
    right = 16.toPx(context),
    bottom = 12.toPx(context)
))
// Set padding using relative directions.
view.setPadding(RelativePadding(
    start = 16.toPx(context),
    top = 12.toPx(context),
    end = 16.toPx(context),
    bottom = 12.toPx(context)
))

In addition to ordinary setting, this feature also supports more flexible incremental operations.

You can directly use += and -= on View.padding to add or subtract a group of padding values. This feels especially natural when handling dynamic offsets, compensation, or before/after animation deltas.

The following example

// Add a group of absolute-direction paddings.
view.padding += AbsolutePadding(
    left = 8.toPx(context),
    right = 8.toPx(context)
)
// Subtract a group of relative-direction paddings.
view.padding -= RelativePadding(
    start = 4.toPx(context),
    end = 4.toPx(context)
)

AbsolutePadding and RelativePadding themselves also support + and -, so you can first compose a final result and then apply it to the View in one place.

The following example

val base = AbsolutePadding(
    left = 16.toPx(context),
    top = 12.toPx(context),
    right = 16.toPx(context),
    bottom = 12.toPx(context)
)
val extra = AbsolutePadding(bottom = 24.toPx(context))
val result = base + extra

view.setPadding(result)

Since AbsolutePadding and RelativePadding are Kotlin data classes, you can also use the copy function to create a new object based on the original one without modifying it, and then apply it to the View.

The following example

val base = RelativePadding(
    start = 16.toPx(context),
    top = 12.toPx(context),
    end = 16.toPx(context),
    bottom = 12.toPx(context)
)
val result = base.copy(bottom = 24.toPx(context))

view.setPadding(result)

If you only want to treat it as an independent padding value object, that also works fine.

PaddingValues itself provides applyTo, so you can manually apply it to the target View at any time.

The following example

val values: PaddingValues = RelativePadding(
    start = 16.toPx(context),
    top = 12.toPx(context),
    end = 16.toPx(context),
    bottom = 12.toPx(context)
)

values.applyTo(view)

Notice

AbsolutePadding and RelativePadding cannot be mixed, as this will cause issues with native RTL support and dimension calculations. Please choose one approach to use from the beginning.

BetterAndroid also provides a View.updateMargins and View.setMargins method, which is used in the same way as View.updatePadding.

This method will only take effect when LayoutParams of View is MarginLayoutParams, and will have no effect in other cases.

The following example

// Assume this is your view.
val view: View
// Update the margin in the horizontal direction.
view.updateMargins(horizontal = 10.toPx(context))
// Update the margin in the vertical direction.
view.updateMargins(vertical = 10.toPx(context))
// Update left margin.
view.updateMargins(left = 10.toPx(context))
// Update start margin.
view.updateMarginsRelative(start = 10.toPx(context))
// Set the margin for all 4 sides.
view.setMargins(10.toPx(context))

Traverse the parent layout and all child layouts.

Normally, we need to use the View.parent method to recursively traverse the parent layout and the ViewGroup.children method to recursively traverse the child layout.

Although the official API already provides View.ancestors and ViewGroup.descendants for similar traversal scenarios, those names are not always the most intuitive choice in everyday app code.

BetterAndroid provides two more friendly names for this: walkToRoot and walkThroughChildren, and their design is inspired by Kotlin's File.walk extension.

The following example

// Assume this is your View object.
val view: View
// Assume this is your ViewGroup object.
val viewGroup: ViewGroup
// Get all parent layouts.
val parents = view.walkToRoot()
// Get all sublayouts.
val children = viewGroup.walkThroughChildren()

Notice

Since 1.1.0, the return types of walkToRoot and walkThroughChildren have changed from List to Sequence for performance reasons. This is a breaking change.

If your previous usage depended on eager evaluation, append toList() or adjust your business logic to work with lazy traversal.

Gets the index of View in the parent layout.

In traditional writing, we need to use ViewGroup.indexOfChild to get the index of View in the parent layout.

This way of writing doesn't seem very friendly, so BetterAndroid provides a simpler way for this.

The following example

// Assume this is your View object.
val view: View
// Get the index of the View in the parent layout.
// If the parent layout does not exist, -1 will be returned.
val index = view.indexOfInParent()

Sets the View's outlineProvider.

OutlineProvider of View is used to set the outer outline of View, its type is ViewOutlineProvider.

In Kotlin, we need to use view.outlineProvider = object : ViewOutlineProvider() to set it, which does not seem friendly.

So BetterAndroid provides a simpler way for this.

The following example

// Assume this is your View object.
val view: View
// Set View's outlineProvider.
view.outlineProvider { view, outline ->
    // For example, you can set the outer outline of a circle here.
    outline.setOval(0, 0, view.width, view.height)
    // Or you can set the outer outline of the rounded rectangle here.
    outline.setRoundRect(0, 0, view.width, view.height, 10f.toPx(context))
}
// Remember to set clipToOutline to true after setting outlineProvider.
view.clipToOutline = true

Manually create a LayoutParams object.

LayoutParams is the layout parameter of View, and its type depends on the parent layout of View, for example, the LayoutParams of LinearLayout is LinearLayout.LayoutParams.

Sometimes, you may need to manually create this object and set it into View, in this case, BetterAndroid provides the ViewLayoutParams method, now, you can save yourself the step of creating an object using the super long ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).

The following example

// Assume this is your view.
val view: View
// Create a LinearLayout.LayoutParams object, by default, width and height are both WRAP_CONTENT.
val layoutParams = ViewLayoutParams<LinearLayout.LayoutParams>()
// Set both width and height to MATCH_PARENT.
val layoutParams = ViewLayoutParams<LinearLayout.LayoutParams>(marchParent = true)
// Just set width to MATCH_PARENT.
val layoutParams = ViewLayoutParams<LinearLayout.LayoutParams>(widthMatchParent = true)
// Manually set width and height.
val layoutParams = ViewLayoutParams<LinearLayout.LayoutParams>(50.toPx(context), 30.toPx(context))
// Set to view.
view.layoutParams = layoutParams

LayoutInflater Extension

Normally we can use getLayoutInflater or LayoutInflater.from(context) in Activity to create a LayoutInflater object, and then use the inflate method to inflate the layout.

BetterAndroid simplifies this step for you, now, you can use layoutInflater in Context to get the LayoutInflater object, and then use the inflate method to load the layout.

The following example

// Assume this is your ViewGroup.
val view: ViewGroup
// Assume this is your context.
val context: Context
// You can inflate the layout in ViewGroup using the following method.
// If root is not null, attachToRoot defaults to true.
val myView = context.layoutInflater.inflate(R.layout.my_layout, root = view, attachToRoot = true)
// You can convert the inflated layout to the specified type without writing an "as".
val myView = context.layoutInflater.inflate<LinearLayout>(R.layout.my_layout, root = view)
// You can use the overloaded function to ensure the
// inflated layout type is correct, inflate failure or type error will return null.
val myView = context.layoutInflater.inflateOrNull<LinearLayout>(R.layout.my_layout, root = view)

TextView Extension

TextView is one of the most commonly used components in Android, BetterAndroid provides TextView with some extended functions that are more convenient to use in Kotlin.

Determine whether there is an ellipsis in TextView.

The following example

// Assume this is your TextView.
val textView: TextView
// Determine whether there is an ellipsis in TextView.
val isEllipsize = textView.isEllipsize

Get and set the underline effect of TextView.

The following example

// Assume this is your TextView.
val textView: TextView
// Get whether the TextView has an underline effect.
val isUnderline = textView.isUnderline
// Set the underline effect of TextView.
textView.isUnderline = true

Get and set the strike through effect of TextView.

The following example

// Assume this is your TextView.
val textView: TextView
// Get whether the TextView has a strike through effect.
val isStrikeThrough = textView.isStrikeThrough
// Set the strikethrough effect of TextView.
textView.isStrikeThrough = true

Notice

When setting the underline or strikethrough effect, you may need to manually enable anti-aliasing for TextView using paint.isAntiAlias = true to achieve better results, this is a historical issue with drawing performance in Android.

Get and set the text color of TextView.

Although you can use the TextView.setTextColor method to set the text color, it is not well recognized as the Getter and Setter methods in Kotlin because the corresponding TextView.getTextColors is a ColorStateList object.

So BetterAndroid has added an extension for this function, now, you can use the following methods to get and set the text color of TextView.

// Assume this is your TextView.
val textView: TextView
// Get the text color of TextView.
val textColor = textView.textColor
// Set the text color of TextView.
textView.textColor = Color.RED

Get the text of TextView and convert it to String.

Normally, the text obtained directly using TextView.getText is a CharSequence object.

If you need to convert it to String, you need getText().toString(), which seems cumbersome.

BetterAndroid provides a simpler way for this. Now, you can use the following method to get the text of TextView and convert it to String.

The following example

// Assume this is your TextView object.
val textView: TextView
// Get the text of TextView and convert it to String.
val text = textView.textToString()

Using TextView.getHint can also be done in a similar way.

The following example

// Assume this is your TextView object.
val textView: TextView
// Get the hint text of TextView and convert it to String.
val hint = textView.hintToString()

Update the text of EditText.

EditText inherits from TextView, directly using setText(...) to update the text will cause the cursor position to still be at the first position.

BetterAndroid provides a more convenient way for this, it will automatically set setSelection for you according to the length of the text to keep the cursor position at the end of the text.

The following example

// Assume this is your EditText.
val editText: EditText
// Update the text of EditText.
editText.updateText("Hello World!")

Clear the text of TextView.

Using setText("") or text = "" does not seem very friendly, so BetterAndroid provides a simpler way for this.

The following example

// Assume this is your TextView.
val textView: TextView
// Clear the text of TextView.
textView.clear()

Update TextView's Typeface.

Typeface is a font in Android, sometimes we only need to care about the thickness and italics of the font without setting a specific font.

BetterAndroid provides a simpler way for this, now, you can update the Typeface of TextView using the following method.

The following example

// Assume this is your TextView.
val textView: TextView
// Only set the TextView's font to bold.
textView.updateTypeface(Typeface.BOLD)

Updated CompoundDrawables of TextView.

CompoundDrawables are compound drawings in TextView that contain Drawable objects.

Using the traditional setCompoundDrawables method requires passing in four parameters and filling in null for unnecessary parameters, which seems very troublesome.

BetterAndroid provides a simpler way for this, now, you can directly update the CompoundDrawables of TextView using the following method.

The following example

// Assume this is your TextView.
val textView: TextView
// Update CompoundDrawables of TextView.
// We only need to fill in the parameters in the required direction,
// and the parameters in other directions will be automatically reset to the original CompoundDrawables.
textView.updateCompoundDrawables(
    left = R.drawable.ic_left,
    top = R.drawable.ic_top,
    right = R.drawable.ic_right,
    bottom = R.drawable.ic_bottom
)
// Use setCompoundDrawablesWithIntrinsicBounds to update CompoundDrawables.
textView.updateCompoundDrawablesWithIntrinsicBounds(
    left = R.drawable.ic_left,
    top = R.drawable.ic_top,
    right = R.drawable.ic_right,
    bottom = R.drawable.ic_bottom
)

Set the input limits (digits) of the TextView.

This attribute can only be conveniently set in XML using digits, but if you need to modify it dynamically, you will need the TextView.setKeyListener method.

For this purpose, BetterAndroid provides a setDigits method for TextView, you can use the following method to set the input limit of TextView.

Since the input function is completed by EditText, and EditText inherits from TextView, this method is generally only used in EditText.

The following example

// Assume this is your EditText.
val editText: EditText
// Set the input box to only input numbers.
editText.setDigits("0123456789")
// Set the input box to only input lowercase letters.
editText.setDigits("abcdefghijklmnopqrstuvwxyz")
// The second parameter is inputType, you can limit the display behavior of the input box here.
// For example, only numbers can be entered and displayed as passwords.
editText.setDigits("0123456789", InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD)
// The third parameter is locale,
// where you can set the country area of the input box. (only available in Android 8 and above)
// For example, set to mainland China.
editText.setDigits("0123456789", locale = Locale.CHINA)

RecyclerView Extension

Contents of This Section

RecyclerView → layoutManageropen in new window

Extensions for RecyclerView.

RecyclerView is one of the most commonly used list components in Android.

BetterAndroid provides some extensions to make it more convenient to use in Kotlin.

Get the LayoutManager of RecyclerView.

BetterAndroid provides a convenient way to get the LayoutManager.

Now, you no longer need to get the layoutManager of RecyclerView and then use as to convert it to the corresponding type.

You can directly use the following method to get the LayoutManager.

The following example

// Assume this is your RecyclerView.
val recyclerView: RecyclerView
// Get the LayoutManager of RecyclerView, if the type is incorrect or does not exist, it will return null.
val layoutManager = recyclerView.layoutManager<LinearLayoutManager>()

ViewBinding Extension

ViewBinding is a new feature in Android, which can help us operate components in the layout more conveniently.

But obviously the official method of using it is not open, you can get the interface ViewBinding of the generated class, and there is no implementation of inflate and other methods, you can only use a method like ActivityMainBinding.inflate(layoutInflater) to inflate the layout.

So BetterAndroid performed reflection processing on it to obtain the inflate method and extract the object type through generics.

These designs are partly inspired by the ViewBindingKTXopen in new window project, many thanks to the author of this project.

Now you can create a ViewBindingBuilder using the following and pass it to wherever needed for manipulation.

The following example

// Create a ViewBindingBuilder.
val builder = ViewBinding<ActivityMainBinding>()
// Inflate the layout when needed.
val binding = builder.inflate(layoutInflater)
// Assume this is your View.
val view: View
// You can also bind to an existing View.
val binding = builder.bind(view)

In this way we have implemented the passing of ViewBinding, and you can use it anywhere.

You can also use delegation to use ViewBinding in Activity and other places.

The following example

class YourActvity : Activity() {

    val binding: ActivityMainBinding by viewBinding()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
    }
}

Notice

When delegate is used from a Fragment, the cached binding follows the Fragment view lifecycle and is cleared in onDestroyView. You can only access it after onCreateView and before onDestroyView.

If you need to encapsulate ViewBinding into the parent class and bind it to the subclass using generics, you can use the following method.

First we need to create a parent class.

The following example

open class YourBaseActvity<VB : ViewBinding> : Activity() {

    lateinit var binding: VB

    override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       // Inflate the view binding.
       binding = ViewBindingBuilder.fromGeneric<VB>(this).inflate(layoutInflater)
       setContentView(binding.root)
    }
}

Then inherit this parent class as a global object into the subclass.

The following example

class YourActivity : YourBaseActivity<ActivityMainBinding>() {

    override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       binding.mainText.text = "Hello World!"
    }
}

Tips

You can also refer to ui-component → Activity to directly use the encapsulated AppBindingActivity or refer to ui-component → Fragment directly uses the encapsulated AppBindingFragment.

Pay Attention

If your application was compiled with obfuscation enabled, you need to refer to R8 & Proguard Obfuscate to correctly configure the obfuscation rules.