夏休みにHiltとJetpack Composeを初めて触ったメモ
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