夏休みにHiltとJetpack Composeを初めて触ったメモ

2021/08/16に公開

Dagger HiltとJetpack Composeを使ってどうアプリを作るのか、
見た目は捨ててStateの管理などをメインに調べたメモです。

tivi(https://github.com/chrisbanes/tivi/)と公式を見ながら作っています。

tiviに書かれているコードの理由が理解できない部分は捨てて公式を優先する方針で触りました。

Hilt

Applicationクラス

@HiltAndroidApp
class WeightApplication : Application()

アノテーションを付けるだけ

Activityなど

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
}

インジェクトをさせたいクラスがあるクラスに@AndroidEntryPointを付けます(語彙力)
Fragmentで何かをインジェクトしたい場合でもActivityに@AndroidEntryPointを付ける必要があります。(公式見てね)

インジェクトさせたいViewModel

@HiltViewModel
internal class WeightListViewModel @Inject constructor() : ViewModel() {
}

@HiltViewModelアノテーションとコンストラクタに@Injectアノテーションを付けます。

ViewModelをインジェクト

@Composable
fun WeightList(navController: NavController) {
    val viewModel: WeightListViewModel = hiltViewModel()
    DailyWeightList(viewModel)
}

今回はtiviを参考にandroidx.hilt.navigation.compose.hiltViewModelを使ってみました。

Compose

Activity

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            WeigtRecorderTheme {
                Main()
            }
        }
    }
}

めっちゃスリムやん
少し失敗したと思ったのが画面のファイルを示すためにファイル名の末尾にScreenを付ければ良かったなと反省
tiviだと気にしてない様子(見間違いならゴメンなさい)

Repositoryからデータ取特して画面に表示

結果を画面に表示する部分
@Composable
private fun DailyWeightList(viewModel: WeightListViewModel) {
    val viewState: WeightListViewState by viewModel.state.collectAsState(initial = WeightListViewState.EMPTY)
    val dailyWeightData: List<Weight> = viewState.dailyWeightData ?: emptyList()

    Scaffold(
        modifier = Modifier.fillMaxWidth(),
        content = {
            LazyColumn {
                if (dailyWeightData.isNotEmpty()) {
                    item {
                        WeightLineChart(dailyWeightData = dailyWeightData)
                    }
                }

                dailyWeightData.forEach { weight ->
                    item {
                        WeightTextLine(weight = weight)
                    }
                }
            }
        },
        floatingActionButton = { addWeightData() }
    )
}

viewStateに体重の一覧データが入ってきます。
collectAsStateを使ってViewModelのFlowをStateに変換しています。

データを一覧で持ってるWeightListViewState
internal data class WeightListViewState(
    val dailyWeightData: List<Weight>?
) {
    companion object {
        val EMPTY = WeightListViewState(null)
    }
}

tiviではリフレッシュ中フラグなども持っていましたが、今回は省略し画面表示に使う体重の一覧データだけを持たせます。

データを取特するViewModel
internal class WeightListViewModel @Inject constructor() : ViewModel() {
    private val dailyWeightData = MutableStateFlow<List<Weight>?>(null)
    val state = dailyWeightData.map {
        WeightListViewState(it)
    }

    init {
        viewModelScope.launch {
            delay(500)
            val random = Random()
            dailyWeightData.value = (1..7).map { index ->
                Weight(random.nextInt().toFloat(), "2021/08/0$index")
            }
        }
    }
}

init内で500ms遅延させてデータを生成しています。
※API通信などの遅延を想定

あとは生成(取特)したデータをmapオペレータでStateにしてあげるだけです。

Dialogを使う

DialogのComposable
@Composable
internal fun AddWeightDialog(isOpenDialog: MutableState<Boolean>, onClickRegist: () -> Unit) {
    if (!isOpenDialog.value) return
    var weight by remember { mutableStateOf("") }

    Dialog(
        onDismissRequest = { isOpenDialog.value = false },
    ) {
        androidx.compose.material.Surface {
            Column {
                Text(text = "体重の記録")
                Text(text = "現在の体重を入力")
                TextField(
                    value = weight,
                    onValueChange = { weight = it },
                    label = { Text("現在の体重") },
                    maxLines = 1,
                    singleLine = true,
                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
                )
                Button(onClick = onClickRegist) {
                    Text(text = "登録")
                }
            }
        }
    }
}

Dialogを開くFAB

@Composable
private fun addWeightData() {
    val isOpenDialog = remember { mutableStateOf(false) }
    AddWeightDialog(isOpenDialog = isOpenDialog) {
        // TODO DBに入れる
        isOpenDialog.value = false
    }
    FloatingActionButton(
        onClick = {
            isOpenDialog.value = true
        }
    ) {
        Icon(Icons.Filled.Add, contentDescription = "追加")
    }
}

isOpenDialog.value = trueここでダイアログの表示をさせています。
これまでのDialogFragmentなどを使う感覚とは違い、常にView.GONEで存在させておいて、
View.VISIBLEに切り替える感じの感覚だったので少し気持ち悪く思いました。

最後に

今回のコード一式はこちら(https://github.com/sobaya-0141/WeightRecord)です。
初めて触ってるので間違いとか非効率なこととかあると思うので、教えてもらえると嬉しいです。
もう少し色々触ってみます。(慣れなくて時間がすごくかかる・・・)

Discussion