UI レイヤ(ビュー)

コンセプトと Jetpack Compose の実装

UI の役割は、アプリデータを画面に表示することであり、また、ユーザー インタラクションの主要なポイントとして機能することです。ユーザー インタラクション(例: ボタンを押す)や外部入力(例: ネットワーク応答)によってデータが変更されるたびに、UI を更新してそのような変更を反映させる必要があります。UI は、実質的には、データレイヤから取得されたアプリの状態を視覚的に表したものです。

ただし、一般的に、データレイヤから取得するアプリデータの形式は、表示する必要がある情報の形式とは異なります。たとえば、UI では、データの一部だけが必要になる場合や、ユーザーに関連する情報を提示するために 2 つの異なるデータソースの統合が求められる場合があります。適用するロジックにかかわらず、UI を完全にレンダリングするために必要なすべての情報を UI に渡す必要があります。UI レイヤは、アプリデータの変更を UI が提示できる形式に変換して表示するパイプラインです。

UI 状態を公開する

UI 状態を定義し、その状態の生成を管理する方法を決定したら、次のステップとして、生成された状態を UI に提示します。UDF を使用して状態の生成を管理するので、生成される状態をストリームとみなすことができます。つまり、時間の経過とともに状態の複数のバージョンが生成されます。したがって、LiveDataStateFlow などの監視可能なデータホルダーで 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 がその画面への遷移アニメーションを開始するようにできます。