Convert RecycleView to LazyColumn - Jetpack Compose

Step-by-step tutorial to convert Android RecycleView(view-based UI approach) to LazyColumn (Jetpack Compose approach)

ยท

6 min read

Convert RecycleView to LazyColumn - Jetpack Compose

This beginner-friendly tutorial provides an example how to convert this simple RecycleView app to Jetpack Compose.

I also take some extra steps to clean up unused code or xml after migrating to Jetpack Compose.

1. Remove RecycleView, Layout, Fragment and Library Files

Other than RecycleView, you can also remove the fragment and layout files, since Jetpack Compose doesn't need them.

Remove unwanted source codes

  • MainFragment.kt
  • RecyceViewAdapter.kt
  • ItemViewHolder.kt
  • ItemDiffCallback.kt

Remove unwanted layout files

  • main_activity.xml
  • main_fragment.xml
  • item.xml

Remove unwanted build features and libraries

In app\build.gradle, remove data binding since this is no longer applicableto Jetpack Compose.

buildFeatures {
    dataBinding true
}

Remove these dependencies as well.

dependencies {
    implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
    implementation 'androidx.fragment:fragment-ktx:1.4.0'
}

Fix compilation issue in MainActivity.kt

Remove this code in MainActivity::onCreate() since you no longer need fragment.

setContentView(R.layout.main_activity)
if (savedInstanceState == null) {
    supportFragmentManager.beginTransaction()
        .replace(R.id.container, MainFragment.newInstance())
        .commitNow()
}

You should be able to build successfully now.

2 Setup Jetpack Compose Libraries

Update build.gradle (project level)

Add compose_version extension inside the buildScript{ } so that the compose version can be referenced later.

buildscript {
    ext {
        compose_version = '1.0.5'
    }
    ...
}

Update app\build.gradle(app level)

Add compose build features and kotlinCompilerExtensionVersion compose options.

android {
    ....
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion compose_version
    }
    ....
}

Replace implementation 'androidx.appcompat:appcompat:1.4.0' with implementation 'androidx.activity:activity-compose:1.4.0' and add the following Jetpack Compose dependencies.

dependencies {
    ...
    implementation 'androidx.activity:activity-compose:1.4.0'
    ...
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
    ...
}

Update MainActivity for compose

In Jetpack Compose, you don't need AppCompatActivity anymore, you can just directly inherit from ComponentActivity

Modify MainActivity to directly inherit from ComponentActivity, overrides onCreate() and call SetContent{} which allow any @composable functions can be called inside.

class MainActivity : ComponentActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
        setContent {
            // Implement composable function here.  
        }  
    }
}

3. Add Theming in Jetpack Compose

Before you add theming in Jetpack Compose, let's clean up the colors.xml and themes.xml.

You only require the themes.xml to provide the color for android:statusBarColor. So you keep it and removing anything else.

Clean up colors.xml and themes.xml

These should be the minimum code required to customize the status bar color.

colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="purple_700">#FF3700B3</color>
</resources>

themes.xml

<resources xmlns:tools="http://schemas.android.com/tools">
    <style name="Theme.RecycleViewDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
    </style>
</resources>

themes.xml (night)

<resources xmlns:tools="http://schemas.android.com/tools">
    <style name="Theme.RecycleViewDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
    </style>
</resources>

Add Compose Theming

Create ui.theme package folder, puts the Colors.kt, Shape.kt, Type.kt into this folder.

Colors.kt

val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)

Shape.kt

val Shapes = Shapes(
    small = RoundedCornerShape(4.dp),
    medium = RoundedCornerShape(4.dp),
    large = RoundedCornerShape(0.dp)
)

Type.kt

val Typography = Typography(
    body1 = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    )
)

Theme.kt

private val DarkColorPalette = darkColors(
    primary = Purple200,
    primaryVariant = Purple700,
    secondary = Teal200
)

private val LightColorPalette = lightColors(
    primary = Purple500,
    primaryVariant = Purple700,
    secondary = Teal200
)

@Composable
fun RecycleViewDemoTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable() () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}

These files allow you to customize your theme for Jetpack Compose.

To theme your app, call the MainContent() composable function from RecycleViewDemoTheme. The code looks like this:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MainScreen()
        }
    }
}

@Composable
fun MainScreen() {
    RecycleViewDemoTheme {
        MainContent()
    }
}

