在从 JSON 获取数据后通知适配器进行数据更改时,在 RecyclerView 中的每个位置设置相同的数据

Same data is set on each position in RecyclerView when adapter is notified for data change after fetching data from JSON

我正在从数据库中获取数据并正确获取 JSON。此外,当我在 RecyclerView 上设置适配器时,除了在每个位置设置相同的数据外,一切似乎都正常,但是我得到了具有 20 个不同值的完整 ArrayList。我不知道我在哪里弄乱了 notifyDatasetChanged,因为这是我第一次使用 RecyclerView。

我在 activity 中使用了一个片段,在代码中,我使用了日志语句(我使用了“############”而不是 LOG_TAG, though) 这告诉我调用 onBindViewHolder 时可以使用包含 20 部电影详细信息的完整数组列表。

代码如下:

DefaultMovieFragment.java

public class DefaultMovieFragment extends Fragment implements LoaderManager.LoaderCallbacks<ArrayList<Movie>> {

private static final int DEFAULT_MOVIE_LOADER_ID = 1;
ArrayList<Movie> movies;
DefaultMovieAdapter mAdapter;
RecyclerView mRecyclerView;

public DefaultMovieFragment() {
    // Required empty public constructor
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    Log.v("############", "onCreateView called");
    // Inflate the layout for this fragment
    View rootView = inflater.inflate(R.layout.fragment_default_movie, container, false);
    Movie movie = new Movie("ram", 2, "path");
    if (savedInstanceState==null){
        movies = new ArrayList<>();
    }

    //First of all check if network is connected or not then only start the loader
    ConnectivityManager connMgr = (ConnectivityManager)
            getActivity().getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
    if (networkInfo != null && networkInfo.isConnected()) {

      /* fetch data. Get a reference to the LoaderManager, in order to interact with loaders. */
        startLoaderManager();
        Log.v("############", "startLoaderManager called");
    }

    // Lookup the recyclerview in activity layout
    mRecyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerViewMovies);

    // Create mAdapter passing in the sample user data
    mAdapter = new DefaultMovieAdapter(getActivity(), movies);
    // Attach the mAdapter to the recyclerview to populate items
    mRecyclerView.setAdapter(mAdapter);

    // First param is number of columns and second param is orientation i.e Vertical or Horizontal
    final StaggeredGridLayoutManager gridLayoutManager =
            new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
    // Attach the layout manager to the recycler view
    mRecyclerView.setLayoutManager(gridLayoutManager);
    // That's all!

    return rootView;
}

private void startLoaderManager() {
    LoaderManager loaderManager = getLoaderManager();
    loaderManager.initLoader(DEFAULT_MOVIE_LOADER_ID, null, this);
    Log.v("############", "startLoaderManager finished");
}


@Override
public Loader<ArrayList<Movie>> onCreateLoader(int id, Bundle args) {
    Log.v("############", "onCreateLoader called");
    Uri baseUri = Uri.parse(UrlsAndConstants.DefaultQuery.DEFAULT_URL);
    Log.v("############", "baseUri is "+baseUri.toString());
    Uri.Builder uriBuilder = baseUri.buildUpon();
    Log.v("############", "uriBuilder is "+uriBuilder.toString());
    uriBuilder.appendQueryParameter(API_KEY_PARAM, API_KEY_PARAM_VALUE);
    Log.v("############", "uriBuilder.toString() is "+uriBuilder.toString());
    String urls = "https://api.themoviedb.org/3/discover/movie?api_key=4182aa25bab27d06344e404f65c4ae76";
    return new DefaultMovieLoader(getActivity().getApplicationContext(), urls);
}

@Override
public void onLoadFinished(Loader<ArrayList<Movie>> loader, ArrayList<Movie> movie) {
    Log.v("############", "startLoaderManager finished");
    if (movie.isEmpty()) {
        Log.v("******************", "movies isEmpty");
        return;
    } else {
        Log.v("############", "movies are"+movie);
        // Attach the mAdapter to the recyclerview to populate items

        mAdapter.setMovieData(movie);
        mRecyclerView.setAdapter(mAdapter);
    }
}

@Override
public void onLoaderReset(Loader<ArrayList<Movie>> loader) {
    Log.v("############", "onLoaderReset called");
}
}

DefaultMovieAdapter.java

