Convert RecycleView to LazyColumn - Jetpack Compose
Tags: android, beginners, kotlin, jetpackcompose
Published: 2022-03-19 00:26:03

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

This beginner-friendly tutorial provides an example how to convert this [simple RecycleView app](https://vtsen.hashnode.dev/step-by-step-guides-to-implement-recycleview) 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](https://vtsen.hashnode.dev/view-binding-vs-data-binding-gotchas#heading-enable-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.
```kotlin
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. 
```kotlin
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
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="purple_700">#FF3700B3</color>
</resources>
```
**themes.xml**
```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)**
```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>
```

### Add Compose Theming
Create **ui.theme** package folder, puts the `Colors.kt`, `Shape.kt`, `Type.kt` into this folder.

**Colors.kt**
```kotlin
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
```
**Shape.kt**
```kotlin
val Shapes = Shapes(
    small = RoundedCornerShape(4.dp),
    medium = RoundedCornerShape(4.dp),
    large = RoundedCornerShape(0.dp)
)
```
**Type.kt**
```kotlin
val Typography = Typography(
    body1 = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    )
)
```
**Theme.kt**
```kotlin
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:

```kotlin
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:
```kotlin
@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:
```kotlin
@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](https://cdn.hashnode.com/res/hashnode/image/upload/v1642752256044/rnElVJ1R2.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:

- [Step-by-step Guides to Implement RecycleView](https://vtsen.hashnode.dev/step-by-step-guides-to-implement-recycleview)

### Create MainViewModel and pass into MainContent

Since the data is coming `MainViewModel`, you create it with `by viewModels` [delegated property](https://vtsen.hashnode.dev/complete-c-to-kotlin-syntax-comparisons#heading-by-lazy-delegated-properties) in the `MainActivity` pass it as parameter to the `MainContent()` composable function.

**MainActivity.kt**
```kotlin
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](https://vtsen.hashnode.dev/recommended-ways-to-create-viewmodel-or-androidviewmodel).

**MainScreen.kt**

```kotlin
@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 of`ListContent()` composable function.

```kotlin
@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](https://vtsen.hashnode.dev/understand-kotlin-function-literal-with-receiver-by-example). 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:
```kotlin
@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.

```kotlin
@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](https://cdn.hashnode.com/res/hashnode/image/upload/v1642901222106/8pVVV-pZb.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
- Conversion diff here: [master vs compose branch](https://github.com/vinchamp77/Demo_SimpleRecycleView/compare/master...compose)
- GitHub Repository: [Demo_SimpleRecycleView (compose branch)](https://github.com/vinchamp77/Demo_SimpleRecycleView/tree/compose)

---

*Originally published at [https://vtsen.hashnode.dev](https://vtsen.hashnode.dev/convert-recycleview-to-lazycolumn-jetpack-compose)*.