diff --git a/docs/ArchitectureLearningJourney.md b/docs/ArchitectureLearningJourney.md index dbfe9e80d..9b1619e01 100644 --- a/docs/ArchitectureLearningJourney.md +++ b/docs/ArchitectureLearningJourney.md @@ -2,10 +2,13 @@ In this learning journey you will learn about the Now in Android app architecture: its layers, key classes and the interactions between them. + ## Goals and requirements The goals for the app architecture are: + + * Follow the [official architecture guidance](https://developer.android.com/jetpack/guide) as closely as possible. * Easy for developers to understand, nothing too experimental. * Support multiple developers working on the same codebase. @@ -17,62 +20,154 @@ The goals for the app architecture are: 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). -![Diagram showing overall app architecture](images/architecture-1-overall.png "Diagram showing overall app architecture") + +
+Diagram showing overall app architecture +
+ The architecture follows a reactive programming model with [unidirectional data flow](https://developer.android.com/jetpack/guide/ui-layer#udf). With the data layer at the bottom, the key concepts are: + + * Higher layers react to changes in lower layers. * Events flow down. * Data flows up. The data flow is achieved using streams, implemented using [Kotlin Flows](https://developer.android.com/kotlin/flow). -**Example: Displaying news on the For You screen** + +### 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. The following diagram shows the events which occur and how data flows from the relevant objects to achieve this. -![Diagram showing how news resources are displayed on the For You screen](images/architecture-2-example.png "Diagram showing how news resources are displayed on the For You screen") - -Here's the code which corresponds to each step. The easiest way to jump to the code is to load the project into Android Studio and search for the following: - -1) Search for instances of `ForYouFeedState.Loading` -2) See `SyncWorker#doWork` +![Diagram showing how news resources are displayed on the For You screen](images/architecture-2-example.png "Diagram showing how news resources are displayed on the For You screen") -3&6) See `OfflineFirstNewsRepository#syncWith` -4&5) See `RetrofitNiaNetwork#getNewsResources` +Here's what's happening in each step. The easiest way to find the associated code is to load the project into Android Studio and search for the text in the Code column (handy shortcut: tap SHIFT twice). -7) See `NewsResourceDao#getNewsResourcesStream` -8) See `OfflineFirstNewsRespository#getNewsResourcesStream` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Step + Description + Code +
1 + On app startup, a WorkManager job to sync all repositories is enqueued. + SyncInitializer.create +
2 + The initial news feed state is set to Loading, which causes the UI to show a loading spinner on the screen. + Search for usages of ForYouFeedState.Loading +
3 + WorkManager executes the sync job which calls OfflineFirstNewsRepository to start synchronizing data with the remote data source. + SyncWorker.doWork +
4 + OfflineFirstNewsRepository calls RetrofitNiaNetwork to execute the actual API request using Retrofit. + OfflineFirstNewsRepository.syncWith +
5 + RetrofitNiaNetwork calls the REST API on the remote server. + RetrofitNiaNetwork.getNewsResources +
6 + RetrofitNiaNetwork receives the network response from the remote server. + RetrofitNiaNetwork.getNewsResources +
7 + OfflineFirstNewsRepository syncs the remote data with NewsResourceDao by inserting, updating or deleting data in a local Room database. + OfflineFirstNewsRepository.syncWith +
8 + When data changes in NewsResourceDao it is emitted into the news resources data stream (which is a Flow). + NewsResourceDao.getNewsResourcesStream +
9 + 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. + OfflineFirstNewsRepository.getNewsResourcesStream +
10 + 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 or author). +

Search for instances of ForYouFeedState.Success +
-9) Search for instances of `ForYouFeedState.Success` ## Data layer The data layer is implemented as an offline-first source of app data and business logic. It is the source of truth for all data in the app. + + ![Diagram showing the data layer architecture](images/architecture-3-data-layer.png "Diagram showing the data layer architecture") -Each repository has its own model. For example, the `TopicsRepository` has a `Topic` model and the `NewsRepository` has a `NewsResource` model. +Each repository has its own models. For example, the `TopicsRepository` has a `Topic` model and the `NewsRepository` has a `NewsResource` model. Repositories are the public API for other layers, they provide the _only_ way to access the app data. The repositories typically offer one or more methods for reading and writing data. ### Reading data -Data is obtained using data streams. This means each client of the repository must be prepared to react to data changes. Data is not exposed as a snapshot (e.g. `getModel`) because there's no guarantee that it will still be valid by the time it is used. +Data is exposed as data streams. This means each client of the repository must be prepared to react to data changes. Data is not exposed as a snapshot (e.g. `getModel`) because there's no guarantee that it will still be valid by the time it is used. _Example: Read a list of authors_ -A list of `Author`s can be obtained by calling `AuthorsRepository.getAuthorsStream()`. This returns a `Flow>`. +A list of Authors can be obtained by subscribing to `AuthorsRepository::getAuthorsStream` flow which emits `List`. -Whenever the list of authors changes (for example, when a new author is added), the updated `List<Author>` is emitted into the stream. +Whenever the list of authors changes (for example, when a new author is added), the updated `List` is emitted into the stream. ### Writing data @@ -101,7 +196,7 @@ A repository may depend on one or more data sources. For example, the `OfflineFi TopicsDao - Room/SQLite + Room/SQLite Persistent relational data associated with Topics @@ -109,7 +204,7 @@ A repository may depend on one or more data sources. For example, the `OfflineFi NiaPreferences - Proto DataStore + Proto DataStore Persistent unstructured data associated with user preferences, specifically which Topics the user is interested in. This is defined and modeled in a .proto file, using the protobuf syntax. @@ -119,7 +214,7 @@ A repository may depend on one or more data sources. For example, the `OfflineFi Remote API accessed using Retrofit - Data for topics, provided through REST API endpoints as JSON. + Data for topics, provided through REST API endpoints as JSON. @@ -139,7 +234,8 @@ 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 view models 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 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. + ![Diagram showing the UI layer architecture](images/architecture-4-ui-layer.png "Diagram showing the UI layer architecture") @@ -167,7 +263,7 @@ 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 hot 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 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. **Example: Displaying followed topics and authors** diff --git a/docs/images/architecture-2-example.png b/docs/images/architecture-2-example.png index ee5c3bdfe..468d8aac8 100644 Binary files a/docs/images/architecture-2-example.png and b/docs/images/architecture-2-example.png differ diff --git a/docs/images/architecture-3-data-layer.png b/docs/images/architecture-3-data-layer.png index 50b29762c..171585f37 100644 Binary files a/docs/images/architecture-3-data-layer.png and b/docs/images/architecture-3-data-layer.png differ