I have a layout with this hierarchy
< NestedScrollView fillViewPort=true>
< LinearLayout>
< Viewgroup/>
< ViewGroup/>
< RecyclerView/>
< ViewGroup/>
< /LinearLayout>
< /NestedScrollView>
Sometimes i need to update my recyclerview elements, but it freeze main thread.
My guess its because scrollview need to measure it all again.
I really like to know how i should do this?
Replace recyclier view with layoutinlfate?
Recyclerview with height fixed?
Replace nestedscrollview, with recyclview? will have recyclview inside reclyerview. This works?
This is a common UI pattern and android:nestedScrollingEnabled="true" is not a fix for this.
Best approach to this pattern is to use a single recyclerview with multiple view types instead of having nested elements. The result is a more complicated recyclerview but you have better performance and more control with it.
Try to wrap current layout with tag like below:
< RelativeLayout> // math_parent
< NestedScrollView fillViewPort=true>
< LinearLayout>
< Viewgroup/>
< ViewGroup/>
< RecyclerView/>
< ViewGroup/>
< /LinearLayout>
< /NestedScrollView>
< /RelativeLayout>
It will prevent scrollview and recyclerview measure again.
First of all, based on Android documentation:
Never add a RecyclerView or ListView to a scroll view. Doing so
results in poor user interface performance and poor user experience.
Updating UI is always done on the main thread. So when you want to update the RecyclerView it affects performance. For a better user experience, you can do your calculation in another thread and update UI in the main thread. in bellow code, I simulate your situation:
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:layout_width="match_parent"
android:layout_height="700dp"
android:background="#color/black"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
in my example, updating the list occurs in every second. here is the code:
public class MainActivity extends AppCompatActivity {
private MyAdapter myAdapter;
int cnt = 0;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myAdapter = new MyAdapter();
RecyclerView recyclerView = findViewById(R.id.recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(myAdapter);
AsyncTask.execute(() -> updateList());
}
private void updateList() {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
#Override
public void run() {
cnt++;
myAdapter.addToList(cnt + "");
}
}, 1, 1000);//Update List every second
}
}
list is updated via updateList() method. for running this in new thread I use AsyncTask :
AsyncTask.execute(() -> updateList());
and finally here is my adapter:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
List<String> list = new ArrayList<>();
#NonNull
#Override
public MyViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false);
return new MyViewHolder(itemView);
}
#Override
public void onBindViewHolder(#NonNull MyViewHolder holder, int position) {
holder.textView.setText(list.get(position));
}
#Override
public int getItemCount() {
return list.size();
}
public class MyViewHolder extends RecyclerView.ViewHolder {
TextView textView;
public MyViewHolder(#NonNull View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.text_view);
}
}
public void addToList(String s) {
list.add(s);
new Handler(Looper.getMainLooper()).post(() -> {
notifyItemInserted(list.size());
});
}
}
As can be seen notifyItemInserted is called in Handler for updating UI on the main thread. for notifying recycler in different situations you can refer to https://stackoverflow.com/a/48959184/12500284.
You can create multiple adapters for all the views in your nestedscrollview and then concat them using concat adapter. In my case, my nestedscrollview had one header and one recyclerview. So I created one adapter for the header and one for the recycler view. You can use lambda or an interface for communicating between header adapter and activity. Here's the code:
val listAdapter = ManageQuoteAdapter(this)
val headerAdapter = ManageQuoteHeaderAdapter(handler)
val concatAdapter = ConcatAdapter(headerAdapter, listAdapter)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = concatAdapter
Pros:
Better performance, specially if your recyclerview has lots of images. Earlier all the images used to load at once, ultimately throwing a memory exception
Best suited for simpler views that has a header, footer and list of items.
Cons:
You need to create at least one more adapter file. So more file to maintain.
Complexity will increase if you have so many actions, input fields etc in header or footer part (Example - Forms). In this case, NestedScrollView will be more suitable as anyways recyclerview will have fewer items.
Here's how my screen looks:
when you are using vertical recycler view inside vertical NestedScrollView without using a constant height, RecyclerView height will be wrap_content even when you set the RecylcerView height to match_parent. so when you update your RecyclerView data, MainThread will be frozen because all the RecyclerView items onBind() method will be called. This means the RecyclerView doesn't recycle anything during scroll! if you can not use the MultipleViewType for your design, try to set a constant height to the RecyclerView.
This solution makes some difficulties for you. such as handling scrollablity.
The most likely reason for a UI freeze is that you're retrieving the data from some slow source, like the network or a database.
If that is the case, the solution is to retrieve the data on a background thread. These days I'd recommend using Kotlin Coroutines and Kotlin Flow.
Your DB would return a Flow<List>, the contents of which will automatically be updated whenever the data in the DB changes, and you can observe the data updates in the ViewModel.
Get a Flow object from your database:
#Dao
interface MyDataDao {
#Query("SELECT * FROM mydata")
fun flowAll(): Flow<List<MyDataEntity>>
}
The Fragment will observe LiveData and update the list adapter when it changes:
class MyScreenFragment(): Fragment() {
override fun onCreate() {
viewModel.myDataListItems.observe(viewLifecycleOwner) { listItems ->
myListAdapter.setItems(listItems)
myListAdapter.notifyDataSetChanged()
}
}
}
In the ViewModel:
#ExperimentalCoroutinesApi
class MyScreenViewModel(
val myApi: MyApi,
val myDataDao: MyDataDao
) : ViewModel() {
val myDataListItems = MutableLiveData<List<MyDataListItem>>()
init {
// launch a coroutine in the background to retrieve our data
viewModelScope.launch(context = Dispatchers.IO) {
// trigger the loading of data from the network
// which you should then save into the database,
// and that will trigger the Flow to update
myApi.fetchMyDataAndSaveToDB()
collectMyData()
}
}
/** begin collecting the flow of data */
private fun collectMyData() {
flowMyData.collect { myData ->
// update the UI by posting the list items to a LiveData
myDataListItems.postValue(myData.asListItems())
}
}
/** retrieve a flow of the data from database */
private fun flowMyData(): Flow<List<MyData>> {
val roomItems = myDataDao.flowAll()
return roomItems.map { it.map(MyDataEntity::toDomain) }
}
/** convert the data into list items */
private fun MyData.asListItems(): MyDataListItem {
return this.map { MyDataListItem(it) }
}
}
In case you didn't know how to define the objects for in a Room Database, I'll give you this as a hint:
// your data representation in the DB, this is a Room entity
#Entity(tableName = "mydata")
class MyDataEntity(
#PrimaryKey
val id: Int,
val date: String,
// ...
)
// Domain representation of your data
data class MyData(
val id: Int,
val date: SomeDateType,
// ...
)
// Map your DB entity to your Domain representation
fun MyDataEntity.toDomain(): MyData {
return MyData(
id = id,
date = SomeDateFormatter.format(date),
// ...
)
}
Related
I have RecyclerView and ViewHolder holding items. Each item has list of other items. These are just inflated in for loop to container within RecyclerView items.
This inflated View is ViewPager with custom Adapter. I want to achieve swiping effect inside Recyclerview item's children.
But no content is visible.
Is this possible to do inside RecyclerView?
So in general RecyclerView item has its children and each children will load its children from API call on swipe.
Update#2:
ViewPager is working as intended if I add pages to list before adapter initialization, but if I rewrite pages from API call, it will add them but swipePager.adapter?.notifyDataSetChanged() will not refresh pages list. List has more than 1 child but I cant swipe because ViewPagerAdapter still thinks it has only 1 page.
This would be last thing I need to fix. I've read that notifyDataSetChanged() is bugged in ViewPager and it has different behaviour than for RecyclerViewAdapter. Anyone has some solution how to refresh adapter?
Custom ViewPager:
<?xml version="1.0" encoding="utf-8"?>
<com.my.project.SwipeViewPager
android:id="#+id/pager"
android:layout_width="match_parent"
android:layout_height="110dp"
android:overScrollMode="never"
xmlns:android="http://schemas.android.com/apk/res/android" />
Code inside onBind for single RecyclerView item:
parts.removeAllViews()
val li = LayoutInflater.from(root.context)
j.parts.forEachIndexed { i, p ->
val v = li.inflate(R.layout.pager_layout, parts, false).apply {
val swipePager = findViewById<SwipeViewPager>(R.id.pager)
var nextParts = emptyList<PageItem>()
val pages = mutableListOf<PageScreen>()
var alreadySwiped = false
val swipeListener = object : OnSwipeOutListener{
override fun onSwipeOutAtEnd(){
//call API on swipe
if (!alreadySwiped){
getUpcomingData(p.itemId){ list->
alreadySwiped = true
nextParts = list
pages.clear()
nextParts.forEach { np->
pages.add(
PageScreen(
a,
app,
map,
np,
onClick = {
onItemClicked(np.nextId)
}
)
)
}
swipePager.adapter?.notifyDataSetChanged()
}
}
}
override fun onSwipeOutAtStart(){
}
}
//add first item which is known
pages.add(PageScreen(
a,
app,
map,
p,
onClick = {
onItemClicked(p.nextId)
}
))
swipePager.apply {
setOnSwipeOutListener(swipeListener)
adapter = SwipePagerAdapter(pages.toList()).also { it.notifyDataSetChanged() }
}
}
parts += v
}
In my app I have two LiveData objects, one for getting items from 0-10 and second to get the items from 11-20. I'm trying to load the data in a RecyclerView but instead of having 20 items, the first 10 (0-10) are replaces with new 10 (11-20). This is what I have tried:
recyclerView = findViewById(R.id.recycler_view);
adapter = new ItemsAdapter();
recyclerView.setAdapter(adapter);
viewModel = new ViewModelProvider(this).get(ItemListViewModel.class);
To get items from 0-10 I use this method:
private void getInitialItems() {
ItemListLiveData liveData = viewModel.getItemsLiveData();
liveData.observe(this, itemtList -> adapter.submitList(itemtList));
}
To get items from 11-20 I use this method:
private void getNextlItems() {
ItemListLiveData liveData = viewModel.getItemsLiveData();
liveData.observe(this, itemtList -> adapter.submitList(itemtList));
}
This is my ViewModel class:
public class ItemListViewModel extends ViewModel {
private ItemListRepository repository = new ItemListRepository();
ItemListLiveData getItemsLiveData() {
return repository.getItemListLiveData();
}
}
In the repository I only get the items from a back-end server. This is my adapter class:
public class ItemsAdapter extends ListAdapter<Item, ItemsAdapter.ItemViewHolder> {
ItemsAdapter() {
super(diffCallback);
}
#NonNull
#Override
public ItemViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
//Inflate the view
}
#Override
public void onBindViewHolder(#NonNull final ItemViewHolder holder, int position) {
//Bind the Item according to position
}
private static DiffUtil.ItemCallback<Item> diffCallback = new DiffUtil.ItemCallback<Item>() {
#Override
public boolean areItemsTheSame(#NonNull Item oldItem, #NonNull Item newItem) {
return oldItem.id.equals(newItem.id);
}
#Override
public boolean areContentsTheSame(#NonNull Item oldItem, #NonNull Item newItem) {
return oldItem.equals(newItem);
}
};
}
My expectation is when using DiffUtil.ItemCallback to get both lists as a cumulative list since all the objects are different. Even if I pass both lists to the same adapter, I end up having only ten items (11-20). How to use submit list so I can have 20 items in my list and not only 10 (11-20)?
DiffUtil.ItemCallback is used for animating smoothly changes in dataset in adapter.
For example if you have have 10 items, than submit list with 9 items that were contained in previous 10, DiffUtil.ItemCallback will determine difference between old and new list, which position that element was and animate changes accordingly. What you are looking for in your case is Pagination where you can expand/show items while scrolling.
You don't need two LiveData for this one, you cast fetch data from some source add it to LiveData of Pagination. First it will be showed 10 items, then if you scroll to end another 10, and so on. You can adjust type of pagination by your needs with provided Configuration.
To do all that without Pagination.
liveData.observe(this, itemtList -> adapter.submitList(adapter.getCurrentList().addAll(itemtList)));
Get previous data, on top of that data add new data and it will all be shown.
I want to know what I can do to maximise RecyclerView performance when having extensive data and media resources. I have literally tried almost every solution I found on google's first 3 pages of search results, every Medium article on the topic, it changed nothing.
On any phone I tested my app, I get this micro-lag or micro-stutter when I first scroll the recyclerView and then everything flows smooth. But the first scroll always has this annoying lag. It happens on all 3 different recyclerView lists in my app as well as a ViewPager2 activity that is based on a recyclerView adapter.
So, more specific details: I code in Kotlin, use Retrofit/Okhttp for networking, Room for local database and I do download all the data in the Splash Screen, including caching the images with Glide without displaying them anywhere. Basically the recyclerViews must only get the data from Room and display it in the list while Glide takes the images from the cache and displays them in every image view.
Still, the app lags when I scroll for the first time. In the past, it used to lag constantly because I was downloading the images with glide only inside the viewHolders and obviously the recyclerView scroll was bricking every time a new image was being downloaded and displayed. After I cached the images in Splash Screen, the only lag that occurs is the one on the first scroll, the rest is smooth as silk.
Tried many tutorials like a pre caching custom layout manager like:
public class PreCachingLinearLayoutManager extends LinearLayoutManager {
private static final int DEFAULT_EXTRA_LAYOUT_SPACE = 600;
private int extraLayoutSpace = -1;
private Context context;
public PreCachingLinearLayoutManager(Context context) {
super(context);
this.context = context;
}
public PreCachingLinearLayoutManager(Context context, int extraLayoutSpace) {
super(context);
this.context = context;
this.extraLayoutSpace = extraLayoutSpace;
}
public PreCachingLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
this.context = context;
}
public void setExtraLayoutSpace(int extraLayoutSpace) {
this.extraLayoutSpace = extraLayoutSpace;
}
#Override
protected int getExtraLayoutSpace(RecyclerView.State state) {
if (extraLayoutSpace > 0) {
return extraLayoutSpace;
}
return DEFAULT_EXTRA_LAYOUT_SPACE;
}
}
and then layoutManager.setExtraLayoutSpace(2 * SharedPreferences.deviceScreenHeight)
or various settings like cache size, etc.
homepageAdapter.setHasStableIds(true)
rvMainBoxList.apply {
adapter = homepageAdapter
isNestedScrollingEnabled = false
hasFixedSize()
setItemViewCacheSize(30)
this.layoutManager = layoutManager
}
Tried simplifying my XML codes by getting rid of all the Constraint Layouts, setting fixed heights to all resources, removing al useless parents, etc. Nothing happened, the recycler still lags the first time you scroll.
Any out of the box ideas on how to fix this would help me a lot!
This is my adapter:
class ItemAdapter : RecyclerView.Adapter<ItemAdapter.MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(parent.inflate(R.layout.item_row))
}
var items: List<Item> = ArrayList()
set(value) {
field = value
this.notifyDataSetChanged()
}
var onClickListener: ((Item) -> (Unit))? = null
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = items[position]
holder.bind(item)
}
override fun getItemCount(): Int {
return items.size
}
inner class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(item: Item) {
setData(item)
setControls(item)
}
private fun setData(item: Item) {
val view: View = itemView.rootView
// Set data here
}
}
private fun setControls(item: Item) {
itemView.rlLoginItemContainer.setOnClickListener {
onClickListener?.invoke(item)
}
}
}
}
You should post your Adapter code so we can get a better idea how you're going about this.
My suspicion is that you may be doing your Room call in your ViewHolder and this is blocking the creation of your view by a brief moment. Since each ViewHolder is only created the first time you scroll past an item you would only see the lag on the first scroll down. After that, all the data is in memory and RecyclerView is just swapping out the data into the views it can fit on screen as you scroll around.
If you are doing the Room call in the adapter at all I would really recommend looking into the Jetpack Library called PagedList. This would handle paging the data from your Room database into your RecyclerView more efficiently. Google has a great sample app on their GitHub page that shows them doing network calls that cache into a Room database as a user scrolls through a RecyclerView using this. If you wanted to fully commit to it you could probably move all of that loading out of your splash screen.
Documentation:
https://developer.android.com/topic/libraries/architecture/paging
Example code:
https://github.com/googlesamples/android-architecture-components/tree/master/PagingWithNetworkSample
Good luck!
Change from 600 to private static final int DEFAULT_EXTRA_LAYOUT_SPACE = 10000;
You will see smooth improve buts dangerous somehow if your layout doesn't have extra space
I am trying to use android Room API to load records from sQlite in pages.
The issue is Paging library is loading entire database into model class and binding it with the adapter which is making UI thread skip frames. It suppose to load 20 records and then keep on adding more when required
This is my view model class
public class UserViewModel extends ViewModel {
public LiveData<PagedList<User>> userList;
public UserViewModel() {
}
public void init(UserDao userDao) {
PagedList.Config pagedListConfig =
(new PagedList.Config.Builder()).setEnablePlaceholders(true)
.setPrefetchDistance(10)
.setPageSize(20).build();
userList = (new LivePagedListBuilder(userDao.usersByFirstName(),
pagedListConfig))
.build();
}
}
Paged adapter
public class UserAdapter extends PagedListAdapter<User, UserAdapter.UserItemViewHolder> {
protected UserAdapter() {
super(User.DIFF_CALLBACK);
}
#Override
public UserItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View view = layoutInflater.inflate(R.layout.item_user_list, parent, false);
return new UserItemViewHolder(view);
}
#Override
public void onBindViewHolder(UserItemViewHolder holder, int position) {
User user= getItem(position);
if(user!=null) {
holder.bindTo(user);
}
}
static class UserItemViewHolder extends RecyclerView.ViewHolder {
TextView userName, userId;
public UserItemViewHolder(View itemView) {
super(itemView);
userId = itemView.findViewById(R.id.userId);
userName = itemView.findViewById(R.id.userName);
}
public void bindTo(User user) {
userName.setText(user.firstName);
userId.setText(String.valueOf(user.userId));
}
}
}
Binding with recycler View:
UserViewModel viewModel =
ViewModelProviders
.of(this)
.get(UserViewModel.class);
viewModel.init(userDao);
final UserAdapter userUserAdapter = new UserAdapter();
viewModel.userList.observe(this, pagedList -> {
Toast.makeText(this, "Page " + pagedList.size(), Toast.LENGTH_SHORT).show();
Log.e("Paging ", "Page " + pagedList.size());
userUserAdapter.setList(pagedList);
});
recyclerView.setAdapter(userUserAdapter);
02-18 10:19:40.409 15310-15310/com.androidkt.archpaging E/Paging: Page
200
Any idea what I am missing.
By the paging implementation, your result count should indeed be the full size of the query (200), as you configured to do so, the RecyclerView will receive placeholders null for the views which data is not ready yet. This is intended for when you want to show the whole list views but only bind the content of it when the data is available. But your RecyclerView should not call onCreateViewHolder and onBindViewHolder for the entire count unless it is visible.
Check (put a breakpoint) the method onMeasure or onLayout on your RecyclerView to see if the method is not returning a bigger height than expected (probably the expected is something around the size of your screen). Sometimes the actual height of RecyclerView is much bigger than the screen and the adapter call onBindViewHolder() for the total number of items because it's "visible" to it instead of the number we can see. This would trigger the DataSource to query the database and bind the views before you want.
try userUserAdapter.setList(pagedList); put out observe . observe use listner list change . You . You need to initialize the list and set up recyclerview normally .
The object list should be included in the pageAdapter in the usual way
You should call userUserAdapter.submitList(pagedList).
setList() is used for RecyclerView.Adapter not for PagedListAdapter.
I am building a notifications list for an application I'm working on and I'm having trouble finding a way to take my list of notifications from the server and displaying them in separate lists in a RecyclerView. The end product would display the list of notifications with headers for Recent notifications and Older notifications, a la:
<RECENT HEADER>
<NOTIF-1>
<NOTIF-2>
<OLDER HEADER>
<NOTIF-3>
<NOTIF-4>
<NOTIF-5>
<NOTIF-6>
except instead of angle-bracket text it's actual views representing those, complete with images, actual notification details and dividers.
I already have code that displays them in a RecyclerView:
XML:
<!-- Main layout -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="#layout/include_toolbar"/>
<RelativeLayout
android:id="#+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.SwipeRefreshLayout
android:id="#+id/notification_swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.mapjungle.mymoose.ui.widget.EmptyRecyclerView
android:id="#+id/notification_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>
</RelativeLayout>
</LinearLayout>
Java:
#InjectView(R.id.notification_list) RecyclerView mRecyclerView;
#Inject Picasso mPicasso;
#Inject NotificationService mUserService;
private NotificationAdapter mAdatper;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_notifications);
ButterKnife.inject(this);
setTitle("Notifications");
mAdatper = new NotificationAdapter(mPicasso);
mRecyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(this)
.color(getResources().getColor(R.color.secondary_color))
.size(1)
.build());
final LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setAdapter(mAdatper);
updateList();
}
#Override
protected int getSelfNavDrawerItem() {
return NAVDRAWER_ITEM_PHOTO_POST;
}
public void updateList() {
mUserService.getNotifications(new Callback<List<Notification>>() {
#Override
public void success(List<Notification> notificationList, Response response) {
mAdatper.replaceWith(notificationList);
}
#Override
public void failure(RetrofitError error) {
Timber.e(error, "Failed to load notifications...");
}
});
}
This all works fine enough to display all of the notifications and they're all sorted in the order from newest to oldest descending. But each has a boolean property "acknowledged" that is set to false if the user hasn't seen them before. I want to put split the list into the two groups I've explained above using this flag, but I don't know how to throw in the headers. I've thought about subclassing Notification to create NotificationHeader views and inserting them into the list where appropriate but that just feels sloppy to me. I've also thought about doing two recycler views, one for the new and another for the old, but visually that didn't work the way I intended (I haven't confirmed it but it looked like each recycler view scrolled independently of the others, something that I do not want). Any suggestions?
I know that the first idea of creating special Notification Headers will probably work, I've done something like that before, but it just feels like bad practice.
RecyclerView.Adapter has a method called getItemViewType() that takes the position of an item in the adapter's list, and returns the view type it should use. In my case, the method looks like this:
#Override
public int getItemViewType(int position){
Notification n = mNotifications.get(position);
boolean useHeader = n.getType().equals(Notification.HEADER_OLDER) ||
n.getType().equals(Notification.HEADER_RECENT);
return useHeader ? this.USE_HEADER : this.DONT_USE_HEADER;
}
Which checks the items in the notification list and sees if they're a special static 'Header notification' object. This is used internally by the Adapter class and it passes the 'viewType' parameter to the onCreateViewHolder() method, which we also override:
#Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
int layout = viewType == USE_HEADER ?
R.layout.view_item_notification_header :
R.layout.view_item_notification;
NotificationItemView view = (NotificationItemView) LayoutInflater.from(viewGroup.getContext())
.inflate(layout, viewGroup, false);
return new ViewHolder(view);
}
Overriding this method allows us to use the viewType parameter to choose the appropriate layout to inflate for the ViewHolder.
There are some better style/good practice decisions things I should have done here, such as making my Notification adapter hold a list of NotificationListItems instead of Notifications, which would allow me to put in a new kind of NotificationHeader object on it's own instead of making Notification objects that weren't really Notifications and using a bunch of constant values. But the underlying principle is still there:
In your Model, have a method that returns the layout view to use for it
In your adapter override getItemViewType() to use the aforementioned method and return an int that corresponds to the layout that should be inflated
In your adapter also override onCreateViewHolder() to use the int from getItemViewType() and inflate the appropriate view accordingly