This is the 8th day of my participation in Gwen Challenge
Fragment communication can be implemented in the following ways:
- EventBus
- Activity (or Parent Fragment)
- ViewModel
- 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:
-
After FragmentA calls the startActivityForResult() method, it jumps to ActivityB, which sets the data to FragmentB via setArguments()
-
FragmentB calls getActivity().setresult () to set the data returned, and FragmentA gets the data in onActivityResult()
At this point, two points need special attention:
-
Instead of using getActivity().startActivityForResult(), call startActivityForResult() directly in your Fragment.
-
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.