Implementing Network Operations in Android Using MVVM and Retrofit
Implementing network operations within the Model-View-ViewModel (MVVM) architecture requires a clear separation of concerns. The repository acts as the single source of truth for data, the ViewModel manages UI-related state, and the View layer observes state changes with out handling business logic directly.
Defining the Network Client and API Interface
Begin by establishing the network configuration. Instead of scatering builder logic, encapsulate Retrofit initialization within a dedicated client module.
object NetworkProvider {
private const val BASE_URL = "https://api.example.com/v1/"
val retrofitClient: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(OkHttpClient.Builder().build())
.build()
}
}
interface ContentEndpoint {
@GET("articles")
suspend fun fetchLatestContent(): List<ArticleEntry>
}
Building the Data Repository
The repository handles raw network calls, error boundaries, and transforms responses into a consumable format. Wrapping results in a sealed interface provides type-safe state management for the upper layers.
sealed class DataState<out T> {
data class Success<T>(val payload: T) : DataState<T>()
data class Error(val exception: Throwable) : DataState<Nothing>()
object Loading : DataState<Nothing>()
}
class ArticleRepository {
private val apiService = NetworkProvider.retrofitClient.create(ContentEndpoint::class.java)
fun requestContent(): Flow<DataState<List<ArticleEntry>>> = flow {
emit(DataState.Loading)
try {
val result = apiService.fetchLatestContent()
emit(DataState.Success(result))
} catch (ex: Exception) {
emit(DataState.Error(ex))
}
}.flowOn(Dispatchers.IO)
}
Constructing the ViewModel
The ViewModel bridges the repository and the UI. It exposes an immutable stream of state and provides a public function to trigger the fetch operation.
class MainScreenViewModel : ViewModel() {
private val repo = ArticleRepository()
private val _uiState = MutableStateFlow<DataState<List<ArticleEntry>>>(DataState.Loading)
val uiState: StateFlow<DataState<List<ArticleEntry>>> = _uiState.asStateFlow()
fun loadContent() {
viewModelScope.launch {
repo.requestContent().collect { state ->
_uiState.value = state
}
}
}
}
Connecting the View Layer
The Activity or Fragment observes the StateFlow and reacts to state transitions. Lifecycle-aware collection ensures that UI updates only occur when the component is active.
class MainActivity : AppCompatActivity() {
private val vm: MainScreenViewModel by viewModels()
private lateinit var adapter: ContentAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
adapter = ContentAdapter()
findViewById<RecyclerView>(R.id.recycler_view).adapter = adapter
vm.loadContent()
lifecycleScope.launch {
vm.uiState.collect { state ->
when (state) {
is DataState.Loading -> showProgressIndicator()
is DataState.Success -> {
hideProgressIndicator()
adapter.submitList(state.payload)
}
is DataState.Error -> {
hideProgressIndicator()
displayErrorMessage(state.exception.message)
}
}
}
}
}
}
This structure decouples networking from presentation logic, ensuring that data flows unidirectionally. The repository isolates HTTP concerns, the ViewModel handles configuration changes and state exposure, and the view layer remains purely declarative, reacting only to emitted state objects.