40
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Guide to app architectureからプラクティスを拾うメモ UIレイヤー編

Last updated at Posted at 2022-01-09

上記から拾ってメモしておきます :pencil:

ちょっと分かりにくい部分として、最初に書いておきたいのが、UIレイヤーとUIは別で、UIレイヤーにはState holder(ViewModelなど)も含み、UIやUI elementはActivityなどを指すようです。

https://developer.android.com/jetpack/guide/ui-layer?hl=en より

(あとでまとめ記事的なのを作るかも?)

Activityなどのアプリのコンポーネントにアプリのデータや状態を持ってはいけない

Given the conditions of this environment, it's possible for your app components to be launched individually and out-of-order, and the operating system or user can destroy them at any time. Because these events aren't under your control, you shouldn't store or keep in memory any application data or state in your app components, and your app components shouldn't depend on each other.

なぜ?

メモリ内のデータはアプリの管理下になく、いつでも消され得るため。

activities, fragments, services, content providersなどのコンポーネント同士が依存しあってはならない

Given the conditions of this environment, it's possible for your app components to be launched individually and out-of-order, and the operating system or user can destroy them at any time. Because these events aren't under your control, you shouldn't store or keep in memory any application data or state in your app components, and your app components shouldn't depend on each other.

なぜ?

個別にコンポーネントは起動され、いつでもシステムによって消され得るため。

すべてをActivityやFragmentに書いてはならない

The most important principle to follow is separation of concerns. It's a common mistake to write all your code in an Activity or a Fragment. These UI-based classes should only contain logic that handles UI and operating system interactions. By keeping these classes as lean as possible, you can avoid many problems related to the component lifecycle, and improve the testability of these classes.

なぜ?

separation of concerns、関心事の分離のため。

ActivityやFragmentはUIやOSとの連携のみを行い、できるだけ小さくする

The most important principle to follow is separation of concerns. It's a common mistake to write all your code in an Activity or a Fragment. These UI-based classes should only contain logic that handles UI and operating system interactions. By keeping these classes as lean as possible, you can avoid many problems related to the component lifecycle, and improve the testability of these classes.

なぜ?

コンポーネントのライフサイクルとの問題を避け、クラスのテスト可能性を向上させる。

UIは完全に持続する(persistent)データモデルによって動かされるべき

Another important principle is that you should drive your UI from data models, preferably persistent models. Data models represent the data of an app. They're independent from the UI elements and other components in your app. This means that they are not tied to the UI and app component lifecycle, but will still be destroyed when the OS decides to remove the app's process from memory.

持続する(persistent)というのはUIエレメントや他のアプリのコンポーネントから独立しており、アプリのライフサイクルに縛られていないということ。ただし、OSが削除を決めた場合はメモリから削除される。

なぜ?

Persistent models are ideal for the following reasons:
- Your users don't lose data if the Android OS destroys your app to free up resources.
- Your app continues to work in cases when a network connection is flaky or not available.
If you base your app architecture on data model classes, you make your app more testable and robust.

OSがアプリのリソースを解放したときにデータを失わなくて済むため。
ネットワーク接続が不安定だったり、使えない場合にアプリを動作し続けさせるため。
またデータモデルクラスによるアーキテクチャベースになっているとアプリはテスト可能で堅牢になる。

アプリでは最低でも2レイヤーが必要になる

Considering the common architectural principles mentioned in the previous section, each application should have at least two layers:

  • The UI layer that displays application data on the screen.
  • The data layer that contains the business logic of your app and exposes application data.
  • You can add an additional layer called the domain layer to simplify and reuse the interactions between the UI and data layers.

https://developer.android.com/jetpack/guide?hl=en#recommended-app-arch より

なぜ?

上記の原則に則るため。

  • UIは完全に持続する(persistent)データモデルによって動かされるべき
  • Activityなどのアプリのコンポーネントにアプリのデータや状態を持ってはいけない

UIはアプリケーションデータを画面に表示し、データが変わったときやユーザーインタラクションでその変更を反映するアップデートする

The role of the UI layer (or presentation layer) is to display the application data on the screen. Whenever the data changes, either due to user interaction (such as pressing a button) or external input (such as a network response), the UI should update to reflect the changes.

The UI layer is made up of two things:

  • UI elements that render the data on the screen. You build these elements using Views or Jetpack Compose functions.
  • State holders (such as ViewModel classes) that hold data, expose it to the UI, and handle logic.

ここから以下も読む。

The role of the UI is to display the application data on the screen and also to serve as the primary point of user interaction. Whenever the data changes, either due to user interaction (like pressing a button) or external input (like a network response), the UI should update to reflect those changes. Effectively, the UI is a visual representation of the application state as retrieved from the data layer.

