Edited at

android-ktxの便利そうな拡張を試す


android-ktx

公式ページ

Android-KTXとは...?


Android KTX は、Android Jetpack ファミリーの一部である Kotlin 拡張機能 セットです。Kotlin で使用するために、Jetpack と Android プラットフォーム API を 最適化します。Android KTX の目的は、Kotlin 言語の機能(拡張機能 / プロパティ、 ラムダ、名前付きパラメータ、パラメータのデフォルト値など)を 活用することで、Kotlin による Android 開発を、より簡潔で快適かつ言語特有のもの にすることです。Android KTX により、既存の Android API に新機能が追加される わけではありません。


Android開発で使うと幸せになれる拡張という感じでしょうか。

※注) 現在こちらのリポジトリでは開発されてないみたいですね:eyes:

ATTENTION: This repository is no longer being used for development, tracking issues, or accepting pull requests.

Please file any bug reports or feature suggestions for core-ktx and the other "KTX" libraries at
https://issuetracker.google.com/issues/new?component=396204

Please contribute to core-ktx and the other "KTX" libraries in AOSP by following the instructions in the README of
https://android.googlesource.com/platform/frameworks/support/

Thanks!


:computer:環境構築


Android-KTXはモジュール化されており、必要に応じたパッケージを依存関係に含めるようになっています。

image.png

今回はコアパッケージに絞って便利そうな拡張を試してみたいと思います。

app/build.gradle に以下を追加していきます。

repositories {

google() // 必要であれば
}

dependencies {
implementation 'androidx.core:core-ktx:1.0.1'
}

コアパッケージのみであれば↑だけでひとまずOKです。


:pencil: 実装



Toast :warning: 1.0.1からは実装自体が無くなっています :warning:



  • toast

    1.0.1未満のバージョンで実装は以下のようになっていました

    (個人的には結構便利だと思うのですが何故無くなってしまったんだろう...)


    Toast.kt

    inline fun Context.toast(text: CharSequence, duration: Int = Toast.LENGTH_SHORT): Toast {
    
    return Toast.makeText(this, text, duration).apply { show() }
    }

    inline fun Context.toast(@StringRes resId: Int, duration: Int = Toast.LENGTH_SHORT): Toast {
    return Toast.makeText(this, resId, duration).apply { show() }
    }


    使い方

       // Contextを拡張しているので、Activity内で以下の様に書ける
    
    toast("android-ktx toast sample")
    toast(R.string.app_name)
    toast("length long", Toast.LENGTH_LONG) // defaultはLENGTH_SHORT
    // Fragment内だと以下の様に書く
    activity?.toast("toast from fragment")



View



  • doOnNextLayout

        // Viewが配置されたら呼ばれる (addOnLayoutChangeListener)
    
    // (widthやheightが取得できる状態)
    button.doOnNextLayout { view ->
    Log.d(TAG, "width: ${view.width}, height: ${view.height}")
    }



  • doOnLayout

        // 既に配置済みであればview自信を返す
    
    // 配置されていない場合は内部でdoOnNextLayoutを呼ぶ
    button.doOnLayout { view ->
    Log.d(TAG, "width: ${view.width}, height: ${view.height}")
    }



  • doOnPreDraw

        // ビューツリーを描画しようとしているときに呼ばれる
    
    button.doOnPreDraw { view ->
    Log.d(TAG, "$view")
    }



  • updatePadding / setPadding

        // paddingの更新 (left, top, right, bottom)
    
    button.updatePadding(5, 15, 5, 15)
    // left, top, right, bottomに同じ値を設定
    button.setPadding(5)



  • postDelayed

        // 200 milisec後に何か処理する
    
    button.postDelayed(200) {
    Log.d(TAG, "200 delayInMillis")
    }



  • drawToBitmap

        // viewに描画されている内容をBitmapとして取得
    
    // 但しviewの配置がされる前に呼ばれるとIllegalStateExceptionを投げる
    button.doOnPreDraw {
    val bitmap = button.drawToBitmap()
    imageView.setImageBitmap(bitmap)
    }



  • isVisible / isInvisible / isGone

        // buttonが表示された状態
    
    Log.d(TAG, "isVisible: ${button.isVisible}, isInvisible: ${button.isInvisible}, isGone: ${button.isGone}")
    // => isVisible: true, isInvisible: false, isGone: false



ArrayMap :warning: こちらも1.0.1からは実装自体が無くなっています :warning:



  • arrayMapOf

    1.0.1未満のバージョンで実装は以下のようになっていました


    ArrayMap.kt

    @RequiresApi(19)
    
    inline fun <K, V> arrayMapOf(): ArrayMap<K, V> = ArrayMap()

    @RequiresApi(19)
    fun <K, V> arrayMapOf(vararg pairs: Pair<K, V>): ArrayMap<K, V> {
    val map = ArrayMap<K, V>(pairs.size)
    for (pair in pairs) {
    map[pair.first] = pair.second
    }
    return map
    }


    使い方

        // 初期値なし
    
    val arrayMap = arrayMapOf<String, String>()
    arrayMap["key1"] = "value1"
    Log.d(TAG, "key1: ${arrayMap["key1"]}") // key1: value1
    Log.d(TAG, "keyX: ${arrayMap["keyX"]}") // keyX: null

    // 初期値をkotlin.Pairで指定
    val arrayMap2 = arrayMapOf(Pair("key2", "value2"), Pair("key3", "value3"))
    Log.d(TAG, "key2: ${arrayMap2["key2"]}") // key2: value2
    Log.d(TAG, "key3: ${arrayMap2["key3"]}") // key3: value3




