【Android】TextFieldのフォーカスをダイアログの外側タップでも解除したい！！


## はじめに
Androoid のアプリ開発中に、Jetpack Compose で ダイアログに配置した `TextField` のフォーカスを、ダイアログの外側をタップしてフォーカスが外れるように実装しようとしました。
その時に少し困ったので備忘録的に書き残します。

---

## 基本実装（サンプル）
ダイアログの内外をタップでフォーカスが外れるようにしたかったので、まずはダイアログ内をタップするとフォーカスが外れるように実装してみます。

### **実装コード**
```kotlin
@Composable
fun KeyboardDialog(modifier: Modifier, onClick: () -> Unit = {}, onDismissRequest: () -> Unit = {}) {
    Dialog(
        properties = DialogProperties(),
        onDismissRequest = onDismissRequest
    ) {
        var value by remember { mutableStateOf("") }
        val focusManager = LocalFocusManager.current
        Column(
            modifier = modifier
                .width(290.dp)
                .background(color = Color.White, shape = RoundedCornerShape(20.dp))
                .padding(10.dp)
                 .pointerInput(Unit) {
                    detectTapGestures(onTap = { focusManager.clearFocus() }) // タップでフォーカスを外す
                },
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Text(
                text = "以下の項目を入力してください",
                fontSize = 16.fontDp,
                fontWeight = FontWeight.SemiBold
            )
            Text("・好きな食べ物", modifier = Modifier.padding(top = 10.dp), fontSize = 16.fontDp)
            TextField(
                value = value,
                onValueChange = { value = it },
                label = { Text("テキストを入力してください") },
                modifier = Modifier.padding(vertical = 4.dp)
            )
            Text(
                text = "入力完了",
                modifier = Modifier
                    .padding(top = 100.dp)
                    .size(height = 40.dp, width = 180.dp)
                    .background(Color.Red, shape = RoundedCornerShape(10.dp))
                    .padding(vertical = 6.dp)
                    .clickable{ onClick() },
                color = Color.White,
                fontSize = 16.fontDp,
                fontWeight = FontWeight.SemiBold,
                textAlign = TextAlign.Center,
            )
        }

    }
}
```

フォーカスを外すようにする処理はいたって簡単。
`val focusManager = LocalFocusManager.current` で FocusManager を取得して、`focusManager.clearFocus()`を利用するだけです。
これでダイアログの内部をタップしてフォーカスが外れるようになりました。

## ダイアログの外側をタップしてフォーカスを外れるようにしたかった......（失敗）

基本実装をもとに、ダイアログの外側をタップしてもフォーカスが外れるようにしていきます。
とりあえず`Dialog`の外側に`Box`を配置して、`fillMaxSize`して`focusManager.clearFocus()`を渡しておけばいいかな？と思っていました。

### **実装コード**
```kotlin
@Composable
fun KeyboardDialog(
    modifier: Modifier = Modifier,
    onClick: () -> Unit = {},
    onDismissRequest: () -> Unit = {},
) {
    val focusManager = LocalFocusManager.current
    // 外側のタップを検知する Box
    Box(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                detectTapGestures(onTap = { focusManager.clearFocus() }) // タップでフォーカスを外す
            },
        contentAlignment = Alignment.Center
    ) {
        Dialog(
            properties = DialogProperties(),
            onDismissRequest = onDismissRequest
        ) {
            var value by remember { mutableStateOf("") }
            Column(
                modifier = modifier
                    .width(290.dp)
                    .background(color = Color.White, shape = RoundedCornerShape(20.dp))
                    .padding(10.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                Text(
                    text = "以下の項目を入力してください",
                    fontSize = 16.fontDp,
                    fontWeight = FontWeight.SemiBold
                )
                Text(
                    "・好きな食べ物",
                    modifier = Modifier.padding(top = 10.dp),
                    fontSize = 16.fontDp
                )
                TextField(
                    value = value,
                    onValueChange = { value = it },
                    label = { Text("テキストを入力してください") },
                    modifier = Modifier
                        .padding(vertical = 4.dp)
                )
                Text(
                    text = "入力完了",
                    modifier = Modifier
                        .padding(top = 100.dp)
                        .size(height = 40.dp, width = 180.dp)
                        .background(Color.Red, shape = RoundedCornerShape(10.dp))
                        .padding(vertical = 6.dp)
                        .clickable { onClick() },
                    color = Color.White,
                    fontSize = 16.fontDp,
                    fontWeight = FontWeight.SemiBold,
                    textAlign = TextAlign.Center,
                )
            }
        }
    }
}
```

するとどうでしょう、フォーカスは外れません。
どうやら`Dialog`が独自の`focusManager`を持っているようで、`Dialog` の外側で`LocalFocusManager.current`しても内部の`focusManager`を取得できず、フォーカスが外せないようです。
これは困った。

## 解決：Dialog を使わずに Box でカスタムダイアログを作成する

で、結局どうしたかというと、`Dailog`を使うのをやめました。
`Box` を活用して **カスタムダイアログを作成** すると、ダイアログの外側をタップしたときにフォーカスを外すことができます。

### **実装コード**
```kotlin
@Composable
fun KeyboardDialog(
    modifier: Modifier = Modifier,
    onClick: () -> Unit = {},
    onDismissRequest: () -> Unit = {},
) {
    val focusManager = LocalFocusManager.current
    // 外側のタップを検知する Box
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black.copy(alpha = 0.6f))
            .pointerInput(Unit) {
                detectTapGestures(onTap = { focusManager.clearFocus() }) // タップでフォーカスを外す
            },
        contentAlignment = Alignment.Center
    ) {
        var value by remember { mutableStateOf("") }
        BackHandler(onBack = onDismissRequest)
        Column(
            modifier = modifier
                .width(290.dp)
                .background(color = Color.White, shape = RoundedCornerShape(20.dp))
                .padding(10.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Text(
                text = "以下の項目を入力してください",
                fontSize = 16.fontDp,
                fontWeight = FontWeight.SemiBold
            )
            Text(
                "・好きな食べ物",
                modifier = Modifier.padding(top = 10.dp),
                fontSize = 16.fontDp
            )
            TextField(
                value = value,
                onValueChange = { value = it },
                label = { Text("テキストを入力してください") },
                modifier = Modifier
                    .padding(vertical = 4.dp)
            )
            Text(
                text = "入力完了",
                modifier = Modifier
                    .padding(top = 100.dp)
                    .size(height = 40.dp, width = 180.dp)
                    .background(Color.Red, shape = RoundedCornerShape(10.dp))
                    .padding(vertical = 6.dp)
                    .clickable { onClick() },
                color = Color.White,
                fontSize = 16.fontDp,
                fontWeight = FontWeight.SemiBold,
                textAlign = TextAlign.Center,
            )
        }
    }
}
```

先ほどのうまくいかなったコードから変更したのは以下の通り
- Dialogを取り外した
- 戻るボタンで`dismiss`できるようにしたかったので、`BackHandler`を利用

これでダイアログの外をタップして、TextFieldのフォーカスを外すことができるようになりました