Tomek Falkon
Tomek Falkon

Reputation: 29

Saving and Restoring ListView (livedata) in Fragments

I'm trying to make a Todo app. I have succesfully implemented livedata and listview in fragments (fragments are default from the project quickstart template). My problem which I can't resolve is saving those todo's so they are still there upon launching app back again.

Browsed tons of answers on stack and blogs and read about whole lifecycle but I still don't get it. I finally gave up and this is what (not working) code I end up with atm:

FragmentLifeCycle to save "state" of the listOfToDoThings

class FragmentLifeCycle : Fragment() {

    private var state: Parcelable? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d("Lifecycle Info", "onCreate()")

    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        Log.d("Lifecycle Info", "onCreateView()")
        return inflater.inflate(R.layout.activity_main, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        Log.d("Lifecycle Info", "onActivityCreated()")

    }

    override fun onResume() {
        super.onResume()

        if (state != null) {
            Log.i("Lifecycle Info", "onResume finally works")
            listOfToDoThings.onRestoreInstanceState(state)
        }

        Log.d("Lifecycle Info", "onResume()")

    }

    override fun onPause() {
        state = listOfToDoThings.onSaveInstanceState()
        super.onPause()
        Log.d("Lifecycle Info", "onStop()")
    }

}

which throws nullpointer:

'android.os.Parcelable android.widget.ListView.onSaveInstanceState()' on a null object reference

And Main_Activity cleared out of tons of commented not-working solutions:

class MainActivity : AppCompatActivity(){

    private var mSectionsPagerAdapter: SectionsPagerAdapter? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        setSupportActionBar(toolbar)
        // Create the adapter that will return a fragment for each of the three
        // primary sections of the activity.
        mSectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager)

        // Set up the ViewPager with the sections adapter.
        container.adapter = mSectionsPagerAdapter

        val fragmentManager = this.supportFragmentManager
        val fragmentTransaction = fragmentManager.beginTransaction()

        val fragmentLifeCycle = FragmentLifeCycle()
        fragmentTransaction.add(R.id.container, fragmentLifeCycle, "Lifecycle Fragment")
        fragmentTransaction.commit()

    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu; this adds items to the action bar if it is present.
        menuInflater.inflate(R.menu.menu_main, menu)


        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        val id = item.itemId

        if (id == R.id.action_settings) {
            return true
        }

        return super.onOptionsItemSelected(item)
    }

    /**
     * A [FragmentPagerAdapter] that returns a fragment corresponding to
     * one of the sections/tabs/pages.
     */
    inner class SectionsPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {

        override fun getItem(position: Int): Fragment {
            // getItem is called to instantiate the fragment for the given page.
            // Return a PlaceholderFragment (defined as a static inner class below).
            return PlaceholderFragment.newInstance(position + 1)
        }

        override fun getCount(): Int {
            // Show 3 total pages.
            return 4
        }
    }


     /**
     * A placeholder fragment containing a simple view.
     */
    class PlaceholderFragment : Fragment(), Renderer<TodoModel> {

        private lateinit var store: TodoStore

        override fun render(model: LiveData<TodoModel>) {
            model.observe(this, Observer { newState ->
                listOfToDoThings.adapter = TodoAdapter(requireContext(), newState?.todos ?: listOf())
            })
        }



        private fun openDialog() {
            val options = resources.getStringArray(R.array.filter_options).asList()
            requireContext().selector(getString(R.string.filter_title), options) { _, i ->
                val visible = when (i) {
                    1 -> Visibility.Active()
                    2 -> Visibility.Completed()
                    else -> Visibility.All()
                }
                store.dispatch(SetVisibility(visible))
            }
        }

        private val mapStateToProps = Function<TodoModel, TodoModel> {
            val keep: (Todo) -> Boolean = when(it.visibility) {

                is Visibility.All -> {_ -> true}
                is Visibility.Active -> {t: Todo -> !t.status}
                is Visibility.Completed -> {t: Todo -> t.status}
            }

            return@Function it.copy(todos = it.todos.filter { keep(it) })
        }

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

            val rootView = inflater.inflate(R.layout.fragment_main, container, false)
            rootView.section_label.text = getString(R.string.section_format, arguments?.getInt(ARG_SECTION_NUMBER))

            @SuppressLint("SetTextI18n")
            when(arguments?.getInt(ARG_SECTION_NUMBER)) {
                1 -> rootView.section_name.text = "Daily Life"
                2 -> rootView.section_name.text = "Work and College"
                3 -> rootView.section_name.text = "Visits"
                4 -> rootView.section_name.text = "Shop"
            }

            store = ViewModelProviders.of(this).get(TodoStore::class.java)
            store.subscribe(this, mapStateToProps)

            // Add task and then reset editText component
            rootView.addNewToDo.setOnClickListener {
                store.dispatch(AddTodo(editText.text.toString()))
                editText.text = null
            }

            rootView.filter.setOnClickListener{ openDialog() }

            // Press to change status of task
            rootView.listOfToDoThings.adapter = TodoAdapter(requireContext(), listOf())
            rootView.listOfToDoThings.setOnItemClickListener { _, _, _, id ->
                store.dispatch(ToggleTodo(id))
            }

            // Hold to delete task
            rootView.listOfToDoThings.setOnItemLongClickListener { _, _, _, id ->
                store.dispatch(RemoveTodo(id))
                true
            }

            return rootView
        }



        companion object {
            /**
             * The fragment argument representing the section number for this
             * fragment.
             */
            private val ARG_SECTION_NUMBER = "section_number"

            /**
             * Returns a new instance of this fragment for the given section
             * number.
             */
            fun newInstance(sectionNumber: Int): PlaceholderFragment {
                val fragment = PlaceholderFragment()
                val args = Bundle()
                args.putInt(ARG_SECTION_NUMBER, sectionNumber)
                fragment.arguments = args
                return fragment
            }
        }
    }
}

