RecyclerView has become a standard element in any kind of Android app. It’s fast and efficient – at least for the user. For developers, however, it’s an entirely different story.
You have to manage so many things – create adapters, keep track of multiple view types and span sizes (when you’re using a GridLayoutManager). Let’s not forget that sometimes you want to use expandable groups with headers… Oh and you also have to manage your overall sanity when something breaks in the process!
Luckily there is a simple RecyclerView solution which works out-of-the-box but it can also be heavily customized if you want to play with it. Groupie is an open-source Android library aimed at helping you get rid of any RecyclerView related headaches.
Groupie organizes items into groups – Sections and ExpandableGroups. It supports the use of headers and updating the groups with changed items could not be simpler – just call update() and you don’t even need to call notifyItemMoved() or any of that stuff.
This post contains all the code that’s been written in this YouTube video.
You can also check out this GitHub repository: https://github.com/ResoCoder/Groupie-LibraryTutorial
build.gradle
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' androidExtensions { experimental = true } android { compileSdkVersion 27 defaultConfig { applicationId "com.resocoder.groupielibrarytutorial" minSdkVersion 19 targetSdkVersion 27 buildToolsVersion "27.0.2" versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" implementation 'com.android.support:appcompat-v7:27.0.2' implementation 'com.android.support.constraint:constraint-layout:1.0.2' implementation 'com.android.support:design:27.0.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' compile 'com.xwray:groupie:2.0.3' compile 'com.xwray:groupie-kotlin-android-extensions:2.0.3' implementation 'com.android.support:cardview-v7:27.0.2' }
item_fancy.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/item_fancy_cardView" android:layout_width="match_parent" android:layout_height="100dp" android:layout_margin="4dp" android:foreground="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true" app:cardCornerRadius="4dp" app:cardElevation="4dp"> <TextView android:id="@+id/item_fancy_number" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" tools:text="1" android:textAppearance="@style/Base.TextAppearance.AppCompat.Large" android:textSize="50sp"/> </android.support.v7.widget.CardView>
FancyItem.kt
package com.resocoder.groupielibrarytutorial import android.support.annotation.ColorInt import com.xwray.groupie.kotlinandroidextensions.Item import com.xwray.groupie.kotlinandroidextensions.ViewHolder import kotlinx.android.synthetic.main.item_fancy.* class FancyItem(@ColorInt private val color: Int, private val number: Int) : Item(){ override fun bind(viewHolder: ViewHolder, position: Int) { viewHolder.item_fancy_cardView.setCardBackgroundColor(color) viewHolder.item_fancy_number.text = number.toString() } override fun getLayout() = R.layout.item_fancy override fun getSpanSize(spanCount: Int, position: Int) = spanCount / 3 }
item_expandable_header.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/item_expandable_header_root" android:layout_width="match_parent" android:layout_height="wrap_content" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"> <TextView android:id="@+id/item_expandable_header_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="2dp" android:layout_marginBottom="2dp" android:layout_centerHorizontal="true" android:textStyle="bold" android:textColor="?attr/colorAccent" tools:text="Group 1"/> <ImageView android:id="@+id/item_expandable_header_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toEndOf="@id/item_expandable_header_title" app:srcCompat="@drawable/ic_keyboard_arrow_down_black_24dp" android:tint="@color/colorAccent"/> </RelativeLayout>
ExpandableHeaderItem.kt
package com.resocoder.groupielibrarytutorial import com.xwray.groupie.ExpandableGroup import com.xwray.groupie.ExpandableItem import com.xwray.groupie.kotlinandroidextensions.Item import com.xwray.groupie.kotlinandroidextensions.ViewHolder import kotlinx.android.synthetic.main.item_expandable_header.* class ExpandableHeaderItem(private val title: String) : Item(), ExpandableItem{ private lateinit var expandableGroup: ExpandableGroup override fun bind(viewHolder: ViewHolder, position: Int) { viewHolder.item_expandable_header_title.text = title viewHolder.item_expandable_header_icon.setImageResource(getRotatedIconResId()) viewHolder.item_expandable_header_root.setOnClickListener { expandableGroup.onToggleExpanded() viewHolder.item_expandable_header_icon.setImageResource(getRotatedIconResId()) } } override fun getLayout() = R.layout.item_expandable_header override fun setExpandableGroup(onToggleListener: ExpandableGroup) { expandableGroup = onToggleListener } private fun getRotatedIconResId() = if (expandableGroup.isExpanded) R.drawable.ic_keyboard_arrow_up_black_24dp else R.drawable.ic_keyboard_arrow_down_black_24dp }
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.resocoder.groupielibrarytutorial.MainActivity"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/AppTheme.PopupOverlay" /> </android.support.design.widget.AppBarLayout> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"/> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="@dimen/fab_margin" app:srcCompat="@drawable/ic_sync_black_24dp" android:tint="@android:color/white"/> </android.support.design.widget.CoordinatorLayout>
MainActivity.kt
package com.resocoder.groupielibrarytutorial import android.graphics.Color import android.os.Bundle import android.support.design.widget.Snackbar import android.support.v7.app.AppCompatActivity import android.support.v7.widget.GridLayoutManager import android.view.Menu import android.view.MenuItem import com.xwray.groupie.ExpandableGroup import com.xwray.groupie.GroupAdapter import com.xwray.groupie.Section import com.xwray.groupie.kotlinandroidextensions.ViewHolder import kotlinx.android.synthetic.main.activity_main.* import java.util.* class MainActivity : AppCompatActivity() { private val excitingSection = Section() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setSupportActionBar(toolbar) val boringFancyItems = generateFancyItems(6) val excitingFancyItems = generateFancyItems(12) val groupAdapter = GroupAdapter<ViewHolder>().apply { spanCount = 3 } recycler_view.apply { layoutManager = GridLayoutManager(this@MainActivity, groupAdapter.spanCount).apply { spanSizeLookup = groupAdapter.spanSizeLookup } adapter = groupAdapter } ExpandableGroup(ExpandableHeaderItem("Boring Group"), true).apply { add(Section(boringFancyItems)) groupAdapter.add(this) } ExpandableGroup(ExpandableHeaderItem("Exciting Group"), false).apply { excitingSection.addAll(excitingFancyItems) add(excitingSection) groupAdapter.add(this) } fab.setOnClickListener { excitingFancyItems.shuffle() excitingSection.update(excitingFancyItems) } } private fun generateFancyItems(count: Int): MutableList<FancyItem>{ val rnd = Random() return MutableList(count){ val color = Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256)) FancyItem(color, rnd.nextInt(100)) } } override fun onCreateOptionsMenu(menu: Menu): Boolean { // Inflate the menu; this adds items to the action bar if it is present. menuInflater.inflate(R.menu.menu_main, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. return when (item.itemId) { R.id.action_settings -> true else -> super.onOptionsItemSelected(item) } } }
This was a great tutorial.
For me, it was better than the official documentation.
How do I implement an onCLicklistener for the item in my activity or fragment.
I am using a viewmodel for my fragment and I want to load a ‘details’ fragment when an item is clicked.
Hi Matej,
I want to send multiple lists to myItem class. For single list, I am using the map function that iterates over the list and give me my model object and I set the data to views in Item class easily but I want to send another list. How can this be achieved??