Android Jetpack Compose Codelab

Android Jetpack Compose | Part 5| FetchRoutes using Clean Architecture, ViewModel, Repository, ApiService, Dagger-Hilt, Retrofit, Coroutines

CodeLabsPro
4 min readFeb 11, 2024
Android Jetpack Compose | Codelab | Part 5

Introduction

In Part 4 of the Jetpack Compose Codelab, we learned how to implement Login and Registration.

In this part, we learn how to parse the response from the following endpoint

http://localhost:8000/api/routes

The above endpoint returns a list of stops on the route in the below format


[
{
"id": 1,
"title": "Loadout from New York Times HQ and distribution",
"slug": "loadout-from-new-york-times-hq-and-distribution",
"route_assigned_to_user": {
"id": 3,
"password": "pbkdf2_sha256$600000$wU0voCXGbeUcWy80qiT16f$VwrpSjkN5Qp5lhmxv6MGgsK0KQ2LROyuoykhThXZuTQ=",
"last_login": "2024-02-09T08:39:35.110Z",
"is_superuser": false,
"username": "johannes",
"first_name": "Johannes",
"last_name": "V",
"is_staff": false,
"date_joined": "2024-02-09T08:30:41.701Z",
"email": "johannes@fr8pro.ai",
"is_deactivated": false,
"is_active": true,
"groups": [],
"user_permissions": []
},
"stops": [
{
"id": 1,
"title": "Loadout from New York Times HQ, Wall Street",
"description": "Loadout from New York Times HQ, Wall Street",
"origin_address": null,
"origin_address_full": null,
"origin_window_range_start": null,
"origin_notes": null,
"destination_address": null,
"destination_address_full": null,
"destination_window_range_start": null,
"destination_notes": null,
"origin_tracking_number": null,
"load_number": null,
"manifest_number": null,
"destination_tracking_number": null,
"image": null,
"thumbnail": null,
"proof_of_pickup_picture_1": null,
"proof_of_delivery_picture_1": null,
"contact_user": null,
"notes": null
},
{
"id": 2,
"title": "Dropoff at New York Stock Exchange, Wall Street",
"description": "Dropoff at New York Stock Exchange, Wall Street",
"origin_address": null,
"origin_address_full": null,
"origin_window_range_start": null,
"origin_notes": null,
"destination_address": null,
"destination_address_full": null,
"destination_window_range_start": null,
"destination_notes": null,
"origin_tracking_number": null,
"load_number": null,
"manifest_number": null,
"destination_tracking_number": null,
"image": null,
"thumbnail": null,
"proof_of_pickup_picture_1": null,
"proof_of_delivery_picture_1": null,
"contact_user": null,
"notes": null
},
{
"id": 3,
"title": "Dropoff at NASDAQ, Wall Street",
"description": "Dropoff at NASDAQ, Wall Street",
"origin_address": null,
"origin_address_full": null,
"origin_window_range_start": null,
"origin_notes": null,
"destination_address": null,
"destination_address_full": null,
"destination_window_range_start": null,
"destination_notes": null,
"origin_tracking_number": null,
"load_number": null,
"manifest_number": null,
"destination_tracking_number": null,
"image": null,
"thumbnail": null,
"proof_of_pickup_picture_1": null,
"proof_of_delivery_picture_1": null,
"contact_user": null,
"notes": null
},
{
"id": 9,
"title": "Dropoff at Grand Central Terminal",
"description": "Dropoff at Grand Central Terminal",
"contact_user": 3
}
],
"notes": null
}
]

Steps

We can follow the steps below to parse and display the routes and stops for user number 3

Step 1 : Constants

Create network/common/Constants.kt as below



class Constants {

companion object {
const val BASE_URL = "https://fr8pro.app/"
// POST endpoint for login
const val LOGIN_URL = "auth/jwt/create/"
// POST endpoint for registration
const val REGISTRATION_URL = "auth/users/"

// GET endpoint for routes
const val GET_ROUTES = "api/routes?route_assigned_to_user=3"
}
}

Step 2: FetchRoutesResponse and related classes as below



import com.google.gson.annotations.SerializedName

data class FetchRoutesResponse(
val items : ArrayList<FetchRoutesResponseItem>
)

data class FetchRoutesResponseItem(
@SerializedName("id")
val id: Int,
@SerializedName("notes")
val notes: Any,
@SerializedName("route_assigned_to_user")
val routeAssignedToUser: CustomUser,
@SerializedName("slug")
val slug: String,
@SerializedName("stops")
val stops: List<Stop>,
@SerializedName("title")
val title: String
)

data class CustomUser(
@SerializedName("date_joined")
val dateJoined: String,
@SerializedName("email")
val email: String,
@SerializedName("first_name")
val firstName: String,
@SerializedName("groups")
val groups: List<Any>,
@SerializedName("id")
val id: Int,
@SerializedName("is_active")
val isActive: Boolean,
@SerializedName("is_deactivated")
val isDeactivated: Boolean,
@SerializedName("is_staff")
val isStaff: Boolean,
@SerializedName("is_superuser")
val isSuperuser: Boolean,
@SerializedName("last_login")
val lastLogin: String,
@SerializedName("last_name")
val lastName: String,
@SerializedName("password")
val password: String,
@SerializedName("user_permissions")
val userPermissions: List<Any>,
@SerializedName("username")
val username: String
)

