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.

Add dependency in your project's SweetDependency configuration file.

libraries:
  com.highcapable.betterandroid:
    ui-extension:
      version: +

Configure dependency in your project build.gradle.kts.

implementation(com.highcapable.betterandroid.ui.extension)

Traditional Method

Configure dependency in your project 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.

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

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.

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)

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.

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

You can also get the LifecycleOwner from a View.

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

Pay Attention

This is essentially an experimental feature, and the LifecycleOwner obtained from View is uncertain.

It essentially determines and prioritizes the first Fragment bound in the Activity by obtaining the context in the View. If no Fragment is bound, it will select the Activity.

We recommend prioritizing the method of passing the current LifecycleOwner to obtain it for custom View, or passing it after the View is loaded.

The following example

// You can pass the LifecycleOwner in the constructor.
class YourView(
    context: Context,
    attrs: AttributeSet?,
    lcOwner: LifecycleOwner
) : View(context, attrs) {

    // Or set the LifecycleOwner after loading.
    var lifecycleOwner: LifecycleOwner = lcOwner

    init {
        // Your code here.
    }
}
// Set in your LifecycleOwner.
// Assume this is your View.
val yourView: YourView
// Assume this is the current LifecycleOwner.
yourView.lifecycleOwner = this

Coroutines Extensions

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

Resources → obtainStyledAttributesopen 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

For the property-related functions of custom View, BetterAndroid provides a TypedArray extension that can be automatically recycled.

You can directly use the obtainStyledAttributes method to create a TypedArray object without having to think about when recycle should be called.

The following example

class MyView(context: Context, attrs: AttributeSet?) : View(context, attrs) {

    init {
        obtainStyledAttributes(attrs, R.styleable.MyView) {
            val myType = it.getInteger(R.styleable.MyView_myType, 0)
        }
    }
}

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" />
obtainStyledAttributes(attrs, R.styleable.MyView) {
    // If "app:myType" is declared in XML, the value of myType is 0xFF000000, otherwise null.
    val myType = it.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 = it.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 = it.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

System Colors

Contents of This Section

SystemColorsopen in new window

Extension for system colors (dynamic colors).

In Android 12, the official provides us with a new feature, namely dynamic colors.

Different from the traditional wallpaper color selection, the dynamic colors in Material 3 are calculated based on the hierarchical tones of the wallpaper.

BetterAndroid encapsulates the theme colors provided by the system into SystemColors, and you can dynamically obtain the system's theme colors at the code level.

Here is an example of creating and using SystemColors.

The following example

// Assume this is your context.
val context: Context
// Create SystemColors object.
val systemColors = SystemColors.from(context)
// Get the color of android.R.color.system_accent1_100 provided by the system.
val accentColor = systemColors.systemAccentPrimary(100)
// Get the color of com.google.android.material.R.color.material_dynamic_primary50 provided by Material.
val primaryColor = systemColors.materialDynamicPrimary(50)

Not every device running Android 12 supports dynamic colors, you can use the following methods to determine.

The following example

// You can directly determine whether the current system supports dynamic colors.
val isAvailable = SystemColors.isAvailable

Retrieving colors will return the system-provided default color when not supported, but if the target device is older than Android 12, any color retrieved from SystemColors will be Color.TRANSPARENT.

The following is a complete list of the color methods supported by SystemColors and their available parameters.

Method NameAvailable Parameters
systemAccentPrimary0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000
systemAccentSecondary0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000
systemAccentTertiary0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000
systemNeutralPrimary0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000
systemNeutralSecondary0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000
materialDynamicPrimary0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100
materialDynamicSecondary0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100
materialDynamicTertiary0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100
materialDynamicNeutral0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100
materialDynamicNeutralVariant0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100

Pay Attention

You can only pass in the values among the available parameters listed above in the given method.

Other values are not supported, otherwise an exception will be thrown.

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 encrypted 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 PNG format, quality is 100.
imageFile.writeBitmap(bitmap, format = Bitmap.CompressFormat.JPEG, quality = 80)

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 PNG format, quality is 100.
    it.compressBitmap(bitmap, format = Bitmap.CompressFormat.JPEG, quality = 80)
}

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

Scale bitmap.

The following example

// Assume this is your bitmap object.
val bitmap: Bitmap
// Scale the bitmap to 100x100 size.
val zoomBitmap = bitmap.zoom(100, 100)
// Scale by multiple, default is 2 times.
val zoomBitmap = bitmap.reduce(3)

Compressed bitmap.

