Playing around with Android Compose, just want to build a simple login page that navigates to home page on success.
Below is code for ViewModel and login screen
enum class UserStatus {
LOADING,
SUCCESS,
EMPTY
}
data class LoginUser(
val token: String,
val status: UserStatus,
)
class LoginViewModel(
private val userRepository: DefaultUserRepository = DefaultUserRepository(),
) : ViewModel() {
val loginUser by lazy { MutableLiveData<LoginUser>() }
fun login(email: String, password: String) {
loginUser.postValue(LoginUser("", UserStatus.LOADING))
val loginBody = LoginBody(email = email, password = password)
viewModelScope.launch(Dispatchers.IO) {
val loginResponse = userRepository.login(loginBody)
loginUser.postValue(LoginUser(loginResponse.access_token, UserStatus.SUCCESS))
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginScreen(
navController: NavController,
viewModel: LoginViewModel = LoginViewModel(),
) {
var email = remember { mutableStateOf("") }
var password = remember { mutableStateOf("") }
val userState by viewModel.loginUser.observeAsState(LoginUser("", UserStatus.EMPTY))
when (userState.status) {
UserStatus.SUCCESS -> navController.navigate("home/" + userState!!.token)
UserStatus.LOADING -> CircularProgressIndicator()
UserStatus.EMPTY -> Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize(),
) {
Column(
modifier = Modifier.align(Alignment.Center),
) {
Text(
text = "login",
fontSize = 50.sp,
)
OutlinedTextField(
value = email.value,
onValueChange = { email.value = it },
label = { Text("Email") }
)
OutlinedTextField(
value = password.value,
onValueChange = { password.value = it },
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
)
Spacer(modifier = Modifier.height(10.dp))
Row() {
Text(
text = "No account? ",
fontSize = 18.sp,
)
ClickableText(
text = AnnotatedString("Signup here"),
onClick = { navController.navigate("signup") }
)
}
Spacer(modifier = Modifier.height(10.dp))
FloatingActionButton(
onClick = {
viewModel.login(email.value, password.value)
}
) {
Text("Login")
}
}
}
}
And the code for the repository that gets jwt is
@JsonClass(generateAdapter = true)
data class LoginResponse(
val access_token: String,
val token_type: String,
)
data class LoginBody(
val email: String,
val password: String,
)
class DefaultUserRepository(
private val client: OkHttpClient = OkHttpClient(),
private val moshi: Moshi = Moshi.Builder().build(),
) : UserRepository {
private val loginAdapter = moshi.adapter(LoginResponse::class.java)
override suspend fun login(loginBody: LoginBody): LoginResponse {
val loginObject = JSONObject()
loginObject.put("email", loginBody.email)
loginObject.put("password", loginBody.password)
val mediaType = "application/json".toMediaType()
val body = loginObject.toString().toRequestBody(mediaType)
val loginRequest = Request.Builder()
.url(/* auth endpoint, returns jwt */)
.post(body)
.build()
val response = client.newCall(loginRequest).execute()
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val loginResponse = loginAdapter.fromJson(response.body!!.source())
return loginResponse!!
}
}
The repository works and returns jwt as expected. When I click the login button, the LiveData variable userState
updates to the loading state as expected, displaying a CircularProgressBar
, but once the auth request completes it does not update to the success state and navigate to home screen. The only thing I can think of is that the suspend
function in repository is responsible and that the loginResponse
used in the postValue
is empty, but when I log loginResponse
before the postValue
call it is populated with jwt. Any ideas?