data class Stop(
@SerializedName("contact_user")
val contactUser: Any,
@SerializedName("description")
val description: String,
@SerializedName("destination_address")
val destinationAddress: Any,
@SerializedName("destination_address_full")
val destinationAddressFull: Any,
@SerializedName("destination_notes")
val destinationNotes: Any,
@SerializedName("destination_tracking_number")
val destinationTrackingNumber: Any,
@SerializedName("destination_window_range_start")
val destinationWindowRangeStart: Any,
@SerializedName("id")
val id: Int,
@SerializedName("image")
val image: Any,
@SerializedName("load_number")
val loadNumber: Any,
@SerializedName("manifest_number")
val manifestNumber: Any,
@SerializedName("notes")
val notes: Any,
@SerializedName("origin_address")
val originAddress: Any,
@SerializedName("origin_address_full")
val originAddressFull: Any,
@SerializedName("origin_notes")
val originNotes: Any,
@SerializedName("origin_tracking_number")
val originTrackingNumber: Any,
@SerializedName("origin_window_range_start")
val originWindowRangeStart: Any,
@SerializedName("proof_of_delivery_picture_1")
val proofOfDeliveryPicture1: Any,
@SerializedName("proof_of_pickup_picture_1")
val proofOfPickupPicture1: Any,
@SerializedName("thumbnail")
val thumbnail: Any,
@SerializedName("title")
val title: String
)

Step 3: ApiService

Declare fetchRoutes function in network/service/ApiService.kt as below


interface ApiService {
@POST(Constants.LOGIN_URL)
suspend fun loginUser(
@Body request: LoginRequest
): Response<LoginResponse>

@POST(Constants.REGISTRATION_URL)
suspend fun registerUser(
@Body request: RegisterRequest
): Response<RegisterResponse>

@GET(Constants.GET_ROUTES)
suspend fun fetchRoutes(
): Response<FetchRoutesResponse>
}

Step 4: RoutesRepository

Implement repo/RoutesRepository as below


@ViewModelScoped
class RoutesRepository @Inject constructor(
private val apiService: ApiService,
private val preferencesDataStore: DataStore<Preferences>
) {
suspend fun fetchRoutes(): Result<FetchRoutesResponse> {
return try {
val response = apiService.fetchRoutes()
if (response.isSuccessful && response.body() != null) {
Result.success(response.body()!!)
} else {
Result.failure(Exception("Error"))
}
} catch (e: Exception) {
Result.failure(e)
}
}

}

Step 5: RoutesViewModel

Implement network/viewmodel/RoutesViewModel as below

@HiltViewModel
open class RoutesViewModel @Inject constructor(
private val routesRepository: RoutesRepository
) : AndroidViewModel(Application()) {
private val _routeUiState = MutableStateFlow<FetchRoutesUiState>(FetchRoutesUiState.Idle)
val routeUiState: StateFlow<FetchRoutesUiState> = _routeUiState.asStateFlow()

fun fetchRoutes() {
_routeUiState.value = FetchRoutesUiState.Loading

viewModelScope.launch {
try {
val result = routesRepository.fetchRoutes()
result.onSuccess { response ->
_routeUiState.value = FetchRoutesUiState.Success(response)
}.onFailure { throwable ->
_routeUiState.value = FetchRoutesUiState.Error(throwable.message ?: "Unknown error")
}
} catch (e: Exception) {
_routeUiState.value = FetchRoutesUiState.Error(e.message ?: "An error occurred")
}
}
}
}



sealed class FetchRoutesUiState {
object Loading : FetchRoutesUiState()
data class Success(val user: FetchRoutesResponse) : FetchRoutesUiState()
data class Error(val exception: String) : FetchRoutesUiState()
object Idle : FetchRoutesUiState() // Added to represent the initial state
}

Step 6: AppModule

Add provideRoutesRepository to di/AppModule as below


@Provides
@Singleton
fun provideRoutesRepository(
apiService: ApiService,
preferencesDataStore: DataStore<Preferences>
): RoutesRepository =
RoutesRepository(apiService, preferencesDataStore)

Step 7: RoutesScreen

Implement screens/RoutesScreen.kt as below


import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.flow.collect

@Composable
fun RoutesScreen(
navController: NavController,
navigator: DestinationsNavigator,
authViewModel: AuthViewModel = hiltViewModel(),
routesViewModel: RoutesViewModel = viewModel()
) {
// Collect the latest state from the ViewModel
val routeUiState by routesViewModel.routeUiState.collectAsState()

// Display UI based on the state
when (routeUiState) {
is LoginUiState.Idle -> {} // Handle idle state if needed
is LoginUiState.Loading -> {
CircularProgressIndicator(modifier = Modifier.padding(16.dp))
}
is LoginUiState.Success -> {
val routes = (routeUiState as LoginUiState.Success).routes
RoutesList(routes)
}
is LoginUiState.Error -> {
Text(
text = (routeUiState as LoginUiState.Error).message,
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colors.error
)
}
}
}

@Composable
fun RoutesList(routes: List<Route>) { // Assuming Route is your data class
LazyColumn {
items(routes) { route ->
RouteItem(route)
}
}
}

@Composable
fun RouteItem(route: Route) {
Card(modifier = Modifier
.fillMaxWidth()
.padding(8.dp)) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = route.name, style = MaterialTheme.typography.h6)
Spacer(modifier = Modifier.height(4.dp))
Text(text = route.description) // Assuming Route has name and description
}
}
}

--

--

CodeLabsPro

www.codelabspro.com Developer @CodeSDK • #EdTech • Android Certified • Activating Open Source