The following example

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

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 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 (but has not been maintained for more than two years).

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)
}.start() // Start it.

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.

You should try to avoid showing Toast in non-main threads, because this may lead to some possible "black box problems" and unknown hidden dangers.

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

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 on versions below 26, which does not essentially solve the problem.

Therefore, BetterAndroid provides a View.tooltipTextCompat method, which will use Toast to simulate the tooltip text on lower sdk systems and can 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))

BetterAndroid also provides a View.updateMargin 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.updateMargin(horizontal = 10.toPx(context))
// Update the margin in the vertical direction.
view.updateMargin(vertical = 10.toPx(context))
// Update left margin.
view.updateMargin(left = 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.

BetterAndroid provides a simpler way for this, its design is inspired by the walk extension method in File provided by Kotlin.

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

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.
val myView = context.layoutInflater.inflate(R.layout.my_layout, root = view)
// You can set the attachToRoot parameter to decide whether to add it to the ViewGroup at the same time.
val myView = context.layoutInflater.inflate(R.layout.my_layout, root = view, attachToRoot = true)

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

Adapter Extensions

The adapter extensions introduced in this section are mostly extensions of RecyclerView.Adapter.

Usually, we need to use methods like notifyItemInserted, notifyItemChanged, etc. to notify the adapter that the data has changed.

When we add data to the dataset all at once, we usually need to use notifyItemRangeInserted to notify the adapter that the data has changed.

The following example

// Assume this is your RecyclerView.Adapter.
val adapter: RecyclerView.Adapter<*>
// Assume this is your dataset, initially empty.
val dataSet: MutableList<MyBean>
// Add some data to the dataset.
dataSet.addAll(...)
// Notify the adapter that the data has changed.
adapter.notifyItemRangeInserted(0, dataSet.size)

When the data is confirmed to be added from 0, BetterAndroid provides you with a simpler way to complete this. Now you can use the following method to notify the adapter that the data has changed.

The following example

// Assume this is your RecyclerView.Adapter.
val adapter: RecyclerView.Adapter<*>
// Assume this is your dataset, initially empty.
val dataSet: MutableList<MyBean>
// Add some data to the dataset.
dataSet.addAll(...)
// Notify the adapter that the data has changed.
adapter.notifyAllItemsInserted()

The above method will use adapter.itemCount by default to get the size of the dataset, no need to manually specify the range to be updated.

At this time, please ensure that your adapter returns the correct itemCount, otherwise, please manually pass in dataSet.

The following example

// Notify the adapter that the data has changed.
adapter.notifyAllItemsInserted(dataSet)

Similarly, when the data is confirmed to have all changed (for example, in a multi-select state list, updating the selected and unselected checkbox states), you can use the following method to notify the adapter that the data has changed.

The following example

// Assume this is your RecyclerView.Adapter.
val adapter: RecyclerView.Adapter<*>
// Assume this is your dataset.
val dataSet: MutableList<MyBean>
// Simulate operating the dataset (e.g., select all action).
dataSet.forEach { it.isSelected = true }
// Notify the adapter that the data has changed.
adapter.notifyAllItemsChanged()

Similarly, please ensure that your adapter returns the correct itemCount, otherwise, please manually pass in dataSet.

The following example

// Notify the adapter that the data has changed.
adapter.notifyAllItemsChanged(dataSet)

When we need to clear the dataset and notify the adapter that the data has changed, we usually need to use notifyItemRangeRemoved to notify the adapter that the data has changed.

The following example

// Assume this is your RecyclerView.Adapter.
val adapter: RecyclerView.Adapter<*>
// Assume this is your dataset.
val dataSet: MutableList<MyBean>
// Save the current data size.
val count = dataSet.size
// Clear the dataset.
dataSet.clear()
// Notify the adapter that the data has changed.
adapter.notifyItemRangeRemoved(0, count)

This process is still cumbersome, BetterAndroid provides a simpler way for this. Now you can use the following method to clear the dataset and notify the adapter that the data has changed. This method will automatically calculate the size of the dataset.

The following example

// Assume this is your RecyclerView.Adapter.
val adapter: RecyclerView.Adapter<*>
// Assume this is your dataset.
val dataSet: MutableList<MyBean>
// Clear the dataset and notify the adapter that the data has changed.
adapter.clearAndNotify(dataSet)

Tips

There are also some other extensions that can be used. notifyDataSetChangedIgnore will ignore the Lint warnings given during coding and directly provide you with the use of notifyDataSetChanged.

However, this method is still not recommended because it will cause the entire list to refresh, which will cause performance issues in large datasets.

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

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.