Jetpack Compose は革命かもしれません。
Composable
View に関わるファイルはこれだけ。
RecyclerView, RecyclerViewAdapter, ViewHolder など不要です。
@Composable
fun TodoScreen(
viewModel: TodoViewModel = hiltViewModel()
) {
val items by viewModel.items.collectAsStateWithLifecycle(initialValue = emptyList())
val target by viewModel.target
val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() }
val scope: CoroutineScope = rememberCoroutineScope()
val listState = rememberLazyListState()
val sheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
confirmStateChange = {
focusManager.clearFocus()
true
}
)
val onShowSheet = {
scope.launch { sheetState.show() }
focusRequester.requestFocus()
}
val onHideSheet = { onScrollToTop: Boolean ->
focusManager.clearFocus()
scope.launch {
sheetState.hide()
if (onScrollToTop) listState.animateScrollToItem(index = 0)
}
}
Column {
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
) {
OutlinedTextField(
value = target.textFieldValue,
onValueChange = viewModel::updateTarget,
modifier = Modifier
.weight(1F)
.focusRequester(focusRequester),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { onHideSheet(false) }),
trailingIcon = {
IconButton(
onClick = viewModel::clearTarget,
) {
Icon(Icons.Filled.Clear, null)
}
}
)
if (target.isAdd()) {
Button( // add
onClick = {
viewModel.insertTodo()
onHideSheet(true)
},
modifier = Modifier.size(56.dp)
) {
Icon(Icons.Filled.Add, null)
}
} else {
Button( // update
onClick = {
viewModel.updateTodo()
onHideSheet(true)
},
modifier = Modifier.size(56.dp)
) {
Icon(Icons.Filled.Refresh, null)
}
Button( // delete
onClick = {
viewModel.deleteTodo()
onHideSheet(false)
},
modifier = Modifier.size(56.dp)
) {
Icon(Icons.Filled.Delete, null)
}
}
}
}
) {
Box {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
verticalArrangement = Arrangement.spacedBy(4.dp),
contentPadding = PaddingValues(4.dp)
) {
items(
items = items,
key = { item -> item.id }
) { item ->
Card(
shape = RoundedCornerShape(4.dp),
elevation = 2.dp,
) {
Column(
modifier = Modifier
.animateItemPlacement()
.fillMaxWidth()
.clickable {
viewModel.setTarget(item.id, item.text)
onShowSheet()
}
.padding(horizontal = 16.dp, vertical = 8.dp),
) {
Text(
text = item.text,
style = MaterialTheme.typography.h5,
)
Text(
text = item.updated.simpleFormat(),
style = MaterialTheme.typography.subtitle1
)
}
}
}
}
FloatingActionButton(
onClick = {
viewModel.newTarget()
onShowSheet()
},
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(12.dp)
) {
Icon(Icons.Filled.Add, null)
}
}
}
}
}
編集中テキストの State
は ViewModel に持たせています。
できるだけ State
は ViewModel に持っていくほうがいいのではないか、ということを考えています。
パフォーマンスに任せてコードの見通しを優先するほうが「recompose沼」にはまらなくていいのではという逆説。
Repository
👉 KMM や マルチプラットフォーム を見据えて SQLDelight で Repository
テーブル定義、実行するメソッドに対するクエリーを Todo.sq として配置します。
-- app/src/main/sqldelight/com/your/package/data/Todo.sq
CREATE TABLE todo (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
updated INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
INSERT INTO todo(text) VALUES ('宿題をする');
INSERT INTO todo(text) VALUES ('マンガを読む');
INSERT INTO todo(text) VALUES ('プールに行く');
selectAll:
SELECT * FROM todo ORDER BY updated DESC;
deleteAll:
DELETE FROM todo;
insert:
INSERT INTO todo (text) VALUES (:text);
update:
UPDATE todo SET text = :text, updated = (strftime('%s', 'now')) WHERE id = :entryId;
delete:
DELETE FROM todo
WHERE id = :entryId;
count:
SELECT COUNT(id) FROM todo;
Hilt 定形で使えます、DatabaseModule。
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): Database {
val driver = AndroidSqliteDriver(Database.Schema, context, DB_NAME)
return Database(driver)
}
private const val DB_NAME = "database.db"
}
ここで、@Singleton としておくことを忘れてはなりません。
時間のかかる処理は Flow を使っておきます。
class TodoRepository @Inject constructor(
private val database: Database
) {
fun load(): Flow<List<Todo>> {
return database.todoQueries.selectAll().asFlow().mapToList(Dispatchers.IO)
}
fun count(): Flow<Long> {
return database.todoQueries.count().asFlow().mapToOne(Dispatchers.IO)
}
fun insert(text: String) {
database.todoQueries.insert(text)
}
fun update(id: Long, text: String) {
database.todoQueries.update(text, id)
}
fun delete(id: Long) {
database.todoQueries.delete(id)
}
}
👉 KMM や マルチプラットフォーム を見据えて SQLDelight で Repository
👉 「Compose Compiler Reports」 recompose される条件とタイミングと範囲を知りたい
👉 TextField の フォーカス と IME 開閉 と カーソル位置
👉 NavBackStackEntry - Composable のスタックとライフサイクル