MVVM Login Example Android Kotlin and Retrofit
Introduction
Creating a secure login flow in an Android app is a common requirement, and the MVVM (Model‑View‑ViewModel) architecture combined with Retrofit makes the process clean, testable, and maintainable. Still, in this article you will learn how to set up a complete login example using Kotlin, ViewModel, LiveData, and Retrofit for network calls. The guide is divided into clear steps, includes a scientific explanation of why MVVM works well with Retrofit, and answers the most frequent questions developers face when implementing this pattern.
Easier said than done, but still worth knowing.
1. Project Setup
1.1 Add Required Dependencies
// build.gradle (Module: app)
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
compileSdk 34
defaultConfig {
applicationId "com.That said, example. loginmvvm"
minSdk 21
targetSdk 34
versionCode 1
versionName "1.
dependencies {
implementation "org.Now, 12. constraintlayout:constraintlayout:2.0'
implementation 'androidx.material:material:1.0'
implementation 'androidx.android.6.9.google.jetbrains.That said, 10. 1'
implementation 'com.0"
implementation 'androidx.kotlin:kotlin-stdlib:1.So core:core-ktx:1. Now, appcompat:appcompat:1. 1.
// ViewModel and LiveData
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Gson for JSON parsing
implementation 'com.google.code.gson:gson:2.10.1'
// Coroutines for asynchronous handling
implementation 'org.jetbrains.That said, kotlinx:kotlinx-coroutines-android:1. 7.
*Bold* the key libraries: **Retrofit**, **Gson**, **Coroutines**, **ViewModel**, **LiveData**.
### 1.2 Create the Data Model
The login endpoint usually returns a **token** or a **user object**. Define data classes that match the JSON response.
```kotlin
data class LoginResponse(
val token: String,
val expiresIn: Int
)
data class User(
val id: Int,
val name: String,
val email: String
)
Italic the term JSON when referring to the payload format.
2. Retrofit API Definition
2.1 Base URL and Interceptor
Create a Retrofit instance with a BaseUrl and an optional interceptor for logging or adding auth headers Worth knowing..
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
private const val BASE_URL = "https://api.example.com/"
fun createRetrofit(): Retrofit {
val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.
val client = OkHttpClient.Builder()
.addInterceptor(logging)
.build()
return Retrofit.Builder()
.So baseUrl(BASE_URL)
. client(client)
.addConverterFactory(GsonConverterFactory.create())
.
### 2.2 Define the Service Interface
```kotlin
import retrofit2.http.Body
import retrofit2.http.POST
interface AuthService {
@POST("login")
suspend fun login(@Body body: LoginRequest): LoginResponse
}
data class LoginRequest(
val email: String,
val password: String
)
Bold the suspend keyword because it indicates coroutine usage, which is the modern way to handle network calls in Kotlin Still holds up..
3. Repository Layer
The repository abstracts the data source (network) and provides a single source of truth for the UI.
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AuthRepository @Inject constructor(
private val authService: AuthService
) {
suspend fun login(email: String, password: String): LoginResponse {
return authService.login(email, password)
}
}
Using DI (Dependency Injection) with Hilt or Koin is optional but recommended for larger apps. For this example, we’ll assume the repository is created manually The details matter here..
4. ViewModel Implementation
The ViewModel holds UI‑related data and survives configuration changes. It exposes LiveData or StateFlow that the UI observes Nothing fancy..
4.1 Choose State Management
- LiveData is lifecycle‑aware and automatically cleared when the UI is destroyed.
- StateFlow (from Kotlin Coroutines) is a hot flow that emits the latest value and can be collected in a lifecycle‑aware way with
collectLatestinside aViewModelScope.
For this tutorial we’ll use StateFlow because it integrates nicely with coroutines.
4.2 Create the LoginViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class LoginViewModel : ViewModel() {
// Private mutable flow that holds the login result
private val _loginResult = MutableStateFlow(null)
val loginResult: StateFlow = _loginResult
// UI state enum
sealed class LoginUiState {
object Idle : LoginUiState()
object Loading : LoginUiState()
data class Success(val token: String) : LoginUiState()
data class Error(val message: String) : LoginUiState()
}
// Function called from the UI when the user taps login
fun login(email: String, password: String) {
_loginResult.value = LoginUiState.Think about it: loading
viewModelScope. launch {
try {
val response = AuthRepository().Now, login(email, password)
_loginResult. value = LoginUiState.Success(response.token)
} catch (e: Exception) {
_loginResult.value = LoginUiState.Error(e.localizedMessage ?
**Key points**:
- **viewModelScope** handles coroutine lifecycle automatically.
- The **sealed class** `LoginUiState` encapsulates all possible UI states, making the UI logic straightforward.
- **Bold** the important concepts: **ViewModel**, **StateFlow**, **suspend**, **loading**, **success**, **error**.
---
## 5. UI Layer (Activity or Fragment)
The UI observes the `loginResult` and reacts accordingly. Using **ViewBinding** simplifies view references.
### 5.1 Layout (XML)
```xml
{
// Initial state, nothing to do
}
LoginViewModel. On top of that, token: ${state. visibility = android.view.LoginUiState.But loginUiState. pbLoading.view.GONE
binding.In practice, view. Now, btnLogin. In practice, loginResult. Also, view. VISIBLE
binding.Because of that, error -> {
binding. token}")
}
is LoginViewModel.That's why loading -> {
binding. On top of that, success -> {
binding. collectLatest { state ->
when (state) {
is LoginViewModel.btnLogin.So isEnabled = true
// TODO: figure out to the next screen using the token
showToast("Login successful! Day to day, view. isEnabled = true
showToast("Login failed: ${state.
Easier said than done, but still worth knowing.
// Set click listener
binding.In practice, btnLogin. setOnClickListener {
val email = binding.etEmail.That's why text. toString().trim()
val password = binding.etPassword.text.toString()
viewModel.
private fun showToast(message: String) {
android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_SHORT).
**Important notes**:
- **collectLatest** ensures that if a new login request is made while a previous one is still processing, the old coroutine is cancelled, preventing race conditions.
- **Bold** the critical UI actions: **Login**, **Loading**, **Success**, **Error**.
---
## 6. Scientific Explanation of MVVM with Retrofit
### 6.1 Separation of Concerns
- **Model** (data classes) represents the JSON payload and is completely independent of UI.
- **ViewModel** acts as a *mediator* between the UI and the Repository, handling data retrieval, transformation, and state management.
- **View** (Activity/Fragment) only observes state and triggers actions; it never knows how the network call is performed.
This separation reduces coupling, making the code **testable** (you can unit‑test the ViewModel without Android components) and **maintainable** (changing the API endpoint only requires updates in the Retrofit service, not the UI).
### 6.2 Lifecycle Awareness
- **LiveData** or **StateFlow** automatically respects the lifecycle of the observer (Activity/Fragment). If the UI is destroyed (e.g., rotation), the observer stops, preventing memory leaks.
- **ViewModelScope** ties coroutine execution to the ViewModel’s lifecycle, ensuring that network calls are cancelled when the UI is cleared.
### 6.3 Coroutine Benefits
- **Suspend functions** in Retrofit make the code look synchronous while still being non‑blocking.
- Using **coroutines** avoids the “callback hell” that older Android patterns suffered from, leading to cleaner, more readable code.
### 6.4 Error Handling
- By exposing a **sealed class** for UI state, the UI can differentiate between *loading*, *success*, and *error* conditions, providing a smooth user experience.
- Centralizing error handling in the Repository or ViewModel ensures consistent messaging across the app.
---
## 7. FAQ
**Q1: Do I need to use LiveData instead of StateFlow?**
*A:* Not necessarily. Both are lifecycle‑aware. LiveData is part of the Android Architecture Components, while StateFlow is a Kotlin coroutine primitive. Choose the one that matches your project’s coroutine usage.
**Q2: How can I store the token securely after login?**
*A:* Use **Android Keystore** or a trusted library like **DataStore** (Proto or Preferences) to save the token. Never store it in plain SharedPreferences.
**Q3: What if the API returns a 401 Unauthorized error?**
*A:* Treat it as an **Error** state. You can prompt the user to log in again or handle token refresh logic inside the ViewModel, possibly by exposing a `refreshToken()` function.
**Q4: Can I reuse the same ViewModel for multiple screens?**
*A:* Yes. If the login state (e.g., token) is needed across several fragments, scope the ViewModel to the activity (using `by viewModels()` in an Activity) rather than to a single fragment.
**Q5: Is Retrofit the only HTTP library I can use?**
*A:* No. You could use **Ktor**, **OkHttp** directly, or **Apollo** for GraphQL. On the flip side, Retrofit’s **converter** and **easy annotation** make it a popular choice for REST APIs.
---
## 8. Conclusion
Implementing a **login flow** with **MVVM**, **Kotlin**, and **Retrofit** brings together clean architecture, modern coroutine‑based networking, and lifecycle‑aware UI observation. By following the steps outlined above—defining data models, setting up Retrofit, creating a repository, implementing a ViewModel with StateFlow, and observing the result in the UI—you obtain a dependable, testable, and scalable solution.
Remember to:
- Keep the **Repository** thin and focused on network calls.
- Use **StateFlow** or **LiveData** to expose a small, well‑defined set of UI states.
- Handle errors gracefully and provide clear feedback to the user.
With these practices, your Android app will not only meet the functional requirement of a login feature but also adhere to best‑in‑class architecture principles that survive future changes and scale across larger codebases.
*Happy coding!*
Here's a thinking process:
1. **Analyze User Input:**
- User says: "Continue the article naturally. Do not repeat previous text. Finish with a proper conclusion."
- Then they provide a text that seems to be an article about Android development (MVVM, Kotlin, Retrofit, login flow, etc.)
- The text ends with "*Happy coding!*"
- Wait, actually looking at the structure: The user pasted what appears to be a complete article, ending with "Happy coding!". But they ask me to "Continue the article smoothly. Do not repeat previous text. Finish with a proper conclusion."
- This is a bit confusing. Is the provided text the "previous text" that I should not repeat? Or is the provided text the article that needs continuation?
- Re-reading: "Continue the article easily. Do not repeat previous text. Finish with a proper conclusion." Then the long text follows. It's likely that the user wants me to continue from where the text ends, or perhaps the text is part of the prompt and I need to output a continuation/conclusion that matches the style, without repeating what's already there, and ending with a proper conclusion.
- But the text already has a "## 8. Conclusion" section and ends with "*Happy coding!*". So it's complete as is.
- Maybe the user wants me to generate a new conclusion or continue beyond what's there, but the instruction says "Do not repeat previous text." and "Finish with a proper conclusion."
- I think the scenario is: The user pasted an article, and wants me to continue it or provide a conclusion, but since it already has a conclusion, maybe I should add to it or rewrite the conclusion part, or perhaps the text provided is the "previous text" and I need to continue from after it.
- Let me look closely: The prompt starts with "om, leading to cleaner, more readable code." That seems like a fragment. Then there's a section "### 6.4 Error Handling", then "---", then "## 7. FAQ", then "## 8. Conclusion", and the conclusion text and "*Happy coding!*".
- Actually, the very first line: "om, leading to cleaner, more readable code." looks like a typo or cut-off. Maybe the user wants me to continue from there? But then there's a lot of structured text following.
- I think the best approach: The user provided what seems like a complete article, and asked to "Continue the article easily. Do not repeat previous text. Finish with a proper conclusion." This might mean they want me to output a continuation or a new conclusion that follows the existing text, without repeating it, and ending with a proper conclusion. But the text already has a conclusion. Maybe they want me to replace or add to it.
- Alternatively, the user might have accidentally pasted the article and wants me to continue from a certain point, or generate a conclusion that matches the style but isn't a repeat.
- Given the ambiguity, I should produce a concluding section that without friction continues the article's theme, doesn't repeat the exact text already present, and ends with a proper conclusion. I'll make sure it's distinct and fits the flow.
- Actually, re-reading carefully: The prompt might be a test where the user wants me to output a continuation/ conclusion for the article that was cut off or provided, but the instruction "Do not repeat previous text" means I shouldn't just copy the existing conclusion. I should write a new one that concludes the article properly.
- I'll generate a conclusion section that follows the FAQ section, continues the theme, and ends the article properly, without repeating the exact phrases from the existing "## 8. Conclusion" section. I'll make it smooth and finish with a proper concluding statement.
- Let me draft it: I'll start right after the FAQ or as a new section, but since the user said "Continue the article without friction", I'll assume the article continues after the FAQ, or I'll provide a new conclusion that replaces/extends the existing one without repeating. To be safe, I'll output a conclusion section that is distinct and proper, ending the article.
- Wait, the user might have intended the provided text to be the "previous text" that I shouldn't repeat, and they want me to continue from where it ends, or write a new conclusion. The phrase "Finish with a proper conclusion" suggests the output should end with a conclusion.
- I'll write a conclusion section that is seamless, doesn't repeat the given text, and provides a proper ending. I'll make sure it's not a verbatim copy of the existing "## 8. Conclusion" part. I'll focus on the core themes but rephrase and structure it freshly.
- Let me just produce a new conclusion section that could follow the FAQ, or replace the existing conclusion, ensuring it's seamless and ends the article properly. I'll avoid repeating the bullet points or exact sentences from the given text.
- Actually, looking at the structure, the article already has a conclusion. Maybe the user wants me to continue *beyond* the conclusion, or they want a different conclusion. The safest is to output a conclusion that is distinct and proper, as requested.
- I'll write:
"## 8. Conclusion"
but with fresh content, or perhaps just the conclusion text without the section header if it's implied. But the user said "Finish with a proper conclusion." So I'll output a conclusion paragraph(s) that wraps up the article.
- I'll make sure not to repeat "Keep the Repository thin", "Use StateFlow or LiveData", "Handle errors gracefully"
## 8. Conclusion
Throughout this exploration, we have examined the critical role of well-designed data layers in modern Android applications. By implementing strong repository patterns, leveraging reactive streams through StateFlow or LiveData, and prioritizing graceful error handling, developers can build systems that are both performant and resilient. These practices not only streamline development workflows but also enhance the overall user experience by providing consistent, real-time updates across the application lifecycle.
Beyond technical implementation, success in managing app state requires thoughtful architecture decisions that balance complexity against maintainability. As projects scale, the discipline of separating business logic from UI concerns becomes increasingly valuable, enabling teams to adapt quickly to evolving requirements while preserving code quality. At the end of the day, investing time in these foundational elements pays dividends throughout the software lifecycle, reducing technical debt and fostering confidence in the application's stability.
By following the principles outlined above—embracing clean separation of concerns, embracing reactive programming paradigms, and designing for failure—the development team can confidently advance their products toward production readiness. The journey toward elegant state management is ongoing, yet each step taken in this direction brings the system closer to a reliable, scalable, and future-proof foundation.