Architecture
User-Interface Layer
The User Interface Layer is likely where most of your changes will happen, and will usually involve much more detailed code.
There are two major concepts in this layer:
- State - the data used to present the user interface
- User Interface - the means of allowing the user to consume and interact with the data
State
State includes data from other layers as well as data that's only used for controlling the user interface. If you have an application that's displaying contact information, the contact data comes from the other layers and is the "what" that you want to display. But you'll also need other information such as
- What screen is user seeing?
- Which position in a list is at the top?
- What field currently has focus?
Some data only makes sense for a specific user interface. It's easiest to determine which data this is if you ask yourself "would I need this data for a graphical user interface, a command-line interface, and a web service?" If so, the data likely belongs in the Data Layer. Otherwise, the data only exists in the User-Interface Layer.
Our goals in the User-Interface Layer are to
- Observe state changes
- Prepare our state for presentation to the user
- Interpret user interaction
- Update the state as needed (which will trigger our observers)
We'll be using Jetpack Compose as our user interface in this class.Compose has great state-management support, often making the state observation invisible, but sometimes it can be tricky to set up. We'll work through several types of state use in this class, but be aware that best practices for state are still being developed and may change from what I present. Keep an eye on https://developer.android.com/ site and the https://android-developers.googleblog.com/ blog for emerging details.
As part of the User-Interface Layer responsibilities, we need to prepare and update state. This function is usually managed by a View Model.
View Models
The concept of a View Model is simple - it's a place to manage state preparation and updates.
flowchart LR
subgraph User Interface Layer
ui[User Interface] --> vm[View Model]
end
vm --> repo
ds1 --> db[(Database)]
ds2 --> file(((File)))
ds3 --> ws(((Web Service)))
subgraph Data Layer
direction LR
repo[Repository] --> ds1[Data Source 1]
repo[Repository] --> ds2[Data Source 2]
repo[Repository] --> ds3[Data Source 3]
end
flowchart LR
subgraph User Interface Layer
ui[User Interface] --> vm[View Model]
end
vm --> uc1
vm --> uc2
vm --> uc3
vm --> data
uc1 --> repo
uc2 --> repo
uc3 --> repo
data --> repo
ds1 --> db[(Database)]
ds2 --> file(((File)))
ds3 --> ws(((Web Service)))
subgraph Domain Layer
direction LR
data[Data]
uc1[Use Case 1]
uc2[Use Case 2]
uc3[Use Case 3]
end
subgraph Data Layer
direction LR
repo[Repository] --> ds1[Data Source 1]
repo[Repository] --> ds2[Data Source 2]
repo[Repository] --> ds3[Data Source 3]
end
(Arrows represent dependencies)
Note
You may have multiple view models. Some developers prefer one view model per screen, others prefer by types of data, and others prefer a single view model. The decision often depends on the size of the application as well.
A view model creates state to be used by the user interface. In some cases, it may just pass the state along. In other cases, it may modify or combine state from underlying layers.
View model functions allow the user interface to tell it what the user wants to do, such as adding/deleting a contact or switching to a different screen. These functions will interact with the Domain Layer (if present) or Data Layer to update the data.
The view model needs to expose the state in a way that the user interface can observe it. Depending on the UI framework being used, the state might be exposed using the Observer pattern, using Android LiveData
, Jetpack Compose State
objects, Kotlin Flow
s or other approaches.
For our work, we'll be using Kotlin Flow
s and Compose State
s. We'll talk about what those are in later modules.
Composable Functions
For our user interfaces, we'll use Jetpack Compose to declare our user interface.
In a nutshell, you'll create Composable functions in Kotlin. These functions will emit descriptions of parts of the user interface to a tree. The Compose UI framework will look at this tree and create a user interface.
The cool thing is that Compose can watch the parameters passed to a Composable function. If the values change from one call to the next, it will recompose, emitting a replacement for the part of the tree that it previously emitted. The Compose UI will then detect changes and refresh only the parts of the UI that need to be refreshed.