UI の役割は、アプリデータを画面に表示することであり、また、ユーザー インタラクションの主要なポイントとして機能することです。ユーザー インタラクション(例: ボタンを押す)や外部入力(例: ネットワーク応答)によってデータが変更されるたびに、UI を更新してそのような変更を反映させる必要があります。UI は、実質的には、データレイヤから取得されたアプリの状態を視覚的に表したものです。
ただし、一般的に、データレイヤから取得するアプリデータの形式は、表示する必要がある情報の形式とは異なります。たとえば、UI では、データの一部だけが必要になる場合や、ユーザーに関連する情報を提示するために 2 つの異なるデータソースの統合が求められる場合があります。適用するロジックにかかわらず、UI を完全にレンダリングするために必要なすべての情報を UI に渡す必要があります。UI レイヤは、アプリデータの変更を UI が提示できる形式に変換して表示するパイプラインです。
UI 状態を公開する
UI 状態を定義し、その状態の生成を管理する方法を決定したら、次のステップとして、生成された状態を UI に提示します。UDF を使用して状態の生成を管理するので、生成される状態をストリームとみなすことができます。つまり、時間の経過とともに状態の複数のバージョンが生成されます。したがって、LiveData や StateFlow などの監視可能なデータホルダーで UI 状態を公開する必要があります。これは、UI が、ViewModel から直接手動でデータを取得する手間をかけずに、状態の変更に反応できるようにするためです。このようなタイプには、常に UI 状態の最新バージョンがキャッシュに保存されるというメリットもあります。このことは、構成変更後に状態をすばやく復元するために役立ちます。
class NewsViewModel(...) : ViewModel() {
val uiState: StateFlow<NewsUiState> = …
}
UiState のストリームを作成する一般的な方法は、可変のバッキング ストリームを ViewModel からの不変のストリームとして公開することです。たとえば、MutableStateFlow<UiState> を StateFlow<UiState> として公開します。
class NewsViewModel(...) : ViewModel() {
private val _uiState = MutableStateFlow(NewsUiState())
val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()
...
}
次に、ViewModel は、内部的に状態を変更するメソッドを公開して、UI が使用する更新を公開できます。たとえば、非同期アクションを実行する必要がある場合を考えてみましょう。この場合、viewModelScope を使用してコルーチンを起動することが可能で、完了したら可変の状態を更新できます。
class NewsViewModel(
private val repository: NewsRepository,
...
) : ViewModel() {
private val _uiState = MutableStateFlow(NewsUiState())
val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()
private var fetchJob: Job? = null
fun fetchArticles(category: String) {
fetchJob?.cancel()
fetchJob = viewModelScope.launch {
try {
val newsItems = repository.newsItemsForCategory(category)
_uiState.update {
it.copy(newsItems = newsItems)
}
} catch (ioe: IOException) {
// Handle the error and notify the UI when appropriate.
_uiState.update {
val messages = getMessagesFromThrowable(ioe)
it.copy(userMessages = messages)
}
}
}
}
}
UI 状態を使用する
UI で監視可能なデータホルダーを使用する際は、必ず UI のライフサイクルを考慮してください。これが重要なのは、ビューがユーザーに表示されていないとき、UI は UI 状態を監視すべきでないからです。このトピックについて詳しくは、こちらのブログ投稿をご覧ください。LiveData を使用している場合は、LifecycleOwner が暗黙的にライフサイクルに関する問題に対処します。フローを使用している場合は、適切なコルーチン スコープと repeatOnLifecycle API を使用してこの問題を処理することをおすすめします。
class NewsActivity : AppCompatActivity() {
private val viewModel: NewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// Update UI elements
}
}
}
}
}
進行中のオペレーションを表示する
UiState クラスで読み込み状態を表現する簡単な方法は、次のブール値フィールドを使用することです。
data class NewsUiState(
val isFetchingArticles: Boolean = false,
...
)
このフラグの値は、UI に進行状況バーが存在するかどうかを表します。
class NewsActivity : AppCompatActivity() {
private val viewModel: NewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Bind the visibility of the progressBar to the state
// of isFetchingArticles.
viewModel.uiState
.map { it.isFetchingArticles }
.distinctUntilChanged()
.collect { progressBar.isVisible = it }
}
}
}
}
アニメーション
滑らかなトップレベル ナビゲーション遷移を実現するには、アニメーションを開始する前に、後続の画面でデータが読み込まれるのを待機します。Android ビュー フレームワークには、postponeEnterTransition() および startPostponedEnterTransition() API を使用して、フラグメント デスティネーション間の遷移を遅延させるフックが用意されています。これらの API により、後続の画面の UI 要素(通常はネットワークからフェッチされた画像)を表示する準備が確実に整ってから、UI がその画面への遷移アニメーションを開始するようにできます。
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- UI 状態生成
- 状態ホルダーと UI の状態 {:#mad-arch}
- アプリ アーキテクチャ ガイド