4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【2022-08-14 更新】Jetpack Compose で Todo アプリを作ってみた

Last updated at Posted at 2022-07-24

Jetpack Compose は革命かもしれません。

Reflector Recording.gif

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 に持たせています。
スクリーンショット 2022-07-26 14.24.01.png
できるだけ 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 のスタックとライフサイクル

👉 Android ファショ通

4
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?