Refactor: Migrate MainViewModel to use StateFlow

This commit refactors the `MainViewModel` to use `StateFlow` instead of `mutableStateOf` for managing its observable properties. This change improves the way UI state is handled and observed, aligning with modern Android development best practices.

Additionally, `OnboardingState.kt` has been moved to the `onboarding` package for better organization.
This commit is contained in:
Mohamad Hamade
2025-07-16 01:13:42 +03:00
parent 245181d736
commit d730acbb79
3 changed files with 86 additions and 48 deletions
@@ -10,9 +10,13 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.Lifecycle
import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.onboarding.BluetoothCheckScreen import com.bitchat.android.onboarding.BluetoothCheckScreen
import com.bitchat.android.onboarding.BluetoothStatus import com.bitchat.android.onboarding.BluetoothStatus
@@ -23,6 +27,7 @@ import com.bitchat.android.onboarding.LocationCheckScreen
import com.bitchat.android.onboarding.LocationStatus import com.bitchat.android.onboarding.LocationStatus
import com.bitchat.android.onboarding.LocationStatusManager import com.bitchat.android.onboarding.LocationStatusManager
import com.bitchat.android.onboarding.OnboardingCoordinator import com.bitchat.android.onboarding.OnboardingCoordinator
import com.bitchat.android.onboarding.OnboardingState
import com.bitchat.android.onboarding.PermissionExplanationScreen import com.bitchat.android.onboarding.PermissionExplanationScreen
import com.bitchat.android.onboarding.PermissionManager import com.bitchat.android.onboarding.PermissionManager
import com.bitchat.android.ui.ChatScreen import com.bitchat.android.ui.ChatScreen
@@ -88,23 +93,39 @@ class MainActivity : ComponentActivity() {
} }
} }
// Collect state changes in a lifecycle-aware manner
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mainViewModel.onboardingState.collect { state ->
handleOnboardingStateChange(state)
}
}
}
// Only start onboarding process if we're in the initial CHECKING state // Only start onboarding process if we're in the initial CHECKING state
// This prevents restarting onboarding on configuration changes // This prevents restarting onboarding on configuration changes
if (mainViewModel.onboardingState == OnboardingState.CHECKING) { if (mainViewModel.onboardingState.value == OnboardingState.CHECKING) {
checkOnboardingStatus() checkOnboardingStatus()
} }
} }
@Composable @Composable
private fun OnboardingFlowScreen() { private fun OnboardingFlowScreen() {
when (mainViewModel.onboardingState) { val onboardingState by mainViewModel.onboardingState.collectAsState()
val bluetoothStatus by mainViewModel.bluetoothStatus.collectAsState()
val locationStatus by mainViewModel.locationStatus.collectAsState()
val errorMessage by mainViewModel.errorMessage.collectAsState()
val isBluetoothLoading by mainViewModel.isBluetoothLoading.collectAsState()
val isLocationLoading by mainViewModel.isLocationLoading.collectAsState()
when (onboardingState) {
OnboardingState.CHECKING -> { OnboardingState.CHECKING -> {
InitializingScreen() InitializingScreen()
} }
OnboardingState.BLUETOOTH_CHECK -> { OnboardingState.BLUETOOTH_CHECK -> {
BluetoothCheckScreen( BluetoothCheckScreen(
status = mainViewModel.bluetoothStatus, status = bluetoothStatus,
onEnableBluetooth = { onEnableBluetooth = {
mainViewModel.updateBluetoothLoading(true) mainViewModel.updateBluetoothLoading(true)
bluetoothStatusManager.requestEnableBluetooth() bluetoothStatusManager.requestEnableBluetooth()
@@ -112,13 +133,13 @@ class MainActivity : ComponentActivity() {
onRetry = { onRetry = {
checkBluetoothAndProceed() checkBluetoothAndProceed()
}, },
isLoading = mainViewModel.isBluetoothLoading isLoading = isBluetoothLoading
) )
} }
OnboardingState.LOCATION_CHECK -> { OnboardingState.LOCATION_CHECK -> {
LocationCheckScreen( LocationCheckScreen(
status = mainViewModel.locationStatus, status = locationStatus,
onEnableLocation = { onEnableLocation = {
mainViewModel.updateLocationLoading(true) mainViewModel.updateLocationLoading(true)
locationStatusManager.requestEnableLocation() locationStatusManager.requestEnableLocation()
@@ -126,7 +147,7 @@ class MainActivity : ComponentActivity() {
onRetry = { onRetry = {
checkLocationAndProceed() checkLocationAndProceed()
}, },
isLoading = mainViewModel.isLocationLoading isLoading = isLocationLoading
) )
} }
@@ -172,7 +193,7 @@ class MainActivity : ComponentActivity() {
OnboardingState.ERROR -> { OnboardingState.ERROR -> {
InitializationErrorScreen( InitializationErrorScreen(
errorMessage = mainViewModel.errorMessage, errorMessage = errorMessage,
onRetry = { onRetry = {
mainViewModel.updateOnboardingState(OnboardingState.CHECKING) mainViewModel.updateOnboardingState(OnboardingState.CHECKING)
checkOnboardingStatus() checkOnboardingStatus()
@@ -185,6 +206,20 @@ class MainActivity : ComponentActivity() {
} }
} }
private fun handleOnboardingStateChange(state: OnboardingState) {
when (state) {
OnboardingState.COMPLETE -> {
// App is fully initialized, mesh service is running
android.util.Log.d("MainActivity", "Onboarding completed - app ready")
}
OnboardingState.ERROR -> {
android.util.Log.e("MainActivity", "Onboarding error state reached")
}
else -> {}
}
}
private fun checkOnboardingStatus() { private fun checkOnboardingStatus() {
android.util.Log.d("MainActivity", "Checking onboarding status") android.util.Log.d("MainActivity", "Checking onboarding status")
@@ -215,7 +250,7 @@ class MainActivity : ComponentActivity() {
bluetoothStatusManager.logBluetoothStatus() bluetoothStatusManager.logBluetoothStatus()
mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus()) mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus())
when (mainViewModel.bluetoothStatus) { when (mainViewModel.bluetoothStatus.value) {
BluetoothStatus.ENABLED -> { BluetoothStatus.ENABLED -> {
// Bluetooth is enabled, check location services next // Bluetooth is enabled, check location services next
checkLocationAndProceed() checkLocationAndProceed()
@@ -286,7 +321,7 @@ class MainActivity : ComponentActivity() {
locationStatusManager.logLocationStatus() locationStatusManager.logLocationStatus()
mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus()) mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus())
when (mainViewModel.locationStatus) { when (mainViewModel.locationStatus.value) {
LocationStatus.ENABLED -> { LocationStatus.ENABLED -> {
// Location services enabled, proceed with permission/onboarding check // Location services enabled, proceed with permission/onboarding check
proceedWithPermissionCheck() proceedWithPermissionCheck()
@@ -325,7 +360,7 @@ class MainActivity : ComponentActivity() {
mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus()) mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus())
when { when {
mainViewModel.locationStatus == LocationStatus.NOT_AVAILABLE -> { mainViewModel.locationStatus.value == LocationStatus.NOT_AVAILABLE -> {
// Show permanent error for devices without location services // Show permanent error for devices without location services
mainViewModel.updateErrorMessage(message) mainViewModel.updateErrorMessage(message)
mainViewModel.updateOnboardingState(OnboardingState.ERROR) mainViewModel.updateOnboardingState(OnboardingState.ERROR)
@@ -346,7 +381,7 @@ class MainActivity : ComponentActivity() {
mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus()) mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus())
when { when {
mainViewModel.bluetoothStatus == BluetoothStatus.NOT_SUPPORTED -> { mainViewModel.bluetoothStatus.value == BluetoothStatus.NOT_SUPPORTED -> {
// Show permanent error for unsupported devices // Show permanent error for unsupported devices
mainViewModel.updateErrorMessage(message) mainViewModel.updateErrorMessage(message)
mainViewModel.updateOnboardingState(OnboardingState.ERROR) mainViewModel.updateOnboardingState(OnboardingState.ERROR)
@@ -450,7 +485,7 @@ class MainActivity : ComponentActivity() {
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
// Handle notification intents when app is already running // Handle notification intents when app is already running
if (mainViewModel.onboardingState == OnboardingState.COMPLETE) { if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
handleNotificationIntent(intent) handleNotificationIntent(intent)
} }
} }
@@ -458,7 +493,7 @@ class MainActivity : ComponentActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// Check Bluetooth and Location status on resume and handle accordingly // Check Bluetooth and Location status on resume and handle accordingly
if (mainViewModel.onboardingState == OnboardingState.COMPLETE) { if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
// Set app foreground state // Set app foreground state
meshService.connectionManager.setAppBackgroundState(false) meshService.connectionManager.setAppBackgroundState(false)
chatViewModel.setAppBackgroundState(false) chatViewModel.setAppBackgroundState(false)
@@ -487,7 +522,7 @@ class MainActivity : ComponentActivity() {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
// Only set background state if app is fully initialized // Only set background state if app is fully initialized
if (mainViewModel.onboardingState == OnboardingState.COMPLETE) { if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
// Set app background state // Set app background state
meshService.connectionManager.setAppBackgroundState(true) meshService.connectionManager.setAppBackgroundState(true)
chatViewModel.setAppBackgroundState(true) chatViewModel.setAppBackgroundState(true)
@@ -523,7 +558,7 @@ class MainActivity : ComponentActivity() {
* Restart mesh services (for debugging/troubleshooting) * Restart mesh services (for debugging/troubleshooting)
*/ */
fun restartMeshServices() { fun restartMeshServices() {
if (mainViewModel.onboardingState == OnboardingState.COMPLETE) { if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
lifecycleScope.launch { lifecycleScope.launch {
try { try {
android.util.Log.d("MainActivity", "Restarting mesh services") android.util.Log.d("MainActivity", "Restarting mesh services")
@@ -550,7 +585,7 @@ class MainActivity : ComponentActivity() {
} }
// Stop mesh services if app was fully initialized // Stop mesh services if app was fully initialized
if (mainViewModel.onboardingState == OnboardingState.COMPLETE) { if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
try { try {
meshService.stopServices() meshService.stopServices()
android.util.Log.d("MainActivity", "Mesh services stopped successfully") android.util.Log.d("MainActivity", "Mesh services stopped successfully")
@@ -1,52 +1,55 @@
package com.bitchat.android package com.bitchat.android
import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.bitchat.android.onboarding.BluetoothStatus import com.bitchat.android.onboarding.BluetoothStatus
import com.bitchat.android.onboarding.LocationStatus import com.bitchat.android.onboarding.LocationStatus
import com.bitchat.android.onboarding.OnboardingState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class MainViewModel : ViewModel() { class MainViewModel : ViewModel() {
private var _onboardingState by mutableStateOf(OnboardingState.CHECKING) private val _onboardingState = MutableStateFlow(OnboardingState.CHECKING)
val onboardingState: OnboardingState get() = _onboardingState val onboardingState: StateFlow<OnboardingState> = _onboardingState.asStateFlow()
private var _bluetoothStatus by mutableStateOf(BluetoothStatus.ENABLED) private val _bluetoothStatus = MutableStateFlow(BluetoothStatus.ENABLED)
val bluetoothStatus: BluetoothStatus get() = _bluetoothStatus val bluetoothStatus: StateFlow<BluetoothStatus> = _bluetoothStatus.asStateFlow()
private var _locationStatus by mutableStateOf(LocationStatus.ENABLED) private val _locationStatus = MutableStateFlow(LocationStatus.ENABLED)
val locationStatus: LocationStatus get() = _locationStatus val locationStatus: StateFlow<LocationStatus> = _locationStatus.asStateFlow()
private var _errorMessage by mutableStateOf("") private val _errorMessage = MutableStateFlow("")
val errorMessage: String get() = _errorMessage val errorMessage: StateFlow<String> = _errorMessage.asStateFlow()
private var _isBluetoothLoading by mutableStateOf(false) private val _isBluetoothLoading = MutableStateFlow(false)
val isBluetoothLoading: Boolean get() = _isBluetoothLoading val isBluetoothLoading: StateFlow<Boolean> = _isBluetoothLoading.asStateFlow()
private var _isLocationLoading by mutableStateOf(false) private val _isLocationLoading = MutableStateFlow(false)
val isLocationLoading: Boolean get() = _isLocationLoading val isLocationLoading: StateFlow<Boolean> = _isLocationLoading.asStateFlow()
// Public update functions for MainActivity // Public update functions for MainActivity
fun updateOnboardingState(state: OnboardingState) { fun updateOnboardingState(state: OnboardingState) {
_onboardingState = state _onboardingState.value = state
} }
fun updateBluetoothStatus(status: BluetoothStatus) { fun updateBluetoothStatus(status: BluetoothStatus) {
_bluetoothStatus = status _bluetoothStatus.value = status
} }
fun updateLocationStatus(status: LocationStatus) { fun updateLocationStatus(status: LocationStatus) {
_locationStatus = status _locationStatus.value = status
} }
fun updateErrorMessage(message: String) { fun updateErrorMessage(message: String) {
_errorMessage = message _errorMessage.value = message
} }
fun updateBluetoothLoading(loading: Boolean) { fun updateBluetoothLoading(loading: Boolean) {
_isBluetoothLoading = loading _isBluetoothLoading.value = loading
} }
fun updateLocationLoading(loading: Boolean) { fun updateLocationLoading(loading: Boolean) {
_isLocationLoading = loading _isLocationLoading.value = loading
} }
} }
@@ -1,4 +1,4 @@
package com.bitchat.android package com.bitchat.android.onboarding
enum class OnboardingState { enum class OnboardingState {
CHECKING, CHECKING,