This is the 8th day of my participation in Gwen Challenge

Fragment communication can be implemented in the following ways:

  1. EventBus
  2. Activity (or Parent Fragment)
  3. ViewModel
  4. Result API

1. Communicate based on EventBus

The pros and cons of EventBus stand out. Advantage is less restrictions can be used at will, the disadvantage is too little restrictions use too arbitrary.

Because EventBus can cause developers to “stop thinking” about architecture, the code becomes less readable and the data flow becomes difficult to track as the project becomes more complex and the structure becomes more chaotic.

Therefore, the larger the scale of the project, the more obvious the negative effects of EvenBus, so many large factories have banned the use of EventBus. Don’t use EventBus as your first choice.

“I don’t recommend using EventBus for communication on a project because it has the ability to communicate, but the downside is that a lot of use of EventBus can make projects difficult to maintain and problems difficult to locate.”

2. Communicate based on the Activity or parent Fragment

In order to more agile, iterative fragments from AOSP moved to AndroidX, as a result, at the same time there are two fragments of the package name:. Android app. The fragments and andoridx fragments. The app. The fragments.

Although the former has been deprecated, a lot of historical code still exists. For older fragments, they often rely on activity-based communication because most other communication methods rely on AndroidX.

class MainActivity : AppCompatActivity() {

    val listFragment: ListFragment by lazy {
        ListFragment()
    }

