Skip to content
This repository has been archived by the owner on Mar 16, 2021. It is now read-only.

Bugfix: Presenter not destroyed when Fragment is popped off the backstack #78

Merged
merged 83 commits into from
Apr 25, 2017

Conversation

ghost
Copy link

@ghost ghost commented Mar 20, 2017

Hello everyone,

currently I'm working on a fix for #33.
We thought it would be better if we add all relevant Unit-Tests first so it would be easier to develop the bugfix. Additionally I've created a new Activity for the "Sample" application which can be be used to interactively test the Fragment lifecycle under different circumstances (e.g. back stack or configuration change).

I would like to open this PR as early as possible to make it open for discussion.

Please feel free to comment and contribute!

Robert Berghegger and others added 30 commits March 10, 2017 14:38
…test class for all methods that don’t require the PutInMapAnswer.
Pascal Welsch added 7 commits April 23, 2017 23:31
I didn’t made it final so somebody could work around this restriction
Added a buch of new tests for TiActivity covering all destroy cases

Removed “Don’t keep Activitites check”. It can be detected by checking isFinishing and isChangingConfigurations. isFinishing is false when the activity moves to background even when “don’t keep activities” is enabled.

Removed support for NonConfigurationInstance. The savior works much better.
@passsy
Copy link
Contributor

passsy commented Apr 24, 2017

I was able to simplify the onDestroy() logic in TiFragment and TiActivity. I also added all required tests for the TiActivity.

Also:

  • removed the NonConfigurationInstance recover method. setUseStaticSaviorToRetain(false) is deprecated and therefore it was never used. The savior does a better job.
  • removed hard "Don't keep Activities" checks. This is not required anymore. isFinishing and isChangingConfigurations are enough information. The "Don't keep Activities" check was unreachable code.
  • TiFragment#setRetainInstanceState(true) will now throw and is not supported.
  • added documentation

I'm now happy with the result and look forward for the merge. Next step: write release notes 😁

@passsy passsy dismissed StefMa’s stale review April 24, 2017 00:46

all fixed, outdated

Copy link

@ar-g ar-g left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked through PR, not very familiar to leave more suggestions yet :)
The PR fixed an issue in our app.

*/
void setFragmentRetainInstance(final boolean retain);
boolean isInBackstack();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isFragmentInBackstack() may fit better

import java.util.List;
import java.util.Map;