The term UI refers to UI elements such as activities and fragments that display the data, independent of what APIs they use to do this (Views or Jetpack Compose). Because the role of the data layer is to hold, manage, and provide access to the app data, the UI layer must perform the following steps:

  • Consume app data and transform it into data the UI can easily render.
  • Consume UI-renderable data and transform it into UI elements for presentation to the user.
  • Consume user input events from those assembled UI elements and reflect their effects in the UI data as needed.
  • Repeat steps 1 through 3 for as long as necessary.

UI layerは以下のステップを行わなければならない。

  • UIがかんたんに表示できるようにデータを変換する
  • 表示できるデータを使って、UIエレメントとしてユーザーに表示する
  • 作ったUIからUserの入力を受け取って、UIのdataに必要に応じて適応する
  • 上記のステップを必要な限り繰り返す

なぜ?

シンプルにアプリケーションのデータを表示するのがUIの役割と定義しているため。

UI Stateを定義する

the information required to fully render the UI can be encapsulated in a NewsUiState data class defined as follows:

UIで情報を表示するための情報をdata classのNewsUiStateで囲う

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    ...
)

なぜ?

後述のUDFを実現するために必要になるため。

UI StateクラスはImmutableにし、UIでUI Stateを変更してはならない

:x: DON'T

bookmarkButton.setOnClickListener {
  // データレイヤーで保持しているデータと競合する
  newsUiState.bookmarked = true
}

The UI state definition in the example above is immutable. The key benefit of this is that immutable objects provide guarantees regarding the state of the application at an instant in time. This frees up the UI to focus on a single role: to read the state and update its UI elements accordingly. As a result, you should never modify the UI state in the UI directly unless the UI itself is the sole source of its data. Violating this principle results in multiple sources of truth for the same piece of information, leading to data inconsistencies and subtle bugs.

上記のUI Stateクラスは変更できず、immutableになっている。このメリットはある瞬間のアプリケーションの状態を保証してくれること。これによってUIは状態を読み取り、それに応じてUI要素を更新するという単一の役割に集中できる。

なぜ?

この原則に違反すると同じ情報を持つ情報源が複数になり、データの不整合や微妙なバグを引き起こす。原則を守ることでSingle source of truthにすることができる。

Unidirectional Data Flow(UDF)によるデータの管理をする

image.png

https://developer.android.com/jetpack/guide/ui-layer?hl=en より。ブックマークボタンを押したときの流れ。

The previous section established that the UI state is an immutable snapshot of the details needed for the UI to render. However, the dynamic nature of data in apps means that state might change over time. This might be due to user interaction or other events that modify the underlying data that is used to populate the app.

These interactions may benefit from a mediator to process them, defining the logic to be applied to each event and performing the requisite transformations to the backing data sources in order to create UI state. These interactions and their logic may be housed in the UI itself, but this can quickly get unwieldy as the UI starts to become more than its name suggests: it becomes data owner, producer, transformer, and more. Furthermore, this can affect testability because the resulting code is a tightly coupled amalgam with no discernable boundaries. Ultimately, the UI stands to benefit from reduced burden. Unless the UI state is very simple, the UI's sole responsibility should be to consume and display UI state.

UIにイベントがあったときにロジックを適応し、UIの状態を作るために変更する処理が全部書かれるが、
データの所有者やアプリケーションのデータを変換するものなどの名前以上の役割を持ってしまい、それがテストを難しくする。

The pattern where the state flows down and the events flow up is called a unidirectional data flow (UDF).

stateの流れを下に流し、イベントを上に上げるのがUDF。

なぜUDFにするか?

Why use UDF?
UDF models the cycle of state production as shown in Figure 4. It also separates the place where state changes originate, the place where they are transformed, and the place where they are finally consumed. This separation lets the UI do exactly what its name implies: display information by observing state changes, and relay user intent by passing those changes on to the ViewModel.
In other words, UDF allows for the following:

  • Data consistency. There is a single source of truth for the UI.
  • Testability. The source of state is isolated and therefore testable independent of the UI.

  • Maintainability. Mutation of state follows a well-defined pattern where mutations are a result of both user events and the sources of data they pull from.

  • データの一貫性。Single source of truthにできる。

  • テスト。stateが分離されているので、UIの独立性が高まる

  • メンテナビリティ。状態の変化が、明確に定義されたパターンに従っている。

UIの唯一の責務はUI Stateを消費して表示することであるべき

These interactions may benefit from a mediator to process them, defining the logic to be applied to each event and performing the requisite transformations to the backing data sources in order to create UI state. These interactions and their logic may be housed in the UI itself, but this can quickly get unwieldy as the UI starts to become more than its name suggests: it becomes data owner, producer, transformer, and more. Furthermore, this can affect testability because the resulting code is a tightly coupled amalgam with no discernable boundaries. Ultimately, the UI stands to benefit from reduced burden. Unless the UI state is very simple, the UI's sole responsibility should be to consume and display UI state.

なぜ?

コードの境界のはっきりしない密結合の集合体となって、テスト容易性に影響を与える可能性があるため。

