One of the common problem we always meet in the world of Fragment is: although we could callstartActivityForResult
directly from Nested Fragment but it appears that onActivityResult
would never been called which brought a lot of trouble to handle Activity Result from Nested Fragment.
Why does this happen? That's because Fragment is not first designed to be nested. Once its capability was expanded, the architecture behind Fragment couldn't cover all the case. And we developers have to handle the problem case by case by ourselves.
But don't worry, we already have a sustainable and robust workaround for this problem. Ok, let's start !
Architecture behind Fragment's startActivityForResult
Although we could call startActivityForResult
directly from Fragment but actually mechanic behind are all handled by Activity. Once you call startActivityForResult from a Fragment,requestCode will be changed to attach Fragment's identity to the code. That will let Activity be able to track back that who send this request once result is received.
Once Activity was navigated back, the result will be sent to Activity's onActivityResult
with the modified requestCode which will be decoded to original requestCode + Fragment's identity. After that, Activity will send the Activity Result to that Fragment through onActivityResult
. And it's all done.
The problem is: Activity could send the result to only the Fragment that has been attached directly to Activity but not the nested one. That's the reason why onActivityResult of nested fragment would never been called no matter what.
The Solution
This behavior is one of the most popular issue in town. We could found a lot of thread related to this in stackoverflow. There are a lot of workaround provided by people there. Anyway none of them is sustainable enough to be used in any case (at least all of those that I discovered). So we spend a day research all the mechanic behind and try to find the way to cover all the cases available. And finally we found one!
The problem, as described above, is the request could be sent from nested fragment but couldn't be received properly. Thus there is no need to do those things in Fragment. Let them be all done in Activity level.
So we will call getActivity().startActivityForResult(...)
from Fragment instead of just startActivityResult(...)
from now on. Like this:
1
2
3
|
// In Fragment
Intent intent =
new
Intent(getActivity(), SecondActivity.
class
);
getActivity().startActivityForResult(intent,
12345
);
|
As a result, all of the result received will be handled at the single place: onActivityResult
of the Activity that Fragment is placed on.
Question is how to send the Activity Result to Fragment?
Due to the fact that we couldn't directly communicate with all of the nested fragment in the normal way, or at least in the easy way. And another fact is, every Fragment knows that which requestCode it has to handled since it is also the one that call startActivityForResult. So we choose the way to"broadcast to every single Fragment that is active at time. And let those Fragments check requestCode and do what they want."
Talk about broadcasting, LocalBroadcastManager could do the job but the mechanic is the way too old. I choose another alternative, an EventBus, which has a lot of choices out there. The one that I chose was Otto from square. It is really good at performance and robustness.
First of all, add a following line in build.gradle
to include Otto to our project:
1
2
3
|
dependencies {
compile 'com.squareup:otto:1.3.6'
}
|
In the Otto way, let's create a Bus Event as a package carry those Activity Result values.
ActivityResultEvent.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
import
android.content.Intent;
/**
* Created by nuuneoi on 3/12/2015.
*/
public
class
ActivityResultEvent {
private
int
requestCode;
private
int
resultCode;
private
Intent data;
public
ActivityResultEvent(
int
requestCode,
int
resultCode, Intent data) {
this
.requestCode = requestCode;
this
.resultCode = resultCode;
this
.data = data;
}
public
int
getRequestCode() {
return
requestCode;
}
public
void
setRequestCode(
int
requestCode) {
this
.requestCode = requestCode;
}
public
int
getResultCode() {
return
resultCode;
}
public
void
setResultCode(
int
resultCode) {
this
.resultCode = resultCode;
}
public
Intent getData() {
return
data;
}
public
void
setData(Intent data) {
this
.data = data;
}
}
|
And of course, also create a Singleton of Event Bus which will be used to send a package from an Activity to all of active Fragments.
ActivityResultBus.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import
android.os.Handler;
import
android.os.Looper;
import
com.squareup.otto.Bus;
/**
* Created by nuuneoi on 3/12/2015.
*/
public
class
ActivityResultBus
extends
Bus {
private
static
ActivityResultBus instance;
public
static
ActivityResultBus getInstance() {
if
(instance ==
null
)
instance =
new
ActivityResultBus();
return
instance;
}
private
Handler mHandler =
new
Handler(Looper.getMainLooper());
public
void
postQueue(
final
Object obj) {
mHandler.post(
new
Runnable() {
@Override
public
void
run() {
ActivityResultBus.getInstance().post(obj);
}
});
}
}
|
You may notice that I also create a custom method named postQueue
in the bus object. This one is used to send a package into the bus. And the reason why we have to do it this way is because we have to delay a package sending a little bit since at the moment that Activitiy's onActivityResult has been called, the Fragment is not become active yet. So we need to let Handler send those commands to the queue of Main Thread with handler.post(...) like coded above.
And then we will override onActivityResult on Activity and add a following line to send the package to the bus once the result is received.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public
class
MainActivity
extends
ActionBarActivity {
...
@Override
protected
void
onActivityResult(
int
requestCode,
int
resultCode, Intent data) {
super
.onActivityResult(requestCode, resultCode, data);
ActivityResultBus.getInstance().postQueue(
new
ActivityResultEvent(requestCode, resultCode, data));
}
...
}
|
In Fragment part, we need to listen to the package sent from Activity. We could do it easily in Otto way like this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public
class
BodyFragment
extends
Fragment {
...
@Override
public
void
onStart() {
super
.onStart();
ActivityResultBus.getInstance().register(mActivityResultSubscriber);
}
@Override
public
void
onStop() {
super
.onStop();
ActivityResultBus.getInstance().unregister(mActivityResultSubscriber);
}
private
Object mActivityResultSubscriber =
new
Object() {
@Subscribe
public
void
onActivityResultReceived(ActivityResultEvent event) {
int
requestCode = event.getRequestCode();
int
resultCode = event.getResultCode();
Intent data = event.getData();
onActivityResult(requestCode, resultCode, data);
}
};
...
}
|
That's all. Fragment's onActivityResult
will be called from now on ! You can now just simply override onActivityResult, check the requestCode and do what you want.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public
class
BodyFragment
extends
Fragment {
...
@Override
public
void
onActivityResult(
int
requestCode,
int
resultCode, Intent data) {
super
.onActivityResult(requestCode, resultCode, data);
// Don't forget to check requestCode before continuing your job
if
(requestCode ==
12345
) {
// Do your job
tvResult.setText(
"Result Code = "
+ resultCode);
}
}
...
}
|
With this solution, it could be applied for any single fragment whether it is nested or not. And yes, it also covers all the case! Moreover, the codes are also nice and clean.
Limitation
There is just only one limitation. Don't use the same requestCode in different Fragment. As you can see, every single Fragment that is active at time will be receive the package. If you use the same requestCode in different Fragment, it may delivers the wrong outcome. Except that you intend to do it, you can.
Make it easy with StatedFragment
Good news! The code we described in this article are already included in our StatedFragment in version 0.9.3 and above. You could now use it easily like this:
Add a dependency in build.gradle
1
2
3
|
dependencies {
compile 'com.inthecheesefactory.thecheeselibrary:stated-fragment-support-v4:0.9.3'
}
|
In case you use Fragment from android.app.*, please add the following instead.
1
2
3
|
dependencies {
compile 'com.inthecheesefactory.thecheeselibrary:stated-fragment:0.9.3'
}
|
To enable it, just simply override method onActivityResult
in the Activity and add a following line:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public
class
MainActivity
extends
ActionBarActivity {
...
@Override
protected
void
onActivityResult(
int
requestCode,
int
resultCode, Intent data) {
super
.onActivityResult(requestCode, resultCode, data);
ActivityResultBus.getInstance().postQueue(
new
ActivityResultEvent(requestCode, resultCode, data));
}
...
}
|
For Fragment, you could simple extends StatedFragment
. onActivityResult
will be now useful.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public
class
BodyFragment
extends
StatedFragment {
...
@Override
public
void
onActivityResult(
int
requestCode,
int
resultCode, Intent data) {
super
.onActivityResult(requestCode, resultCode, data);
// Add your code here
Toast.makeText(getActivity(),
"Fragment Got it: "
+ requestCode +
", "
+ resultCode, Toast.LENGTH_SHORT).show();
}
...
}
|
As I said. Easy, huh?
Hope that this article is helpful to you all. Best wishes to you all =)