@Composable
fun MainContent() {
    //Todo: Implement LazyColumn
}

4. Add Top App Bar

Since you have removed AppCompatActivity, the top app bar is not created anymore. You need to create it using Jetpack Compose.

Add Scaffold() composable function

To create top app bar, you use ScaffoldI() composable function. The code looks like this:

@Composable
fun MainScreen() {
    RecycleViewDemoTheme {
        Scaffold(
            topBar = { TopAppBar (title = {Text(stringResource(R.string.app_name))})
            }
        ) {
            MainContent()
        }
    }
}

Preview a composable function

In order to preview a composable function, you add the following code:

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MainScreen()
}

After you compile, you should see something like this at your right. If you run your app, you should see the same UI as in preview.

Convert_RecycleView_to_LazyColumn_02.png

Now the app is fully implemented with Jetpack Compose code. At this point, the UI is exactly same as the view-based UI approach without the recycle view content.

5. Implement LazyColumn Composable Function

The equivalent RecycleView in Jetpack compose is LazyColumn composable function.

Strictly speaking, they're not the same. LazyColumn does not really recycle the item UI. It just recreates the entire item UI. So in theory, RecycleView performance should be better than the LazyColumn.

The good thing about LazyColumn it uses less code since RecycleView has a lot of boilerplate code. See how many steps are required to implement RecyceView here:

Create MainViewModel and pass into MainContent

Since the data is coming MainViewModel, you create it with by viewModels delegated property in the MainActivity pass it as parameter to the MainContent() composable function.

MainActivity.kt

class MainActivity : ComponentActivity() {  
    val viewModel by viewModels<MainViewModel>()  

    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
        setContent {  
            MainScreen(viewModel)  
        }  
    }  
}

by viewModels is used so that you don't recreate the MainViewModel instance when the MainActivity is destroyed and recreated. See explaination here.

MainScreen.kt

@Composable
fun MainScreen(viewModel: MainViewModel) {
    RecycleViewDemoTheme {
        Scaffold(
            topBar = { TopAppBar (title = {Text(stringResource(R.string.app_name))})
            }
        ) {
            MainContent(viewModel)
        }
    }
}

Convert the LiveData to State

In Jetpack Compose, you need to convert the LiveData<T> to State<T> so it can recompose correctly when the data is changed or updated. To convert it, you use observeAsState() LiveData function.

Before that, you need to add this library dependency:

implementation "androidx.compose.runtime:runtime-livedata:$compose_version"

After converting to State<T>, you past the value(i.e. List<ItemData>) as parameters ofListContent() composable function.

@Composable
fun MainContent(viewModel: MainViewModel) {
    val itemsState = viewModel.items.observeAsState()

    itemsState.value?.let { items ->
        ListContent(items)
    }
}

Implement LazyColumn

Since the RecycleView item original implementation fill up the entire screen width and center aligned, you need to do the same. This can be done through modifer and horizontalAlignment parameters of LazyColumn

In the last parameter of LazyColumn is Function Literal (Lambda Function) with Receiver. The LazyListScope is the receiver.

To add the items (i.e List<ItemData>), you call the LazyListSciope.items() composable function. To add the items content, you implement the ShowItem() composable function which just show the text.

To match the original RecycleView implementation, we set the font size to 34.sp and FontWeight.Bold.

The code looks like this:

@Composable
fun ListContent(items: List<ItemData>) {
    LazyColumn (
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        items(items = items) { item ->
            ShowItem(item)
        }
    }
}

@Composable
fun ShowItem(item: ItemData) {
    Text(
        text = item.id.toString(),
        fontSize = 34.sp,
        fontWeight = FontWeight.Bold
    )
}

Update Preview to include MainViewModel creation

Since the MainScreen() takes in MainViewModel as parameter, you need to create it and pass it in.

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    val viewModel = MainViewModel()
    MainScreen(viewModel)
}

6. Done

It is finally done!. The app looks like this, which is exactly the same with the RecycleView view-based UI approach.

Convert_RecycleView_to_LazyColumn_03.png

If you want, you can also refactor the code by moving out the MainContent() composable function to a seperate file which is a bit cleaner.

Reference

Did you find this article valuable?

Support Vincent Tsen by becoming a sponsor. Any amount is appreciated!

ย