Search code examples
androidkotlinmvvmviewmodel

Android: Variable gets uninitialized in ViewModel after being initialized in the Fragment


I have a callback method in my fragment which gets called from it's ViewModel. It initializes the variable in the OnCreateView() method of the fragment, but when the ViewModel calls it to use it, its null.

I am thinking that it has something to do with maybe the VM getting recreated somehow? I just can't seem to figure it out.

I am following this answer's of how the VM drives the UI. They provide Google's sample of a callback interface being created (TasksNavigator.java), Overriding the method in the View (TasksActivity.java), and then calling that method from the VM (TasksViewModel.java) but it doesn't seem to work for me.

Fragment

class SearchMovieFragment : Fragment(), SearchNavigator {

    companion object {
        fun newInstance() = SearchMovieFragment()
    }

    private lateinit var searchMovieFragmentViewModel: SearchMovieFragmentViewModel
    private lateinit var binding: SearchMovieFragmentBinding
    private lateinit var movieRecyclerView: RecyclerView

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

        searchMovieFragmentViewModel = ViewModelProvider(this).get(SearchMovieFragmentViewModel::class.java)
        binding = DataBindingUtil.inflate(inflater, R.layout.search_movie_fragment, container, false)
        binding.viewmodel = searchMovieFragmentViewModel
        searchMovieFragmentViewModel.setNavigator(this)

        setUpRecyclerView(container!!.context)
        return binding.root
    }

    private fun setUpRecyclerView(context: Context) {
        movieRecyclerView = binding.searchMovieFragmentRecyclerView.apply {
            this.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
        }
        val adapter = MovieListAdapter()
        binding.searchMovieFragmentRecyclerView.adapter = adapter
        searchMovieFragmentViewModel.movieList.observe(viewLifecycleOwner, Observer {
            adapter.submitList(it)
        })
    }

    override fun openDetails() {
        Log.d("TEST", "opening details")
    }
}

ViewModel


class SearchMovieFragmentViewModel : ViewModel(), MovieSearchItemViewModel {

    private lateinit var searchNavigator: SearchNavigator
    val editTextContent = MutableLiveData<String>()
    var movieList = Repository.getMovieList("batman")

    fun setNavigator(_searchNavigator: SearchNavigator) {
        this.searchNavigator = _searchNavigator
        if (searchNavigator != null) {
            Log.d("TEST", "its not null $searchNavigator") // Here it is not null
        }
    }

    private fun getMovieDetail(movieId: String) {
        val movie = Repository.getMovieDetail(movieId)

        Log.d("TEST", "checking ${this.searchNavigator}") // Here is where I call it but it is null
//        searchNavigator.openDetails()
    }
    private fun getMovieList(movieSearch: String): MutableLiveData<List<Movie>> = Repository.getMovieList(movieSearch)

    override fun displayMovieDetailsButton(movieId: String) {
        Log.d("TEST", "button clicked $movieId")
        getMovieDetail(movieId)
    }
}

CallBack Interface

interface SearchNavigator {
    fun openDetails()
}

Solution

  • Initiate ViewModel in below method of fragment

    override onActivityCreated(@Nullable final Bundle savedInstanceState){
        searchMovieFragmentViewModel = ViewModelProvider(this).get(SearchMovieFragmentViewModel::class.java)
    }
    

    I will recommend use live data to create connection between ViewModel and and Fragment it will be safer and correct approach.

    Trigger openDetails based on the trigger's from your live data.It's forbidden to send your view(context) instance to ViewModel even if you wrap it as there is high probability of memory leaks.

    But if you still want to follow this approach then you should Register and unregister fragment instance in your ViewModel (keep a list of SearchNavigator) it onStop() and onStart() .

    and loop through them to call openDetails