Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SavedStateHandle returning null values

I have an ActionBar menu icon that opens a CategoryFragment. This fragment takes in a category object SafeArgs argument passed from another fragment. In the CategoryFragment, I store the category's name and id into the fragment's shared ViewModel as SavedStateHandle values. I've setup it up so that the fragment uses the stored SavedStateHandle values for the category name and id when it needs to. For example, for the first time, the CategoryFragment uses the category object passed from the sending fragment, but subsequent creation of the CategoryFrgament will use the SavedStateHandle values.

The problem is, if after first opening CategoriesFragment and then exiting the app by either pressing the phone's physical back button or terminating the app from the phone's recent's button in the navbar, now opening the CategoryFragment directly by pressing the ActionBar menu icon displays a blank screen. This is because the values returned from SavedStateHandle are null. How can I fix this?

Category Fragment

class CategoryFragment : Fragment(), SearchView.OnQueryTextListener {

    lateinit var navController: NavController
    private var adapter: TasksRecyclerAdapter? = null
    private val viewModel: CategoryTasksViewModel by activityViewModels()
    private var fromCategoriesFragment: Boolean = false

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_category, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        navController = Navigation.findNavController(view)

        observerSetup()
        recyclerSetup()

        var searchView = category_tasks_searchview
        searchView.setOnQueryTextListener(this)

        fab_new_task.setOnClickListener {
            navController.navigate(R.id.action_categoryFragment_to_newTaskDialogFragment)
        }

        showTasks()
    }

private fun showTasks() {

        if(fromCategoriesFragment){

            PomoPlayObservablesSingleton.fromCategoriesFragment.onNext(false)

            if (!arguments?.isEmpty!!) {

                var args = CategoryFragmentArgs.fromBundle(arguments!!)
                category_title.text = args.category?.name

                var category = args.category
                viewModel.setPomoCategoryName(category.name)
                viewModel.setCategoryId(category.id)
                viewModel.searchTasksByCategoryId(category.id)

            }

        }
        else{
            category_title.text = viewModel.getPomoCategoryName()
            viewModel.searchTasksByCategoryId(viewModel.getCategoryId())

            Log.i("CategoryFrag-CatName", viewModel.getPomoCategoryName().toString())
            Log.i("CategoryFrag-CatId", viewModel.getCategoryId().toString())
        }
    }

    private fun observerSetup() {

        viewModel.getSearchTasksByCategoryIdResults().observe(this,androidx.lifecycle.Observer { tasks ->

            if(tasks.isNotEmpty()){

                adapter?.setTasksList(tasks.sortedBy { task -> task.name?.toLowerCase() })
                task_not_found_bubble.visibility = View.GONE
                task_not_found_text.visibility = View.GONE
            }
            else{
                task_not_found_bubble.visibility = View.VISIBLE
                task_not_found_text.visibility = View.VISIBLE
            }
        })

        PomoPlayObservablesSingleton.fromCategoriesFragment.subscribe {value -> fromCategoriesFragment = value}

    }

    private fun recyclerSetup() {
        adapter = context?.let { TasksRecyclerAdapter(it) }
        tasks_list?.layoutManager = LinearLayoutManager(context)
        tasks_list?.adapter = adapter
    }

    override fun onQueryTextSubmit(query: String?): Boolean {

        Log.i("Lifecycle-CatFragment", "onQueryTextSubmit() called")

        var q = query?.toLowerCase()?.trim()?.replace("\\s+".toRegex(), " ")
        setLastSearchQuery(q.toString())

        viewModel.searchTasksByName(viewModel.getLastSearchQuery().toString())

        return false
    }

    private fun setLastSearchQuery(lastSearchQuery: String) {
        viewModel.setLastSearchQuery(lastSearchQuery)
    }
}

CategoryTasksViewModel

class CategoryTasksViewModel(application: Application, state: SavedStateHandle) : AndroidViewModel(application) {

