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

## android-ktx

[公式ページ](https://developer.android.com/kotlin/ktx?hl=ja)

**Android-KTXとは...?**

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

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


※注) 現在[こちら](https://github.com/android/android-ktx)のリポジトリでは開発されてないみたいですね: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](https://qiita-image-store.s3.amazonaws.com/0/299271/5a128601-19e2-5dbd-5408-9cdf6a508e88.png)

今回は[コアパッケージ](https://developer.android.com/kotlin/ktx?hl=ja#core-packages)に絞って便利そうな拡張を試してみたいと思います。
`app/build.gradle` に以下を追加していきます。

```gradle
repositories {
    google() // 必要であれば
}

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

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

### :pencil: 実装
****

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

- **toast**

    1.0.1未満のバージョンで実装は以下のようになっていました
    (個人的には結構便利だと思うのですが何故無くなってしまったんだろう...)

    ```kotlin: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() }
    }
    ```

    使い方


    ```kotlin
       // 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**

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

-  **doOnLayout**

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

- **doOnPreDraw**

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

- **updatePadding** / **setPadding**

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

- **postDelayed**

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

- **drawToBitmap**

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

- **isVisible** / **isInvisible** / **isGone**

    ```kotlin
        // 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未満のバージョンで実装は以下のようになっていました

    ```kotlin: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
    }
    ```

    使い方


    ```kotlin
        // 初期値なし
        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**

    ```kotlin
        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**

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

- **forEach**

    ```kotlin
        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**

    ```kotlin
        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**

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

- **trimmedLength**

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

- **htmlEncode/parseAsHtml/toHtml**

    ```kotlin
        // 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)](https://img.esa.io/uploads/production/attachments/5529/2018/10/20/18452/9cd08475-f597-49f7-9525-c44f1191488b.png)

```kotlin
    // 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)](https://img.esa.io/uploads/production/attachments/5529/2018/10/20/18452/e1d9702b-a487-4c8a-9a9b-15a49015b436.png)

#### Bundle

- **bundleOf**

    ```kotlin
        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**

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

#### Trace

- **trace**

    ```kotlin
        // 起動して約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)](https://img.esa.io/uploads/production/attachments/5529/2018/11/08/18452/7284615d-8c38-4916-bce2-b4d10018d606.png)

### :tea: その他
****

公式の拡張には実装されていないのですが、[こちら](https://github.com/android/android-ktx/issues/587)の拡張がとても便利だったので試してみました。

#### Resource

- resolveAttribute / resolveAttributeResId

```kotlin
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:

使い方

```kotlin
theme.resolveAttribute(R.attr.colorAccent, false)
theme.resolveAttributeResId(R.attr.colorAccent)
```

## まとめ

コアパッケージだけでも十分便利な拡張が実装されている印象でした。
そして自分で拡張書く時の参考になりますw
他のモジュールに関しても時間があれば色々試してみたいと思います。