0

I have an activity using fragments. To communicate from the fragment to the activity, I use interfaces. Here is the simplified code:

Activity:

class HomeActivity : AppCompatActivity(), DiaryFragment.IAddEntryClickedListener, DiaryFragment.IDeleteClickedListener {

    override fun onAddEntryClicked() {
        //DO something
    }

    override fun onEntryDeleteClicked(isDeleteSet: Boolean) {
        //Do something
    }

    private val diaryFragment: DiaryFragment = DiaryFragment()

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

        diaryFragment.setOnEntryClickedListener(this)
        diaryFragment.setOnDeleteClickedListener(this)

        supportFragmentManager.beginTransaction().replace(R.id.content_frame, diaryFragment)
    }
}

The fragment:

class DiaryFragment: Fragment() {

    private var onEntryClickedListener: IAddEntryClickedListener? = null
    private var onDeleteClickedListener: IDeleteClickedListener? = null


    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view: View = inflater.inflate(R.layout.fragment_diary, container, false)

        //Some user interaction
        onDeleteClickedListener!!.onEntryDeleteClicked()
        onDeleteClickedListener!!.onEntryDeleteClicked()

        return view
    }

    interface IAddEntryClickedListener {
        fun onAddEntryClicked()
    }

    interface IDeleteClickedListener {
        fun onEntryDeleteClicked()
    }

    fun setOnEntryClickedListener(listener: IAddEntryClickedListener) {
        onEntryClickedListener = listener
    }

    fun setOnDeleteClickedListener(listener: IDeleteClickedListener) {
        onDeleteClickedListener = listener
    }
}

This works, but when the fragment is active and the orientation changes from portrait to landscape or otherwise, the listeners are null. I can't put them to the savedInstanceState, or can I somehow? Or is there another way to solve that problem?

Apri
  • 1,241
  • 1
  • 8
  • 33
  • 4
    "I can't put them to the savedInstanceState" -- correct. "Or is there another way to solve that problem?" -- the modern solution is to use a shared `ViewModel` and `LiveData`. The fragments would post events to a `LiveData` that the activity would observe. The Kotlin-flavored legacy solution would be to cast `requireActivity()` to be your interfaces and call functions on it, since your activity is where the interfaces are implemented. – CommonsWare Jul 14 '19 at 14:18
  • @CommonsWare What if my listener has a reference to context? As far as I understand, shared ViewModel members should not reference, directly or indirectly, to context. – Neoh May 24 '20 at 14:00
  • @Neoh: If you are using the Jetpack `ViewModel`, use `AndroidViewModel` to have access to the `Application` singleton. – CommonsWare May 24 '20 at 14:10

2 Answers2

2

Your Problem:

When you switch orientation, the system saves and restores the state of fragments for you. However, you are not accounting for this in your code and you are actually ending up with two (!!) instances of the fragment - one that the system restores (WITHOUT the listeners) and the one you create yourself. When you observe that the fragment's listeners are null, it's because the instance that has been restored for you has not has its listeners reset.

The Solution

First, read the docs on how you should structure your code. Then update your code to something like this:

class HomeActivity : AppCompatActivity(), DiaryFragment.IAddEntryClickedListener, DiaryFragment.IDeleteClickedListener {

    override fun onAddEntryClicked() {
        //DO something
    }

    override fun onEntryDeleteClicked(isDeleteSet: Boolean) {
        //Do something
    }

    // DO NOT create new instance - only if starting from scratch
    private lateinit val diaryFragment: DiaryFragment

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

        // Null state bundle means fresh activity - create the fragment
        if (savedInstanceState == null) {
            diaryFragment = DiaryFragment()
            supportFragmentManager.beginTransaction().replace(R.id.content_frame, diaryFragment)
        }
        else { // We are being restarted from state - the system will have
               // restored the fragment for us, just find the reference
            diaryFragment = supportFragmentManager().findFragment(R.id.content_frame)
        }

        // Now you can access the ONE fragment and set the listener on it
        diaryFragment.setOnEntryClickedListener(this)
        diaryFragment.setOnDeleteClickedListener(this)        
    }
}

Hope that helps!

dominicoder
  • 9,338
  • 1
  • 26
  • 32
0

the short answer without you rewriting your code is you have to restore listeners on activiy resume, and you "should" remove them when you detect activity losing focus. The activity view is completely destroyed and redrawn on rotate so naturally there will be no events on brand new objects.

When you rotate, "onDestroy" is called before anything else happens. When it's being rebuilt, "onCreate" is called. (see https://developer.android.com/guide/topics/resources/runtime-changes) One of the reasons it's done this way is there is nothing forcing you to even use the same layout after rotating. There could be different controls. All you really need to do is make sure that your event hooks are assigned in OnCreate. See this question's answers for an example of event assigning in oncreate. onSaveInstanceState not working

John Lord
  • 1,941
  • 12
  • 27