1

I'm using RecyclerView with StaggeredGridLayoutManager on a Dialog Fragment. It's called SearchFragment.

My problem is when i change the data (making other query for getting other data from server) i got a huge empty space on top between item 0 and item 1. If i scroll up i will see the new data but app crashes after that.

Logcat:

java.lang.NullPointerException: Attempt to read from field 'int android.support.v7.widget.StaggeredGridLayoutManager$Span.mIndex' on a null object reference
                                                            at android.support.v7.widget.StaggeredGridLayoutManager.hasGapsToFix(StaggeredGridLayoutManager.java:344)
                                                            at android.support.v7.widget.StaggeredGridLayoutManager.checkForGaps(StaggeredGridLayoutManager.java:272)
                                                            at android.support.v7.widget.StaggeredGridLayoutManager.onScrollStateChanged(StaggeredGridLayoutManager.java:307)
                                                            at android.support.v7.widget.RecyclerView.dispatchOnScrollStateChanged(RecyclerView.java:3977)
                                                            at android.support.v7.widget.RecyclerView.setScrollState(RecyclerView.java:1219)
                                                            at android.support.v7.widget.RecyclerView.access$3900(RecyclerView.java:147)
                                                            at android.support.v7.widget.RecyclerView$ViewFlinger.run(RecyclerView.java:4128)
                                                            at android.view.Choreographer$CallbackRecord.run(Choreographer.java:777)
                                                            at android.view.Choreographer.doCallbacks(Choreographer.java:590)
                                                            at android.view.Choreographer.doFrame(Choreographer.java:559)
                                                            at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:763)
                                                            at android.os.Handler.handleCallback(Handler.java:739)
                                                            at android.os.Handler.dispatchMessage(Handler.java:95)
                                                            at android.os.Looper.loop(Looper.java:145)
                                                            at android.app.ActivityThread.main(ActivityThread.java:6897)
                                                            at java.lang.reflect.Method.invoke(Native Method)
                                                            at java.lang.reflect.Method.invoke(Method.java:372)
                                                            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1404)
                                                            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1199)

Search Fragment:

public class SearchFragment extends BaseFragment implements SearchView.OnQueryTextListener, MusicPlayerService.MusicPlayerCallback {

public static final String KEY_SONGS = "KEY_SONGS";
public static final String KEY_ALBUMS = "KEY_ALBUMS";
public static final String KEY_VIDEOS = "KEY_VIDEOS";

private RecyclerView rcResult;
private StaggeredGridLayoutManager layoutManager;
private SearchAdapter searchAdapter;
private boolean gotPlayingItem;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setDefaultTitle("");
}

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.frm_search, container, false);
    rcResult = (RecyclerView) view.findViewById(R.id.rc_search_result);
    layoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
    rcResult.setLayoutManager(layoutManager);
    return view;
}

@Override
public void onResume() {
    super.onResume();
    getMainActivity().setSearchQueryListener(this);
    getThisApplication().getPlayerService().addMediaPlayerCallback(this);
}

@Override
public void onPause() {
    getMainActivity().setSearchQueryListener(null);
    getThisApplication().getPlayerService().removeMediaPlayerCallback(this);
    super.onPause();
}

@Override
public boolean onQueryTextSubmit(String query) {
    getMainActivity().clearSearchViewFocus();
    fetchData(query);
    return true;
}

@Override
public boolean onQueryTextChange(String newText) {
    return true;
}

private void fetchData(String query) {
    ProgressHUD.show(mContext, null, false, false, null);
    NetworkManager.searchKeyword(new Callback<SingerSearchResponse>() {
        @Override
        public void onResponse(Call<SingerSearchResponse> call, Response<SingerSearchResponse> response) {
            // TODO: Handle search response data
            KeyboardUtil.hideSoftKeyboard(getMainActivity());
            SingerSearchResponse singerSearchResponse = response.body();
            if (null == singerSearchResponse) {

            } else {
                SingerSearchResponse.SearchResponse searchResponse = singerSearchResponse.getSearchResponse();
                Map<String, List<? extends BaseDBModel>> searchResult = new HashMap<>();
                if (null != searchResponse) {
                    if (null == searchResponse.getSongs() || searchResponse.getSongs().size() == 0) {

                    } else {
                        searchResult.put(KEY_SONGS, searchResponse.getSongs());
                    }
                    if (null == searchResponse.getAlbums() || searchResponse.getAlbums().size() == 0) {

                    } else {
                        searchResult.put(KEY_ALBUMS, searchResponse.getAlbums());
                    }
                    if (null == searchResponse.getVideos() || searchResponse.getVideos().size() == 0) {

                    } else {
                        searchResult.put(KEY_VIDEOS, searchResponse.getVideos());
                    }
                    bindData(searchResult);
                }
            }
            ProgressHUD.dismissHUD();
        }

        @Override
        public void onFailure(Call<SingerSearchResponse> call, Throwable t) {
            ProgressHUD.dismissHUD();
        }
    }, query);
}

