3

Good morning, I am recently taken up playing around with RxJava and I am facing a problem.

I have a Fragment in a ViewPager which on onViewCreated() launches an HTTP request (handled by Retrofit and RxJava) to retrieve a list of Strings. The way it does it is by creating a subscription and adding it to a CompositeSubscription object

here is the code

final Subscription subscription = MyManager.getService().getListStringsFromWeb()
            .subscribeOn(Schedulers.newThread())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Observer<List<String>() {
                @Override
                public void onCompleted() {
                    // DO NOTHING
                }

                @Override
                public void onError(Throwable e) {
                    // DO SOMETHING
                }

                @Override
                public void onNext(List<String> result) {
                    returnResultToView(result);
                }
            });
addSubscription(subscription);

In the Fragment on onViewCreated() I check for the result being there, if there is not we call the method above, if there is we are happy as a clam.

The problem is that when I rotate the device the "first" subscription has yet to return, therefore the data is not remembered by the fragment and it launches another search, and if I keep rotating I keep adding subscriptions

I would like to know if there is a way to hold on the first subscription and when its result arrives just give it back to the Fragment and disregarding all the other subscriptions

STEP BY STEP DESIRED BEHAVIOR:

  • Launch Fragment and request
  • Screen shows loading image and background the subscription/request is launched
  • Multiple screen rotation holding the loading image since the first request is still pending
  • Once the result of the first call comes back return it to the fragment, and hence hide loading image and show result

Thank you very much

kioli
  • 635
  • 1
  • 10
  • 26

1 Answers1

2

The unfortunate thing regarding Fragments and RxJava is that you have to keep the observable around somehow. One method is to make them static to the Fragment so that you only create them once, then subscribe/unsubscribe in onStart()/onStop() or however you decide.

The alternate approach if you don't like making static variables is to use a retained Fragment which handles the data. The View Fragment which shoes the data can then "bind" itself to the data Fragment. The advantages of this method is that the data is easily available to all components of an Activity, it abstracts away the data from the view for easy testing, and it outlives lifecycle components.

So for a retained fragment you'd do something like this:

public class DataFragment extends Fragment {

   private Observable<List<String> stringObs;

   @Override
   public void onAttach(Context ctx) {
      super.onAttach(ctx);
      setRetainInstance(true);
   }

   @Override
   public void onCreateView(LayoutInflater inflater, ViewGroup group, Bundle savedInsatnceState) {
      return null; // no view, no problem.
   }

   public Observable<List<String>> getStrings() {
     if (stringsObs == null) {
         stringsObs = MyManager.getService()
            .getListStringsFromWeb()
            .cache()
            .subscribeOn(Schedulers.newThread())
            .observeOn(AndroidSchedulers.mainThread());
     }
     return stringsObs;
   }
}

Add the DataFragment to the FragmentManager like so:

FragmentManager fm = getFragmentManager();
DataFragment df = fm.findFragmentByTag("DataFragment");
if (df == null) {
   // Only add the retained fragment if it doesn't exist.  Otherwise, use the old one.
   fm.beginTransaction()
   .add(new DataFragment(), "DataFragment")
   .commit();
}

Then on the ViewFragment you can subscribe to it similarly like this:

public class ViewFragment extends Fragment {

   private Subscription s;

   @Override
   public void onStart() {
     super.onStart();
     DataFragment dataFragment = (DataFragment) getFragmentManager().findFragmentByTag("DataFragment");
     s = dataFragment.getStrings()
            .subscribe(new Observer<List<String>() {
              // Do things
            });
   }

   @Override
   public void onStop() {
      super.onStop();
      s.unsubscribe();
   }
}

Now, your ViewFragment can disappear and re-appear on a whim without obstructing the download. It's easier to manipulate the Observables in ways that you need as well.

DeeV
  • 35,865
  • 9
  • 108
  • 95
  • Yes I remember a post from **[Alex Lockwood](http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html)** and others about retaining the state of the fragment via setRetainInstance() to be used with a grain of salt and only for fragments that would not hold UI components What would the drawbacks of having a static Observable be? And Isn't there a way then to bufferize or disregard the subscriptions in a RxJava-way? – kioli Dec 22 '16 at 09:28
  • You are not providing a UI to the retained fragment. It exists for reasons like this. – DeeV Dec 22 '16 at 12:35
  • Static variables will last throughout the entire life of the application, so it could exist in memory even when you no longer need the Fragment. It (in my opinion) looks uglier. It's harder to test. It also limits your ability to extend the class should you ever want to. – DeeV Dec 22 '16 at 12:39
  • I am currently using a MVP pattern with presenters handling the subscriptions creation/cancellation based on the lifecycle, but then even if I keep the same Observable caching it, then each time that will be subscribed in a new subscription and this does not solve the problem of having the loading screen keep being shown while rotating the device – kioli Dec 22 '16 at 13:03
  • Loading screen would be shown at the start of the Fragment then removed in the subscription. When subscribing to a cached Observable, it would get the data almost immediately. So the result would be a progress dialog that's always on until it gets data. I'm not sure what your objection is with the caching. An alternate approach is the retained fragment can hold on to the data itself rather than relying on RxJava caching. – DeeV Dec 22 '16 at 13:16
  • I am doing as suggested, but I still see multiple calls made to the server In the Fragment I pick the old DataFragment and get the oldObservable from it `private DataFragment getDataFragment() { DataFragment dataFragment = (DataFragment) getFragmentManager().findFragmentByTag(DataFragment.TAG); if(dataFragment == null) { dataFragment = new DataFragment(); getFragmentManager() .beginTransaction() .add(dataFragment, DataFragment.TAG) .commit(); } return dataFragment; }` But the observable gotten from it is always different, should it be a singleton? – kioli Dec 22 '16 at 13:37
  • Sorry, I believe the reason is because you're re-adding a new DataFragment each time. You're actually supposed to check if it already exists, and then add it. I'll edit my answer. – DeeV Dec 22 '16 at 13:54
  • No I was already doing that check, and I was indeed retrieving the same fragment, but the observable was every time different. So instead of returning return `MyManager.getService().getListStringsFromWeb().cache();` The fragment should probably have an Observable field where the Observable gets stored and referred every time – kioli Dec 22 '16 at 14:04
  • No problem and thank you for your answer, now I use a DataFragment with an Observable field and re-use them both at each device rotation.... The problem is that now every time I attempt a rotation the subscription goes to OnError with the message **InterruptedIOException: Thread Interrupted** :( :( :( Any idea? – kioli Dec 22 '16 at 14:24
  • Uh... Hmmm.. That I don't know. It sounds like the Observer is being interrupted which I don't think should be happening. – DeeV Dec 22 '16 at 14:42
  • Solved that as well, I just needed to include the methods subscribeOn and observeOn in the observable definition inside the DataFragment instead of doing it again on subscription :) Many thanks – kioli Dec 22 '16 at 14:44