    private val repository: PomoPlayRepository = PomoPlayRepository(application)
    private val allCategories: LiveData<List<Category>>?
    private val allPomoTasks: LiveData<List<PomoTask>>?
    private val searchCategoriesByNameResults: MutableLiveData<List<Category>>
    private val searchCategoryByIdResults: MutableLiveData<Category>
    private val searchTasksByIdResults: MutableLiveData<PomoTask>
    private val searchTasksByNameResults: MutableLiveData<List<PomoTask>>
    private val searchTasksByCategoryIdResults: MutableLiveData<List<PomoTask>>

    private val savedStateHandle = state

    companion object{

        private const val LAST_SEARCH_QUERY = "lastSearchQuery"

    }

    init {
        allCategories = repository.allCategories
        allPomoTasks = repository.allPomoTasks
        searchTasksByIdResults = repository.searchTasksByIdResults
        searchTasksByNameResults = repository.searchTasksByNameResults
        searchTasksByCategoryIdResults = repository.searchTasksByCategoryIdResults
        searchCategoryByIdResults = repository.searchCategoriesByIdResults
        searchCategoriesByNameResults = repository.searchCategoriesByNameResults
    }

    fun setLastSearchQuery(lastSearchName: String){
        savedStateHandle.set(LAST_SEARCH_QUERY, lastSearchName)
    }

    fun getLastSearchQuery(): String?{
        return savedStateHandle.get<String>(LAST_SEARCH_QUERY)
    }

    fun setPomoCategoryName(name: String?){
        savedStateHandle.set("categoryName", name)
    }

    fun getPomoCategoryName(): String?{
        return savedStateHandle.get<String>("categoryName")
    }

    fun setCategoryId(id: Int){
        savedStateHandle.set("categoryId", id)
    }

    fun getCategoryId(): Int?{
        return savedStateHandle.get<Int>("categoryId")
    }

    fun insertTask(pomoTask: PomoTask?) {
        repository.insertTask(pomoTask)
    }

    fun deleteTask(pomoTask: PomoTask) {
        repository.deleteTask(pomoTask)
    }

    fun updateTask(pomoTask: PomoTask) {
        repository.updateTask(pomoTask)
    }


    fun searchTasksByName(name: String) {
        repository.searchTasksByName(name)
    }


    fun searchTasksById(pomoTaskId: Int){
        repository.searchTasksById(pomoTaskId)
    }

    fun searchTasksByCategoryId(categoryId: Int?){
        repository.searchTasksByCategoryId(categoryId)
    }

    fun getAllPomoTasks() : LiveData<List<PomoTask>>? {
        return allPomoTasks
    }

    fun getSearchTasksbyNameResults() : MutableLiveData<List<PomoTask>> {
        return searchTasksByNameResults
    }

    fun getSearchTasksByIdResults() : MutableLiveData<PomoTask> {
        return searchTasksByIdResults
    }

    fun getSearchTasksByCategoryIdResults() : MutableLiveData<List<PomoTask>> {
        return searchTasksByCategoryIdResults
    }

}
like image 358
rajndev Avatar asked Feb 04 '20 22:02

rajndev


1 Answers

SavedStateHandle was not designed to do, what you expect it to do: It ...

... is a key-value map that will let you write and retrieve objects to and from the saved state. These values will persist after the process is killed by the system and remain available via the same object.

Killed by the system, not if the user closes the app willfully or even destroys ("navigates away permanently") the Fragment/Activity acting as its scope. See the docs on Saving UI State - User-initiated UI state dismissal:

The user's assumption in these complete dismissal cases is that they have permanently navigated away from the activity, and if they re-open the activity they expect the activity to start from a clean state. The underlying system behavior for these dismissal scenarios matches the user expectation - the activity instance will get destroyed and removed from memory, along with any state stored in it and any saved instance state record associated with the activity.

Maybe save the information you expect to survive your scenario in SharedPreferences.

like image 63
outta comfort Avatar answered Oct 19 '22 06:10

outta comfort