Search code examples
androidmvvmdata-bindingbroadcastreceiverandroid-viewbinding

Data binding not working for FragmentA after returning from FragmentB


My app has two fragments: HomeFragment and StatusFragment. HomeFragment binds some connection info to the UI. Info is obtained from broadcast receiver. At the beginning, data binding works in HomeFragment but when user go to the StatusFragment and then back to the HomeFragment, info is not bind.

HomeFragment, StatusFragment and HomeFragmentViewModel classes are shown below:

class HomeFragment : Fragment() {

    private var binding: FragmentHomeBinding? = null
    private var connectionStatus: String? = "Offline"
    private var connectionType: String? = ""

    private lateinit var mReceiver: ConnectivityChangeBroadcastReceiver
    lateinit var homeFragmentViewModel: HomeFragmentViewModel
    lateinit var repo: Repository

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentHomeBinding.inflate(inflater, container, false)
        binding?.lifecycleOwner = viewLifecycleOwner
        
        repo = Repository().getInstance()!!
        val viewModelFactory = HomeFragmentViewModelFactory(repo)
        homeFragmentViewModel =
            ViewModelProvider(this, viewModelFactory)[HomeFragmentViewModel::class.java]

        binding?.homeFragmentViewModel = homeFragmentViewModel
        this.mReceiver = ConnectivityChangeBroadcastReceiver()

        mReceiver.mData.observe(this, Observer {
            repo.setInfo(mReceiver.getData())
            homeFragmentViewModel.getConnectionInfo()
        })

        homeFragmentViewModel.navToStatusFragment.observe(this, Observer {
            if (it==true){
                this.findNavController().navigate(R.id.action_homeFragment_to_statusFragment)
                homeFragmentViewModel.doneNavigateToStatusFragment()
            }
        })

        return binding?.root
    }


    override fun onStart() {
        super.onStart()

        requireActivity().registerReceiver(
            mReceiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
        )
        repo.setInfo(mReceiver.getData())
    }

    override fun onStop() {
        super.onStop()
        requireActivity().unregisterReceiver(mReceiver)
    }

}


class StatusFragment : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        requireActivity().onBackPressedDispatcher.addCallback(this){
            requireActivity().supportFragmentManager.popBackStack()
        }
        return inflater.inflate(R.layout.fragment_status, container, false)
    }

}
class HomeFragmentViewModel(private val repo: Repository?) : ViewModel() {

    private val viewModelJob = Job()

    private val _navToStatusFragment = MutableLiveData<Boolean?>()
    val navToStatusFragment: LiveData<Boolean?>
        get() = _navToStatusFragment

    private val _connected = MutableLiveData<String?>()
    val connected: LiveData<String?>
        get() = _connected

    private val _connectionType = MutableLiveData<String?>()
    val connectionType: LiveData<String?>
        get() = _connectionType

    init {
        _navToStatusFragment.value = false
    }

    var isConnected: LiveData<Boolean> = connected.map {
        when (it) {
            "Online" -> true
            else -> false
        }
    }

    fun doneNavigateToStatusFragment(){
        _navToStatusFragment.value = false
    }

    fun navigateToStatusFragment(){
        _navToStatusFragment.value = true
    }

    fun getConnectionInfo() {

        when (repo?.getInfo()?.value) {
            0 -> {
                _connected.value = "Offline"
                _connectionType.value = "Check Your Connection"
                _connectionDetails.value = null
            }
            1 -> {
                _connected.value = "Online"
                _connectionType.value = "Cellular"
                _connectionDetails.value = null
            }
            2 -> {
                _connected.value = "Online"
                _connectionType.value = "WIFI"
                _connectionDetails.value = null
            }
            3 -> {
                _connected.value = "Online"
                _connectionType.value = "VPN"
                _connectionDetails.value = null
            }
        }
        Log.d("LOGGED", "Info Checked ...")
    }


    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel()
    }
}

I did:

  • using regiterReceiver() in onResume(), too
  • using viewLifeCycleOwner instead of this for binding.lifeCycleOwner
  • using popBackStack() in StatusFragment when back button pressed

UPDATE: The problem is that after going back to HomeFragment, onCreateView() is invoked, but HomeFragmentViewModelFactory() is not invoked! -> I need it for sending new repo object to view model


Solution

  • About your update note: viewModel will not be recreated upon coming back to a fragment so the factory is not called.

    You need to know about viewModel Owner. in order to create a viewModel we need a ViewModelStoreOwner which is an interface which we will see in a bit, and activity, fragment, navgraph and navBackStackEntry classes implement this interface. This is the first argument of ViewModelProvider which in your case is this which means the current fragment.

    What does this ViewModelStoreOwner do? basically it has a map which it keeps the instances of the viewModel created, when asking for a viewModel using ViewModelProvider first it looks inside this map to see if it has created it or not, if it is created it returns the previously created viewModel otherwise it will call the ViewModelFactory to create a new one, after that it stores it in this map for later use.

    What does that mean? for a fragment this map is create in onCreate callback and destroyed in onDestroy callback (not onDestroyView) so when you navigate to a new fragment, since this fragment is not yet destroyed, The viewModel still exists in this map, therefore upon coming back the one from the map which is previously created is returned.

    PossibleSolution: Create a function in viewModel and pass the repo to it (instead of constructor arguments), or do not use viewModel at all!