0

I wonder what's the idiomatic way to pass datas from fragments back to its container activity?

Consider this sign up activity:

class SignUpActivity : AppCompatActivity() {

    lateinit var uiBinding: ActivitySignUpBinding
    lateinit var btnNext: MaterialButton
    lateinit var currentFragment: Fragment

    var fragmentIdx: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        uiBinding = ActivitySignUpBinding.inflate(layoutInflater)

        currentFragment = InputBasicDataFragment()

        supportFragmentManager.beginTransaction().replace(R.id.fragment_signup_container,
            currentFragment).commit()

        btnNext = uiBinding.btnSignUpNext
        
        val pref = SharedPrefUtil()

        btnNext.setOnClickListener {

            when (fragmentIdx){
                0 -> {
                    ++fragmentIdx
                    
                    // read all the data on InputBasicData fragment, then save them on shared pref
                    pref.write('FRAGMENT1_NAME', '....')
                    pref.write('FRAGMENT1_MAIL', '....')
                    
                    currentFragment = InputAdditionalDataFragment()
                }

                1-> {
                    ++fragmentIdx
                    
                    // read all the data on InputAdditionalData fragment, then save them on shared pref as well
                    pref.write('FRAGMENT2_HOME_ADDRESS', '....')
                    pref.write('FRAGMENT2_JOB', '....')
                    pref.write('FRAGMENT2_WORK_ADDRESS', '....')
                    
                    currentFragment = PreviewDataFragment()
                }

                2 -> {
                    // done. ready to upload data that are stored in shared pref
                    // let's hit the API ...
                    intent iii = Intent(this@SignUpActivity, NextActivity::class.java)
                    startActivity(iii)
                }
            }

            supportFragmentManager.beginTransaction().replace(R.id.fragment_signup_container,
                currentFragment).commit()
        }

        setContentView(uiBinding.root)
    }
}

There's no button on those 3 fragments, basically only TextView and EditText. The Button is located on the container activity. First the fragment container loads InputBasicDataFragment. When the button is clicked, how to access all the inputs so can be saved on shared pref?

anta40
  • 6,511
  • 7
  • 46
  • 73
  • If you r using Using MVVM then use a Shared `ViewModel` otherwise you have rely on method calling or a callback Interface. Its better you have the button in fragment itself if you could . – ADM Oct 10 '21 at 04:44
  • As @ADM suggested, have an interface in the Activity, pass an instance to the fragment through the constructor and trigger the interface callbacks to get the action – gtxtreme Oct 10 '21 at 06:11
  • @ADM No I don't use MVVM. And the if button is moved to fragment itself, then how can it trigger `supportFragmentManager.beginTransaction().replace()` (switch to next fragment)? – anta40 Oct 10 '21 at 13:56
  • @gtxtreme hmm... something like this: https://stackoverflow.com/questions/14439941/passing-data-between-fragments-to-activity ? – anta40 Oct 10 '21 at 13:59
  • @gtxtreme It’s unreliable to pass the activity instance to a Fragment constructor. Only Fragment’s empty constructor can be trusted, because that’s what the OS uses when recreating Fragments. That’s why the documentation only uses extras to set up Fragment properties. Instead, the fragment should use its own `getActivity()` or `requireActivity()` to get the reference and if it is going to store the reference, it should do it in `onAttach` in case it gets attached to a new instance of the activity. – Tenfour04 Oct 10 '21 at 14:17
  • @Tenfour04 you're right. It's incorrect to treat `Activity` as just another Java or Kotlin class because it has a life. It can feel stuff xD. Thanks for the tip though. But I was suggesting passing an interface instance or maybe an anonymous object or kotlin singleton object for the fragment to refer to and call methods upon – gtxtreme Oct 11 '21 at 06:58
  • @anta40 you're right, my friend, and Android Docs have articles for that specific topic i.e. sharing data between activity and it's children i.e. the fragment – gtxtreme Oct 11 '21 at 06:59
  • @gtxtreme Well, you shouldn’t pass *anything* to the Fragment constructor for the same reason I mentioned. And a singleton listener that modifies UI elements would be leaking whichever views it was holding a reference to. – Tenfour04 Oct 11 '21 at 12:46

2 Answers2

0

The typical pattern I’ve seen in the Jetpack libraries and at least once in the documentation about Fragments is for the Fragment to supply a callback interface that the Activity can optionally implement. When the trigger action occurs in the Fragment, check if the attached activity implements the interface, and call it if it does.

class MyFragment(): Fragment(R.layout.my_layout) {

    interface MyActionListener {
        onSomethingReady(someData: Something)
    }

    // In some callback …

    (activity as? MyActionListener)?.onSomethingReady(something)

In your use case though, your activity’s functionality is closely tied to the fragments and just wants to get the current values of their UI controls. I think in this case it makes more sense for the Activity to get the fragment instances from the FragmentManager and read from them. Otherwise you will have to put TextWatchers on all the EditTexts, and pass the updated text every time a character is typed for the Activity to cache in a property. It’s kind of a high price for separation of concerns in this case.

Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Like this: https://stackoverflow.com/questions/14439941/passing-data-between-fragments-to-activity? The problem is there's no button on all fragments, so not sure who will trigger `onSomethingReady()`. Unless I misunderstand you... – anta40 Oct 10 '21 at 15:24
  • Only fragments that have data to share would call the interface function. – Tenfour04 Oct 10 '21 at 16:16
0

I suggest using a ViewModel.

The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.

see https://developer.android.com/topic/libraries/architecture/viewmodel.

Then the extension to a standard view model is that of an shared view model where a single instance can be shared between multiple Fragments and their Activity.

https://developer.android.com/topic/libraries/architecture/viewmodel#sharing

This would allow for the fragments to update some value inside the view model, and the activity could react to changes to those variables.

There are many other well documented benefits of using viewModels.

Chris
  • 4,662
  • 2
  • 19
  • 27