[Prevent inherited CustomViewModel multiple instan...
# compose
a
[Prevent inherited CustomViewModel multiple instances] I'm working on an app where each screen specific VM inherit a common viewmodel. I have a flow related problem
đź§µ 3
Copy code
fun LoginScreen(
    loginViewModel: LoginViewModel = hiltViewModel(),
) { 
    val lifecycleOwner = LocalLifecycleOwner.current

    LaunchedEffect(key1 = lifecycleOwner) {
        loginViewModel.performIntent(ScanUiIntent.StartScan)
    }
 }
Copy code
fun ScanMachineScreen(
    scanMachineViewModel: ScanMachineViewModel = hiltViewModel(),
) { 
    val lifecycleOwner = LocalLifecycleOwner.current

    LaunchedEffect(key1 = lifecycleOwner) {
        scanMachineViewModel.performIntent(ScanUiIntent.StartScan)
    }
 }
Both LoginVM and ScanMachineVM inherit of ScanVM, where I collect a scanData flow
Copy code
@JvmInline
value class Scan(
    val data: String,
)

data class ScanUiState(
    val scanData: Scan? = null,
)

sealed class ScanUiIntent {
    data object StartScan : ScanUiIntent()
    data object StopScan : ScanUiIntent()
}

@HiltViewModel
open class ScanViewModel
    @Inject
    constructor(
        private val scanRepository: ScanRepository,
        scanDataSource: ScanDataSource,
    ) : BaseViewModel() {
        var scanUiState by mutableStateOf(ScanUiState())
            private set

        private var scanJob: Job? = null

        private val scanData: SharedFlow<Scan?> =
            scanDataSource
                .scanDataFlow
                .stateIn(
                    scope = viewModelScope,
                    started = SharingStarted.WhileSubscribed(5000),
                    initialValue = null,
                )

        fun performIntent(intent: ScanUiIntent) {
            when (intent) {
                is ScanUiIntent.StartScan -> startScan()
                is ScanUiIntent.StopScan -> stopScan()
            }
        }

        init {
//             Collect the hot flow and update the state
//            startScan()
        }

        private fun startScan() {
            Log.d("ScanViewModel", "startScan() scanJob = $scanJob")
            if (scanJob != null) return // Prevent multiple collections
            scanJob =
                viewModelScope.launch {
                    scanData
                        .filter { scannedData -> !scannedData?.data.isNullOrEmpty() }
                        .collectLatest { scanData ->
                            scanUiState = scanUiState.copy(scanData = scanData)
                            Log.d("ScanViewModel", "scanUiState.scanData set to :: ${scanUiState.scanData}")
                        }
                }
        }

        // Stop collecting scan data
        private fun stopScan() {
            scanJob?.cancel()
            scanJob = null
        }
    }
The problem I face is that, on each navigation event, the startScan() method seems to be triggered once again (the amount of logs of the method increment) This behavior occurs whether I use the startScan method in the ScanVM init block or in each screen
Copy code
class ScanDataSource
    @Inject
    constructor(
        @ApplicationContext private val context: Context,
    ) {
        private val _scanDataFlow = MutableSharedFlow<Scan?>(replay = 1)
        val scanDataFlow: SharedFlow<Scan?> = _scanDataFlow.asSharedFlow()

        init {
            val scanReceiver =
                object : BroadcastReceiver() {
                    override fun onReceive(
                        context: Context,
                        intent: Intent,
                    ) {
                        if (intent.action != Datawedge.SCAN_ACTION) return

                        val scanData = intent.getStringExtra(Datawedge.STRING_EXTRA)
                        val emitted = _scanDataFlow.tryEmit(if (scanData.isNullOrBlank()) null else Scan(scanData))
                        Log.d("ScanDataSource", "scan data emitted ? $emitted -> $scanData")
                    }
                }

            val intentFilter =
                IntentFilter().also {
                    it.addAction(Datawedge.SCAN_ACTION)
                    it.addCategory(Datawedge.SCAN_CATEGORY)
                }

            context.registerReceiver(scanReceiver, intentFilter)
            Log.d("ScanDataSource", "getScanData: register receiver")
        }
    }
The ScanDataSource works correctly and only emit once, the problem seems to come from the way I collect it in the VM Thank you for your help Nota : I think I messed up with startScan collecting a cold flow and scanData being a hot flow
u
By debugging and checking the call stack of startScan, I think we can identify where it's being called from and find a solution. https://developer.android.com/studio/debug#the-debug-window