Last active
February 4, 2023 09:06
-
-
Save ioannisa/44c839d12e4fa9e1f5b4d47f17252e5b to your computer and use it in GitHub Desktop.
Jetpack Compose RecyclerView with Sticky Header and network images using Coil
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package eu.anifantakis.composeapp | |
import android.os.Bundle | |
import android.widget.Toast | |
import androidx.activity.ComponentActivity | |
import androidx.activity.compose.setContent | |
import androidx.compose.foundation.* | |
import androidx.compose.foundation.layout.* | |
import androidx.compose.foundation.lazy.LazyColumn | |
import androidx.compose.foundation.lazy.items | |
import androidx.compose.material.* | |
import androidx.compose.runtime.Composable | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.layout.ContentScale | |
import androidx.compose.ui.tooling.preview.Preview | |
import androidx.compose.ui.unit.dp | |
import androidx.compose.ui.unit.sp | |
import coil.compose.rememberImagePainter | |
import eu.anifantakis.composeapp.ui.theme.ComposeAppTheme | |
// =========================[ NOTE ]========================= | |
// add Coil for Jetpack Compose at your app dependencies | |
// | |
// implementation("io.coil-kt:coil-compose:1.3.2") | |
// ========================================================== | |
// define a simple data class to hold Person information | |
data class Person( | |
val id: Int, | |
val section: Int, | |
val name: String, | |
val imageUrl: String, | |
val landingPage: String | |
) | |
class MainActivity : ComponentActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
val persons = arrayListOf<Person>() // create an array list of persons | |
var section = 1 | |
for (i in 1..200){ // create 200 Person instances in that list | |
if (i%15 == 0){ | |
section++ | |
} | |
persons.add( | |
Person( | |
id = i, | |
section = section, // all persons are assigned section | |
name = "Ioannis Anifantakis", | |
imageUrl = "https://anifantakis.eu/wp-content/uploads/2021/05/ioannis-anifantakis-firebase-small.jpg", | |
landingPage = "https://anifantakis.eu" | |
) | |
) | |
} | |
setContent { | |
ComposeAppTheme { | |
// A surface container using the 'background' color from the theme | |
Surface(color = MaterialTheme.colors.background) { | |
// [Demo1]: A Scrollable ListView equivalent | |
// ScrollableColumnDemo() | |
// [Demo2]: A RecyclerView equivalent | |
// LazyColumnDemo() | |
// [Demo3]: A RecyclerView equivalent with click listeners on its items | |
// LazyColumnClickableDemo{ | |
// Toast.makeText(this, "Person $it", Toast.LENGTH_SHORT).show() | |
// } | |
// [Demo 4]: A RecyclerView equivalent with rich UI populating it's items from a dataset | |
// LazyColumnClickableAdvDemo(persons){ | |
// Toast.makeText(this, "${it.name} ${it.id}", Toast.LENGTH_SHORT).show() | |
// } | |
// [Demo 5]: A RecyclerView equivalent with rich UI populating it's items from a dataset with a sticky header | |
LazyColumnClickableAdvStickyDemo(persons){ | |
Toast.makeText(this, "${it.name} ${it.id}", Toast.LENGTH_SHORT).show() | |
} | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Demo1: Column | |
* A Scrollable ListView equivalent | |
*/ | |
@Composable | |
fun ScrollableColumnDemo(){ | |
val scrollState = rememberScrollState() | |
Column( | |
modifier = Modifier | |
.verticalScroll(scrollState) | |
.fillMaxSize() | |
) { | |
for (i in 1..200){ | |
Text( | |
text = "Person $i", | |
fontSize = 36.sp, | |
modifier = Modifier.padding(8.dp) | |
) | |
Divider(color = Color.Gray, thickness = 1.dp) | |
} | |
} | |
} | |
/** | |
* Demo2: LazyColumn | |
* A RecyclerView equivalent | |
*/ | |
@Composable | |
fun LazyColumnDemo(){ | |
LazyColumn(){ | |
items(100){ | |
Text( | |
text ="Person ${it+1}", | |
style = MaterialTheme.typography.h3, | |
modifier = Modifier.padding(8.dp) | |
) | |
Divider(color = Color.Gray, thickness = 1.dp) | |
} | |
} | |
} | |
/** | |
* Demo3 (bad): LazyColumn with clickable items | |
* A RecyclerView equivalent with click listeners on its items | |
* | |
* This is a "bad" approach, but works... we will build on the better version next | |
* Here we display a Toast using the LocalContext directly from inside the Composable Function | |
* This shows minimal change from the previous code, just to display how we can add a "clickable" | |
* functionality. Ofcourse handling business logic from inside the Composable Function is "bad"... | |
* | |
* Next we will create a delegation passing data back to the caller, and the caller should handle the | |
* business logic that displays the toast when the item is clicked ;) | |
*/ | |
@Composable | |
fun LazyColumnClickableDemoBad() { | |
val context = LocalContext.current | |
LazyColumn() { | |
items(200){ | |
Surface(modifier = Modifier.clickable { Toast.makeText(context, "Person $it", Toast.LENGTH_SHORT).show() }) { | |
Text( | |
text = "Person ${it + 1}", | |
fontSize = 36.sp, | |
modifier = Modifier.padding(8.dp) | |
) | |
Divider(color = Color.Gray, thickness = 1.dp) | |
} | |
} | |
} | |
} | |
/** | |
* Demo3: LazyColumn with clickable items | |
* A RecyclerView equivalent with click listeners on its items | |
* @selectedPerson: a lambda function to return back to the caller the number of the current item iteration on click | |
*/ | |
@Composable | |
fun LazyColumnClickableDemo(selectedPerson: (Int) -> Unit){ | |
LazyColumn(){ | |
items(200){ | |
Surface(modifier = Modifier.clickable { selectedPerson(it+1) }) { | |
Text( | |
text = "Person ${it+1}", | |
fontSize = 36.sp, | |
modifier = Modifier.padding(8.dp) | |
) | |
Divider(color = Color.Gray, thickness = 1.dp) | |
} | |
} | |
} | |
} | |
/** | |
* Used in Demo4 & Demo5 | |
* Composable function to use Coil to download and display an image | |
* @imageUrl: The URL of the image | |
*/ | |
@Composable | |
fun ImageLoader(imageUrl: String){ | |
Image( | |
painter = rememberImagePainter(imageUrl), | |
contentDescription = null, | |
contentScale = ContentScale.Crop, | |
modifier = Modifier.size(120.dp) | |
) | |
} | |
/** | |
* Used in Demo4 & Demo5 | |
* Composable function to represent a list item | |
* @person: The Person instance, who's information will be displayed in the list item | |
* @selectedPerson: a lambda function to return back to the caller the Person instance on click | |
*/ | |
@Composable | |
fun ListItem(person: Person, selectedPerson: (Person)->Unit){ | |
Card( | |
modifier = Modifier | |
.padding(8.dp) | |
.fillMaxWidth() | |
.clickable { selectedPerson(person) }, | |
elevation = 8.dp, | |
) { | |
Row{ | |
ImageLoader(person.imageUrl) | |
Spacer(modifier = Modifier.width(8.dp)) | |
Text( | |
person.name+' '+person.id, | |
style = MaterialTheme.typography.h5, | |
modifier = Modifier.padding(8.dp) | |
) | |
} | |
} | |
} | |
/** | |
* Demo 4: LazyColumn displaying a List<Person> with clickable items | |
* A RecyclerView equivalent with rich UI populating it's items from a dataset | |
* @persons: a List<Person> passed as dataset | |
* @selectedPerson: a lambda function to return back to the caller the number of the current item iteration on click | |
*/ | |
@Composable | |
fun LazyColumnClickableAdvDemo(persons: List<Person>, selectedPerson: (Person)->Unit){ | |
LazyColumn(){ | |
items( | |
items = persons, | |
itemContent = { | |
ListItem(person = it, selectedPerson = selectedPerson) | |
} | |
) | |
} | |
} | |
/** | |
* Demo 5: LazyColumn displaying a List<Person> with clickable items, utilizing a StickyHeader | |
* A RecyclerView equivalent with rich UI populating it's items from a dataset with a sticky header | |
* @persons: a List<Person> passed as dataset | |
* @selectedPerson: a lambda function to return back to the caller the number of the current item iteration on click | |
*/ | |
@OptIn(ExperimentalFoundationApi::class) | |
@Composable | |
fun LazyColumnClickableAdvStickyDemo(persons: List<Person>, selectedPerson: (Person)->Unit){ | |
val grouped = persons.groupBy{it.section} | |
LazyColumn(){ | |
grouped.forEach { (section, sectionPersons) -> | |
stickyHeader { | |
Text( | |
text = "SECTION: $section", | |
color = Color.White, | |
modifier = Modifier | |
.background(color = Color.Black) | |
.padding(8.dp) | |
.fillMaxWidth() | |
) | |
} | |
items( | |
items = sectionPersons, | |
itemContent = { | |
ListItem(person = it, selectedPerson = selectedPerson) | |
} | |
) | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment