User authentication with validation, network requests, and local caching
The User Component manages user authentication, providing login functionality with email and password validation. It demonstrates domain-driven validation, network authentication, and persistent user sessions.
Sealed interface defining all possible login errors.
package com.denisbrandi.androidrealca.user.domain.modelsealed interface LoginError { data object InvalidEmail : LoginError data object InvalidPassword : LoginError data object GenericError : LoginError data object IncorrectCredentials : LoginError}
Error Types Explained
InvalidEmail: Email format is invalid (fails regex validation)InvalidPassword: Password is too short (less than 8 characters)IncorrectCredentials: Server returned 401 (wrong email/password)GenericError: Network error or other unexpected failure
The repository defines the contract for user operations.
package com.denisbrandi.androidrealca.user.domain.repositoryimport com.denisbrandi.androidrealca.foundations.Answerimport com.denisbrandi.androidrealca.user.domain.model.*internal interface UserRepository { suspend fun login(loginRequest: LoginRequest): Answer<Unit, LoginError> fun getUser(): User fun isLoggedIn(): Boolean}
Implements the UserRepository interface with networking and caching.
package com.denisbrandi.androidrealca.user.data.repositoryimport com.denisbrandi.androidrealca.cache.*import com.denisbrandi.androidrealca.foundations.Answerimport com.denisbrandi.androidrealca.httpclient.AccessTokenProviderimport com.denisbrandi.androidrealca.user.data.model.*import com.denisbrandi.androidrealca.user.domain.model.*import com.denisbrandi.androidrealca.user.domain.repository.UserRepositoryimport io.ktor.client.HttpClientimport io.ktor.client.call.bodyimport io.ktor.client.request.*import io.ktor.client.statement.HttpResponseimport io.ktor.http.*internal class RealUserRepository( private val client: HttpClient, cacheProvider: CacheProvider) : UserRepository { private val cachedObject: CachedObject<JsonUserCacheDTO> by lazy { cacheProvider.getCachedObject( fileName = "user-cache", serializer = JsonUserCacheDTO.serializer(), defaultValue = DEFAULT_USER ) } override suspend fun login(loginRequest: LoginRequest): Answer<Unit, LoginError> { return try { val response = client.post("https://api.json-generator.com/templates/Q7s_NUVpyBND/data") { headers { append(HttpHeaders.ContentType, ContentType.Application.Json.toString()) val accessTokenHeader = AccessTokenProvider.getAccessTokenHeader() append(accessTokenHeader.first, accessTokenHeader.second) } setBody(JsonLoginRequestDTO(loginRequest.email, loginRequest.password)) } if (response.status.isSuccess()) { handleSuccessfulLoginResponse(response) } else { handleFailingLoginResponse(response) } } catch (t: Throwable) { t.printStackTrace() Answer.Error(LoginError.GenericError) } } private suspend fun handleSuccessfulLoginResponse(httpResponse: HttpResponse): Answer<Unit, LoginError> { val responseBody = httpResponse.body<JsonLoginResponseDTO>() cachedObject.put(JsonUserCacheDTO(responseBody.id, responseBody.fullName)) return Answer.Success(Unit) } private fun handleFailingLoginResponse(httpResponse: HttpResponse): Answer<Unit, LoginError> { val error = if (httpResponse.status.value == 401) { LoginError.IncorrectCredentials } else { LoginError.GenericError } return Answer.Error(error) } override fun getUser(): User { val cachedUser = cachedObject.get() return User(cachedUser.id, cachedUser.fullName) } override fun isLoggedIn(): Boolean { return cachedObject.get() != DEFAULT_USER } companion object { val DEFAULT_USER = JsonUserCacheDTO("", "") }}
Implementation Highlights
HTTP POST for login: Sends credentials to authentication endpointStatus code mapping: 401 → IncorrectCredentials, other errors → GenericErrorPersistent session: Stores user data in cache after successful loginDefault user pattern: Uses empty DTO to represent “not logged in” state
package com.denisbrandi.androidrealca.user.data.modelimport kotlinx.serialization.*@Serializableinternal class JsonLoginRequestDTO( @SerialName("email") val email: String, @SerialName("password") val password: String)@Serializableinternal class JsonLoginResponseDTO( @SerialName("id") val id: String, @SerialName("fullName") val fullName: String)@Serializableinternal data class JsonUserCacheDTO( @SerialName("id") val id: String, @SerialName("fullName") val fullName: String)
JsonLoginRequestDTO: Sent to server with credentialsJsonLoginResponseDTO: Received from server on successJsonUserCacheDTO: Stored locally for session persistence
The user component demonstrates the Value Object pattern for domain validation:
// Instead of primitive obsession:fun validateEmail(email: String): Boolean { /* ... */ }// Use value objects:class Email(private val value: String) { fun isValid(): Boolean { /* ... */ }}// Benefits:// 1. Encapsulation - validation logic lives with the data// 2. Type safety - can't accidentally pass email as password// 3. Reusability - validation is consistent everywhere// 4. Testability - easy to test Email in isolation
Value objects are a key DDD (Domain-Driven Design) pattern that makes your domain model more expressive and robust.
Here’s how the user component is used throughout the application:
// In Login UI ViewModelclass LoginViewModel( private val login: Login, private val isUserLoggedIn: IsUserLoggedIn) { fun attemptLogin(email: String, password: String) { viewModelScope.launch { val result = login(LoginRequest(email, password)) result.fold( success = { // Navigate to main screen navigateToHome() }, error = { loginError -> when (loginError) { LoginError.InvalidEmail -> showError("Invalid email format") LoginError.InvalidPassword -> showError("Password must be 8+ characters") LoginError.IncorrectCredentials -> showError("Wrong email or password") LoginError.GenericError -> showError("Network error, try again") } } ) } }}// In other components (Cart, Wishlist, etc.)class AddCartItemUseCase( private val getUser: GetUser, private val cartRepository: CartRepository) { override fun invoke(cartItem: CartItem) { val userId = getUser().id // Get current user ID cartRepository.updateCartItem(userId, cartItem) }}
Always call isUserLoggedIn() before accessing getUser() in contexts where the user might not be authenticated.