public class ActivityScopedPresenters {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ActivityScopedPresenters not necessary but it makes code more readable

Copy link
Contributor

@StefMa StefMa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't yet looked into the updated code.
But I got always the following output, when I destroy an activity:

Stacktrace
04-25 09:23:54.032 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: SampleFragment:TiFragment@cd04102: binding the cached view to Presenter MainThreadProxy@d1f9626-SampleFragment@cd04102{presenter=SamplePresenter@2067a13}
04-25 09:23:54.033 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: SamplePresenter:TiPresenter@2067a13: onAttachView(TiView)
04-25 09:23:54.033 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: SamplePresenter:TiPresenter@2067a13: deprecated onWakeUp()
04-25 09:23:54.033 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: HelloWorldActivity:TiActivity@f765445: binding the cached view to Presenter DistinctUntilChangedProxy@edd875f-MainThreadProxy@520e03-HelloWorldActivity:TiActivity@f765445{presenter = HelloWorldPresenter@acefc1}
04-25 09:23:54.033 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: HelloWorldPresenter:TiPresenter@acefc1: onAttachView(TiView)
04-25 09:23:54.034 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: HelloWorldPresenter:TiPresenter@acefc1: deprecated onWakeUp()
04-25 09:23:54.409 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: SamplePresenter:TiPresenter@e9e1afa: deprecated onSleep()
04-25 09:23:54.409 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: SamplePresenter:TiPresenter@e9e1afa: onDetachView()
04-25 09:23:54.409 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: HelloWorldPresenter:TiPresenter@7132889: deprecated onSleep()
04-25 09:23:54.409 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: HelloWorldPresenter:TiPresenter@7132889: onDetachView()
04-25 09:23:54.409 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: ActivityInstanceObserver: destroying HelloWorldActivity:TiActivity@7d2b253{presenter = HelloWorldPresenter@7132889}
04-25 09:23:54.409 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: ActivityInstanceObserver: isFinishing = true
04-25 09:23:54.409 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: ActivityInstanceObserver: isChangingConfigurations = false
04-25 09:23:54.409 23793-23793/net.grandcentrix.thirtyinch.sample D/ThirtyInch: PresenterSavior: Activity is finishing, free remaining presenters HelloWorldActivity:TiActivity@7d2b253{presenter = HelloWorldPresenter@7132889}
04-25 09:23:54.410 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: SamplePresenter:TiPresenter@e9e1afa: onDestroy()
04-25 09:23:54.410 23793-23793/net.grandcentrix.thirtyinch.sample D/ThirtyInch: ActivityScopedPresenters@c78308e: remove SamplePresenter:245242618:1798809768000 SamplePresenter:TiPresenter@e9e1afa{view = null}
04-25 09:23:54.410 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: HelloWorldPresenter:TiPresenter@7132889: onDestroy()
04-25 09:23:54.410 23793-23793/net.grandcentrix.thirtyinch.sample D/ThirtyInch: ActivityScopedPresenters@c78308e: remove HelloWorldPresenter:118696073:1798798494000 HelloWorldPresenter:TiPresenter@7132889{view = null}
04-25 09:23:54.410 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: SamplePresenter:TiPresenter@e9e1afa: not calling onDetachView(), not woken up
04-25 09:23:54.411 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: SampleFragment:TiFragment@e45c925: Hosting Activity is finishing, destroying presenter SamplePresenter:TiPresenter@e9e1afa{view = null}
04-25 09:23:54.411 23793-23793/net.grandcentrix.thirtyinch.sample W/ThirtyInch: SamplePresenter:TiPresenter@e9e1afa: not calling onDestroy(), destroy was already called
04-25 09:23:54.412 23793-23793/net.grandcentrix.thirtyinch.sample V/ThirtyInch: HelloWorldActivity:TiActivity@7d2b253: Activity is finishing, destroying presenter HelloWorldPresenter:TiPresenter@7132889{view = null}
04-25 09:23:54.412 23793-23793/net.grandcentrix.thirtyinch.sample W/ThirtyInch: HelloWorldPresenter:TiPresenter@7132889: not calling onDestroy(), destroy was already called
The
HelloWorldPresenter:TiPresenter@7132889: not calling onDestroy(), destroy was already called

makes me a little bit stomach pain...

It seems that we calling destroy() multiple times 🤔
That needs to be fixed...

How to test:
Add to the sample:
startActivity(new Intent(HelloWorldActivity.this, HelloWorldActivity.class));
In recreate click listener and remove recreate().
Then start the sample. Click on recreate button and press back.

Update:

This behaviour is "ok".
We have discussed that internally.
See also #78 (comment)

private boolean isDontKeepActivities() {
// default behaviour
int dontKeepActivities = 0;
try {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use your util here now. Otherwise AndroidDeveloperOptions is not used and can removed

0);
}

return alwaysFinishActivitiesInt == 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use that in our sample:
Change that to alwaysFinishActivitiesInt != DEFAULT_VALUE.

if (Build.VERSION.SDK_INT >= 17) {
alwaysFinishActivitiesInt = Settings.System
.getInt(context.getContentResolver(), Settings.Global.ALWAYS_FINISH_ACTIVITIES,
0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use that.
Move this zero and the zero below to an constant like DEFAULT_VALUE = -1

* {@link FragmentManager}. When {@link FragmentTransaction#remove(Fragment)} or {@link
* FragmentTransaction#replace(int, Fragment)} results in removing this {@link TiFragment} from the
* {@link FragmentManager} the {@link TiPresenter} gets destroyed. When the same {@link TiFragment}
* instance will be added again a new {@link TiPresenter} will be created by calling {@link
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is not true.
Replace the following in our sample app:

- recreate()
+                 final Fragment fragment = getSupportFragmentManager()
+                        .findFragmentById(R.id.fragment_container);
+               getSupportFragmentManager().beginTransaction().remove(fragment).commitNow();

+                Observable.just(null).delay(3, TimeUnit.SECONDS).subscribe(
+                       o -> getSupportFragmentManager().beginTransaction()
+                               .add(R.id.fragment_container, fragment).addToBackStack(null)
+                                .commit());

I got the following output:

Logcat
04-25 09:50:41.876 5895-5895/net.grandcentrix.thirtyinch.sample V/ThirtyInch: SamplePresenter:TiPresenter@70beced: deprecated onSleep()
04-25 09:50:41.876 5895-5895/net.grandcentrix.thirtyinch.sample V/ThirtyInch: SamplePresenter:TiPresenter@70beced: onDetachView()
04-25 09:50:41.876 5895-5895/net.grandcentrix.thirtyinch.sample V/ThirtyInch: SamplePresenter:TiPresenter@70beced: not calling onDetachView(), not woken up
04-25 09:50:44.881 5895-5895/net.grandcentrix.thirtyinch.sample V/ThirtyInch: CallOnMainThreadInterceptor: wrapping View SampleFragment@cd04102{presenter=SamplePresenter@70beced} in MainThreadProxy@60c384c-SampleFragment@cd04102{presenter=SamplePresenter@70beced}
04-25 09:50:44.881 5895-5895/net.grandcentrix.thirtyinch.sample V/ThirtyInch: DistinctUntilChangedInterceptor: wrapping View MainThreadProxy@60c384c-SampleFragment@cd04102{presenter=SamplePresenter@70beced} in MainThreadProxy@60c384c-SampleFragment@cd04102{presenter=SamplePresenter@70beced}
04-25 09:50:44.881 5895-5895/net.grandcentrix.thirtyinch.sample V/ThirtyInch: CallOnMainThreadInterceptor: wrapping View MainThreadProxy@60c384c-SampleFragment@cd04102{presenter=SamplePresenter@70beced} in MainThreadProxy@3235095-MainThreadProxy@60c384c-SampleFragment@cd04102{presenter=SamplePresenter@70beced}
04-25 09:50:44.881 5895-5895/net.grandcentrix.thirtyinch.sample V/ThirtyInch: DistinctUntilChangedInterceptor: wrapping View MainThreadProxy@3235095-MainThreadProxy@60c384c-SampleFragment@cd04102{presenter=SamplePresenter@70beced} in MainThreadProxy@3235095-MainThreadProxy@60c384c-SampleFragment@cd04102{presenter=SamplePresenter@70beced}
04-25 09:50:44.881 5895-5895/net.grandcentrix.thirtyinch.sample V/ThirtyInch: SampleFragment:TiFragment@cd04102: binding NEW view to Presenter MainThreadProxy@3235095-MainThreadProxy@60c384c-SampleFragment@cd04102{presenter=SamplePresenter@70beced}
04-25 09:50:44.881 5895-5895/net.grandcentrix.thirtyinch.sample V/ThirtyInch: SamplePresenter:TiPresenter@70beced: onAttachView(TiView)
04-25 09:50:44.881 5895-5895/net.grandcentrix.thirtyinch.sample V/ThirtyInch: SamplePresenter:TiPresenter@70beced: deprecated onWakeUp()

You see the same (TiPresenter@70beced) presenter instance will be used

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this case to the fragment lifecycle test Activity and can't reproduce it. The presenter is always destroyed and a new one is generated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was discussed and had the right behaviour.
The second (or following) added fragment got the same presenter.
Why? Because we added it to the backstack. And the default behaviour of TiFragments-Presenter is to keep as long as you are managed by the fragment manager. And if you are in the backstack, you are managed by them ...

* {@link TiPresenter} will be destroyed accordingly.
* </p>
* <p>
* Using {@code setRetainInstance(true)} is now allowed as it causes many troubles. You should favor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is now not allowed

@Override
public void onActivityDestroyed(final Activity activity) {

// TODO check if "Don't keep Activities" case is handled correctly
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that todo done?

/**
* Simple wrapper around a {@link HashMap} to save {@link TiPresenter} by id. For every {@link
* android.app.Activity} containing a {@link TiPresenter} a corresponding {@link
* ActivityScopedPresenters} will be created
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the info, that this will store also the Presenters for the Fragments which are added to that Activity.

* android.app.Activity} containing a {@link TiPresenter} a corresponding {@link
* ActivityScopedPresenters} will be created
*/
public class ActivityScopedPresenters {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think about implementing Map directly or extending from HashMap. But not needed now. So keep it like it is ;)

/**
* Access to the {@link PresenterSavior} singleton to save presenters across orientation changes
*
* @return singleton for
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove that. I guess that is clear :D

public void free(final String presenterId) {
mPresenters.remove(presenterId);
public void onActivityFinished(final Activity activity, final String activityId) {
final ActivityScopedPresenters scope = mScopes.remove(activityId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move that below the statement mScope.isEmpty(). We need it only after this statement...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the scope is only required in on branch, true. But removing the scope is the important part here. required for both branches.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then maybe moving up the second statement? 🤔

@passsy
Copy link
Contributor

passsy commented Apr 25, 2017

multiple calls to TiPresenter#destroy() are ok. We have multiple triggers to detect the Activity destroy case and we can't say which one fires first. We could guard them with if (!presenter.isDestroyed()) { presenter.destroy(); }. I don't think this is useful.

Pascal Welsch and others added 3 commits April 25, 2017 13:57
This happens every time because of multiple triggers (Savior, Fragment and Activity)
@StefMa
Copy link
Contributor

StefMa commented Apr 25, 2017

I've checked the ViewPager behaviour like we discussed.
See #84

@StefMa StefMa merged commit 57fc2af into master Apr 25, 2017
@passsy passsy deleted the bugfix/fragment_backstack_presenter_not_destroyed branch April 25, 2017 16:28
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Development

Successfully merging this pull request may close these issues.

4 participants