Hi everyone,
I’m building an Android app using Jetpack Compose and MVVM architecture with Hilt for dependency injection. On my RegisterScreen
, I have a Register button that’s supposed to trigger the registration process via RegisterViewModel.register(...)
, but **clicking the button does nothing, no transition to the next screen.
Here’s what I have:
Components Involved
RegisterScreen.kt
: Composable UI with state handling, text inputs, and button click launching a coroutine.RegisterActivity.kt
: Collects the state fromRegisterViewModel
and navigates toMainActivity
on success.RegisterViewModel.kt
: Contains logic to validate input, test backend connectivity, and callUserRepository.register(...)
.
Observations
- UI renders fine and button becomes enabled after filling all fields.
- Clicking the button triggers the coroutine in the
onClick
lambda. - Logs like
"Launching register inside coroutine"
and">>> register function called OUTSIDE coroutine"
appear. - But registration doesn’t complete and
RegisterState
doesn’t seem to transition toSuccess
.
Suspected Issue
It seems like the callback onSuccess
in UserRepository.register(...)
may not be firing, or the ViewModel isn’t correctly updating _registerState
, or perhaps the UI isn’t reacting to state changes.
What I’ve Tried
- Verified role, name, email, and password validation logic — all return valid.
- Logs confirm that the
register()
method inRegisterViewModel
is being called. - Checked backend connectivity and it logs as reachable.
- Checked that
registerState
is collected in bothRegisterScreen.kt
andRegisterActivity.kt
.
What I Need Help With
Can someone help me identify why the register button doesn’t complete the registration flow or why the state doesn’t update to Success
after clicking the button?
Any help debugging the flow or checking what might be going wrong with the coroutine, ViewModel state, or callback logic would be appreciated.
Here is the code:
RegisterScreen.kt:
package com.example.eventmanagement.ui
import android.widget.Toast
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.derivedStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.eventmanagement.viewmodel.RegisterViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RegisterScreen(
viewModel: RegisterViewModel,
onRegisterSuccess: () -> Unit,
onLoginClick: () -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
// Collect state from ViewModel
val registerState by viewModel.registerState.collectAsState()
val selectedRole by viewModel.selectedRole.collectAsState()
// Use remember to prevent unnecessary recompositions
val name = remember { mutableStateOf("") }
val email = remember { mutableStateOf("") }
val password = remember { mutableStateOf("") }
val confirmPassword = remember { mutableStateOf("") }
// Show loading state
val isLoading = remember(registerState) {
registerState is RegisterViewModel.RegisterState.Loading
}
// Derive button enabled state from current values
val buttonEnabled = derivedStateOf {
!isLoading && selectedRole != null && name.value.trim().isNotEmpty() &&
email.value.trim().isNotEmpty() && password.value.isNotEmpty() &&
confirmPassword.value.isNotEmpty()
}
// Password visibility states
val passwordVisibility = remember { mutableStateOf(false) }
val confirmPasswordVisibility = remember { mutableStateOf(false) }
// Handle register state changes
LaunchedEffect(registerState) {
when (registerState) {
is RegisterViewModel.RegisterState.Success -> {
withContext(Dispatchers.Main.immediate) {
Toast.makeText(context, "Registration Successful", Toast.LENGTH_SHORT).show()
onRegisterSuccess()
}
}
is RegisterViewModel.RegisterState.Error -> {
val errorMessage = (registerState as RegisterViewModel.RegisterState.Error).message
withContext(Dispatchers.Main.immediate) {
Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show()
}
}
else -> {}
}
}
// Test backend connectivity on first load
LaunchedEffect(Unit) {
android.util.Log.d("RegisterScreen", "Testing backend connectivity...")
// This will be handled by the UserRepository when registration is attempted
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Create Account") }
)
},
snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Register",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
// Role Selection Section
Text(
text = "Are you an owner or client?",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { viewModel.setRole("Owner") },
modifier = Modifier
.weight(1f)
.height(48.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (selectedRole == "Owner")
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.secondary
)
) {
Text("Owner")
}
Button(
onClick = { viewModel.setRole("Client") },
modifier = Modifier
.weight(1f)
.height(48.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (selectedRole == "Client")
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.secondary
)
) {
Text("Client")
}
}
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = name.value,
onValueChange = { name.value = it },
label = { Text("Name") },
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next
)
)
OutlinedTextField(
value = email.value,
onValueChange = { email.value = it },
label = { Text("Email") },
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
)
)
OutlinedTextField(
value = password.value,
onValueChange = { password.value = it },
label = { Text("Password") },
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next
),
visualTransformation = if (passwordVisibility.value) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
IconButton(onClick = { passwordVisibility.value = !passwordVisibility.value }) {
Icon(
imageVector = if (passwordVisibility.value) Icons.Default.Visibility else Icons.Default.VisibilityOff,
contentDescription = if (passwordVisibility.value) "Hide password" else "Show password"
)
}
}
)
OutlinedTextField(
value = confirmPassword.value,
onValueChange = { confirmPassword.value = it },
label = { Text("Confirm Password") },
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
),
visualTransformation = if (confirmPasswordVisibility.value) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
IconButton(onClick = { confirmPasswordVisibility.value = !confirmPasswordVisibility.value }) {
Icon(
imageVector = if (confirmPasswordVisibility.value) Icons.Default.Visibility else Icons.Default.VisibilityOff,
contentDescription = if (confirmPasswordVisibility.value) "Hide password" else "Show password"
)
}
}
)
// Help text
if (!buttonEnabled.value && !isLoading) {
val missingFields = mutableListOf<String>()
if (selectedRole == null) missingFields.add("role")
if (name.value.trim().isEmpty()) missingFields.add("name")
if (email.value.trim().isEmpty()) missingFields.add("email")
if (password.value.isEmpty()) missingFields.add("password")
if (confirmPassword.value.isEmpty()) missingFields.add("confirm password")
Text(
text = "Please fill in: ${missingFields.joinToString(", ")}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
Text(
text = "Debug: Role=$selectedRole, Loading=$isLoading, Name=${name.value.isNotEmpty()}, Email=${email.value.isNotEmpty()}, Password=${password.value.isNotEmpty()}, Confirm=${confirmPassword.value.isNotEmpty()}, ButtonEnabled=${buttonEnabled.value}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Button(
onClick = {
scope.launch {
android.util.Log.d("RegisterScreen", "Launching register inside coroutine")
if (selectedRole == null) {
Toast.makeText(context, "Please select a role (Owner or Client)", Toast.LENGTH_SHORT).show()
return@launch
}
if (name.value.trim().isEmpty()) {
Toast.makeText(context, "Please enter your name", Toast.LENGTH_SHORT).show()
return@launch
}
if (email.value.trim().isEmpty()) {
Toast.makeText(context, "Please enter your email", Toast.LENGTH_SHORT).show()
return@launch
}
if (password.value.isEmpty()) {
Toast.makeText(context, "Please enter a password", Toast.LENGTH_SHORT).show()
return@launch
}
if (password.value != confirmPassword.value) {
Toast.makeText(context, "Passwords do not match", Toast.LENGTH_SHORT).show()
return@launch
}
viewModel.register(
name.value.trim(),
email.value.trim(),
password.value
)
}
},
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
enabled = buttonEnabled.value,
colors = ButtonDefaults.buttonColors(
containerColor = if (buttonEnabled.value) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary
)
} else {
Text(
text = if (buttonEnabled.value) "Register" else "Fill all fields to register",
color = if (buttonEnabled.value) MaterialTheme.colorScheme.onPrimary
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
)
}
}
TextButton(
onClick = onLoginClick,
modifier = Modifier.padding(top = 8.dp)
) {
Text("Already have an account? Login")
}
}
}
}
RegisterActivity.kt:
package com.example.eventmanagement.ui
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.SnackbarHostState
import androidx.lifecycle.lifecycleScope
import com.example.eventmanagement.MainActivity
import com.example.eventmanagement.viewmodel.RegisterViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@AndroidEntryPoint
class RegisterActivity : AppCompatActivity() {
private val viewModel: RegisterViewModel by viewModels()
private val snackbarHostState = SnackbarHostState()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Set content first to show UI immediately
setContent {
RegisterScreen(
viewModel = viewModel,
onRegisterSuccess = { /* Handled by state collection */ },
onLoginClick = { navigateToLoginActivity() }
)
}
// Start background operations after UI is shown
lifecycleScope.launch(Dispatchers.IO) {
try {
val isLoggedIn = viewModel.isLoggedIn()
if (isLoggedIn) {
withContext(Dispatchers.Main.immediate) {
navigateToMainActivity()
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main.immediate) {
showError("Failed to check login status: ${e.message}")
}
}
}
// Collect state changes in a separate coroutine
lifecycleScope.launch(Dispatchers.IO) {
try {
android.util.Log.d("RegisterActivity", "Starting to collect register state changes")
viewModel.registerState.collectLatest { state ->
android.util.Log.d("RegisterActivity", "State changed to: $state")
when (state) {
is RegisterViewModel.RegisterState.Success -> {
android.util.Log.d("RegisterActivity", "Registration successful, navigating to MainActivity")
withContext(Dispatchers.Main.immediate) {
navigateToMainActivity()
}
}
is RegisterViewModel.RegisterState.Error -> {
android.util.Log.d("RegisterActivity", "Registration error: ${state.message}")
withContext(Dispatchers.Main.immediate) {
showError(state.message)
}
}
is RegisterViewModel.RegisterState.Loading -> {
android.util.Log.d("RegisterActivity", "Registration loading...")
}
else -> {
android.util.Log.d("RegisterActivity", "Other state: $state")
}
}
}
} catch (e: Exception) {
android.util.Log.e("RegisterActivity", "Failed to collect state changes: ${e.message}")
withContext(Dispatchers.Main.immediate) {
showError("Failed to collect state changes: ${e.message}")
}
}
}
}
private fun navigateToMainActivity() {
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish()
}
private fun navigateToLoginActivity() {
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
finish()
}
private fun showError(message: String) {
lifecycleScope.launch(Dispatchers.Main.immediate) {
snackbarHostState.showSnackbar(message)
}
}
}
RegisterViewModel.kt:
package com.example.eventmanagement.viewmodel
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class RegisterViewModel @Inject constructor(
application: Application,
private val userRepository: UserRepository
) : AndroidViewModel(application) {
private val _registerState = MutableStateFlow<RegisterState>(RegisterState.Idle)
val registerState: StateFlow<RegisterState> = _registerState
private val _selectedRole = MutableStateFlow<String?>(null)
val selectedRole: StateFlow<String?> = _selectedRole
suspend fun isLoggedIn(): Boolean {
return try {
userRepository.isLoggedIn()
} catch (e: Exception) {
withContext(Dispatchers.Main.immediate) {
_registerState.value = RegisterState.Error(e.message ?: "Failed to check login status")
}
false
}
}
fun setRole(role: String) {
_selectedRole.value = role
}
fun register(name: String, email: String, password: String) {
android.util.Log.d("RegisterViewModel", ">>> register function called OUTSIDE coroutine")
viewModelScope.launch {
android.util.Log.d("RegisterViewModel", ">>> INSIDE coroutine launch block")
try {
android.util.Log.d("RegisterViewModel", "Setting state to Loading")
_registerState.value = RegisterState.Loading
val isBackendReachable = userRepository.testBackendConnectivity()
android.util.Log.d("RegisterViewModel", "Backend connectivity test result: $isBackendReachable")
if (!isBackendReachable) {
_registerState.value = RegisterState.Error("Cannot connect to server. Please check your internet connection or try again later.")
return@launch
}
if (!validateInput(name, email, password)) {
android.util.Log.d("RegisterViewModel", "Input validation failed")
_registerState.value = RegisterState.Error("Invalid input")
return@launch
}
if (_selectedRole.value == null) {
android.util.Log.d("RegisterViewModel", "No role selected")
_registerState.value = RegisterState.Error("Please select a role")
return@launch
}
android.util.Log.d("RegisterViewModel", "Calling userRepository.register")
userRepository.register(
name = name,
email = email,
password = password,
role = _selectedRole.value!!,
onSuccess = {
android.util.Log.d("RegisterViewModel", "Registration successful")
_registerState.value = RegisterState.Success
},
onFailure = { error ->
android.util.Log.d("RegisterViewModel", "Registration failed: $error")
_registerState.value = RegisterState.Error((error ?: "Registration failed").toString())
}
)
} catch (e: Exception) {
android.util.Log.e("RegisterViewModel", "Exception in register: ${e.message}")
_registerState.value = RegisterState.Error(e.message ?: "Registration failed")
}
}
}
private fun isNameValid(name: String): Boolean {
return name.trim().length >= 2
}
private fun isEmailValid(email: String): Boolean {
val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\$")
return emailRegex.matches(email.trim())
}
private fun isPasswordValid(password: String): Boolean {
return password.length >= 6
}
private fun validateInput(name: String, email: String, password: String): Boolean {
android.util.Log.d("RegisterViewModel", "Validating input - Name: '$name' (${name.trim().length}), Email: '$email', Password: '${"*".repeat(password.length)}' (${password.length})")
if (!isNameValid(name)) {
android.util.Log.d("RegisterViewModel", "Name validation failed: ${name.trim().length} < 2")
_registerState.value = RegisterState.Error("Name must be at least 2 characters long")
return false
}
if (!isEmailValid(email)) {
android.util.Log.d("RegisterViewModel", "Email validation failed: $email")
_registerState.value = RegisterState.Error("Please enter a valid email address")
return false
}
if (!isPasswordValid(password)) {
android.util.Log.d("RegisterViewModel", "Password validation failed: length ${password.length} < 6")
_registerState.value = RegisterState.Error("Password must be at least 6 characters long")
return false
}
android.util.Log.d("RegisterViewModel", "Input validation passed")
return true
}
sealed class RegisterState {
object Idle : RegisterState()
object Loading : RegisterState()
object Success : RegisterState()
data class Error(val message: String) : RegisterState()
}
}