private void bindData(Map<String, List<? extends BaseDBModel>> searchResult) {
    searchAdapter = new SearchAdapter(getMainActivity(), searchResult);
    layoutManager.invalidateSpanAssignments();
    rcResult.setAdapter(searchAdapter);
}

@Override
public void onDestroy() {
    getMainActivity().closeSearchView();
    super.onDestroy();
}

@Override
public void onStartPlaying(BaseDBModel<? extends BaseDBModel> currentPlayingItem) {
    mUIThread.post(new Runnable() {
        @Override
        public void run() {
            gotPlayingItem = false;
            searchAdapter.notifyDataSetChanged();
        }
    });
}

@Override
public void onPlaying(BaseDBModel<? extends BaseDBModel> currentPlayingItem, int currentPositionMils) {
    if (!gotPlayingItem) {
        mUIThread.post(new Runnable() {
            @Override
            public void run() {
                gotPlayingItem = true;
                if (null != searchAdapter) {
                    searchAdapter.notifyDataSetChanged();
                }
            }
        });
    }
}

@Override
public void onPlayerResume() {

}

@Override
public void onPlayerPause() {

}

@Override
public void onPlayingCompleted() {
    mUIThread.post(new Runnable() {
        @Override
        public void run() {
            gotPlayingItem = false;
            searchAdapter.notifyDataSetChanged();
        }
    });
}

@Override
public void onError() {

}

@Override
public void onShuffle() {

}

SearchAdapter

public class SearchAdapter extends RecyclerView.Adapter implements OnAdapterUpdate{

private final int TYPE_TITLE = 0;
private final int TYPE_SONG = 1;
private final int TYPE_ALBUM = 2;
private final int TYPE_VIDEO = 3;

private int POS_SONG_TITLE;
private int POS_ALBUM_TITLE;
private int POS_VIDEO_TITLE;

private MainActivity mainActivity;
private LayoutInflater layoutInflater;
private Map<String, List<? extends BaseDBModel>> data;
private List<? extends BaseDBModel> listSongs;
private List<? extends BaseDBModel> listAlbums;
private List<? extends BaseDBModel> listVideos;
private int totalRecords;

public SearchAdapter(MainActivity mainActivity, Map<String, List<? extends BaseDBModel>> data) {
    this.mainActivity = mainActivity;
    layoutInflater = (LayoutInflater) mainActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    this.data = data;
    preparingData();
}

private void preparingData() {
    totalRecords = 0;
    POS_SONG_TITLE = POS_ALBUM_TITLE = POS_VIDEO_TITLE = -1;
    if (data.containsKey(SearchFragment.KEY_SONGS)) {
        listSongs = data.get(SearchFragment.KEY_SONGS);
        totalRecords += listSongs.size() + 1;
        POS_SONG_TITLE = 0;
        POS_ALBUM_TITLE = totalRecords;
    }
    if (data.containsKey(SearchFragment.KEY_ALBUMS)) {
        listAlbums = data.get(SearchFragment.KEY_ALBUMS);
        totalRecords += listAlbums.size() + 1;
        POS_VIDEO_TITLE = totalRecords;
    }
    if (data.containsKey(SearchFragment.KEY_VIDEOS)) {
        listVideos = data.get(SearchFragment.KEY_VIDEOS);
        totalRecords += listVideos.size() + 1;
    }
}

@Override
public int getItemViewType(int position) {
    if ((position == POS_SONG_TITLE && null != listSongs) || (position == POS_ALBUM_TITLE && null != listAlbums) || (position == POS_VIDEO_TITLE && null != listVideos)) {
        return TYPE_TITLE;
    } else if (null != listSongs && position > POS_SONG_TITLE && position <= listSongs.size()) {
        return TYPE_SONG;
    } else if (null != listAlbums && position > POS_ALBUM_TITLE && position <= POS_ALBUM_TITLE + listAlbums.size()) {
        return TYPE_ALBUM;
    } else if (null != listVideos && position > POS_VIDEO_TITLE && position <= totalRecords) {
        return TYPE_VIDEO;
    } else {
        return super.getItemViewType(position);
    }
}

private ICloseMoreAction iCloseMoreAction = new ICloseMoreAction() {
    @Override
    public void closeAllMoreMenu() {
        closeAllMenu();
    }
};

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    RecyclerView.ViewHolder viewHolder = null;
    View itemView = null;
    switch (viewType) {
        case TYPE_TITLE:
            itemView = layoutInflater.inflate(R.layout.item_title, parent, false);
            viewHolder = new TitleViewHolder(itemView);
            break;
        case TYPE_SONG:
            itemView = layoutInflater.inflate(R.layout.item_media_songs, parent, false);
            viewHolder = new SongViewHolder(itemView, mainActivity, iCloseMoreAction, this);
            break;
        case TYPE_ALBUM:
            itemView = layoutInflater.inflate(R.layout.item_media_album, parent, false);
            viewHolder = new AlbumViewHolder(itemView, mainActivity);
            break;
        case TYPE_VIDEO:
            itemView = layoutInflater.inflate(R.layout.item_media_videos, parent, false);
            viewHolder = new VideoViewHolder(itemView, mainActivity);
            break;
    }
    return viewHolder;
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    StaggeredGridLayoutManager.LayoutParams layoutParams = (StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams();
    if (null != listSongs && position > POS_SONG_TITLE && position < listSongs.size() + 1) {
        MediaSongModel mediaSongModel = (MediaSongModel) listSongs.get(position - 1);
        ((SongViewHolder) holder).bindData(mediaSongModel, null, BaseDBModel.TYPE_ONLINE, false);
        layoutParams.setFullSpan(true);
    } else if (null != listAlbums && position > POS_ALBUM_TITLE && position < POS_ALBUM_TITLE + listAlbums.size() + 1) {
        MediaAlbumModel mediaAlbumModel = (MediaAlbumModel) listAlbums.get(position - POS_ALBUM_TITLE - 1);
        ((AlbumViewHolder) holder).bindData(mediaAlbumModel, null);
    } else if (null != listVideos && position > POS_VIDEO_TITLE && position < totalRecords) {
        MediaVideoModel mediaVideoModel = (MediaVideoModel) listVideos.get(position - POS_VIDEO_TITLE - 1);
        ((VideoViewHolder) holder).bindData(mediaVideoModel, null);
    } else {
        if (POS_SONG_TITLE != -1 && position == POS_SONG_TITLE) {
            ((TitleViewHolder) holder).bindData("MP3");
        } else if (POS_ALBUM_TITLE != -1 && position == POS_ALBUM_TITLE) {
            ((TitleViewHolder) holder).bindData("Albums");
        } else if (POS_VIDEO_TITLE != -1 && position == POS_VIDEO_TITLE) {
            ((TitleViewHolder) holder).bindData("Videos");
        }
    }
}

@Override
public int getItemCount() {
    return totalRecords;
}

private void closeAllMenu() {
    int songListSize = listSongs.size();
    for (int i = 0; i < songListSize; i++) {
        if (((MediaSongModel) listSongs.get(i)).isExpanded()){
            ((MediaSongModel) listSongs.get(i)).setExpanded(false);
            notifyDataSetChanged();
            break;
        }
    }
}

@Override
public void updateAdapter() {
    notifyDataSetChanged();
}

@Override
public void removePosition(int position) {
    listSongs.remove(position);
    notifyItemRemoved(position);
}

public interface ICloseMoreAction {
    void closeAllMoreMenu();
}

}