Not sure if its usefull but that's how TodoStore.kt looks like:

class TodoStore : Store<TodoModel>, ViewModel(){

    private val state: MutableLiveData<TodoModel> = MutableLiveData()

    // Start with all tasks visible regardless of previous state
    private val initState = TodoModel(listOf(), Visibility.All())

    override fun dispatch(action: Action) {
        state.value = reduce(state.value, action)
    }


    private fun reduce(state: TodoModel?, action: Action): TodoModel {
        val newState= state ?: initState

        return when(action){

            // Adds stuff upon creating new todo
            is AddTodo -> newState.copy(
                todos = newState.todos.toMutableList().apply {
                    add(Todo(action.text, action.id))
                }
            )

            is ToggleTodo -> newState.copy(
                todos = newState.todos.map {
                    if (it.id == action.id) {
                        it.copy(status = !it.status)
                    } else it
                } as MutableList<Todo>
            )

            is SetVisibility -> newState.copy(
                visibility = action.visibility
            )

            is RemoveTodo -> newState.copy(
                todos = newState.todos.filter {
                    it.id != action.id
                } as MutableList<Todo>
            )
        }
    }

    override fun subscribe(renderer: Renderer<TodoModel>, func: Function<TodoModel, TodoModel>) {
        renderer.render(Transformations.map(state, func))
    }

}

Upvotes: 0

Views: 1012

Answers (2)

TomD88
TomD88

Reputation: 751

If I understand correctly you need to add a persistence layer to your application. Try to use Room Database when load the ListView. SavedInstanceState has some limitations and it should not be used to save a large amount of data or complex objects.

Android Persistence

Room Database

Hope this help.

Upvotes: 1

Alex Ferreira
Alex Ferreira

Reputation: 48

If you need to save the position that the user is in the listView, save only the Int in a bundle on the method onSaveInstanceState() of the fragment. If you want to save the data inside the listView, you do not need to do this, because Android already did that, you just need to put the loadData (your code that init the data and set an adapter to the listView) in onActivityCreated and just restore the position in onViewStateRestored().

Upvotes: 0

Related Questions