ViewModelで観測可能なLiveDataやStateFlowなどのデータホルダーでデータを保持する

After you define your UI state and determine how you will manage the production of that state, the next step is to present the produced state to the UI. Because you're using UDF to manage the production of state, you can consider the produced state to be a stream—in other words, multiple versions of the state will be produced over time. As a result, you should expose the UI state in an observable data holder like LiveData or StateFlow. The reason for this is so that the UI can react to any changes made in the state without having to manually pull data directly from the ViewModel. These types also have the benefit of always having the latest version of the UI state cached, which is useful for quick state restoration after configuration changes.

LiveDataやStateFlow、またはComposeのStateを使う。

なぜ?

UIがデータを手動で引っ張ってくる必要なしにデータを反映でき、最新のデータを保持するという性質もあり、これらはStateの復元に役立つ。

UiStateのクラスでデータをラップする

In cases where the data exposed to the UI is relatively simple, it's often worth wrapping the data in a UI state type because it conveys the relationship between the emission of the state holder and its associated screen or UI element. Furthermore, as the UI element grows more complex, it’s always easier to add to the definition of the UI state to accommodate the extra information needed to render the UI element.

なぜ?

画面やUI要素とそのデータの関連が分かりやすくなるため。またUI要素に必要なデータが増えたときに拡張しやすくなる。

UiStateが複数ある場合は相互に関連するUiStateそれぞれをうまく扱う必要がある。

DON'T

class NewsViewModel {
  val bookmarkCount: StateFlow<Int>
  val articles: StateFlow<List<Article>>

  // 片方だけ更新されるとバグる
  fun bookmark() { bookmarkCount.value = bookmarkCount.value++ }
}

A UI state object should handle states that are related to each other. This leads to fewer inconsistencies and it makes the code easier to understand. If you expose the list of news items and the number of bookmarks in two different streams, you might end up in a situation where one was updated and the other was not. When you use a single stream, both elements are kept up to date.

なぜ?

例えばブックマークの数とニュースのリストが有ったときに片方だけ更新すると不整合が起こるため。

UiStateを関連する状態を処理できるようにするために単一のクラスにまとめる

Furthermore, some business logic may require a combination of sources. For example, you might need to show a bookmark button only if the user is signed in and that user is a subscriber to a premium news service. You could define a UI state class as follows:

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf()
)
val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium

In this declaration, the visibility of the bookmark button is a derived property of two other properties. As business logic gets more complex, having a singular UiState class where all properties are immediately available becomes increasingly important.

なぜ?

例えば、ログインしていて、プレミアムの場合だけボタンを出したいみたいな場合に、クラスがまとまっていないとクラスをまたいで利用できないため。

StateFlowやLiveDataなどのUiStateを適切に分割する

UI states: single stream or multiple streams? The key guiding principle for choosing between exposing UI state in a single stream or in multiple streams is the previous bullet point: the relationship between the items emitted. The biggest advantage to a single-stream exposure is convenience and data consistency: consumers of state always have the latest information available at any instant in time. However, there are instances where separate streams of state from the ViewModel might be appropriate:

Unrelated data types: Some states that are needed to render the UI might be completely independent from each other. In cases like these, the costs of bundling these disparate states together might outweigh the benefits, especially if one of these states is updated more frequently than the other.

UiState diffing: The more fields there are in a UiState object, the more likely it is that the stream will emit as a result of one of its fields being updated. Because views don't have a diffing mechanism to understand whether consecutive emissions are different or the same, every emission causes an update to the view. This means that mitigation using the Flow APIs or methods like distinctUntilChanged() on the LiveData might be necessary.

StateFlowなどを一つにする利点はデータの一貫性。最新の状態を入手できること。ただ、以下の場合で分けるほうが適切な場合がある。

  • 無関係なデータ
    完全に独立していて、他の状態よりも頻繁に更新される場合に、コストが1つにするメリットを上回る場合がある。

  • 変更を見る必要がある
    一つのUiStateにたくさんのデータがある場合は、データが流れやすくなり、すべてのviewが更新されるので、UI側でdistinctUntilChanged()のような軽減が必要になる。

ViewModelから呼び出される処理はメインスレッドから呼び出しても安全であるべき

Any work performed in a ViewModel should be main-safe—safe to call from the main thread. This is because the data and domain layers are responsible for moving work to a different thread.

なぜ?

ドメインレイヤーやデータレイヤーに別スレッドで動かす責務があるため。

Pagingライブラリを使う場合はUiStateに入れずフィールドを分ける

The Paging library is consumed in the UI with a type called PagingData. Because PagingData represents and contains items that can change over time—in other words, it is not an immutable type—it should not be represented in an immutable UI state. Instead, you should expose it from the ViewModel independently in its own stream. See the Android Paging codelab for a specific example of this.

なぜ?
PagingData自体が時間とともに変わるので、不変データの中に入れることができないため。

40
18
0

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
40
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?