SparseArray



  • getOrDefault/getOrElse

        val sa = SparseArray<String>()
    
    sa.put(1, "apple")
    sa.put(2, "banana")
    sa.put(3, "orange")
    Log.d(TAG, "getOrDefault: ${sa.getOrDefault(4, "lemon")}") // getOrDefault: lemon
    Log.d(TAG, "getOrElse: ${sa.getOrElse(4, defaultValue = { "${sa.size}" })}") // getOrElse: 3



  • isEmpty/isNotEmpty

        val sa = SparseArray<String>()
    
    Log.d(TAG, "isEmpty: ${sa.isEmpty()} / isNotEmpty: ${sa.isNotEmpty()}") // isEmpty: true / isNotEmpty: false



  • forEach

        val sa = SparseArray<String>()
    
    sa.put(1, "apple")
    sa.put(2, "banana")
    sa.put(3, "orange")
    sa.forEach { key, value -> Log.d(TAG, "$key : $value") } // 1 : apple, 2 : banana, 3 : orange



  • keyIterator / valueIterator

        val sa = SparseArray<String>()
    
    sa.put(1, "apple")
    sa.put(2, "banana")
    sa.put(3, "orange")
    for (k in sa.keyIterator()) {
    Log.d(TAG, "key: $k") // key: 1, ...
    }
    for (v in sa.valueIterator()) {
    Log.d(TAG, "value: $v") // value: apple, ...
    }



CharSequence / String



  • isDigitsOnly

        Log.d(TAG, "12345 idDigitsOnly: ${"12345".isDigitsOnly()}") // true
    
    Log.d(TAG, "12s45 idDigitsOnly: ${"12s45".isDigitsOnly()}") // false



  • trimmedLength

        // トリムされた文字列の長さを返してくれる
    
    Log.d(TAG, "[ aa ] trimmedLength: ${" aa ".trimmedLength()}") // 2
    Log.d(TAG, "[ b b ] trimmedLength: ${" b b ".trimmedLength()}") // 4



  • htmlEncode/parseAsHtml/toHtml

        // htmlEncode
    
    Log.d(TAG, "<>&\"".htmlEncode()) // &lt;&gt;&amp;&quot;

    // parseAsHtml
    val parsedHtml = "<p>http://www.example.com</p>".parseAsHtml() // Spannedを返す
    Log.d(TAG, parsedHtml.toHtml()) // <p dir="ltr">http://www.example.com</p>




SpannableString/SpannableStringBuilder/SpannedString

装飾前

image.png (16.1 kB)

    // index:0~4の間の文字を太文字にする

val s1 = "abcdefg".toSpannable()
s1[0, 5] = StyleSpan(Typeface.BOLD)
spannableText1.text = s1

// index:0~3の間の文字をテキストサイズ100spにする
val s2 = "hijklmn".toSpannable()
s2[0..4] = AbsoluteSizeSpan(100)
spannableText2.text = s2

// index:0~5の間の文字をリンク付き文字にする
val s3 = "opqrstuvwxyz".toSpannable()
s3[0..6] = object : ClickableSpan() {
override fun onClick(view: View) {
Log.d(TAG, "ClickableSpan#onClick")
}
}
spannableText3.movementMethod = LinkMovementMethod.getInstance()
spannableText3.text = s3

装飾後

image.png (17.8 kB)


Bundle



  • bundleOf

        data class User(val name: String) : Serializable
    
    // key/valueのペアから新しいBundleを作成してくれる
    val bundle = bundleOf(Pair("a", null), Pair("b", 1), Pair("c", true),
    Pair("d", intArrayOf(4, 5)), Pair("e", User("taro")))
    Log.d(TAG, "bundle a => ${bundle.get("a")}") // bundle a => null
    Log.d(TAG, "bundle b => ${bundle.get("b")}") // bundle b => 1
    Log.d(TAG, "bundle c => ${bundle.get("c")}") // bundle c => true
    Log.d(TAG, "bundle d => ${(bundle.get("d") as? IntArray)?.size}") // bundle d => 2
    Log.d(TAG, "bundle e => ${bundle.get("e")}") // bundle e => User(name=taro)



Handler



  • postDelayed / postAtTime

        // 元々あるpostDelayedとpostAtTimeメソッドのパラメータ位置を変更し書きやすくした拡張
    
    val handler = Handler()
    handler.postDelayed(200) {
    Log.d(TAG, "Handler#postDelayed")
    }
    handler.postAtTime(200) {
    Log.d(TAG, "Handler#postAtTime")
    }



Trace



  • trace

        // 起動して約10秒後にセクション名「section-X」として記録
    
    handler.postDelayed(10000) {
    trace("section-X") {
    Thread.sleep(1000)
    }
    }

    記録されたデータはAndroidStudioの「Profiler」>「CPU」で確認できる

    https://developer.android.com/studio/profile/cpu-profiler

    image.png (152.8 kB)




:tea: その他


公式の拡張には実装されていないのですが、こちらの拡張がとても便利だったので試してみました。


Resource


  • resolveAttribute / resolveAttributeResId

inline fun Resources.Theme.resolveAttribute(

resId: Int,
resolveRefs: Boolean = true
): TypedValue = TypedValue().apply { resolveAttribute(resId, this, resolveRefs) }

// こっちは↑の実装をさらに拡張したもの
inline fun Resources.Theme.resolveAttributeResId(
resId: Int,
resolveRefs: Boolean = true
): Int = this.resolveAttribute(resId, resolveRefs).resourceId

Themeのattributeのidを取得したい時とかに便利:sparkles:

使い方

theme.resolveAttribute(R.attr.colorAccent, false)

theme.resolveAttributeResId(R.attr.colorAccent)


まとめ

コアパッケージだけでも十分便利な拡張が実装されている印象でした。

そして自分で拡張書く時の参考になりますw

他のモジュールに関しても時間があれば色々試してみたいと思います。