Search code examples
javaandroidloaderasynctaskloader

AsyncTaskLoader keeps reloading data when I come back to the MainActivity


I am quite new to the Android Development and I really need your help. My problem is in the MainActivity below. The app essentially displays a list of movies in the main activity and the movie details in another activity. And the problem is that whenever a user comes back from the MovieActivity to the MainActivity, the loader starts loading data again, although the movies are already there. And then it can not stop loading the data. It is really annoying. I want to get rid of this. So when a user comes back to the MainActivity, the loader will know that there is already loaded data and will not load anything again.If it helps, here is my full GitHub repo https://github.com/mateuszwojnarowicz/PopularMovies

I am stuck for about 3 weeks and have tried hundreds of possible solutions. Nothing seems to work. I feel really desperate.

Thank you so much for help,

Matthew

public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<String> {

private MovieAdapter mAdapter;
private ArrayList<Movie> mMoviesCollection;
private SharedPreferences sharedPreferences;
private Resources resources;
private LoaderManager loaderManager;
private Loader<String> loader;
private RecyclerView.LayoutManager layoutManager;
private String sortBy;

@BindView(R.id.pb)
ProgressBar progressBar;
@BindView(R.id.er)
TextView errorTextView;
@BindView(R.id.rv)
RecyclerView recyclerView;



@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ButterKnife.bind(this);
    mMoviesCollection = new ArrayList<Movie>();
    sharedPreferences = getSharedPreferences(Constants.SHARED_PREFS, Activity.MODE_PRIVATE);
    resources = getResources();
    sortBy = sharedPreferences.getString(Constants.KEY_SORT, null);
    setSharedPref();

    layoutManager = new GridLayoutManager(this, calculateNoOfColumns(this));
    loaderManager = getLoaderManager();
    loader = loaderManager.getLoader(Constants.LOADER_MOVIES_ID);
    initialize();
    makeOperationLoadMovies(sortBy);

}

public static int calculateNoOfColumns(Context context) {
    DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
    float dpWidth = displayMetrics.widthPixels / displayMetrics.density;
    int noOfColumns = (int) (dpWidth / 150);
    return noOfColumns;
}


//Set first-launch pref and set title according to pref
private void setSharedPref(){
    if(!sharedPreferences.contains(Constants.KEY_SORT)) {
        saveData(Constants.VALUE_POP);
        setTitle(resources.getString(R.string.title_pop));
    } else {
        if (Objects.equals(sharedPreferences.getString(Constants.KEY_SORT, null), Constants.VALUE_POP)) {
            setTitle(resources.getString(R.string.title_pop));
        }
        if (Objects.equals(sharedPreferences.getString(Constants.KEY_SORT, null), Constants.VALUE_TOP)) {
            setTitle(resources.getString(R.string.title_top));
        }
    }
}

//Set up the RecyclerView
private void initialize(){
    recyclerView.setLayoutManager(layoutManager);
    recyclerView.setHasFixedSize(true);
    mMoviesCollection = new ArrayList<>();
    mAdapter = new MovieAdapter(mMoviesCollection, this, this);
    recyclerView.setAdapter(mAdapter);
}

private void makeOperationLoadMovies(String SORT_BY){
    Bundle bundle = new Bundle();
    bundle.putString(Constants.LOADER_MOVIES_EXTRA, SORT_BY);
    if(recyclerView.isDirty()){

    }
    else if(loader==null){
        loaderManager.initLoader(Constants.LOADER_MOVIES_ID, bundle, this);
    }else{
        loaderManager.restartLoader(Constants.LOADER_MOVIES_ID, bundle, this);
    }

}

//Update shared pref
private void saveData(String SORT_VALUE){
    SharedPreferences.Editor editor = sharedPreferences.edit();
    editor.putString(Constants.KEY_SORT, SORT_VALUE);
    editor.apply();
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.main, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    int id = item.getItemId();

    switch (id){
        case R.id.menu_fav:
            startActivity(new Intent(MainActivity.this, FavoritesActivity.class));
            break;
        case R.id.menu_pop:
            saveData(Constants.VALUE_POP);
            Toast.makeText(this, resources.getString(R.string.message_popularity),Toast.LENGTH_LONG).show();
            break;
        case R.id.menu_top:
            saveData(Constants.VALUE_TOP);
            Toast.makeText(this, resources.getString(R.string.message_rating),Toast.LENGTH_LONG).show();
            break;
    }

    return super.onOptionsItemSelected(item);
}

