Android Studio Project

Online version of the directory app

Click the button above to download the code for our next example project. This project is an online version of the Directory app. This version of the app communicates with a web service that stores all of the directory entries for all of the users who use the app.

We will be using the same back end Directory application running at https://cmsc106.net/directory that used in the earlier assignment to serve the needs of this app.

Retrofit Interface

Once again we are going to be using Retrofit to manage the communication with our back end server. The first step in using Retrofit is to set up an interface with methods for each distinct server interaction we plan to do. Here is the interface for this app:

import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.DELETE
import retrofit2.http.Body
import retrofit2.http.Query
import retrofit2.http.Path

interface DirectoryApiService {
    @POST("users")
    suspend fun newUser(@Body user : User) : String

    @GET("users")
    suspend fun login(@Query("user") user : String,@Query("password") password : String) : String

    @GET("people/{key}")
    suspend fun getPeople(@Path("key") key : String) : List<Person>

    @POST("people/{key}")
    suspend fun postPerson(@Path("key") key : String,@Body person : Person) : Person

    @DELETE("people/{key}/{id}")
    suspend fun removePerson(@Path("key") key : String,@Path("id") id : Int) : String
}

You have already worked with most of these methods in the assigment. This version adds one additional method to handle a DELETE request.

Gateway class

Once again this example is going to feature the use of a View Model class that acts as the gateway to the back end service. Here is the code for that class:

package edu.lawrence.androiddirectoryonline

import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class DirectoryModel : ViewModel() {
    private lateinit var restInterface : DirectoryApiService
    val key = mutableStateOf("")
    val people = mutableStateOf<List<Person>?>(null)

    init {
        val retrofit : Retrofit = Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl("https://cmsc106.net/directory/")
            .build()
        restInterface = retrofit.create(DirectoryApiService::class.java)
    }

    fun newUser(name : String,password : String,onSuccess : ()->Unit) {
        val newUser = User(name,password)
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val result = restInterface.newUser(newUser)
                withContext(Dispatchers.Main) {
                    key.value = result
                    if(result.isNotEmpty())
                        onSuccess()
                }
            } catch (e:Exception) {
                e.printStackTrace()
            }
        }
    }

    fun login(name : String,password : String,onSuccess : ()->Unit) {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val result = restInterface.login(name,password)
                withContext(Dispatchers.Main) {
                    key.value = result
                    if(result.isNotEmpty())
                        getPeople(onSuccess = onSuccess)
                }
            } catch (e:Exception) {
                e.printStackTrace()
            }
        }
    }

    fun getPeople(onSuccess : ()->Unit) {
        val keyStr = key.value
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val result = restInterface.getPeople(keyStr)
                withContext(Dispatchers.Main) {
                    people.value = result
                    onSuccess()
                }
            } catch (e:Exception) {
                e.printStackTrace()
            }
        }
    }

    fun addPerson(person : Person,onSuccess : ()->Unit) {
        val keyStr = key.value
        viewModelScope.launch(Dispatchers.IO) {
            try {
                restInterface.postPerson(keyStr,person)
                val result = restInterface.getPeople(keyStr)
                withContext(Dispatchers.Main) {
                    people.value = result
                    onSuccess()
                }
            } catch (e:Exception) {
                e.printStackTrace()
            }
        }
    }

    fun removePerson(id : Int) {
        val keyStr = key.value
        viewModelScope.launch(Dispatchers.IO) {
            try {
                restInterface.removePerson(keyStr,id)
                val result = restInterface.getPeople(keyStr)
                withContext(Dispatchers.Main) {
                    people.value = result
                }
            } catch (e:Exception) {
                e.printStackTrace()
            }
        }
    }
}

This class stores a list of Person objects downloaded from the server, and offers appropriate methods for each distinct server interaction we plan to do.

An important aspect of this version of the app to point out is the fact that we are now dealing with a multi-user application. The back end service can store directories for a large number of different users. Users will have to start by logging in to their account: in response to a successful login request the back end will send us a key that the user will use to identify themselves in subsequent requests. The gateway class will store that key in a member variable so we have access to it for each subsequent request.

Logging in

Since users need to be logged in to make requests, the app will need to ensure that the user logs in before we do anything else. To support this requirement I have added a third screen to the application that prompts the user to log in or create a new account. This screen is now the intial screen for the app. The Log In button on that screen contains code that calls the view model's login() method and passes it a success callback function that navigates us to the screen that display's the user's directory data.