RecyclerView with MVVM ( Model View and ViewModel ) for Jetpack Compose
Berjumpa lagi dengan Saya Nanda Adisaputra, S.Kom. Pada kesempatan kali ini Saya akan sharing bagaimana cara membuat RecyclerView dengan design pattern MVVM menggunakan modern UI yaitu Jetpack Compose untuk pemula.
Jetpack Compose merupakan framework untuk mendesain UI dengan pendekatan declarative yang cepat, mudah, simpel dan kode yang dihasilkan akan lebih sedikit karena cukup perlu menggunakan bahasa pemprograman kotlin saja.
Kenapa perlu Jetpack Compose ?
- Lebih sedikit code
- Cukup hanya menggunakan bahasa pemprograman kotlin tanpa XML
- Deklaratif yang Intuitif
- Kecil dan Idependen
- Separation of Concern(SoC)
- Interoperability dengan XML
- Powerful API & Tool
Nah menarik bukan, yuk langsung saja Kita bahas.
- Pertama-tama Kita tambahkan library pada build.gradle (Module : App) seperti pada gambar dibawah ini.
dependencies {
------------------------
//viewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2"
//Coil
implementation "io.coil-kt:coil-compose:2.2.2"
//Hilt
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03"
kapt "androidx.hilt:hilt-compiler:1.1.0"
implementation 'androidx.hilt:hilt-navigation-compose:1.1.0'
//navigation
implementation "androidx.navigation:navigation-compose:2.7.5"
// Fix Duplicate class
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))
........................
}
Kemudian tambahkan plugin seperti dibawah ini.
plugins {
..................
id 'kotlin-kapt'
id 'kotlin-parcelize'
id 'dagger.hilt.android.plugin'
}
Selanjutnya modifikasi buildTypes menjadi seperti dibawah ini.
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
manifestPlaceholders = [usesCleartextTraffic: false, crashlyticsEnabled: true, performanceEnabled: true]
}
debug {
debuggable true
manifestPlaceholders = [usesCleartextTraffic: true, crashlyticsEnabled: false, performanceEnabled: false]
}
}
2. Selanjutnya pada bagian build.gradle ( Project : App ) tambahkan classpath dan ext version library hilt seperti dibawah ini.
buildscript {
ext {
..................
hilt_version = '2.39'
}
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
Seperti biasa ketika Kita melakukan perubahan atau penambahan code di gradle Kita perlu sinkronisasi agar terbaca oleh android studio dengan cara klik sync now pada pojok kanan atas.
3. Langkah selanjutnya Kita akan membuat package base dengan cara klik kanan pada package semarangtourism -> New -> Package-> beri nama package dengan nama base -> Klik Enter.
4. Kemudian didalam package base Kita dapat membuat class App dengan cara klik kanan pada package base -> New -> Kotlin Class / File -> Beri nama class dengan nama App -> Klik Enter.
5. Langkah selanjutnya Kita dapat tambahkan source code dibawah ini pada class App.kt.
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class App : Application()
6. Kemudian Kita perlu membuat package model dan package repository dengan cara klik kanan pada data -> New -> Package -> beri nama package -> Klik Enter.
7. Langkah berikutnya Kita tambahkan object SemarangTourismData.kt dan class SemarangTourismModel.kt seperti pada gambar dibawah ini.
8. Setelah berhasil menambahkan object SemarangTourismData.kt Kita dapat memasukkan asset data dummy seperti dibawah ini.
object SemarangTourismData {
val dummyTourism = listOf(
SemarangTourismModel(
id = 0,
name = "Lawang Sewu",
description = "Lawang Sewu adalah gedung bersejarah milik PT Kereta Api Indonesia (Persero) yang awalnya digunakan sebagai Kantor Pusat perusahaan kereta api swasta Nederlandsch-Indische Spoorweg Maatschappij (NISM). Gedung Lawang Sewu dibangun secara bertahap di atas lahan seluas 18.232 m2. Bangunan utama dimulai pada 27 Februari 1904 dan selesai pada Juli 1907. Sedangkan bangunan tambahan dibangun sekitar tahun 1916 dan selesai tahun 1918.",
location = "Jl. Pemuda No.160, Sekayu, Kec. Semarang Tengah, Kota Semarang, Jawa Tengah 50132",
photoUrl = "https://asset.kompas.com/crops/5fOXY3K4oKdIzs-F7qNcs0qtt68=/0x0:1430x953/750x500/data/photo/2022/01/17/61e57605c2256.jpg",
),
SemarangTourismModel(
id = 1,
name = "SAM POO KONG",
description = "Kelenteng Gedung Kuno Sam Poo Kong yaitu bekas tempat persinggahan dan pendaratan pertama seorang Laksamana Tiongkok beragama Islam yang bernama Zheng He/Cheng Ho, yang juga dikenal dengan nama Sam Poo.",
location = "Jl. Simongan No.129, Bongsari, Kec. Semarang Barat, Kota Semarang, Jawa Tengah 50148",
photoUrl = "https://lh3.googleusercontent.com/p/AF1QipOaiGz0_SRag95BfXuY-LweU3YBZC7ljn5SX11u=s1360-w1360-h1020",
),
SemarangTourismModel(
id = 2,
name = "Masjid Agung Jawa Tengah (MAJT)",
description = "Masjid Agung Jawa Tengah adalah masjid yang terletak di Semarang, provinsi Jawa Tengah, Indonesia. Masjid ini mulai dibangun sejak tahun 2001 hingga selesai secara keseluruhan pada tahun 2006. Masjid ini berdiri di atas lahan 10 hektare.",
location = "Jl. Gajah Raya, Sambirejo, Kec. Gayamsari, Kota Semarang, Jawa Tengah 50166",
photoUrl = "https://lh3.googleusercontent.com/p/AF1QipOOhlerBecAJhsBntCj_YhzWmRdgeCCRx6yV0HL=s1360-w1360-h1020",
),
SemarangTourismModel(
id = 3,
name = "Tugu Muda Semarang",
description = "Tugu Muda adalah sebuah monumen yang dibuat untuk mengenang jasa-jasa para pahlawan yang telah gugur dalam Pertempuran Lima Hari di Semarang. Monumen ini terletak di Jalan Nasional Rute 20 yang mengarah ke Solo.",
location = "Jl. Pandanaran, Mugassari, Kec. Semarang Sel., Kota Semarang, Jawa Tengah 50245",
photoUrl = "https://lh3.googleusercontent.com/p/AF1QipN3NL09R0sJCIj98MZSuI5A2rpG0Y9YB94C5gYi=s1360-w1360-h1020",
),
SemarangTourismModel(
id = 4,
name = "Candi Gedong Songo",
description = "Candi Gedong Songo adalah nama sebuah kompleks bangunan candi peninggalan budaya Hindu yang terletak di desa Candi, Kecamatan Bandungan, Kabupaten Semarang, Jawa Tengah, Indonesia tepatnya di lereng Gunung Ungaran. Di kompleks candi ini terdapat sembilan buah candi.",
location = "Jalan Ke Candi Gedong Songo, Candi, Krajan, Banyukuning, Bandungan, Kabupaten Semarang, Jawa Tengah 50614",
photoUrl = "https://ksmtour.com/media/images/articles14/candi-gedong-songo-jawa-tengah.jpg",
),
SemarangTourismModel(
id = 5,
name = "Air Terjun Curug Lawe Benowo Kalisidi",
description = "Curug Benowo terletak di Lereng Gunung Ungaran, Desa Kalisidi Kecamatan Ungaran Barat Kabupaten Semarang. Curug dalam Bahasa Jawa berarti Air Terjun.",
location = "RT.01/RW.06, Hutan, Kalisidi, Kec. Ungaran Bar., Kabupaten Semarang, Jawa Tengah 50519",
photoUrl = "https://asset.kompas.com/crops/6z1Bn48FAk0LmimhPKd4nO3f3nc=/0x0:1620x1080/750x500/data/photo/2021/04/08/606e3dcfdf7b3.jpg",
),
SemarangTourismModel(
id = 6,
name = "Obyek Wisata Goa Kreo",
description = "Gua Kreo adalah objek wisata yang terdapat di Kota Semarang. Gua Kreo merupakan Gua yang terbentuk oleh alam dan terletak di tengah-tengah Waduk Jatibarang, sebuah bendungan yang membendung Kali Kreo.",
location = "Jl. Raya Goa Kreo, Kandri, Kec. Gn. Pati, Kota Semarang, Jawa Tengah 50222",
photoUrl = "https://awal.id/data/uploads/2022/06/Kreo-2.jpg",
),
SemarangTourismModel(
id = 7,
name = "Mawar Camp Area",
description = "Mawar Camp merupakan salah satu destinasi wisata terbaik untuk kamu yang ingin bermalam dengan suasana pemandangan di pegunungan. Terletak tak jauh dari lokasi Gunung Ungaran, camping ground ini dijadikan alternatif kamu yang ingin mendaki tapi tidak kuat fisik.",
location = "Jalan Goa Jepang Km 5 Sidomukti, Bandungan, Semarang, Jawa Tengah",
photoUrl = "https://phinemo.com/wp-content/uploads/2017/12/Screenshot_2017-07-13-08-53-41.jpg",
),
SemarangTourismModel(
id = 8,
name = "Kampung Pelangi",
description = "Desa yang terkenal dengan inisiatif pelestarian & lebih dari 200 rumah bergambar pelangi.",
location = "Jl. DR. Sutomo No.89, Randusari, Kec. Semarang Sel., Kota Semarang, Jawa Tengah 50244",
photoUrl = "https://res.cloudinary.com/wegowordpress/image/upload/v1496128906/IMG_20170519_172231_tbva2u.jpg",
),
SemarangTourismModel(
id = 9,
name = "Museum Kereta Api Ambarawa",
description = "Museum Kereta Api Ambarawa adalah sebuah stasiun kereta api yang sudah dialihfungsikan menjadi sebuah museum serta merupakan museum perkeretaapian pertama di Indonesia. Museum ini memiliki koleksi kereta api yang pernah berjaya pada zamannya. Museum ini secara administratif berada di Desa Panjang, Ambarawa, Semarang.",
location = " Jl. Stasiun No.1, Panjang Kidul, Panjang, Kec. Ambarawa, Kabupaten Semarang, Jawa Tengah 50614",
photoUrl = "https://asset.kompas.com/crops/Sh_NJnewhlUzysjYuWZGgm7o6Kc=/63x0:1410x898/750x500/data/photo/2021/09/29/6153df663bba2.jpeg",
),
SemarangTourismModel(
id = 10,
name = "Saloka Theme Park",
description = "Taman hiburan yang fantastis dengan banyak wahana bertema, konsesi, acara & air mancur \"menari\".",
location = "Jl. Fatmawati No.154, Gumuksari, Lopait, Kec. Tuntang, Kabupaten Semarang, Jawa Tengah 50773",
photoUrl = "https://akcdn.detik.net.id/community/media/visual/2022/09/30/saloka-theme-park-semarang-1_169.jpeg?w=700&q=90",
),
)
}
9. Langkah selanjutnya Kita dapat memasukkan data class pada class SemarangTourismModel.kt seperti dibawah ini.
data class SemarangTourismModel(
val id: Int,
val description: String,
val location: String,
val name: String,
val photoUrl: String
)
10. Selanjutnya tambahkan object pada class Const dan pada string.xml seperti dibawah ini.
class Const {
object Cons {
...........
const val TAG ="lazy list"
const val EMPTY ="Data Kosong"
const val BACK ="back_button"
const val SCROLL ="scroll"
}
}
res -> values -> strings.xml
<resources>
............
............
<string name="placeholder_search">Search..</string>
<string name="empty_data">Data Kosong</string>
<string name="place_information">Informasi Tempat</string>
<string name="place_location">Lokasi</string>
<string name="back">Back</string>
</resources>
11. Kemudian tambahkan interface SemarangTourismRepository.kt dan class SemarangTourismRepositoryImpl.kt seperti pada gambar dibawah ini.
12. Selanjutnya masukkan source code pada interface SemarangTourismRepository.kt seperti dibawah ini.
import com.nandaadisaputra.semarangtourism.data.model.SemarangTourismModel
import kotlinx.coroutines.flow.Flow
interface SemarangTourismRepository {
fun getTourismPlaceById(id: Int): Flow<SemarangTourismModel>
fun getSemarangTourism(): Flow<List<SemarangTourismModel>>
fun searchSemarangTourism(query: String): Flow<List<SemarangTourismModel>>
}
13. Kemudian pada class SemarangTourismRepositoryImpl.kt Kita dapat masukkan code seperti dibawah ini.
import com.nandaadisaputra.semarangtourism.data.model.SemarangTourismData
import com.nandaadisaputra.semarangtourism.data.model.SemarangTourismModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SemarangTourismRepositoryImpl @Inject constructor(
) : SemarangTourismRepository {
private val dummyTourism = mutableListOf<SemarangTourismModel>()
init {
if (dummyTourism.isEmpty()) {
dummyTourism.addAll(SemarangTourismData.dummyTourism)
}
}
override fun searchSemarangTourism(query: String) = flow {
val data = dummyTourism.filter {
it.name.contains(query, ignoreCase = true)
}
emit(data)
}
override fun getSemarangTourism() = flow {
emit(dummyTourism)
}
override fun getTourismPlaceById(id: Int): Flow<SemarangTourismModel> {
return flowOf(dummyTourism.first { it.id == id })
}
}
14. Langkah selanjutnya Kita akan membuat package injection dengan cara klik kanan pada package semarangtourism -> New -> Package -> Beri nama package dengan nama injection -> Klik Enter.
15. Langkah berikutnya Kita akan membuat class DataModule.kt didalam package injection seperti pada gambar dibawah ini.
16. Setelah berhasil membuat class DataModule.kt , kemudian Kita isikan source code pada class tersebut seperti dibawah ini.
import com.nandaadisaputra.semarangtourism.data.repository.SemarangTourismRepository
import com.nandaadisaputra.semarangtourism.data.repository.SemarangTourismRepositoryImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
abstract class DataModule {
@Binds
@Singleton
abstract fun provideSemarangTourismRepository(semarangTourismRepositoryImpl: SemarangTourismRepositoryImpl) : SemarangTourismRepository
}
17. Selanjutnya buatlah package custom dan package utils didalam package ui seperti dibawah ini.
18. Kemudian didalam package utils , buatlah class UiState.kt seperti gambar dibawah ini.
19. Setelah class UiState.kt berhasil dibuat, tambahkan source code seperti dibawah ini.
sealed class UiState<out T: Any?> {
object Loading : UiState<Nothing>()
data class Error(val message: String) : UiState<Nothing>()
data class Success<out T: Any>(val data: T) : UiState<T>()
}
20. Tambahkan resource color dibawah ini didalam theme ->Color.kt
........................
val cyan = Color(0xFF008577)
val greenishCyan = Color(0xFF00574B)
val orange = Color(0xFFFFA143)
val orangeRed = Color(0xFFFF7142)
val backgroundColor = Color(0xFFFFF7D6)
21. Selanjutnya buatlah class Empty .kt, class ItemLayout.kt, dan class SearchView.kt pada package custom seperti dibawah ini.
22. Setelah class Empty.kt, ItemLayout.kt, dan SearchView.kt berhasil dibuat. Masukkan source seperti dibawah ini.
Empty .kt
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import com.nandaadisaputra.semarangtourism.data.constant.Const
import com.nandaadisaputra.semarangtourism.ui.theme.backgroundColor
import com.nandaadisaputra.semarangtourism.ui.theme.orangeRed
@Composable
fun Empty(
modifier: Modifier = Modifier,
contentText: String
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.background(backgroundColor)
.fillMaxSize()
) {
Text(
color = orangeRed,
text = contentText,
fontWeight = FontWeight.Bold,
fontSize = 24.sp,
textAlign = TextAlign.Center,
)
}
}
@Preview(showBackground = true)
@Composable
fun EmptyPreview() {
Empty(
contentText = Const.Cons.EMPTY,
)
}
ItemLayout .kt
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.nandaadisaputra.semarangtourism.ui.theme.SemarangTourismTheme
import com.nandaadisaputra.semarangtourism.ui.theme.orangeRed
@Composable
fun ItemLayout(
title: String,
photoUrl: String,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp))
) {
Column {
AsyncImage(
model = photoUrl,
contentDescription = title,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(4.dp))
.height(150.dp)
)
Spacer(modifier = Modifier.height(4.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
fontWeight = FontWeight.Bold,
color = orangeRed,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(
end = 8.dp,
start = 8.dp,
top = 4.dp,
bottom = 4.dp
)
.weight(1f)
)
}
}
}
}
@Preview(showBackground = true)
@Composable
fun ItemTourismPreview() {
SemarangTourismTheme {
ItemLayout(
photoUrl = "https://asset.kompas.com/crops/5fOXY3K4oKdIzs-F7qNcs0qtt68=/0x0:1430x953/750x500/data/photo/2022/01/17/61e57605c2256.jpg",
title = "Lawang Sewu"
)
}
}
SearchView.kt
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Search
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.nandaadisaputra.semarangtourism.R
import com.nandaadisaputra.semarangtourism.ui.theme.backgroundColor
@Composable
fun SearchView(
query: String,
onQueryChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
value = query,
onValueChange = onQueryChange,
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = null
)
},
singleLine = true,
shape = RoundedCornerShape(50),
colors = TextFieldDefaults.textFieldColors(
unfocusedIndicatorColor = Color.Transparent,
backgroundColor = Color.White,
disabledIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
),
placeholder = {
Text(stringResource(R.string.placeholder_search))
},
modifier = modifier
.shadow(65.dp)
.background(backgroundColor)
.heightIn(min = 50.dp)
.padding(
start = 16.dp,
end = 16.dp,
bottom = 8.dp,
top = 8.dp
)
.fillMaxWidth()
)
}
@Preview(showBackground = true)
@Composable
fun SearchViewPreview() {
SearchView(
query = "",
onQueryChange = {}
)
}
23. Langkah Selanjutnya buatlah class HomeViewModel.kt didalam package home.
24. Setelah class HomeViewModel.kt berhasil terbuat. Langkah selanjutnya dapat Kita tambahkan source code dibawah ini pada class HomeViewModel.kt.
@HiltViewModel
class HomeViewModel @Inject constructor(
private val semarangTourismRepository: SemarangTourismRepository
) : ViewModel() {
private val _uiState: MutableStateFlow<UiState<List<SemarangTourismModel>>> = MutableStateFlow(UiState.Loading)
val uiState get() = _uiState.asStateFlow()
private val _query = mutableStateOf("")
val query: State<String> get() = _query
fun search(newQuery: String) = viewModelScope.launch {
_query.value = newQuery
semarangTourismRepository.searchSemarangTourism(_query.value)
.catch {
_uiState.value = UiState.Error(it.message.toString())
}
.collect {
_uiState.value = UiState.Success(it)
}
}
}
25. Selanjutnya Kita akan memodifikasi HomeScreen.kt. Dengan mengubah source nya menjadi seperti dibawah ini.
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.nandaadisaputra.semarangtourism.R
import com.nandaadisaputra.semarangtourism.data.constant.Const
import com.nandaadisaputra.semarangtourism.data.model.SemarangTourismModel
import com.nandaadisaputra.semarangtourism.ui.custom.Empty
import com.nandaadisaputra.semarangtourism.ui.custom.ItemLayout
import com.nandaadisaputra.semarangtourism.ui.custom.SearchView
import com.nandaadisaputra.semarangtourism.ui.theme.SemarangTourismTheme
import com.nandaadisaputra.semarangtourism.ui.theme.backgroundColor
import com.nandaadisaputra.semarangtourism.ui.utils.UiState
@Composable
fun HomeScreen(
navigateToDetail: (Int) -> Unit,
viewModel: HomeViewModel = hiltViewModel()
) {
val query by viewModel.query
viewModel.uiState.collectAsState(initial = UiState.Loading).value.let { uiState ->
when (uiState) {
is UiState.Loading -> {
viewModel.search(query)
}
is UiState.Success -> {
Home(
listTourism = uiState.data,
query = query,
onQueryChange = viewModel::search,
navigateToDetail = navigateToDetail
)
}
is UiState.Error -> {
}
}
}
}
@Composable
fun Home(
query: String,
onQueryChange: (String) -> Unit,
listTourism: List<SemarangTourismModel>,
navigateToDetail: (Int) -> Unit
) {
Column {
SearchView(
query = query,
onQueryChange = onQueryChange
)
if (listTourism.isNotEmpty()) {
ListTourism(
listTourism = listTourism,
navigateToDetail = navigateToDetail
)
} else {
Empty(
contentText = stringResource(R.string.empty_data),
modifier = Modifier
.testTag(Const.Cons.EMPTY)
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ListTourism(
listTourism: List<SemarangTourismModel>,
navigateToDetail: (Int) -> Unit,
modifier: Modifier = Modifier,
contentPaddingTop: Dp = 0.dp,
) {
LazyColumn(
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 16.dp, top = contentPaddingTop),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier
.background(backgroundColor)
.testTag(Const.Cons.TAG)
) {
items(listTourism, key = { it.id }) { item ->
ItemLayout(
photoUrl = item.photoUrl,
title = item.name,
modifier = Modifier
.background(backgroundColor)
.animateItemPlacement(tween(durationMillis = 200))
.clickable { navigateToDetail(item.id) }
)
}
}
}
@Preview(showBackground = true)
@Composable
fun HomeContentPreview() {
SemarangTourismTheme{
Home(
query = "",
listTourism = emptyList(),
onQueryChange = {},
navigateToDetail = {}
)
}
}
26. Langkah selajutnya Kita juga akan memodifikasi bagian MainActivity.kt dengan source code seperti dibawah ini.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.SnackbarDefaults.backgroundColor
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import com.nandaadisaputra.semarangtourism.SemarangTourismApp
import com.nandaadisaputra.semarangtourism.ui.theme.SemarangTourismTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SemarangTourismTheme {
Surface(
modifier = Modifier
.fillMaxSize(),
color = backgroundColor
) {
SemarangTourismApp()
}
}
}
}
}
27. Selanjutnya Kita akan modifikasi bagian SemarangTourismApp.kt bagian Screen.Home.route seperti dibawah ini.
composable(Screen.Home.route) {
HomeScreen(
navigateToDetail = { tourismId ->
navController.navigate(
Screen.Detail.createRoute(
tourismId
)
)
}
)
}
28. Langkah selanjutnya Kita akan beri permission internet di bagian AndroidManifest.xml seperti dibawah ini.
<uses-permission android:name="android.permission.INTERNET"/>
29. Karena Kita menggunakan DI Hilt maka dibagian AndroidManifest perlu Kita tambahkan android:name=”.base.App” seperti dibawah ini.
Jangan lupa tambahkan juga <meta-data seperti code dibawah ini pada AndroidManifest.xml
<meta-data
android:name="android.app.lib_name"
android:value="" />
30. Langkah selanjutnya buatlah package detail. Kemudian dipackage detail buatlah class DetailScreen.kt dan DetailViewModel.kt seperti dibawah ini.
31. Selanjutnya tambahkan source code dibawah ini pada class DetailViewModel.kt.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nandaadisaputra.semarangtourism.data.model.SemarangTourismModel
import com.nandaadisaputra.semarangtourism.data.repository.SemarangTourismRepository
import com.nandaadisaputra.semarangtourism.ui.utils.UiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class DetailViewModel @Inject constructor(
private val semarangTourismRepository: SemarangTourismRepository
) : ViewModel() {
private val _uiState: MutableStateFlow<UiState<SemarangTourismModel>> = MutableStateFlow(UiState.Loading)
val uiState = _uiState.asStateFlow()
fun getTourismPlaceById(id: Int) = viewModelScope.launch {
semarangTourismRepository .getTourismPlaceById(id)
.catch {
_uiState.value = UiState.Error(it.message.toString())
}
.collect {
_uiState.value = UiState.Success(it)
}
}
}
32. Selanjutnya tambahkan gambar dibawah ini kedalam drawable
33. Selanjutnya modifikasi bagian theme -> Type.kt dengan source code dibawah ini.
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import com.nandaadisaputra.semarangtourism.R
@Immutable
data class SemarangTourismTypography(
val caption: TextStyle,
val body: TextStyle,
val title: TextStyle
)
val LocalSemarangTourismTypography = staticCompositionLocalOf {
SemarangTourismTypography(
caption = TextStyle.Default,
body = TextStyle.Default,
title = TextStyle.Default
)
}
val fontFamily = FontFamily(
Font(R.font.regular, FontWeight.Normal),
Font(R.font.medium, FontWeight.Medium),
Font(R.font.bold, FontWeight.Bold)
)
34. Selanjutnya modifikasi juga dibagian theme-> Theme.kt dengan source code dibawah ini.
import androidx.compose.material.MaterialTheme
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
private val colorPalette = lightColors(
primary = cyan,
primaryVariant = greenishCyan,
)
@Composable
fun SemarangTourismTheme(
content: @Composable () -> Unit
) {
val semarangTourismTypography = SemarangTourismTypography(
body = TextStyle(
fontFamily = fontFamily ,
fontWeight = FontWeight.Medium
),
title = TextStyle(
fontFamily = fontFamily ,
fontWeight = FontWeight.Bold
),
caption = TextStyle(
fontFamily = fontFamily ,
fontWeight = FontWeight.Normal
)
)
CompositionLocalProvider(
LocalSemarangTourismTypography provides semarangTourismTypography
) {
MaterialTheme(
colors = colorPalette,
content = content
)
}
}
object SemarangTourismTheme {
val typography: SemarangTourismTypography
@Composable
get() = LocalSemarangTourismTypography.current
}
35.Langkah berikutnya Kita tambahkan source code dibawah ini pada class DetailScreen.kt.
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import coil.compose.AsyncImage
import com.nandaadisaputra.semarangtourism.R
import com.nandaadisaputra.semarangtourism.data.constant.Const
import com.nandaadisaputra.semarangtourism.ui.theme.SemarangTourismTheme
import com.nandaadisaputra.semarangtourism.ui.theme.backgroundColor
import com.nandaadisaputra.semarangtourism.ui.theme.orangeRed
import com.nandaadisaputra.semarangtourism.ui.utils.UiState
@Composable
fun DetailScreen(
tourismId: Int,
navigateBack: () -> Unit,
viewModel: DetailViewModel = hiltViewModel()
) {
viewModel.uiState.collectAsState(initial = UiState.Loading).value.let { uiState ->
when (uiState) {
is UiState.Loading -> {
viewModel.getTourismPlaceById(tourismId)
}
is UiState.Success -> {
val data = uiState.data
Detail(
placeName = data.name,
description = data.description,
photoUrl = data.photoUrl,
location = data.location,
navigateBack = navigateBack,
title =stringResource(id =R.string.place_information),
titleLocation = stringResource(id =R.string.place_location)
)
}
is UiState.Error -> {
}
}
}
}
@Composable
fun Detail(
title: String,
titleLocation: String,
placeName: String,
description: String,
photoUrl: String,
location: String,
navigateBack: () -> Unit,
) {
Box(
modifier = Modifier
.background(backgroundColor)
.fillMaxSize()
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(bottom = 16.dp)
) {
AsyncImage(
model = photoUrl,
contentDescription = placeName,
placeholder = painterResource(id = R.drawable.empty),
error = painterResource(id = R.drawable.empty),
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(450.dp)
.padding(
start = 16.dp,
end = 16.dp,
top = 8.dp,
bottom = 8.dp
)
.testTag(Const.Cons.SCROLL)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = placeName,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
color = orange,
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Start)
.padding(
start = 16.dp,
top = 8.dp,
bottom = 8.dp,
end = 16.dp
)
)
Spacer(modifier = Modifier.height(8.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth()
) {
Text(
text = titleLocation ,
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(
start = 16.dp,
top = 2.dp,
end = 16.dp,
bottom = 2.dp
),
style = SemarangTourismTheme.typography.title,
fontSize = 16.sp
)
Spacer(
modifier = Modifier
.padding(
top = 16.dp
)
)
Text(
text = location,
overflow = TextOverflow.Visible,
color = orangeRed,
modifier = Modifier
.padding(
start = 4.dp,
top = 2.dp,
end = 4.dp,
bottom = 2.dp
)
)
}
Divider(modifier = Modifier.padding(horizontal = 24.dp, vertical = 24.dp))
Text(
text =title ,
modifier = Modifier
.align(Alignment.Start)
.padding(
start = 16.dp,
top = 2.dp,
end = 16.dp,
bottom = 2.dp
),
style = SemarangTourismTheme.typography.title,
fontSize = 16.sp
)
Spacer(
modifier = Modifier
.padding(
top = 16.dp
)
)
Text(
text = description,
fontSize = 16.sp,
lineHeight = 40.sp,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(
start = 16.dp,
top = 2.dp,
end = 16.dp,
bottom = 2.dp
)
.fillMaxWidth()
)
}
IconButton(
onClick = navigateBack,
modifier = Modifier
.padding(start = 16.dp, top = 16.dp)
.align(Alignment.TopStart)
.clip(CircleShape)
.size(40.dp)
.testTag(Const.Cons.BACK)
.background(orange)
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.back),
)
}
}
}
@Preview(showBackground = true, device = Devices.PIXEL_4)
@Composable
fun DetailContentPreview() {
SemarangTourismTheme {
Detail(
title ="Informasi Tempat",
titleLocation ="Lokasi",
navigateBack = {},
placeName = "SAM POO KONG",
description = "Kelenteng Gedung Kuno Sam Poo Kong yaitu bekas tempat persinggahan dan pendaratan pertama seorang Laksamana Tiongkok beragama Islam yang bernama Zheng He/Cheng Ho, yang juga dikenal dengan nama Sam Poo.",
photoUrl = "https://lh3.googleusercontent.com/p/AF1QipOaiGz0_SRag95BfXuY-LweU3YBZC7ljn5SX11u=s1360-w1360-h1020",
location = "Jl. Simongan No.129, Bongsari, Kec. Semarang Barat, Kota Semarang, Jawa Tengah 5014",
)
}
}
36. Pada langkah selanjutnya Kita modifikasi lagi di bagian SemarangTourismApp.kt dengan menambahkan composable untuk route detail screen seperti pada gambar dibawah ini.
composable(
route = Screen.Detail.route,
arguments = listOf(
navArgument("id") { type = NavType.IntType }
)
) {
val id = it.arguments?.getInt("id") ?: -1
DetailScreen(
tourismId = id,
navigateBack = {
navController.navigateUp()
}
)
}
Selanjutnya Kita running aplikasi yang telah Kita buat.
Kalau menemui error dibawah ini, silahkan atur versi dibagian gradle nya.
Untuk versi versinya seperti pada source code di bawah ini.
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'kotlin-parcelize'
id 'dagger.hilt.android.plugin'
}
android {
namespace 'com.nandaadisaputra.semarangtourism'
compileSdk 34
defaultConfig {
applicationId "com.nandaadisaputra.semarangtourism"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
manifestPlaceholders = [usesCleartextTraffic: false, crashlyticsEnabled: true, performanceEnabled: true]
}
debug {
debuggable true
manifestPlaceholders = [usesCleartextTraffic: true, crashlyticsEnabled: false, performanceEnabled: false]
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.1.1'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.activity:activity-compose:1.6.1'
implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
implementation 'androidx.compose.material:material:1.3.1'
implementation 'androidx.navigation:navigation-runtime-ktx:2.5.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
//navigation
implementation "androidx.navigation:navigation-compose:2.5.3"
//Coil
implementation "io.coil-kt:coil-compose:2.2.2"
//viewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
//Hilt
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03"
kapt "androidx.hilt:hilt-compiler:1.0.0"
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
// system ui controller
implementation "com.google.accompanist:accompanist-systemuicontroller:0.23.1"
// Android Studio Preview support
implementation 'androidx.compose.ui:ui-tooling-preview'
debugImplementation 'androidx.compose.ui:ui-tooling'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
}
kapt {
correctErrorTypes true
}
buildscript {
ext {
compose_ui_version = '1.1.0'
hilt_version = '2.39'
}
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.2.2' apply false
id 'com.android.library' version '7.2.2' apply false
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
}
Nah akhirnya, Kita telah berhasil membuat RecyclerView with design pattern MVVM menggunakan Jetpack Compose.
Source code lengkapnya dapat dilihat dilink github Saya :
https://github.com/NandaAdisaputra/DevFest2023/tree/Part02_MVVM
Apabila ada yang kurang paham terkait tutorial diatas , silahkan hubungi Saya melalui DM Instagram dengan akun nanda_coding_android
Selamat mencoba dan semoga sukses selalu….