public class DefaultMovieAdapter extends  RecyclerView.Adapter<DefaultMovieAdapter.ViewHolder>{

// Store a member variable for the movies
private ArrayList<Movie> mDefaultMovie;
// Store the context for easy access
private Context mContext;
private Movie currentMovie;

// Pass in the movies array into the constructor
public DefaultMovieAdapter(Context context, ArrayList<Movie> movies) {
    mDefaultMovie = movies;
    mContext = context;
}

// Easy access to the context object in the recyclerview
private Context getContext() {
    return mContext;
}
/*
 Provide a direct reference to each of the views within a data item
 Used to cache the views within the item layout for fast access
 */
public static class ViewHolder extends RecyclerView.ViewHolder {
    /*
    Your holder should contain a member variable
    for any view that will be set as you render a row
    */
    public final TextView movieTitleTextView;
    public final ImageView movieTitleImageView;
    /*
    We also create a constructor that accepts the entire item row
    and does the view lookups to find each subview
    */
    public ViewHolder(View itemView) {
        /*
        Stores the itemView in a public final member variable that can be used
        to access the context from any ViewHolder instance.
        */
        super(itemView);
        movieTitleTextView = (TextView) itemView.findViewById(R.id.grid_item_movie_title);
        movieTitleImageView = (ImageView) itemView.findViewById(R.id.grid_item_movie_image);
    }
}

@Override
public DefaultMovieAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

    View v = LayoutInflater.from(parent.getContext()).
            inflate(R.layout.item_movies, parent, false);
    return new ViewHolder(v);
}

@Override
public void onBindViewHolder(DefaultMovieAdapter.ViewHolder viewHolder, int position) {
    Log.v("############", "onBindViewHolder called");
    // Get the data model based on position
    currentMovie = mDefaultMovie.get(position);
    Log.v("############", "currentMovie called is "+currentMovie.toString());
    Log.v("############", "currentMovie's title is "+currentMovie.getMovieTitle().toString());
    /*
    Set item views based on your views and data model
    TextView textView = viewHolder.movieTitleTextView;
    */
    viewHolder.movieTitleTextView.setText(currentMovie.getMovieTitle());
    Log.v("############", "title is :>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"+currentMovie.getMovieTitle());
    //ImageView button = viewHolder.movieTitleImageView;
    //viewHolder.movieTitleImageView.setImageResource(R.mipmap.ic_launcher);
    String url = "https://image.tmdb.org/t/p/w500/"+currentMovie.getMoviePosterPath().toString();
    Log.v("############", "poster path is :>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"+currentMovie.getMoviePosterPath().toString());
    Picasso.with(getContext())
            .load(url)
            .placeholder(R.mipmap.ic_launcher)
            .into(viewHolder.movieTitleImageView);
}

@Override
public int getItemCount() {
    Log.v("############", "getItemCount called with size "+ mDefaultMovie.size());
    return mDefaultMovie.size();
}
public void setMovieData(ArrayList<Movie> weatherData) {
    Log.v("############", "setMovieData Called");
    mDefaultMovie = weatherData;
    Log.v("############", "mDefaultMovie is "+mDefaultMovie);
    notifyDataSetChanged();
    Log.v("############", "notifyDataSetChanged Finished");
}
}

DefaultMovieLoader.java

public class DefaultMovieLoader extends AsyncTaskLoader {

/**
 * Query URL
 */
private String mUrl;

/**
 * Constructs a new {@link DefaultMovieLoader}.
 *
 * @param context of the activity
 * @param url     to load data from
 */
public DefaultMovieLoader(Context context, String url) {
    super(context);
    mUrl = url;
    Log.v("############", "url is "+mUrl);
}

@Override
protected void onStartLoading() {
    forceLoad();
    Log.v("############", "onStartLoading called");
}

/**
 * This is on a background thread.
 */
@Override
public ArrayList<Movie> loadInBackground() {
    if (mUrl == null) {
        return null;
    }

    // Perform the network request, parse the response, and extract a list of news.
    ArrayList<Movie> movies = QueryUtils.fetchMovieData(mUrl);
    Log.v("############", "loadInBackground called");
    return movies;
}
}

注意:代码也可以在 Gist 中找到:https://gist.github.com/rajtheinnovator/4ae0ab873129eff84db68d5645ac64d8

编辑:

