androidkotlinandroid-jetpack-composeandroid-jetpack

Jetpack compose Null Value for variable on ViewModel


I am setting the value of a variable using the LiveData on my view model. The Data is fetched from Firebase. Below is the ViewModel structure class FarmViewModel(): ViewModel() {

var firestoreDB = InitializeFirestore()
private var _farms = MutableStateFlow<List<FarmEntity>>(emptyList())
private var _farm = MutableLiveData<FarmEntity>()

var farms = _farms.asStateFlow()
var farm : LiveData<FarmEntity> = _farm

init{
   getFarms()
}




fun getFarm(id:String){

    firestoreDB.collection("farms")
        .whereEqualTo("id",id)
        .get()
        .addOnSuccessListener {
            if (it != null) {
                //the farm name value is as expected
                Log.d("farms", "DocumentSnapshot data: ${it.toObjects<FarmEntity>()[0].farm_name}")

                _farm.value=it.toObjects<FarmEntity>()[0]
            } else {
                Log.d("farms", "No such document")
            }
        }
        .addOnFailureListener{
            Log.e("farms",it.message.toString())
            setError(
                ErrorData(
                    code=500,
                    service="CreateFarm",
                    message = it.message.toString()
                )
            )

        }
}

The getFarm function is called using an Onclick Event that fetches the selected id and also to navigate to the Farm Screen.

On the Composable, the farm value is Null

val farm = farmViewModel.farm.observeAsState()

Log.i("farm_", farm.value.toString())

Solution

  • When you inject FarmViewModel in different screens with viewModel() you in fact inject different instances of that ViewModel, each instance scoped to its own screen. Different instances obviously won't know about values of each other fields.

    It is possible to share ViewModel between navigation destinations, but it is recommended to have one ViewModel class per screen, e.g. FarmViewModel for FarmScreen and FarmsViewModel for FarmsScreen.

    You can pass farm ID to FarmScreen with navigation arguments and fetch data on FarmViewModel creation.

    Edit:

    Example navigation:

    @Serializable
    data class Farm(val id: Int)
    
    @Serializable
    private object Farms
    
    @Composable
    fun FarmsNavigation() {
        val navController = rememberNavController()
        NavHost(navController, startDestination = Farms) {
            composable<Farms> {
                FarmsScreen(
                    onNavigateToFarm = { farmId ->
                        navController.navigate(Farm(farmId))
                    },
                )
            }
            composable<Farm> { backStackEntry ->
                FarmScreen(
                    //..
                )
            }
        }
    }
    

    FarmViewModel:

    class FarmViewModel(
        savedStateHandle: SavedStateHandle,
    ) : ViewModel() {
    
        // Obtain farm id from SavedStateHandle
        init {
            val farm = savedStateHandle.toRoute<Farm>()
            val id = farm.id
        }
    }
    

    FarmsScreen:

    @Composable
    fun FarmsScreen(
        onNavigateToFarm: (Int) -> Unit,
        viewModel: FarmsViewModel = viewModel(),
    ) {
        Button(onClick = { onNavigateToFarm(123) }) {
            Text("Go to farm 123")
        }
    }