Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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
他のモジュールに関しても時間があれば色々試してみたいと思います。

Slowhand0309
Android, iOS, Rails, C/C++, Typescript
sikmi
しくみ製作所株式会社は、世の中の「しくみ」を素敵にするためのソフトウェア開発集団です。オフィスのない弊社は、メンバー全員リモートワークです!
https://sikmi.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away