Android Architecture series · Part 7 of 9
Data, Network and Configuration Changes
This part fills the Data box (Room, Retrofit, gRPC) and then shows what happens when rotation, language, font or window size changes, and why state must not live in the Activity.
- Local: Room for tables, DataStore for settings.
- Remote: Retrofit + OkHttp; gRPC for contract-first and streaming.
- Microservices: independent teams; use a gateway so the phone calls one API.
- Config changes: Activity is recreated; keep state in ViewModel, SavedStateHandle or storage.
Contents
1. Local data with Room
Room is Jetpack’s layer over SQLite. You describe tables as classes and queries as functions; Room checks the SQL at compile time. It belongs to the Data box, behind your repository.
@Entity(tableName = "profile")
data class ProfileEntity(@PrimaryKey val id: String, val name: String, val active: Boolean)
@Dao
interface ProfileDao {
@Query("SELECT * FROM profile WHERE id = :id")
suspend fun find(id: String): ProfileEntity?
@Query("SELECT * FROM profile WHERE id = :id")
fun observe(id: String): Flow<ProfileEntity?> // emits again when data changes
@Upsert suspend fun save(entity: ProfileEntity)
}
@Database(entities = [ProfileEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() { abstract fun profileDao(): ProfileDao }
For small key-value settings (theme, language, onboarding done) use DataStore instead of SharedPreferences. Keep ProfileEntity (database shape) separate from Profile (what the app uses) and convert with a small mapper, so a database change does not ripple into the UI.
2. Remote data with Retrofit
Retrofit turns a REST API into a Kotlin interface, usually on top of OkHttp (the HTTP client) and a JSON converter such as kotlinx.serialization or Moshi.
interface ProfileApi {
@GET("profiles/{id}")
suspend fun profile(@Path("id") id: String): ProfileDto
}
val api = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(OkHttpClient.Builder().build())
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
.create(ProfileApi::class.java)
Together with the repository from Part 2 you get the classic offline-first pattern: the database is the single source of truth. The repository refreshes from the network into the database, and the screen observes the database.
3. What is gRPC?
gRPC is a way for two programs to call each other’s functions over the network. Instead of REST + JSON (“send text to a URL”), you write a contract in a .proto file, and tools generate the client and server code in your language.
// profile.proto
syntax = "proto3";
service ProfileService {
rpc GetProfile (ProfileRequest) returns (ProfileReply);
rpc WatchProfile (ProfileRequest) returns (stream ProfileReply); // streaming
}
message ProfileRequest { string id = 1; }
message ProfileReply { string id = 1; string name = 2; bool active = 3; }
| REST + JSON | gRPC + protobuf | |
|---|---|---|
| Format | Text, human-readable | Binary, smaller and faster to parse |
| Contract | Often docs or OpenAPI | The .proto file is the contract; code is generated |
| Transport | HTTP/1.1 or 2 | HTTP/2, supports streaming both ways |
| Browser/debug | Easy (curl, browser) | Needs tooling |
For Android: gRPC is a good fit when the backend already uses it or you need streaming. Architecturally nothing changes: the generated client sits behind a RemoteRepository, and the rest of the app never knows. The same protobuf messages can also be stored locally with Proto DataStore.
4. How microservices help
A microservice backend is split into many small services (accounts, orders, payments), each with its own data and its own deploys, instead of one giant server. For a mobile team this matters in three ways:
- Independent change: the payments team can ship without waiting for the profile team. This mirrors why you split the app into feature modules.
- Clear contracts: every service publishes a contract (OpenAPI or proto), which your repositories map to domain models.
- Resilience: if recommendations are down, the rest of the app can still work (handle the error per feature, Part 6).
Do not call ten services from the phone. Put an API gateway or a “backend for frontend” in front that combines them. Fewer round trips save battery and data, and the app talks to one tidy API.
5. Surviving configuration changes
By default, when the device configuration changes, Android destroys and recreates the Activity so it can load matching resources. These changes include:
- Rotation (orientation)
- Language (locale)
- Font size (
fontScale), plus dark mode - Screen size: split-screen, multi-window, foldables, desktop windows
The recreated Activity starts with empty fields. This is where architecture earns its keep: state must live outside the Activity.
| Where state lives | Rotation / language / font / size change | Process death (system kills app in background) |
|---|---|---|
| Activity / View fields | Lost | Lost |
ViewModel (MVVM) or retained presenter | Survives | Lost |
SavedStateHandle / onSaveInstanceState | Survives | Survives (small data only) |
| Room / DataStore / files | Survives | Survives |
class FormViewModel(private val saved: SavedStateHandle) : ViewModel() {
// typed text survives rotation AND process death
val draft: StateFlow<String> = saved.getStateFlow("draft", "")
fun onDraftChanged(text: String) { saved["draft"] = text }
}
What each architecture does
- MVVM / MVI: the ViewModel survives the recreation, and the new Activity just collects the same state again. Easiest.
- MVP: the presenter dies with the Activity unless you retain it (for example inside a ViewModel). Many teams make the presenter state a plain object held by a ViewModel.
- VIPER: same as MVP for the presenter; interactors are stateless so nothing to save.
Language, font and size specifically
- Language: do not store translated strings in the ViewModel; store a resource id or a small
UiTextwrapper and resolve it in the view, so the new language appears after recreation. For an in-app language picker useAppCompatDelegate.setApplicationLocales()and persist the choice (DataStore). - Font size: use
spfor text, let containers grow or scroll, and test at the largest size. Do not hide controls with fixed heights. - Screen size: design with window size classes (compact, medium, expanded) and layout resource qualifiers. The same ViewModel can serve a phone and a tablet layout.
- Avoid the shortcut: declaring
android:configChangesto stop recreation hides bugs. Only use it for special cases such as video players, and handle the change yourself.
Turn on “Don’t keep activities” in Developer options, rotate the phone, change the language and font size. If the screen keeps its state, your architecture works.