A note that i change every items heigh to wrap_content!

Thanks!

Andrew Thompson
  • 168,117
  • 40
  • 217
  • 433
Long Đình
  • 31
  • 1
  • 8

1 Answers1

0

I had the same problem when binding to an new cursor after swapping the backing database. As my app is very modular I also notied this did not happen when using one of the other layouts I support, which uses ListLayoutManager.

Somehow this only happened when changing to a null cursor (making the adapter report 0 items, which it also does at other points) and then back while also changing the db. Injecting a null cursor between searches did not cause the same problems and setting the adapter to report unstable ids did not help either. This leads me to belive there is something odd happening with my cursors. But with the clue from above I came to a kind of workaround similar to what I do when changing column counts, layouts and viewtypes at runtime: simply recreate the LayoutManager when a situation you know to cause this kind of issue comes up.

Note: I checked via LeakCanary that the old LayoutManager is collected as expected and everything else is still in use, so there are no memory leaks from this. Only the inherent overhead of creating and setting up the LayoutManager

Here is an example (edited for clarity) from my Adapter:

public void receiveCursor(Cursor newCursor) {
    if(oldCursor == null && recyclerView.getLayoutManager() instanceof StaggeredGridLayoutManager){
        // there was a library switch before this, otherwise there would be
        // a non-null, empty, cursor
        setCursor(newCursor);

        // this is the pertinent exerpt from my re-setup code which is also called
        // from code that switches layouts etc.
        StaggeredGridLayoutManager manager = 
            new StaggeredGridLayoutManager(numCols, StaggeredGridLayoutManager.VERTICAL);
            manager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
            recyclerView.setLayoutManager(manager);
            recyclerView.setHasFixedSize(false);
    }else {
        setCursor(newCursor);
    }
}

I will have to add that swapping the backing DB is a rather rare task and I am perfectly willing to take that performance tradeoff for a simple solution. As your use-case is search which might happen several times a second while the user is interactiong this solution may not be for you due to the overhad from object creation, initiaization and garbage collection while doing heavy layout changes. I just thought I'd document this for other people finding this question on Google.

  • I don't think it's belong adapter problem, a note that when i change nums column to 3 from 2 and fix height of imageview item. Everything is ok. I think problem comes from layout manager, many open bugs are still on code.google.com about StaggeredGridLayout. So sad – Long Đình Jul 27 '16 at 06:24
  • That is exactly what I think aswell. When I talked about the adapter I was trying to make the point that the error is unique to StaggerdGridLayoutManager. That is what led me to the workaround of simply re-creating the layoutmanager when a situation known to cause this bug comes up. I had the same stack trace you posted above and this fixed it for me (You do say that fixing the image size worked for you, but in my case the col number is a user preference so I did not even explore that option). – Eric Hoffmann Jul 29 '16 at 17:47