Skip to content

Commit 6fc315f

Browse files
Merge pull request #137 from MihaiCristianCondrea/codex/add-android-architecture-recommendations-to-docs
docs: add architecture recommendations
2 parents 7a5ccf8 + a9e0cc3 commit 6fc315f

File tree

2 files changed

+194
-0
lines changed

2 files changed

+194
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ operations through a small set of use cases. This pattern was inspired by the
5151
implemented in a simplified form to avoid adding heavy dependencies. The result keeps the UI
5252
simple and ensures a clear separation of concerns across the whole app.
5353

54+
For detailed guidance, see [Recommendations for Android architecture](docs/android-architecture-recommendations.md).
55+
5456
## Feedback
5557

5658
We are constantly updating and improving Android Studio Tutorials: Java Edition to give you the best possible experience. If you have suggested features or improvements, please leave a review. If something isn't working correctly, let us know so we can fix it.
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# Recommendations for Android architecture (Java Projects)
2+
3+
This page presents several Architecture best practices and recommendations. Adopt them to improve your app’s quality, robustness, and scalability. They also make it easier to maintain and test your app.
4+
5+
**Note:** These recommendations are guidelines, not strict requirements. Adapt them to your app as needed.
6+
7+
The best practices below are grouped by topic. Each has a priority that reflects how strongly the team recommends it. The list of priorities is as follows:
8+
9+
* **Strongly recommended:** You should implement this practice unless it clashes fundamentally with your approach.
10+
* **Recommended:** This practice is likely to improve your app.
11+
* **Optional:** This practice can improve your app in certain circumstances.
12+
13+
Before adopting these recommendations, you should be familiar with the Architecture guidance.
14+
15+
## Layered architecture
16+
17+
Our recommended layered architecture favors separation of concerns. It drives the UI from data models, complies with the single source of truth principle, and follows unidirectional data flow principles. Here are some best practices for layered architecture:
18+
19+
| Recommendation | Description |
20+
| --- | --- |
21+
| **Use a clearly defined data layer.**<br/>Strongly recommended | The data layer exposes application data to the rest of the app and contains the vast majority of business logic of your app.<br/>You should create repositories even if they just contain a single data source.<br/>In small apps, you can choose to place data layer types in a `data` package or module. |
22+
| **Use a clearly defined UI layer.**<br/>Strongly recommended | The UI layer displays the application data on the screen and serves as the primary point of user interaction.<br/>In small apps, you can choose to place UI layer types in a `ui` package or module.<br/>More UI layer best practices here. |
23+
| **The data layer should expose application data using a repository.**<br/>Strongly recommended | Components in the UI layer such as Activities, Fragments, or ViewModels shouldn't interact directly with a data source. Examples of data sources are databases, SharedPreferences, Firebase APIs, GPS, Bluetooth, or network status providers. |
24+
| **Use coroutines and flows.**<br/>Strongly recommended | Use Kotlin coroutines and flows to communicate between layers. Java code can access these APIs through interoperability. |
25+
| **Use a domain layer.**<br/>Recommended in big apps | Use a domain layer (use cases) if you need to reuse business logic that interacts with the data layer across multiple ViewModels, or you want to simplify the business logic complexity of a particular ViewModel. |
26+
27+
## UI layer
28+
29+
The role of the UI layer is to display the application data on the screen and serve as the primary point of user interaction. Here are some best practices for the UI layer:
30+
31+
| Recommendation | Description |
32+
| --- | --- |
33+
| **Follow Unidirectional Data Flow (UDF).**<br/>Strongly recommended | Follow UDF principles, where ViewModels expose UI state using the observer pattern and receive actions from the UI through method calls. |
34+
| **Use AAC ViewModels if their benefits apply to your app.**<br/>Strongly recommended | Use AndroidX ViewModels to handle business logic and fetch application data to expose UI state to the UI. |
35+
| **Use lifecycle-aware UI state collection.**<br/>Strongly recommended | Collect UI state from the UI using the appropriate lifecycle-aware coroutine builder: `repeatOnLifecycle` in the View system and `collectAsStateWithLifecycle` in Jetpack Compose. |
36+
| **Do not send events from the ViewModel to the UI.**<br/>Strongly recommended | Process the event immediately in the ViewModel and cause a state update with the result of handling the event. |
37+
| **Use a single-activity application.**<br/>Recommended | Use Navigation Fragments or Navigation Compose to navigate between screens and deep link to your app if your app has more than one screen. |
38+
| **Use Jetpack Compose.**<br/>Recommended | Use Jetpack Compose to build new apps for phones, tablets, foldables, and Wear OS. Compose code lives in Kotlin but can be integrated with Java-based projects. |
39+
40+
The following snippet outlines how to collect the UI state in a lifecycle-aware manner from a Java `Fragment`:
41+
42+
```java
43+
public class MyFragment extends Fragment {
44+
45+
private MyViewModel viewModel;
46+
47+
@Override
48+
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
49+
super.onViewCreated(view, savedInstanceState);
50+
viewModel = new ViewModelProvider(this).get(MyViewModel.class);
51+
52+
LifecycleOwner owner = getViewLifecycleOwner();
53+
LifecycleOwnerKt.getLifecycleScope(owner).launch(() ->
54+
LifecycleKt.repeatOnLifecycle(owner.getLifecycle(), Lifecycle.State.STARTED, () ->
55+
viewModel.getUiState().collect(value -> {
56+
// Process item
57+
return Unit.INSTANCE;
58+
})
59+
)
60+
);
61+
}
62+
}
63+
```
64+
65+
```kotlin
66+
// Compose (Kotlin)
67+
class MyFragment : Fragment() {
68+
69+
private val viewModel: MyViewModel by viewModel()
70+
71+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
72+
super.onViewCreated(view, savedInstanceState)
73+
74+
viewLifecycleOwner.lifecycleScope.launch {
75+
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
76+
viewModel.uiState.collect {
77+
// Process item
78+
}
79+
}
80+
}
81+
}
82+
}
83+
```
84+
85+
## ViewModel
86+
87+
ViewModels are responsible for providing the UI state and access to the data layer. Here are some best practices for ViewModels:
88+
89+
| Recommendation | Description |
90+
| --- | --- |
91+
| **ViewModels should be agnostic of the Android lifecycle.**<br/>Strongly recommended | ViewModels shouldn't hold a reference to any lifecycle-related type. Don't pass `Activity`, `Fragment`, `Context`, or `Resources` as a dependency. If something needs a `Context` in the ViewModel, evaluate if it is in the right layer. |
92+
| **Use coroutines and flows.**<br/>Strongly recommended | The ViewModel interacts with the data or domain layers using Kotlin flows for receiving application data and suspend functions to perform actions using `viewModelScope`. |
93+
| **Use ViewModels at screen level.**<br/>Strongly recommended | Do not use ViewModels in reusable pieces of UI. Use ViewModels in screen-level composables, Activities, Fragments, or destinations/graphs when using Jetpack Navigation. |
94+
| **Use plain state holder classes in reusable UI components.**<br/>Strongly recommended | Use plain state holder classes for handling complexity in reusable UI components. By doing this, the state can be hoisted and controlled externally. |
95+
| **Do not use AndroidViewModel.**<br/>Recommended | Use the `ViewModel` class, not `AndroidViewModel`. The `Application` class shouldn't be used in the ViewModel. Instead, move the dependency to the UI or the data layer. |
96+
| **Expose a UI state.**<br/>Recommended | ViewModels should expose data to the UI through a single property called `uiState`. You should make `uiState` a `StateFlow`. Use the `stateIn` operator with the `WhileSubscribed(5000)` policy when the data comes as a stream. For simpler cases, expose a `MutableStateFlow` as an immutable `StateFlow`. |
97+
98+
The following snippet outlines how to expose UI state from a ViewModel in Java:
99+
100+
```java
101+
@HiltViewModel
102+
public class BookmarksViewModel extends ViewModel {
103+
104+
private final StateFlow<NewsFeedUiState> feedState;
105+
106+
@Inject
107+
public BookmarksViewModel(NewsRepository newsRepository) {
108+
feedState = newsRepository
109+
.getNewsResourcesStream()
110+
.mapToFeedState(savedNewsResourcesState)
111+
.stateIn(
112+
ViewModelKt.getViewModelScope(this),
113+
SharingStarted.Companion.WhileSubscribed(5000),
114+
NewsFeedUiState.Loading
115+
);
116+
}
117+
118+
public StateFlow<NewsFeedUiState> getFeedState() {
119+
return feedState;
120+
}
121+
}
122+
```
123+
124+
## Lifecycle
125+
126+
The following are some best practices for working with the Android lifecycle:
127+
128+
| Recommendation | Description |
129+
| --- | --- |
130+
| **Do not override lifecycle methods in Activities or Fragments.**<br/>Strongly recommended | Do not override lifecycle methods such as `onResume` in Activities or Fragments. Use `LifecycleObserver` instead. If the app needs to perform work when the lifecycle reaches a certain state, use the `repeatOnLifecycle` API. |
131+
132+
The following snippet outlines how to perform operations given a certain lifecycle state:
133+
134+
```java
135+
public class MyFragment extends Fragment {
136+
@Override
137+
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
138+
super.onViewCreated(view, savedInstanceState);
139+
140+
getViewLifecycleOwner().getLifecycle().addObserver(new DefaultLifecycleObserver() {
141+
@Override
142+
public void onResume(@NonNull LifecycleOwner owner) {
143+
// ...
144+
}
145+
146+
@Override
147+
public void onPause(@NonNull LifecycleOwner owner) {
148+
// ...
149+
}
150+
});
151+
}
152+
}
153+
```
154+
155+
## Handle dependencies
156+
157+
There are several best practices you should observe when managing dependencies between components:
158+
159+
| Recommendation | Description |
160+
| --- | --- |
161+
| **Use dependency injection.**<br/>Strongly recommended | Use dependency injection best practices, mainly constructor injection when possible. |
162+
| **Scope to a component when necessary.**<br/>Strongly recommended | Scope to a dependency container when the type contains mutable data that needs to be shared or the type is expensive to initialize and is widely used in the app. |
163+
| **Use Hilt.**<br/>Recommended | Use Hilt or manual dependency injection in simple apps. Use Hilt if your project is complex enough. For example, if you have multiple screens with ViewModels, WorkManager usage, or advanced usage of Navigation such as ViewModels scoped to the nav graph. |
164+
165+
## Testing
166+
167+
The following are some best practices for testing:
168+
169+
| Recommendation | Description |
170+
| --- | --- |
171+
| **Know what to test.**<br/>Strongly recommended | Unless the project is roughly as simple as a hello world app, you should test it, at minimum with: unit test ViewModels (including flows), unit test data layer entities (repositories and data sources), and UI navigation tests that are useful as regression tests in CI. |
172+
| **Prefer fakes to mocks.**<br/>Strongly recommended | Read more in the “Use test doubles in Android” documentation. |
173+
| **Test StateFlows.**<br/>Strongly recommended | When testing `StateFlow`, assert on the `value` property whenever possible. Create a collectJob if using `WhileSubscribed`. For more information, check the “What to test in Android” guide. |
174+
175+
## Models
176+
177+
You should observe these best practices when developing models in your apps:
178+
179+
| Recommendation | Description |
180+
| --- | --- |
181+
| **Create a model per layer in complex apps.**<br/>Recommended | In complex apps, create new models in different layers or components when it makes sense. For example, a remote data source can map the model it receives through the network to a simpler class with just the data the app needs; repositories can map DAO models to simpler data classes with just the information the UI layer needs; ViewModel can include data layer models in `UiState` classes. |
182+
183+
## Naming conventions
184+
185+
When naming your codebase, you should be aware of the following best practices:
186+
187+
| Recommendation | Description |
188+
| --- | --- |
189+
| **Naming methods.**<br/>Optional | Methods should be a verb phrase. For example, `makePayment()`. |
190+
| **Naming properties.**<br/>Optional | Properties should be a noun phrase. For example, `inProgressTopicSelection`. |
191+
| **Naming streams of data.**<br/>Optional | When a class exposes a `Flow` stream, `LiveData`, or any other stream, the naming convention is `get{Model}Stream()`. For example, `getAuthorStream(): Flow<Author>`. If the function returns a list of models the model name should be in the plural: `getAuthorsStream(): Flow<List<Author>>`. |
192+
| **Naming interfaces implementations.**<br/>Optional | Names for the implementations of interfaces should be meaningful. Use `Default` as the prefix if a better name cannot be found. For example, for a `NewsRepository` interface, you could have an `OfflineFirstNewsRepository` or `InMemoryNewsRepository`. If you can find no good name, use `DefaultNewsRepository`. Fake implementations should be prefixed with `Fake`, as in `FakeAuthorsRepository`. |

0 commit comments

Comments
 (0)