    val CreatorFragment: CreatorFragment by lazy {
        // Set Callback while building Fragment to establish communication
        CreatorFragment().apply { 
            setOnItemCreated { 
                listFragment.addItem(it)
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        supportFragmentManager.beginTransaction().apply {
            add(R.id.fragmentContainer, listFragment)
            commit()
        }
    }
}
Copy the code

As above, create a child Fragment in the Activity or parent Fragment and set a Callback for it

In this case, the Fragment creation depends on manual configuration and cannot be automatically restored and rebuilt during ConfigurationChangeed. Therefore, it is not recommended to use the Fragment except for the historical code used to deal with Android.app. Fragment.

3. Communication based on ViewModel

ViewModel is one of the most widely used communication methods, and when used in Kotlin, fragment-KTX needs to be introduced

class ListViewModel : ViewModel() {
    private val originalList: LiveData<List<Item>>() = ...
    val itemList: LiveData<List<Item>> = ...

    fun addItem(item: Item) { 
      / / update the LiveData}}class ListFragment : Fragment() {
    // With KTX, get the ViewModel using the activityViewModels() proxy
    private val viewModel: ListViewModel by activityViewModels()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
        viewModel.itemList.observe(viewLifecycleOwner, Observer { list ->
            // Update the list UI}}}class CreatorFragment : Fragment() {
    private val viewModel: ListViewModel by activityViewModels()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
        
        button.setOnClickListener {
          val item = ...
          viewModel.addItem(item)
        }
    }
}

Copy the code

As above, you receive notifications of data workarounds by subscribing to the ViewModel’s LiveData. Because both fragments need to share the ViewModel, the ViewModel must be created within the Activity’s Scope

There are many articles about the implementation principle of ViewModel, so this article will not repeat them. Let’s focus on the Result API:

4. Communication based on Resutl API

Since Fragment 1.3.0-Alpha04, FragmentManager has added the FragmentResultOwner interface. As the name suggests, the FragmentManager is the owner of the FragmentResult. Communication between fragments can be performed.

Suppose you need to listen on FragmentA for data returned by FragmentB, first set up a listener on FragmentA

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    / / is fragments - KTX setFragmentResultListener provides extension function
    setFragmentResultListener("requestKey") { requestKey, bundle ->
        // Listen for the result whose key is "requestKey" and get it from the bundle
        val result = bundle.getString("bundleKey")
        // ...}}/ / setFragmentResultListener fragments extension functions, internal call FragmentManger method of the same name
public fun Fragment.setFragmentResultListener(
    requestKey: String,
    listener: ((requestKey: String, bundle: Bundle) -> Unit)
) {
    parentFragmentManager.setFragmentResultListener(requestKey, this, listener)
}

Copy the code

When returning a result from FragmentB:

button.setOnClickListener {
    val result = "result"
    setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

//setFragmentResult is also a Fragment extension function that internally calls the same method as FragmentManger
public fun Fragment.setFragmentResult(requestKey: String, result: Bundle) {
    parentFragmentManager.setFragmentResult(requestKey, result)
}
Copy the code

The above code can be represented as follows:

The Result API works very simply. FragmentA registers a ResultListener with a Key to the FragmentManager. When FragmentB returns a Result, FM calls back the result to FragmentA by Key. Note that result is actually returned only if FragmentB returns, and if setFragmentResult is returned multiple times, only the last result is retained.

The life cycle is perceptible

By combing through the source code, you can see that the Result API is LifecycleAware

Source based on androidx. Fragments: fragments: 1.3.0

SetFragmentResultListener implementation:

//FragmentManager.java
private final Map<String, LifecycleAwareResultListener> mResultListeners =
            Collections.synchronizedMap(new HashMap<String, LifecycleAwareResultListener>());


public final void setFragmentResultListener(@NonNull final String requestKey,
            @NonNull final LifecycleOwner lifecycleOwner,
            @NonNull final FragmentResultListener listener) {
        final Lifecycle lifecycle = lifecycleOwner.getLifecycle();
        LifecycleEventObserver observer = new LifecycleEventObserver() {
                if (event == Lifecycle.Event.ON_START) {
                    // once we are started, check for any stored results
                    Bundle storedResult = mResults.get(requestKey);
                    if(storedResult ! =null) {
                        // if there is a result, fire the callback
                        listener.onFragmentResult(requestKey, storedResult);
                        // and clear the resultclearFragmentResult(requestKey); }}if (event == Lifecycle.Event.ON_DESTROY) {
                    lifecycle.removeObserver(this); mResultListeners.remove(requestKey); }}; lifecycle.addObserver(observer); LifecycleAwareResultListener storedListener = mResultListeners.put(requestKey,new LifecycleAwareResultListener(lifecycle, listener, observer));
        if(storedListener ! =null) { storedListener.removeObserver(); }}Copy the code
  • Listener. OnFragmentResult in Lifecycle. Event. Only when ON_START call, that is to say, only when the FragmentA returned to the front desk, will receive as a result, this behavior consistent with LiveData logic, Are LifecycleAware

  • When multiple calls setFragmentResultListener, Will create a new LifecycleEventObserver object, at the same time, the old observer as storedListener. RemoveObserver () removed from the lifecycle, cannot be called back again.

That is to say, for the same requestKey, only the last set the listener at a time, it seems is granted, don’t call addFragmentResultListener after all.

SetFragmentResult implementation:

 private final Map<String, Bundle> mResults =
            Collections.synchronizedMap(new HashMap<String, Bundle>());
            
 public final void setFragmentResult(@NonNull String requestKey, @NonNull Bundle result) {
        // Check if there is a listener waiting for a result with this key
        LifecycleAwareResultListener resultListener = mResultListeners.get(requestKey);
        // if there is and it is started, fire the callback
        if(resultListener ! =null && resultListener.isAtLeast(Lifecycle.State.STARTED)) {
            resultListener.onFragmentResult(requestKey, result);
        } else {
            // else, save the result for latermResults.put(requestKey, result); }}Copy the code

SetFragmentResult is very simple. If the listener is currently in the foreground, setFragmentResult() is called immediately; otherwise, mResults is saved and the listener is called when it switches to the foreground.

A listener why have the concept of the foreground/background, which is seen before LifecycleAwareResultListener, perceived life cycle because of its internal have a Lifecycle, And that Lifecycle is actually the Fragment that sets the listener

 private static class LifecycleAwareResultListener implements FragmentResultListener {
        private final Lifecycle mLifecycle;
        private final FragmentResultListener mListener;
        private final LifecycleEventObserver mObserver;

        LifecycleAwareResultListener(@NonNull Lifecycle lifecycle,
                @NonNull FragmentResultListener listener,
                @NonNull LifecycleEventObserver observer) {
            mLifecycle = lifecycle;
            mListener = listener;
            mObserver = observer;
        }

        public boolean isAtLeast(Lifecycle.State state) {
            return mLifecycle.getCurrentState().isAtLeast(state);
        }

        @Override
        public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle result) {
            mListener.onFragmentResult(requestKey, result);
        }

        public void removeObserver(a) { mLifecycle.removeObserver(mObserver); }}Copy the code

Recoverable reconstruction

The data in mResult is recoverable as the Fragment rebuilds, so FragmentA never loses the result returned by FragmentB. Of course, once a Result is consumed, it is cleared from the mResult

The preservation of mResults

//FragmentManager.java
void restoreSaveState(@Nullable Parcelable state) {
    / /...
    ArrayList<String> savedResultKeys = fms.mResultKeys;
        if(savedResultKeys ! =null) {
            for (int i = 0; i < savedResultKeys.size(); i++) { mResults.put(savedResultKeys.get(i), fms.mResults.get(i)); }}}Copy the code

The recovery of mResults

Parcelable saveAllState(a) {
    // FragmentManagerState implements Parcelable
    FragmentManagerState fms = new FragmentManagerState();
    / /...
    fms.mResultKeys.addAll(mResults.keySet());
    fms.mResults.addAll(mResults.values());
    / /...
    return fms;
}
Copy the code

How to choose? Result the API and the ViewModel

ResultAPI and ViewModel + LiveData have certain similarities, both are life cycle aware, both can save data during recovery and reconstruction, then how to choose the two communication modes?

The official advice is as follows:

The Fragment library provides two options for communication: a shared ViewModel and the Fragment Result API. The recommended option depends on the use case. To share persistent data with any custom APIs, you should use a ViewModel. For a one-time result with data that can be placed in a Bundle, you should use the Fragment Result API.

  • The ResultAPI is primarily suitable for those one-off communication scenarios (FragmentB returns the result and ends itself). If the ViewModel is used, the common parent Scope of the Fragment mentioned above is needed, and the enlargement of Scope is not conducive to data management.

  • In non-one-time communication scenarios, because FragmentA and FragmentB coexist during communication, you are advised to share ViewModel and use LiveData for responsive communication.

5. Communication across activities

Finally, take a look at the communication between Fragmnets across different activities

There are two main ways to communicate across activities:

  • startActivityResult
  • Activity Result API

startActivityResult

Prior to the Result API, you needed to communicate with startActivityResult, which is the only option available to Android.app. Fragment.

The communication process is as follows:

  1. After FragmentA calls the startActivityForResult() method, it jumps to ActivityB, which sets the data to FragmentB via setArguments()

  2. FragmentB calls getActivity().setresult () to set the data returned, and FragmentA gets the data in onActivityResult()

At this point, two points need special attention:

  1. Instead of using getActivity().startActivityForResult(), call startActivityForResult() directly in your Fragment.

  2. The activity needs to override onActivityResult, which must call super.onActivityResult(requestCode, resultCode, data).

The onActivityResult can only be sent to the activity, not to the Fragment

Result API

Since 1.3.0 – alpha02 fragments support registerForActivityResult (use of), by the Activity of ResultAPI allow communications across the Activity.

FragmentA setting callback:

class FragmentA : Fragment() {
    private val startActivityLauncher: ActivityResultLauncher<Intent> =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            if (it.resultCode == Activity.RESULT_OK) {
                //
            } else if (it.resultCode == Activity.RESULT_CANCELED) {
                //}}override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        startActivityLauncher.launch(Intent(requireContext(), ActivityB::class.java))
    }
}
Copy the code

FragmentB Returns the result

button.setOnClickListener {
    val result = "result"
    // Use the Kotlin extension in the fragment-ktx artifact
    setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
Copy the code

Those of you familiar with the Activity Result API should be familiar with this process.

Take a quick look at the source code.

Source based on androidx. Fragments: fragments: 1.3.0

We launch the target ActivityB in FragmentA by creating an ActivityResultLauncher and then calling Launch

//Fragment # prepareCallInternal

return new ActivityResultLauncher<I>() {
            @Override
            public void launch(I input, @Nullable ActivityOptionsCompat options) {
                ActivityResultLauncher<I> delegate = ref.get();
                if (delegate == null) {
                    throw new IllegalStateException("Operation cannot be started before fragment "
                            + "is in created state");
                }
                delegate.launch(input, options);
            }

            / /...
        };
        
Copy the code

As you can see, there is an internal call to delegate.launch. Let’s trace the delegate back to the value set in ref

//Fragment # prepareCallInternal

registerOnPreAttachListener(new OnPreAttachedListener() {
            @Override
            void onPreAttached(a) {
                // a launcher is registered in the ref, from the ActivityResultRegistry provided by the registryProvider
                final String key = generateActivityResultKey();
                ActivityResultRegistry registry = registryProvider.apply(null);
                ref.set(registry.register(key, Fragment.this, contract, callback)); }});public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
            @NonNull final ActivityResultContract<I, O> contract,
            @NonNull final ActivityResultCallback<O> callback) {
        return prepareCallInternal(contract, new Function<Void, ActivityResultRegistry>() {
            @Override
            public ActivityResultRegistry apply(Void input) {
                //registryProvider provides ActivityResultRegistry from the Activity
                if (mHost instanceof ActivityResultRegistryOwner) {
                    return ((ActivityResultRegistryOwner) mHost).getActivityResultRegistry();
                }
                return requireActivity().getActivityResultRegistry();
            }
        }, callback);
    }
Copy the code

You can see the ActivityResultLauncher set in ref, the ActivityResultRegistry from your Activity, the Fragment launch, The Activity is ultimately proxied by its mHost.

The Activity Result API is essentially implemented based on startActivityForResult. See this article for details

conclusion

In this paper, several common methods of Fragment communication are summarized, and the realization principle of Result API is analyzed emphatically. After fragment-1.3.0, it is recommended to use the Result API instead of the old startActivityForResult for one-time communication. In reactive communication scenarios, ViewModel + LiveData (or StateFlow) is recommended instead of using tools such as EventBus.