@Override
protected void onPause() {
    super.onPause();
    Parcelable recyclerViewState;
    recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState();//save
    recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);
}

@Override
protected void onPostResume() {
    super.onPostResume();
    Parcelable recyclerViewState;
    recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState();//save
    recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);

}

@SuppressLint("StaticFieldLeak")
@Override
public Loader<String> onCreateLoader(int id, final Bundle args) {
    return new AsyncTaskLoader<String>(this) {


        @Override
        protected void onStartLoading() {
            super.onStartLoading();
                forceLoad();
                progressBar.setVisibility(View.VISIBLE);
                errorTextView.setVisibility(View.INVISIBLE);

        }

        @Override
        public void deliverResult(String data) {
            super.deliverResult(data);
        }

        @Override
        public String loadInBackground() {



            String jsonString = "";
            URL url = NetworkUtils.buildUrl(args.getString(Constants.LOADER_MOVIES_EXTRA));
            try {
                jsonString += NetworkUtils.getResponseFromHttpUrl(url);
            } catch (IOException e) {
                e.printStackTrace();
            }


            if(jsonString.isEmpty()){


            } else {

                try {
                    JSONObject jsonObject = new JSONObject(jsonString);
                    JSONArray jsonArray = jsonObject.getJSONArray(Constants.JSON_KEY_MOVIE_RESULTS);
                    for (int i = 0; i < jsonArray.length(); i++) {
                        //Get 1 movie from JSON
                        String mTitle;
                        int mId;
                        String mPosterUrl;
                        String mPlot;
                        double mUserRating;
                        String mReleaseDate;

                        JSONObject Jmovie = (JSONObject) jsonArray.get(i);
                        mTitle = Jmovie.getString(Constants.JSON_KEY_MOVIE_TITLE);
                        mId = Jmovie.getInt(Constants.JSON_KEY_MOVIE_ID);
                        mPosterUrl = NetworkUtils.getPosterString(Jmovie.getString(Constants.JSON_KEY_MOVIE_POSTER_PATH));
                        mPlot = Jmovie.getString(Constants.JSON_KEY_MOVIE_OVERVIEW);
                        mUserRating = Jmovie.getDouble(Constants.JSON_KEY_MOVIE_VOTE_AVERAGE);
                        mReleaseDate = Jmovie.getString(Constants.JSON_KEY_MOVIE_RELEASE_DATE);
                        //Get videos
                        ArrayList<Video> mVideos = new ArrayList<Video>();
                        URL videosURL = NetworkUtils.buildUrlVideos(String.valueOf(mId));
                        String videosJSON = NetworkUtils.getResponseFromHttpUrl(videosURL);
                        JSONObject jsonObjectVideos = new JSONObject(videosJSON);
                        JSONArray jsonArrayVideos = jsonObjectVideos.getJSONArray(Constants.JSON_KEY_VIDEO_RESULTS);
                        if(jsonArrayVideos.length()==0){
                            mVideos = null;
                        } else {
                            for(int v = 0; v < jsonArrayVideos.length(); v++){
                                JSONObject Jvideo = (JSONObject) jsonArrayVideos.get(v);
                                String mVideoName;
                                String mVideoUrlString;
                                mVideoName = Jvideo.getString(Constants.JSON_KEY_VIDEO_NAME);
                                mVideoUrlString = "https://www.youtube.com/watch?v="+Jvideo.getString(Constants.JSON_KEY_VIDEO_KEY);
                                Video video = new Video(mVideoName, mVideoUrlString);
                                mVideos.add(video);
                            }
                        }
                        //GetReviews
                        ArrayList<Review> mReviews = new ArrayList<Review>();
                        URL reviewsURL = NetworkUtils.buildUrlReviews(String.valueOf(mId));
                        String reviewsJSON = NetworkUtils.getResponseFromHttpUrl(reviewsURL);
                        JSONObject jsonObjectReviews = new JSONObject(reviewsJSON);
                        JSONArray jsonArrayReviews = jsonObjectReviews.getJSONArray(Constants.JSON_KEY_REVIEW_RESULTS);
                        if(jsonArrayReviews.length()!=0) {
                            for(int r = 0; r < jsonArrayReviews.length(); r++){
                                JSONObject Jreview = (JSONObject) jsonArrayReviews.get(r);
                                String mReviewName;
                                String mReviewText;
                                mReviewName = Jreview.getString(Constants.JSON_KEY_REVIEW_AUTHOR);
                                mReviewText = Jreview.getString(Constants.JSON_KEY_REVIEW_CONTENT);
                                Review review = new Review(mReviewName, mReviewText);
                                mReviews.add(review);
                            }
                        }
                        Movie movie = new Movie(mTitle, mId, mPosterUrl, mPlot, mUserRating, mReleaseDate, mVideos, mReviews);
                        mMoviesCollection.add(movie);
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                } catch (IOException e){
                    e.printStackTrace();
                }
            }
            return null;
        }


    };
}

@Override
public void onLoadFinished(Loader<String> loader, String data) {

    progressBar.setVisibility(View.GONE);

    mAdapter.notifyDataSetChanged();
}

@Override
public void onLoaderReset(Loader<String> loader) {

}
}

