From a0b2e2d600ae23a58dd57e9247d4ccce3477bed5 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Mon, 6 Jul 2015 15:31:06 -0700 Subject: [PATCH 001/163] fix: memory leak issue #15 construct fragment + backstack properly This was a red herring and not caused by the Rx activity. Basic fragment construction + backstack was at fault --- .../android/rxjava/MainActivity.java | 7 +- .../android/rxjava/MainFragment.java | 79 +++++++++++-------- .../com/morihacky/android/rxjava/MyApp.java | 3 + app/src/main/res/layout/activity_main.xml | 6 -- 4 files changed, 52 insertions(+), 43 deletions(-) delete mode 100644 app/src/main/res/layout/activity_main.xml diff --git a/app/src/main/java/com/morihacky/android/rxjava/MainActivity.java b/app/src/main/java/com/morihacky/android/rxjava/MainActivity.java index 69e9c6a3..a0c77692 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/MainActivity.java +++ b/app/src/main/java/com/morihacky/android/rxjava/MainActivity.java @@ -3,7 +3,6 @@ import android.os.Bundle; import android.support.v4.app.FragmentActivity; import com.morihacky.android.rxjava.rxbus.RxBus; -import timber.log.Timber; public class MainActivity extends FragmentActivity { @@ -22,14 +21,10 @@ public RxBus getRxBusSingleton() { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - Timber.plant(new Timber.DebugTree()); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() - .addToBackStack(this.toString()) - .replace(R.id.activity_main, new MainFragment(), this.toString()) + .replace(android.R.id.content, new MainFragment(), this.toString()) .commit(); } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/MainFragment.java b/app/src/main/java/com/morihacky/android/rxjava/MainFragment.java index 1d1c236d..c0b0cc61 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/MainFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/MainFragment.java @@ -25,10 +25,10 @@ public View onCreateView(LayoutInflater inflater, public void demoConcurrencyWithSchedulers() { getActivity().getSupportFragmentManager() .beginTransaction() - .addToBackStack(this.toString()) - .replace(R.id.activity_main, + .addToBackStack(ConcurrencyWithSchedulersDemoFragment.class.getName()) + .replace(android.R.id.content, new ConcurrencyWithSchedulersDemoFragment(), - this.toString()) + ConcurrencyWithSchedulersDemoFragment.class.getName()) .commit(); } @@ -36,8 +36,10 @@ public void demoConcurrencyWithSchedulers() { public void demoBuffer() { getActivity().getSupportFragmentManager() .beginTransaction() - .addToBackStack(this.toString()) - .replace(R.id.activity_main, new BufferDemoFragment(), this.toString()) + .addToBackStack(BufferDemoFragment.class.toString()) + .replace(android.R.id.content, + new BufferDemoFragment(), + BufferDemoFragment.class.toString()) .commit(); } @@ -45,24 +47,31 @@ public void demoBuffer() { public void demoThrottling() { getActivity().getSupportFragmentManager() .beginTransaction() - .addToBackStack(this.toString()) - .replace(R.id.activity_main, new DebounceSearchEmitterFragment(), this.toString()) + .addToBackStack(DebounceSearchEmitterFragment.class.toString()) + .replace(android.R.id.content, + new DebounceSearchEmitterFragment(), + DebounceSearchEmitterFragment.class.toString()) .commit(); } @OnClick(R.id.btn_demo_retrofit) public void demoRetrofitCalls() { - getActivity().getSupportFragmentManager().beginTransaction().addToBackStack(this.toString()) - //.replace(R.id.activity_main, new RetrofitAsyncTaskDeathFragment(), this.toString()) - .replace(R.id.activity_main, new RetrofitFragment(), this.toString()).commit(); + getActivity().getSupportFragmentManager() + .beginTransaction().addToBackStack(RetrofitFragment.class.toString()) + //.replace(android.R.id.content, new RetrofitAsyncTaskDeathFragment(), RetrofitAsyncTaskDeathFragment.class.toString()) + .replace(android.R.id.content, + new RetrofitFragment(), + RetrofitFragment.class.toString()).commit(); } @OnClick(R.id.btn_demo_double_binding_textview) public void demoDoubleBindingWithPublishSubject() { getActivity().getSupportFragmentManager() .beginTransaction() - .addToBackStack(this.toString()) - .replace(R.id.activity_main, new DoubleBindingTextViewFragment(), this.toString()) + .addToBackStack(DoubleBindingTextViewFragment.class.toString()) + .replace(android.R.id.content, + new DoubleBindingTextViewFragment(), + DoubleBindingTextViewFragment.class.toString()) .commit(); } @@ -70,8 +79,8 @@ public void demoDoubleBindingWithPublishSubject() { public void demoPolling() { getActivity().getSupportFragmentManager() .beginTransaction() - .addToBackStack(this.toString()) - .replace(R.id.activity_main, new PollingFragment(), this.toString()) + .addToBackStack(PollingFragment.class.toString()) + .replace(android.R.id.content, new PollingFragment(), PollingFragment.class.toString()) .commit(); }*/ @@ -79,8 +88,10 @@ public void demoPolling() { public void demoRxBus() { getActivity().getSupportFragmentManager() .beginTransaction() - .addToBackStack(this.toString()) - .replace(R.id.activity_main, new RxBusDemoFragment(), this.toString()) + .addToBackStack(RxBusDemoFragment.class.toString()) + .replace(android.R.id.content, + new RxBusDemoFragment(), + RxBusDemoFragment.class.toString()) .commit(); } @@ -88,8 +99,10 @@ public void demoRxBus() { public void demoTimeout() { getActivity().getSupportFragmentManager() .beginTransaction() - .addToBackStack(this.toString()) - .replace(R.id.activity_main, new TimeoutDemoFragment(), this.toString()) + .addToBackStack(TimeoutDemoFragment.class.toString()) + .replace(android.R.id.content, + new TimeoutDemoFragment(), + TimeoutDemoFragment.class.toString()) .commit(); } @@ -97,29 +110,31 @@ public void demoTimeout() { public void formValidation() { getActivity().getSupportFragmentManager() .beginTransaction() - .addToBackStack(this.toString()) - .replace(R.id.activity_main, + .addToBackStack(FormValidationCombineLatestFragment.class.toString()) + .replace(android.R.id.content, new FormValidationCombineLatestFragment(), - this.toString()) + FormValidationCombineLatestFragment.class.toString()) .commit(); } @OnClick(R.id.btn_demo_pseudo_cache) public void pseudoCacheDemo() { - getActivity().getSupportFragmentManager() - .beginTransaction() - .addToBackStack(this.toString()) - //.replace(R.id.activity_main, new PseudoCacheConcatFragment(), this.toString()) - .replace(R.id.activity_main, new PseudoCacheMergeFragment(), this.toString()) - .commit(); + getActivity().getSupportFragmentManager().beginTransaction().addToBackStack( + PseudoCacheMergeFragment.class.toString()) + //.replace(android.R.id.content, new PseudoCacheConcatFragment(), PseudoCacheConcatFragment.class.toString()) + .replace(android.R.id.content, + new PseudoCacheMergeFragment(), + PseudoCacheMergeFragment.class.toString()).commit(); } @OnClick(R.id.btn_demo_timing) public void demoTimerIntervalDelays() { getActivity().getSupportFragmentManager() .beginTransaction() - .addToBackStack(this.toString()) - .replace(R.id.activity_main, new TimingDemoFragment(), this.toString()) + .addToBackStack(TimingDemoFragment.class.toString()) + .replace(android.R.id.content, + new TimingDemoFragment(), + TimingDemoFragment.class.toString()) .commit(); } @@ -127,8 +142,10 @@ public void demoTimerIntervalDelays() { public void demoExponentialBackoff() { getActivity().getSupportFragmentManager() .beginTransaction() - .addToBackStack(this.toString()) - .replace(R.id.activity_main, new ExponentialBackoffFragment(), this.toString()) + .addToBackStack(ExponentialBackoffFragment.class.toString()) + .replace(android.R.id.content, + new ExponentialBackoffFragment(), + ExponentialBackoffFragment.class.toString()) .commit(); } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/MyApp.java b/app/src/main/java/com/morihacky/android/rxjava/MyApp.java index 4dad4cc7..b66f855f 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/MyApp.java +++ b/app/src/main/java/com/morihacky/android/rxjava/MyApp.java @@ -3,6 +3,7 @@ import android.app.Application; import com.squareup.leakcanary.LeakCanary; import com.squareup.leakcanary.RefWatcher; +import timber.log.Timber; public class MyApp extends Application { @@ -24,5 +25,7 @@ public void onCreate() { _instance = (MyApp) getApplicationContext(); _refWatcher = LeakCanary.install(this); + + Timber.plant(new Timber.DebugTree()); } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index bcdf97bb..00000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,6 +0,0 @@ - From ae93f349c2680dde6484f0b1c867b2a64ab61559 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Thu, 9 Jul 2015 13:49:28 -0700 Subject: [PATCH 002/163] feat: wip add a persist over rotation fragment --- .../android/rxjava/MainActivity.java | 3 +- .../android/rxjava/MainFragment.java | 11 ++ .../rxjava/RotationPersistFragment.java | 130 ++++++++++++++++++ .../rxjava/RotationPersistWorkerFragment.java | 97 +++++++++++++ app/src/main/res/layout/fragment_main.xml | 6 + .../res/layout/fragment_rotation_persist.xml | 30 ++++ app/src/main/res/values/strings.xml | 2 + 7 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/morihacky/android/rxjava/RotationPersistFragment.java create mode 100644 app/src/main/java/com/morihacky/android/rxjava/RotationPersistWorkerFragment.java create mode 100644 app/src/main/res/layout/fragment_rotation_persist.xml diff --git a/app/src/main/java/com/morihacky/android/rxjava/MainActivity.java b/app/src/main/java/com/morihacky/android/rxjava/MainActivity.java index a0c77692..84c38e28 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/MainActivity.java +++ b/app/src/main/java/com/morihacky/android/rxjava/MainActivity.java @@ -24,7 +24,8 @@ protected void onCreate(Bundle savedInstanceState) { if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() - .replace(android.R.id.content, new MainFragment(), this.toString()) + //.replace(android.R.id.content, new MainFragment(), this.toString()) + .replace(android.R.id.content, new RotationPersistFragment(), this.toString()) .commit(); } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/MainFragment.java b/app/src/main/java/com/morihacky/android/rxjava/MainFragment.java index c0b0cc61..4239bbad 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/MainFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/MainFragment.java @@ -148,4 +148,15 @@ public void demoExponentialBackoff() { ExponentialBackoffFragment.class.toString()) .commit(); } + + @OnClick(R.id.btn_demo_rotation_persist) + public void demoRotationPersist() { + getActivity().getSupportFragmentManager() + .beginTransaction() + .addToBackStack(RotationPersistFragment.class.toString()) + .replace(android.R.id.content, + new RotationPersistFragment(), + RotationPersistFragment.class.toString()) + .commit(); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/RotationPersistFragment.java b/app/src/main/java/com/morihacky/android/rxjava/RotationPersistFragment.java new file mode 100644 index 00000000..0a5d6fe4 --- /dev/null +++ b/app/src/main/java/com/morihacky/android/rxjava/RotationPersistFragment.java @@ -0,0 +1,130 @@ +package com.morihacky.android.rxjava; + +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; +import butterknife.ButterKnife; +import butterknife.InjectView; +import butterknife.OnClick; +import com.morihacky.android.rxjava.wiring.LogAdapter; +import java.util.ArrayList; +import java.util.List; +import rx.Observable; +import rx.Observer; +import rx.subscriptions.CompositeSubscription; +import timber.log.Timber; + +import static android.os.Looper.getMainLooper; + +public class RotationPersistFragment + extends BaseFragment + implements RotationPersistWorkerFragment.IAmYourMaster { + + @InjectView(R.id.list_threading_log) ListView _logList; + private LogAdapter _adapter; + private List _logs; + + private CompositeSubscription _subscriptions = new CompositeSubscription(); + + @Override + public void onResume() { + super.onResume(); + _subscriptions = RxUtils.getNewCompositeSubIfUnsubscribed(_subscriptions); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + _setupLogger(); + } + + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_rotation_persist, container, false); + ButterKnife.inject(this, layout); + return layout; + } + + @Override + public void onPause() { + super.onPause(); + + RxUtils.unsubscribeIfNotNull(_subscriptions); + } + + // ----------------------------------------------------------------------------------- + + @OnClick(R.id.btn_rotate_persist) + public void startRetryingWithExponentialBackoffStrategy() { + _logs = new ArrayList<>(); + _adapter.clear(); + + String FRAG_TAG = RotationPersistWorkerFragment.class.getName(); + FragmentManager fm = getActivity().getSupportFragmentManager(); + RotationPersistWorkerFragment frag =// + (RotationPersistWorkerFragment) fm.findFragmentByTag(FRAG_TAG); + + if (frag == null) { + Timber.d("---- never created frag before"); + + // we've never created the frag before + frag = new RotationPersistWorkerFragment(); + fm.beginTransaction().add(frag, FRAG_TAG).commit(); + } else { + Timber.d("---- Worker frag already spawned"); + } + } + + @Override + public void observeResults(Observable ints) { + Timber.d("observable instance ints %s", ints.toString()); + + _subscriptions.add(ints.subscribe(new Observer() { + @Override + public void onCompleted() { + _log("Observable is complete"); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "Error in worker demo frag observable"); + _log("Dang! something went wrong."); + } + + @Override + public void onNext(Integer integer) { + _log(String.format("Worker frag spits out - %d", integer)); + } + })); + } + + // ----------------------------------------------------------------------------------- + + private void _setupLogger() { + _logs = new ArrayList<>(); + _adapter = new LogAdapter(getActivity(), new ArrayList()); + _logList.setAdapter(_adapter); + } + + private void _log(String logMsg) { + _logs.add(0, logMsg); + + // You can only do below stuff on main thread. + new Handler(getMainLooper()).post(new Runnable() { + + @Override + public void run() { + _adapter.clear(); + _adapter.addAll(_logs); + } + }); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/morihacky/android/rxjava/RotationPersistWorkerFragment.java b/app/src/main/java/com/morihacky/android/rxjava/RotationPersistWorkerFragment.java new file mode 100644 index 00000000..9af14341 --- /dev/null +++ b/app/src/main/java/com/morihacky/android/rxjava/RotationPersistWorkerFragment.java @@ -0,0 +1,97 @@ +package com.morihacky.android.rxjava; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import java.util.List; +import java.util.concurrent.TimeUnit; +import rx.Observable; +import rx.functions.Func1; +import timber.log.Timber; + +public class RotationPersistWorkerFragment + extends Fragment { + + private Observable _intsObservable; + private IAmYourMaster _masterFrag; + + /** + * hold a refernce to the activity -> caller fragment + * this way when the worker frag kicks off + * we can talk back to the master and send results + */ + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + _masterFrag = (RotationPersistFragment) getActivity().getSupportFragmentManager() + .findFragmentByTag(RotationPersistFragment.class.getName()); + + List frags = ((MainActivity) activity).getSupportFragmentManager().getFragments(); + + Timber.d("---- there are currently %d fragments attached", frags.size()); + + for (Fragment f : frags) { + if (f instanceof IAmYourMaster) { + _masterFrag = (IAmYourMaster) f; + } + } + + if (_masterFrag == null) { + throw new ClassCastException("We did not find a master who can understand us :( !"); + } + } + + /** + * This method will only be called once when the retained + * Fragment is first created. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Retain this fragment across configuration changes. + setRetainInstance(true); + + + if (_intsObservable != null) { + return; + } + + // Create and execute the "Hot" observable + Timber.d("---- Creating a new ints observable"); + _intsObservable =// + Observable.interval(1, TimeUnit.SECONDS)// + .map(new Func1() { + @Override + public Integer call(Long aLong) { + return aLong.intValue(); + } + })// + .take(20)// cause i don't want this thing to run indefinitely + .share(); // we need a "hot" observable for demoing the example + } + + /** + * The Worker fragment has started doing it's thing + */ + @Override + public void onStart() { + super.onStart(); + _masterFrag.observeResults(_intsObservable); + } + + /** + * Set the callback to null so we don't accidentally leak the + * Activity instance. + */ + @Override + public void onDetach() { + super.onDetach(); + _masterFrag = null; + } + + public interface IAmYourMaster { + void observeResults(Observable ints); + } +} diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 6ca9b982..73b3e120 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -93,5 +93,11 @@ android:layout_width="match_parent" android:text="@string/btn_demo_exponential_backoff" /> +