1

I followed several tutorials and open sources on the web and built simple app based on MVP architecture using RxJava, Dagger 2 and Retrofit. Everything works fine except when I start downloading data and immediately rotate the screen previous request is being canceled and new request is being made.

The reason why network request is being canceled is that I'm unsubscribing from Observable inside onDestroyView of my View. That is to prevent memory leak!

How can I retain previous network request not letting Subscription to leak?

Here's View:

public class MoviesFragment extends Fragment implements MoviesView{

    @Inject
    MoviesPresenter moviesPresenter;
   //....

    public MoviesFragment(){

    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);
        setRetainInstance(true);
        ((BaseApplication) getActivity().getApplication()).createListingComponent().inject(this);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    {
        View rootView = inflater.inflate(R.layout.fragment_movies, container, false);
        ButterKnife.bind(this, rootView);
        return rootView;
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState)
    {
        super.onViewCreated(view, savedInstanceState);
        moviesPresenter.setView(this);
    }

    // ....

    @Override
    public void onDestroyView()
    {
        super.onDestroyView();
        moviesPresenter.destroy();
        ButterKnife.unbind(this);
    }

    @Override
    public void onDetach()
    {
        super.onDetach();
    }

    @Override
    public void onDestroy()
    {
        super.onDestroy();
        ((BaseApplication)getActivity().getApplication()).releaseListingComponent();
    }
    //....
}

Here's Presenter:

public class MoviesPresenterImpl implements MoviesPresenter {

    private final MoviesInteractor moviesInteractor;
    private MoviesView view;
    private Subscription fetchSubscription;

    public MoviesPresenterImpl(MoviesInteractor moviesInteractor) {
        this.moviesInteractor = moviesInteractor;
    }

    @Override
    public void downloadMovies() {

        fetchSubscription = moviesInteractor.getMovieList(new MoviesInteractorImpl.GetMovieListCallback() {
            @Override
            public void onSuccess(List<MovieModel> movieModels) {
                onMovieFetchSuccess(movieModels);
            }

            @Override
            public void onError(NetworkError networkError) {
                onMovieFetchFailed(new Throwable(networkError));
            }
        });

    }

    @Override
    public void setView(MoviesView view) {
        this.view = view;
        downloadMovies();
    }

    @Override
    public void destroy() {
        view = null;
        fetchSubscription.unsubscribe();
    }

    private void onMovieFetchSuccess(List<MovieModel> movies) {
        if (isViewAttached()) {
            view.showMovies(movies);
        }
    }

    //....
}

Here's Interactor between API and Presenter:

public class MoviesInteractorImpl implements MoviesInteractor {

    private Observable<MoviesResponseModel> call;

    public MoviesInteractorImpl(MoviesRetrofitService moviesRetrofitService) {
        call = moviesRetrofitService.getMovies("en", "popularity.desc", "MY_API_KEY");
    }

    @Override
    public Subscription getMovieList(final GetMovieListCallback callback) {

        return call
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<MoviesResponseModel>() {
                    @Override
                    public void onStart() {
                        super.onStart();
                    }

                    @Override
                    public void onCompleted() {
                    }

                    @Override
                    public void onError(Throwable e) {
                        callback.onError(new NetworkError(e));
                    }

                    @Override
                    public void onNext(MoviesResponseModel cityListResponse) {
                        callback.onSuccess(cityListResponse.getMovieList());
                    }
                });
    }

    public interface GetMovieListCallback {
        void onSuccess(List<MovieModel> movieModels);

        void onError(NetworkError networkError);
    }

}

Custom Scope so that Presenter stays as long as View is alive.

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ListingScope {
}

Dagger Module:

@Module
public class ListingModule {

    @Provides
    public MoviesRetrofitService provideMoviesRetorfitService(Retrofit retrofit) {
        return retrofit.create(MoviesRetrofitService.class);
    }

    @Provides
    MoviesInteractor provideMoviesInteractor(MoviesRetrofitService moviesRetrofitService){
        return new MoviesInteractorImpl(moviesRetrofitService);
    }

    @Provides
    MoviesPresenter provideMoviesPresenter(MoviesInteractor moviesInteractor){
        return new MoviesPresenterImpl(moviesInteractor);
    }

}

Dagger Component - Subcomponent:

@ListingScope
@Subcomponent(modules = {ListingModule.class})
public interface ListingComponent {
    MoviesFragment inject(MoviesFragment moviesFragment);
}
SpiralDev
  • 7,011
  • 5
  • 28
  • 42

2 Answers2

0

You can wait with unsubscribing until you receive response from MoviesInteractor via GetMovieListCallback.

If setView where called before onMovieFetchSuccess or onMovieFetchFailed, then fetchSubscription will not unsubscribe. If fetching finished before reset of MoviesView you can save that movies and update view immediately after MoviesView reset.

public class MoviesInteractorImpl implements MoviesInteractor {
    private boolean needToUnsubscribe = false;
    private List<MovieModel> lastMovies;

    //....

    @Override
    public void setView(MoviesView view) {
        this.view = view;
        needToUnsubscribe = false;
        if(lastMovies != null) {
            view.showMovies(movies);
        }
        downloadMovies();
    }

    @Override
    public void destroy() {
        view = null;
        needToUnsubscribe = true;
    }

    private void onMovieFetchSuccess(List<MovieModel> movies) {
        lastMovies = movies;
        if (isViewAttached()) {
            view.showMovies(movies);
        }
        if(needToUnsubscribe) {
            fetchSubscription.unsubscribe();
        }
    }

    private void onMovieFetchFailed(Throwable throwable) {
         //....
        lastMovies = null;
        if(needToUnsubscribe) {
            fetchSubscription.unsubscribe();
        }
    }
}
obywatelgcc
  • 93
  • 2
  • 6
-3

you can prevent to destroy the activity (and also it's fragments) with a simple attribute on manifest:

android:configChanges="orientation|screenSize"

With this option, when you rotate the screen, the activity won't execute any lifecycle method (onResume, onPause, etc.)

There is a specific question about it: Android, how to not destroy the activity when I rotate the device?

And official documentation: https://developer.android.com/guide/topics/resources/runtime-changes.html

Community
  • 1
  • 1
nelsito
  • 69
  • 1
  • 12
  • From the docs "Handling the configuration change yourself can make it much more difficult to use alternative resources, because the system does not automatically apply them for you. This technique should be considered a last resort when you must avoid restarts due to a configuration change and is not recommended for most applications." – Shmuel Mar 20 '17 at 00:21