Solution

  • Because you are new to Android there is a lot wrong. So, many people probably won't want to chime in. Regardless, I'm new as well and in the same class as you are right now, so I'll give it a shot.

    First, your loader is not returning the correct data type. Your loader should be of Loader<List<Movie>> and it should return a new AsyncTaskLoader<List<Movie>>. The reason you want this is to make use of everything the AsyncTaskLoader has to offer. I'll explain further.

    Second, we'll cache the data inside the loader by moving the initial reference from the Activity into the loader.

    So move private ArrayList<Movie> mMoviesCollection; as an instance variable of your AsyncTaskLoader. Remove the line mMoviesCollection = new ArrayList<Movie>(); from both your onCreate and initialize methods.

    In your AsyncTaskLoader, you need to check if your data exists already in your onStartLoading before forceLoad and implement deliverResult.

    So, your onStartLoading() should look like this:

    @Override
        protected void onStartLoading() {
            super.onStartLoading();
                if(mMoviesCollection.isEmpty()){
                    forceLoad();
                    progressBar.setVisibility(View.VISIBLE);
                    errorTextView.setVisibility(View.INVISIBLE);
                } else {
                    deliverResult(mMoviesCollection)
                }
        }
    

    And your deliverResult should look like this:

    @Override
        public void deliverResult(List<Movie> data) {
            mMoviesCollection = data;
            super.deliverResult(data);
        }
    

    Now you need to implement a setData(List<Movie> movies) method that sets your adapter's data instance variable and calls notifyDataSetChanged() in your Adapter. Like so:

    public void setData(List<Movie> movies){
        mMovies = movies;
        notifyDataSetChanged();
    }
    

    Get rid of the List<Movie> from your adapter's constructor. This way you can construct the adapter without any data. The adapter's getItemCount() should return 0 if the data is null and the recyclerView will not try to build the view.

    With that done you can then call onLoadFinished like this:

    @Override
        public void onLoadFinished(Loader<List<Movie>> loader, List<Movie> data) {
            progressBar.setVisibility(View.GONE);
            mAdapter.setData(data);
        }
    

    EDIT: Made a correction to account for the ArrayList instantiating as an Instance variable. You can either not instantiate the mMoviesCollection there and then do so later or just check if its empty with mMoviesCollection.isEmpty() as I changed above in onStartLoading.:

    EDIT: You need to get your libraries straight, you are using android.app in some places and android.support in others.

    So in your imports change these:

    import android.app.LoaderManager;
    import android.content.AsyncTaskLoader;
    import android.content.Loader;
    

    all to:

    import android.support.v4.app.LoaderManager;
    import android.support.v4.content.AsyncTaskLoader;
    import android.support.v4.content.Loader;
    

    Now the TMDB.org API has a request limit of 40 requests per 10 seconds. https://developers.themoviedb.org/3/getting-started/request-rate-limiting

    Because of this, your Loader is not even completing everything and is throwing an exception. I would suggest breaking up when you call the videos and reviews into the MovieActivity by creating another AsyncTaskLoader there and calling each when the details screen loads.

    You could also technically add a Thread.sleep(300) or less to your AsyncTaskLoader but it makes it seriously slow. In other words, you would have to push the data beyond the 10-second mark to load completely.

    Now, with that and the changes we have made, everything does survive config changes such as screen rotation.

    If you want the data to survive any further you will have to persist the data somehow. Like saving the json response as a string in onSaveInstanceState or saving the JSon String to the database you created.