diff --git a/docs/ArchitectureLearningJourney.md b/docs/ArchitectureLearningJourney.md index e371f482b..743ab877c 100644 --- a/docs/ArchitectureLearningJourney.md +++ b/docs/ArchitectureLearningJourney.md @@ -18,7 +18,7 @@ The goals for the app architecture are: ## Architecture overview -The app architecture has two layers: a [data layer](https://developer.android.com/jetpack/guide/data-layer) and [UI layer](https://developer.android.com/jetpack/guide/ui-layer) (a third, [the domain layer](https://developer.android.com/jetpack/guide/domain-layer), is currently in development). +The app architecture has three layers: a [data layer](https://developer.android.com/jetpack/guide/data-layer), a [domain layer](https://developer.android.com/jetpack/guide/domain-layer) and a [UI layer](https://developer.android.com/jetpack/guide/ui-layer).
@@ -39,7 +39,7 @@ The data flow is achieved using streams, implemented using [Kotlin Flows](https: ### Example: Displaying news on the For You screen -When the app is first run it will attempt to load a list of news resources from a remote server (when the `staging` or `release` build variant is selected, `debug` builds will use local data). Once loaded, these are shown to the user based on the interests they choose. +When the app is first run it will attempt to load a list of news resources from a remote server (when the `prod` build flavor is selected, `demo` builds will use local data). Once loaded, these are shown to the user based on the interests they choose. The following diagram shows the events which occur and how data flows from the relevant objects to achieve this. @@ -70,7 +70,7 @@ Here's what's happening in each step. The easiest way to find the associated cod 2 - The initial news feed state is set to Loading, which causes the UI to show a loading spinner on the screen. + The ForYouViewModel calls GetSaveableNewsResourcesUseCase to obtain a stream of news resources with their bookmarked/saved state. No items will be emitted into this stream until both the user and news repositories emit an item. While waiting, the feed state is set to Loading. Search for usages of NewsFeedUiState.Loading @@ -78,13 +78,21 @@ Here's what's happening in each step. The easiest way to find the associated cod 3 + The user data repository obtains a stream of UserData objects from a local data source backed by Proto DataStore. + + NiaPreferencesDataSource.userData + + + + 4 + WorkManager executes the sync job which calls OfflineFirstNewsRepository to start synchronizing data with the remote data source. SyncWorker.doWork - 4 + 5 OfflineFirstNewsRepository calls RetrofitNiaNetwork to execute the actual API request using Retrofit. @@ -92,7 +100,7 @@ Here's what's happening in each step. The easiest way to find the associated cod - 5 + 6 RetrofitNiaNetwork calls the REST API on the remote server. @@ -100,7 +108,7 @@ Here's what's happening in each step. The easiest way to find the associated cod - 6 + 7 RetrofitNiaNetwork receives the network response from the remote server. @@ -108,7 +116,7 @@ Here's what's happening in each step. The easiest way to find the associated cod - 7 + 8 OfflineFirstNewsRepository syncs the remote data with NewsResourceDao by inserting, updating or deleting data in a local Room database. @@ -116,7 +124,7 @@ Here's what's happening in each step. The easiest way to find the associated cod - 8 + 9 When data changes in NewsResourceDao it is emitted into the news resources data stream (which is a Flow). @@ -124,7 +132,7 @@ Here's what's happening in each step. The easiest way to find the associated cod - 9 + 10 OfflineFirstNewsRepository acts as an intermediate operator on this stream, transforming the incoming PopulatedNewsResource (a database model, internal to the data layer) to the public NewsResource model which is consumed by other layers. @@ -132,11 +140,19 @@ Here's what's happening in each step. The easiest way to find the associated cod - 10 + 11 + + GetSaveableNewsResourcesUseCase combines the list of news resources with the user data to emit a list of SaveableNewsResources. + + GetSaveableNewsResourcesUseCase.invoke - When ForYouViewModel receives the news resources it updates the feed state to Success. ForYouScreen then uses the news resources in the state to render the screen. -

-The screen shows the newly retrieved news resources (as long as the user has chosen at least one topic). + + + 12 + + When ForYouViewModel receives the saveable news resources it updates the feed state to Success. + + ForYouScreen then uses the saveable news resources in the state to render the screen. Search for instances of NewsFeedUiState.Success @@ -233,6 +249,14 @@ In the case of errors during data synchronization, an exponential backoff strate See the `OfflineFirstNewsRepository.syncWith` for an example of data synchronization. +## Domain layer +The [domain layer](https://developer.android.com/topic/architecture/domain-layer) contains use cases. These are classes which have a single invocable method (`operator fun invoke`) containing business logic. + +These use cases are used to simplify and remove duplicate logic from ViewModels. They typically combine and transform data from repositories. + +For example, `GetSaveableNewsResourcesUseCase` combines a stream (implemented using `Flow`) of `NewsResource`s from a `NewsRepository` with a stream of `UserData` objects from a `UserDataRepository` to create a stream of `SaveableNewsResource`s. This stream is used by various ViewModels to display news resources on screen with their bookmarked state. + +Notably, the domain layer in Now in Android _does not_ (for now) contain any use cases for event handling. Events are handled by the UI layer calling methods on repositories directly. ## UI Layer @@ -243,7 +267,7 @@ The [UI layer](https://developer.android.com/topic/architecture/ui-layer) compri * UI elements built using [Jetpack Compose](https://developer.android.com/jetpack/compose) * [Android ViewModels](https://developer.android.com/topic/libraries/architecture/viewmodel) -The ViewModels receive streams of data from repositories and transform them into UI state. The UI elements reflect this state, and provide ways for the user to interact with the app. These interactions are passed as events to the view model where they are processed. +The ViewModels receive streams of data from use cases and repositories, and transforms them into UI state. The UI elements reflect this state, and provide ways for the user to interact with the app. These interactions are passed as events to the view model where they are processed. ![Diagram showing the UI layer architecture](images/architecture-4-ui-layer.png "Diagram showing the UI layer architecture") @@ -272,20 +296,11 @@ The `feedState` is passed to the `ForYouScreen` composable, which handles both o ### Transforming streams into UI state -View models receive streams of data as cold [flows](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html) from one or more repositories. These are [combined](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/combine.html) together to produce a single flow of UI state. This single flow is then converted to a hot flow using [stateIn](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/state-in.html). The conversion to a state flow enables UI elements to read the last known state from the flow. +View models receive streams of data as cold [flows](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html) from one or more use cases or repositories. These are [combined](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/combine.html) together, or simply [mapped](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html), to produce a single flow of UI state. This single flow is then converted to a hot flow using [stateIn](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/state-in.html). The conversion to a state flow enables UI elements to read the last known state from the flow. **Example: Displaying followed topics** -The `InterestsViewModel` exposes `uiState` as a `StateFlow`. This hot flow is created by combining two data streams: - - - -* List of topics -* List of topic IDs which the current user is following - -The list of `Topic`s is mapped to a new list of `FollowableTopic`s. `FollowableTopic` is a wrapper for `Topic` which also indicates whether the current user is following that topic. - -The new list is used to create a `InterestsUiState.Interests` state which is exposed to the UI. +The `InterestsViewModel` exposes `uiState` as a `StateFlow`. This hot flow is created by obtaining the cold flow of `List` provided by `GetFollowableTopicsUseCase`. Each time a new list is emitted, it is converted into an `InterestsUiState.Interests` state which is exposed to the UI. ### Processing user interactions diff --git a/docs/images/architecture-1-overall.png b/docs/images/architecture-1-overall.png index a00065c48..3e381f2b3 100644 Binary files a/docs/images/architecture-1-overall.png and b/docs/images/architecture-1-overall.png differ diff --git a/docs/images/architecture-2-example.png b/docs/images/architecture-2-example.png index 468d8aac8..e70b6debc 100644 Binary files a/docs/images/architecture-2-example.png and b/docs/images/architecture-2-example.png differ