Jetpack Compose 湯婆婆!!

湯婆婆ネタが流行っているなーと眺めていたら、Jetpack Compose 版がないじゃないかと思ったので、作ってみました。
（Jetpack Compose 湯婆婆!!って飛んでいきそうな勢いがあって好きです。作中でも時々飛んでいましたっけ？）

元ネタ：[Javaで湯婆婆を実装してみる](https://qiita.com/Nemesis/items/c7192a7c510788d2cba2)

---

## そもそも Jetpack Composeって？

> Jetpack Compose は、Android のネイティブ UI を構築するための最新のツールキットです。

細かい説明は[こちら](https://developer.android.com/jetpack/compose?hl=ja)をどうぞ

## 開発環境

- Jetpack Compose は `Android Studio ４．２ Preview版`で開発できます。
- まだアルファ版ですので、本格的な開発には向かないですが、今回のような**ネタ開発**にはちょうど良いお題:point_up:

## コード

処理は`MainActivity` と `YubabaViewModel`に分かれています。

``` MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            YubabaAppTheme {
                Surface(color = MaterialTheme.colors.background) {
                    YubabaTalk()
                }
            }
        }
    }
}

@Composable
fun YubabaTalk(yubabaViewModel: YubabaViewModel = viewModel()){

    val isSend: Boolean by yubabaViewModel.sendState.observeAsState(initial = false)
    val inputName: String by yubabaViewModel.inputName.observeAsState("")
    val yubabaNaming: String by yubabaViewModel.yubabaNaming.observeAsState(initial = "")

    Column(
        modifier = Modifier.padding(16.dp),
    ) {
        Text(
            text = "契約書だよ。そこに名前を書きな。",
            style = typography.h5,
            modifier = Modifier.padding(bottom = 8.dp)
        )

        val textState = remember { mutableStateOf(TextFieldValue()) }
        TextField(
            value = textState.value,
            onValueChange = {
                if (!isSend) {
                    textState.value = it
                }
            },
            backgroundColor = if (isSend) Color.Transparent else Color.DarkGray,
            modifier = Modifier.padding(bottom = 8.dp)
        )

        when {
            !isSend -> {
                Button(
                    onClick = {
                        yubabaViewModel.onSendStateChanged(true)
                        yubabaViewModel.onNameSend(textState.value.text)
                    },
                    modifier = Modifier.padding(bottom = 8.dp)
                ) {
                    Text("名前を教える")
                }
            }
            isSend -> {
                Text(
                    text = "フン。${inputName}というのかい。贅沢な名だねぇ。",
                    style = typography.h5,
                    modifier = Modifier.padding(bottom = 8.dp)
                )
                Text(
                    text = "今からお前の名前は${yubabaNaming}だ。いいかい、${yubabaNaming}だよ。分かったら返事をするんだ、${yubabaNaming}!!",
                    style = typography.h4
                )
            }
        }
    }
}
``` 

``` YubabaViewModel.kt
class YubabaViewModel: ViewModel() {

    private val _sendState = MutableLiveData(false)
    val sendState: LiveData<Boolean> = _sendState

    private val _inputName = MutableLiveData("")
    val inputName: LiveData<String> = _inputName

    private val _yubabaNaming = MutableLiveData("")
    val yubabaNaming: LiveData<String> = _yubabaNaming

    fun onSendStateChanged(sendState: Boolean) {
        _sendState.value = sendState
    }
    fun onNameSend(name: String) {
        _inputName.value = name
        val newNameIndex: Int = Random.nextInt(name.length)
        val editName = name.substring(newNameIndex, newNameIndex + 1)
        _yubabaNaming.value = editName
    }
}

```

### 解説

#### MainActivity

- Android ですので`Activity`のライフサイクルに合わせた実装になります。
- `setContent`ブロックの中で、表示するUIの構築を実施します。
    - UIの構築はコンポーズ可能な関数を用います。
- `@Composable`アノテーションをメソッドに付与する事で、その中のUIが構築される動きになります。
- UIの書き方は Android 特有の XMLではなく、`Text`、`TextField `、`Button`などComposeUIを呼び出して登録していきます。
- `Column`はそのまま列を構築するComposeUIです。（ListViewやGridView的なイメージのもの）
- ComposeUIの多くは状態を持たない特性があるので、今の状態なんかはViewModelやプロパティーで保持しています。
    - `textState` は常にテキストの入力値を持っています。
    - `isSend` は名前を教えたかの状態
    - `inputName` は入力した名前
    - `yubabaNaming` は湯婆婆に決めてもらった名前
- LiveDataの状態を ComposeUI へ反映させるため、`observeAsState(初期値)`を利用しています。下のライブラリを取り込んでおいてください。

``` build.gradle
dependencies {
    implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
}
```
#### YubabaViewModel
- Android の`androidx.lifecycle.ViewModel()`を元にしたViewModelで、保持している値はLiveDataを利用して伝えています。

## うごきはこんな感じ

![device-2020-11-12-011022.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495289/d2fbb8b2-5966-2167-b168-8d46576e2f6c.gif)

- 0文字入力時には、しっかり落ちます:sweat:

## 感想
- 実はちゃんと Jetpack Compose にさわってアプリっぽく作ったのは初めてな気がします。
- **状態を持たない**という特性を理解するのにちょっと苦労しました！
- まだまだ Jetpack Compose はα版ですので、いろいろと変わるかなーとおもっていますが、なかなかおもしろかったです:blush:


## 追記(2020/11/12)

- Jetpack Compose のViewModelからの購読処理には`MutableState`もCompose用に用意されていますので、`MutableState`版も差分箇所だけ載せておきます。

``` MainActivity.kt

@Composable
fun YubabaTalk(yubabaViewModel: YubabaViewModel = viewModel()){

    val isSend: Boolean = yubabaViewModel.sendState
    val inputName: String = yubabaViewModel.inputName
    val yubabaNaming: String = yubabaViewModel.yubabaNaming

    //・・・省略
}
``` 

``` YubabaViewModel.kt
class YubabaViewModel: ViewModel() {

    var sendState: Boolean by mutableStateOf(false)
        private set
    var inputName: String by mutableStateOf("")
        private set
    var yubabaNaming: String by mutableStateOf("")
        private set

    fun onSendStateChanged(sendState: Boolean) {
        this.sendState = sendState
    }
    fun onNameSend(name: String) {
        this.inputName = name
        val newNameIndex: Int = Random.nextInt(name.length)
        val editName = name.substring(newNameIndex, newNameIndex + 1)
        yubabaNaming = editName
    }
}

```
- 'LiveData'特有の遠回りなデータ更新で書かなくて済むのはいい感じですね！
- ただ、通常のViewレイアウト実装の場合には使えなくなるらしいので、注意も必要らしいです。
- CodeLabにあった、どっちだよ感が:sweat_smile:
![codelab_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495289/2c98fc60-a5cb-502f-d36d-2bbbd492a534.png)