public class QueryUtils {
private static String movieTitle;
private static int movieId;
private static String moviePosterPath;

/**
 * Create a private constructor because no one should ever create a {@link QueryUtils} object.
 * This class is only meant to hold static variables and methods, which can be accessed
 * directly from the class name QueryUtils (and an object instance of QueryUtils is not needed).
 */
private QueryUtils() {
}

/**
 * Query the GUARDIAN dataset and return an {@link Movie} ArrayList to represent a single Movie.
 */
public static ArrayList<Movie> fetchMovieData(String requestUrl) {
    Log.v("############", "fetchMovieData called");
    // Create URL object
    URL url = createUrl(requestUrl);

    // Perform HTTP request to the URL and receive a JSON response back
    String jsonResponse = null;
    try {
        jsonResponse = makeHttpRequest(url);
    } catch (IOException e) {
        //handle exception
    }

    // Extract relevant fields from the JSON response and create an {@link Event} object
    ArrayList<Movie> movies = extractFeatureFromJson(jsonResponse);

    // Return the {@link Event}
    return movies;
}

/**
 * Returns new URL object from the given string URL.
 */
private static URL createUrl(String stringUrl) {
    URL url = null;
    try {
        url = new URL(stringUrl);
    } catch (MalformedURLException e) {
        //handle exception
    }
    return url;
}

/**
 * Make an HTTP request to the given URL and return a String as the response.
 */
private static String makeHttpRequest(URL url) throws IOException {
    Log.v("############", "makeHttpRequest called");
    String jsonResponse = "";

    // If the URL is null, then return early.
    if (url == null) {
        return jsonResponse;
    }

    HttpURLConnection urlConnection = null;
    InputStream inputStream = null;
    try {
        urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.setReadTimeout(10000 /* milliseconds */);
        urlConnection.setConnectTimeout(15000 /* milliseconds */);
        urlConnection.setRequestMethod("GET");
        urlConnection.connect();

        /*
        If the request was successful (response code 200),
        then read the input stream and parse the response.
        */
        if (urlConnection.getResponseCode() == 200) {
            inputStream = urlConnection.getInputStream();
            jsonResponse = readFromStream(inputStream);
        } else {
            //handle exception
        }
    } catch (IOException e) {
        //handle exception
    } finally {
        if (urlConnection != null) {
            urlConnection.disconnect();
        }
        if (inputStream != null) {
            inputStream.close();
        }
    }
    return jsonResponse;
}

/**
 * Convert the {@link InputStream} into a String which contains the
 * whole JSON response from the server.
 */
private static String readFromStream(InputStream inputStream) throws IOException {
    StringBuilder output = new StringBuilder();
    if (inputStream != null) {
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream, Charset.forName("UTF-8"));
        BufferedReader reader = new BufferedReader(inputStreamReader);
        String line = reader.readLine();
        while (line != null) {
            output.append(line);
            line = reader.readLine();
        }
    }
    return output.toString();
}

/**
 * Return a list of {@link Movie} objects that has been built up from
 * parsing a JSON response.
 */
public static ArrayList<Movie> extractFeatureFromJson(String jsonResponse) {
    Log.v("############", "extractFeatureFromJson called");
    Log.v("############", "jsonResponse"+jsonResponse);

    // Create an empty ArrayList that we can start adding movies to
    ArrayList<Movie> movies = new ArrayList<Movie>();

    /*
    Try to parse the received jsonResponse. If there's a problem with the way the JSON
    is formatted, a JSONException exception object will be thrown. Catch the exception
    so the app doesn't crash, and handle exception.
    */
    try {
        // Parse the jsonResponse string
        JSONObject movie_json_response = new JSONObject(jsonResponse);
        Log.v("############", "JSONObject is: " + movie_json_response.toString());
        if (movie_json_response.has("results")) {
            JSONArray resultsArray = movie_json_response.getJSONArray("results");
            if (resultsArray.length() > 0) {
                for (int i = 0; i < resultsArray.length(); i++) {
                    JSONObject movieDetail = resultsArray.getJSONObject(0);
                    if (movieDetail.has("title")) {
                        movieTitle = movieDetail.getString("title");
                    }
                    if (movieDetail.has("id")) {
                        movieId = movieDetail.getInt("id");
                    }
                    if (movieDetail.has("poster_path")) {
                        moviePosterPath = movieDetail.getString("poster_path");
                    }
                    Log.v("############", " title is "+movies + "############ id is"+movieId+" ############ poster path is "+moviePosterPath);
                    movies.add(new Movie(movieTitle, movieId, moviePosterPath));
                }
            }
        }
    } catch (JSONException e) {
        //handle exception
    }
    Log.v("############", "Movies returned is: " + movies.toString());
    // Return the list of movies
    return movies;
}
}

更新:

要点中的 StackTrace:https://gist.github.com/rajtheinnovator/4ae0ab873129eff84db68d5645ac64d8#file-stacktrace-xml

更新存在于 QueryUtils.

中的解析器逻辑
for (int i = 0; i < resultsArray.length(); i++) {
 Change zeroth index to i
//JSONObject movieDetail =  resultsArray.getJSONObject(0);

我建议您创建一个局部变量 currentMovie 而不是全局变量。

@Override
public void onBindViewHolder(DefaultMovieAdapter.ViewHolder viewHolder, int position) {
// Use Local variable
Movie currentMovie = mDefaultMovie.get(position);
}

问题出在您的 QueryUtils class 中。您总是在索引 0 处检索元素。 更改自:

JSONObject movieDetail = resultsArray.getJSONObject(0);

JSONObject movieDetail = resultsArray.getJSONObject(i);

上面代码中的一切都很好,除了我在处理 JSON 时搞砸了。正如@rahul 和@marcelo 所建议的那样,问题出在 class QueryUtils.java[=20= 的方法 extractFeatureFromJson 中的 for 循环中] 我编码的地方:

for (int i = 0; i < resultsArray.length(); i++) {
                JSONObject movieDetail = resultsArray.getJSONObject(0);
//other code
}

但实际的行应该是:

for (int i = 0; i < resultsArray.length(); i++) {
                JSONObject movieDetail = resultsArray.getJSONObject(i);
//other code
}

上面的其余代码非常好并且工作正常。