Membuat Aplikasi Woof
Pada kesempatan kali ini kita akan mencoba untuk membuat aplikasi woof, yakni aplikasi yang dapat menampilkan daftar anjing berisi gambar dan informasi lainnya. Ilustrasi dari hasil aplikasi yang akan dibuat seperti berikut. Yuk kita mulai mengikuti tutorial-nya.
Pada halaman tutorial kita diberikan starter project dimana berisi resource-resource yang diperlukan, seperti gambar dan data informasi. Anda dapat mendownload pada github ini pada branch starter. dan kita akan mulai memodifikasi starter projek.
Menambahkan warna
Dalam menyusun warna yang baik, kita dapat menggunakan bantuan website ini. Pada starter projek kita sudah tersedia file Color.kt pada folder ui.theme. Ubah sumber kode menjadi berikut:
This file contains 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 com.example.woof.ui.theme | |
import androidx.compose.ui.graphics.Color | |
val md_theme_light_primary = Color(0xFF006C4C) | |
val md_theme_light_onPrimary = Color(0xFFFFFFFF) | |
val md_theme_light_primaryContainer = Color(0xFF89F8C7) | |
val md_theme_light_onPrimaryContainer = Color(0xFF002114) | |
val md_theme_light_secondary = Color(0xFF4D6357) | |
val md_theme_light_onSecondary = Color(0xFFFFFFFF) | |
val md_theme_light_secondaryContainer = Color(0xFFCFE9D9) | |
val md_theme_light_onSecondaryContainer = Color(0xFF092016) | |
val md_theme_light_tertiary = Color(0xFF3D6373) | |
val md_theme_light_onTertiary = Color(0xFFFFFFFF) | |
val md_theme_light_tertiaryContainer = Color(0xFFC1E8FB) | |
val md_theme_light_onTertiaryContainer = Color(0xFF001F29) | |
val md_theme_light_error = Color(0xFFBA1A1A) | |
val md_theme_light_errorContainer = Color(0xFFFFDAD6) | |
val md_theme_light_onError = Color(0xFFFFFFFF) | |
val md_theme_light_onErrorContainer = Color(0xFF410002) | |
val md_theme_light_background = Color(0xFFFBFDF9) | |
val md_theme_light_onBackground = Color(0xFF191C1A) | |
val md_theme_light_surface = Color(0xFFFBFDF9) | |
val md_theme_light_onSurface = Color(0xFF191C1A) | |
val md_theme_light_surfaceVariant = Color(0xFFDBE5DD) | |
val md_theme_light_onSurfaceVariant = Color(0xFF404943) | |
val md_theme_light_outline = Color(0xFF707973) | |
val md_theme_light_inverseOnSurface = Color(0xFFEFF1ED) | |
val md_theme_light_inverseSurface = Color(0xFF2E312F) | |
val md_theme_light_inversePrimary = Color(0xFF6CDBAC) | |
val md_theme_light_shadow = Color(0xFF000000) | |
val md_theme_light_surfaceTint = Color(0xFF006C4C) | |
val md_theme_light_outlineVariant = Color(0xFFBFC9C2) | |
val md_theme_light_scrim = Color(0xFF000000) | |
val md_theme_dark_primary = Color(0xFF6CDBAC) | |
val md_theme_dark_onPrimary = Color(0xFF003826) | |
val md_theme_dark_primaryContainer = Color(0xFF005138) | |
val md_theme_dark_onPrimaryContainer = Color(0xFF89F8C7) | |
val md_theme_dark_secondary = Color(0xFFB3CCBE) | |
val md_theme_dark_onSecondary = Color(0xFF1F352A) | |
val md_theme_dark_secondaryContainer = Color(0xFF354B40) | |
val md_theme_dark_onSecondaryContainer = Color(0xFFCFE9D9) | |
val md_theme_dark_tertiary = Color(0xFFA5CCDF) | |
val md_theme_dark_onTertiary = Color(0xFF073543) | |
val md_theme_dark_tertiaryContainer = Color(0xFF244C5B) | |
val md_theme_dark_onTertiaryContainer = Color(0xFFC1E8FB) | |
val md_theme_dark_error = Color(0xFFFFB4AB) | |
val md_theme_dark_errorContainer = Color(0xFF93000A) | |
val md_theme_dark_onError = Color(0xFF690005) | |
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) | |
val md_theme_dark_background = Color(0xFF191C1A) | |
val md_theme_dark_onBackground = Color(0xFFE1E3DF) | |
val md_theme_dark_surface = Color(0xFF191C1A) | |
val md_theme_dark_onSurface = Color(0xFFE1E3DF) | |
val md_theme_dark_surfaceVariant = Color(0xFF404943) | |
val md_theme_dark_onSurfaceVariant = Color(0xFFBFC9C2) | |
val md_theme_dark_outline = Color(0xFF8A938C) | |
val md_theme_dark_inverseOnSurface = Color(0xFF191C1A) | |
val md_theme_dark_inverseSurface = Color(0xFFE1E3DF) | |
val md_theme_dark_inversePrimary = Color(0xFF006C4C) | |
val md_theme_dark_shadow = Color(0xFF000000) | |
val md_theme_dark_surfaceTint = Color(0xFF6CDBAC) | |
val md_theme_dark_outlineVariant = Color(0xFF404943) | |
val md_theme_dark_scrim = Color(0xFF000000) |
Perlu diingat, penamaan warna dimulai dengan nilai alpha, dimana 00 mengartikan opasitas minimum, yakni transparan total, sedangkan ff opasitas maksimum, yakni solid penuh. Pada file Color.kt ini lah kita menentukan warna untuk masing-masing tema gelap dan terang.
Pada file Theme.kt juga akan diubah menjadi berikut:
This file contains 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 com.example.woof.ui.theme | |
import android.app.Activity | |
import android.os.Build | |
import android.view.View | |
import androidx.compose.foundation.isSystemInDarkTheme | |
import androidx.compose.material3.MaterialTheme | |
import androidx.compose.material3.darkColorScheme | |
import androidx.compose.material3.dynamicDarkColorScheme | |
import androidx.compose.material3.dynamicLightColorScheme | |
import androidx.compose.material3.lightColorScheme | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.SideEffect | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.graphics.toArgb | |
import androidx.compose.ui.platform.LocalContext | |
import androidx.compose.ui.platform.LocalView | |
import androidx.core.view.WindowCompat | |
private val LightColors = lightColorScheme( | |
primary = md_theme_light_primary, | |
onPrimary = md_theme_light_onPrimary, | |
primaryContainer = md_theme_light_primaryContainer, | |
onPrimaryContainer = md_theme_light_onPrimaryContainer, | |
secondary = md_theme_light_secondary, | |
onSecondary = md_theme_light_onSecondary, | |
secondaryContainer = md_theme_light_secondaryContainer, | |
onSecondaryContainer = md_theme_light_onSecondaryContainer, | |
tertiary = md_theme_light_tertiary, | |
onTertiary = md_theme_light_onTertiary, | |
tertiaryContainer = md_theme_light_tertiaryContainer, | |
onTertiaryContainer = md_theme_light_onTertiaryContainer, | |
error = md_theme_light_error, | |
errorContainer = md_theme_light_errorContainer, | |
onError = md_theme_light_onError, | |
onErrorContainer = md_theme_light_onErrorContainer, | |
background = md_theme_light_background, | |
onBackground = md_theme_light_onBackground, | |
surface = md_theme_light_surface, | |
onSurface = md_theme_light_onSurface, | |
surfaceVariant = md_theme_light_surfaceVariant, | |
onSurfaceVariant = md_theme_light_onSurfaceVariant, | |
outline = md_theme_light_outline, | |
inverseOnSurface = md_theme_light_inverseOnSurface, | |
inverseSurface = md_theme_light_inverseSurface, | |
inversePrimary = md_theme_light_inversePrimary, | |
surfaceTint = md_theme_light_surfaceTint, | |
outlineVariant = md_theme_light_outlineVariant, | |
scrim = md_theme_light_scrim, | |
) | |
private val DarkColors = darkColorScheme( | |
primary = md_theme_dark_primary, | |
onPrimary = md_theme_dark_onPrimary, | |
primaryContainer = md_theme_dark_primaryContainer, | |
onPrimaryContainer = md_theme_dark_onPrimaryContainer, | |
secondary = md_theme_dark_secondary, | |
onSecondary = md_theme_dark_onSecondary, | |
secondaryContainer = md_theme_dark_secondaryContainer, | |
onSecondaryContainer = md_theme_dark_onSecondaryContainer, | |
tertiary = md_theme_dark_tertiary, | |
onTertiary = md_theme_dark_onTertiary, | |
tertiaryContainer = md_theme_dark_tertiaryContainer, | |
onTertiaryContainer = md_theme_dark_onTertiaryContainer, | |
error = md_theme_dark_error, | |
errorContainer = md_theme_dark_errorContainer, | |
onError = md_theme_dark_onError, | |
onErrorContainer = md_theme_dark_onErrorContainer, | |
background = md_theme_dark_background, | |
onBackground = md_theme_dark_onBackground, | |
surface = md_theme_dark_surface, | |
onSurface = md_theme_dark_onSurface, | |
surfaceVariant = md_theme_dark_surfaceVariant, | |
onSurfaceVariant = md_theme_dark_onSurfaceVariant, | |
outline = md_theme_dark_outline, | |
inverseOnSurface = md_theme_dark_inverseOnSurface, | |
inverseSurface = md_theme_dark_inverseSurface, | |
inversePrimary = md_theme_dark_inversePrimary, | |
surfaceTint = md_theme_dark_surfaceTint, | |
outlineVariant = md_theme_dark_outlineVariant, | |
scrim = md_theme_dark_scrim, | |
) | |
@Composable | |
fun WoofTheme( | |
darkTheme: Boolean = isSystemInDarkTheme(), | |
// Dynamic color is available on Android 12+ | |
dynamicColor: Boolean = false, | |
content: @Composable () -> Unit | |
) { | |
val colorScheme = when { | |
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { | |
val context = LocalContext.current | |
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) | |
} | |
darkTheme -> DarkColors | |
else -> LightColors | |
} | |
val view = LocalView.current | |
if (!view.isInEditMode) { | |
SideEffect { | |
setUpEdgeToEdge(view, darkTheme) | |
} | |
} | |
MaterialTheme( | |
colorScheme = colorScheme, | |
shapes = Shapes, | |
typography = Typography, | |
content = content | |
) | |
} | |
/** | |
* Sets up edge-to-edge for the window of this [view]. The system icon colors are set to either | |
* light or dark depending on whether the [darkTheme] is enabled or not. | |
*/ | |
private fun setUpEdgeToEdge(view: View, darkTheme: Boolean) { | |
val window = (view.context as Activity).window | |
WindowCompat.setDecorFitsSystemWindows(window, false) | |
window.statusBarColor = Color.Transparent.toArgb() | |
val navigationBarColor = when { | |
Build.VERSION.SDK_INT >= 29 -> Color.Transparent.toArgb() | |
Build.VERSION.SDK_INT >= 26 -> Color(0xFF, 0xFF, 0xFF, 0x63).toArgb() | |
// Min sdk version for this app is 24, this block is for SDK versions 24 and 25 | |
else -> Color(0x00, 0x00, 0x00, 0x50).toArgb() | |
} | |
window.navigationBarColor = navigationBarColor | |
val controller = WindowCompat.getInsetsController(window, view) | |
controller.isAppearanceLightStatusBars = !darkTheme | |
controller.isAppearanceLightNavigationBars = !darkTheme | |
} |
pada file ini kita mendefinisikan variable untuk menyimpan daftar warna untuk tema gelap dan terang. Selain itu, pada kode ini juga terdapat metode composable WoofTheme, dimana di sinilah penentuan penggunaan tema. variabel dynamicColor menentukan apakah aplikasi akan mengikuti setting tema gelap/terang dari android atau menggunakan setting yang kita custom sendiri. Apabila TRUE, maka akan menggunakan bawaan android. Pada fungsi MaterialTheme kita memanggil beberapa parameter yang telah kita definisikan colorScheme untuk warna, Shapes dari file Shapes.kt, Typography dari Type.kt. Untuk shape dan typography ada pada bagian berikutnya.
Menambahkan bentuk
Dilakukan perubahan pada file Shape.kt dan MainActivity,kt untuk merubah bentuk card anjing sehingga lebih enak dipandang, yakni dengan membuat tampilan sudut yang rounded. Pada Shape.kt sumber kode menjadi seperti ini:
This file contains 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
/* | |
* Copyright (C) 2023 The Android Open Source Project | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* https://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package com.example.woof.ui.theme | |
import androidx.compose.foundation.shape.RoundedCornerShape | |
import androidx.compose.material3.Shapes | |
import androidx.compose.ui.unit.dp | |
val Shapes = Shapes( | |
small = RoundedCornerShape(50.dp), | |
medium = RoundedCornerShape(bottomStart = 16.dp, topEnd = 16.dp) | |
) | |
Pada file ini kita mendefinisikan bentuk untuk tipe small dan medium. Variabel ini berikutnya dapat kita panggilpada DogIcon di MainActivity dengan cara berikut .clip(MaterialTheme.shapes.small).
Menambahkan tipografi
Kita perlu mendownload file font style pada google font dan mencari Montserrat dan Abril Fatface. Untuk montserrat kita menggunakan Bold dan Regular, ubah nama file menjadi montserrat_bold.ttf dan montserrat_regular.ttf. Sedangkan untuk Abril Faface menjadi abril_fatface_regular.ttf. Buatlah folder font pada folder res dengan cara berikut: klik kanan pada res, New, Anroid Resource Directory, lalu pilih
Ubah nama direktori menjadi font, dan tipe resource menjadi font. klik OK.
Pindahkan file .ttf ke direktori font yang baru saja dibuat. Lalu kita memodifikasi sumber kode Type.kt untuk menggunakan font yang kita inginkan.
This file contains 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
/* | |
* Copyright (C) 2023 The Android Open Source Project | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* https://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package com.example.woof.ui.theme | |
import androidx.compose.material3.Typography | |
import androidx.compose.ui.text.TextStyle | |
import androidx.compose.ui.unit.sp | |
import androidx.compose.ui.text.font.Font | |
import androidx.compose.ui.text.font.FontFamily | |
import com.example.woof.R | |
import androidx.compose.ui.text.font.FontWeight | |
val AbrilFatface = FontFamily( | |
Font(R.font.abril_fatface_regular) | |
) | |
val Montserrat = FontFamily( | |
Font(R.font.montserrat_regular), | |
Font(R.font.montserrat_bold, FontWeight.Bold) | |
) | |
val Typography = Typography( | |
displayLarge = TextStyle( | |
fontFamily = AbrilFatface, | |
fontWeight = FontWeight.Normal, | |
fontSize = 36.sp | |
), | |
displayMedium = TextStyle( | |
fontFamily = Montserrat, | |
fontWeight = FontWeight.Bold, | |
fontSize = 20.sp | |
), | |
labelSmall = TextStyle( | |
fontFamily = Montserrat, | |
fontWeight = FontWeight.Bold, | |
fontSize = 14.sp | |
), | |
bodyLarge = TextStyle( | |
fontFamily = Montserrat, | |
fontWeight = FontWeight.Normal, | |
fontSize = 14.sp | |
) | |
) |
Contoh penggunaan font pada kode Main seperti ini MaterialTheme.typography.bodyLarge.
Menambahkan panel atas / header
Lakukan modifikasi sumber kode MainActivity.kt menjadi berikut:
This file contains 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
/* | |
* Copyright (C) 2023 The Android Open Source Project | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* https://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package com.example.woof | |
import android.os.Bundle | |
import androidx.activity.ComponentActivity | |
import androidx.activity.compose.setContent | |
import androidx.annotation.DrawableRes | |
import androidx.annotation.StringRes | |
import androidx.compose.foundation.Image | |
import androidx.compose.foundation.layout.Column | |
import androidx.compose.foundation.layout.Row | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.layout.size | |
import androidx.compose.foundation.lazy.LazyColumn | |
import androidx.compose.foundation.lazy.items | |
import androidx.compose.material3.Card | |
import androidx.compose.material3.MaterialTheme | |
import androidx.compose.material3.Scaffold | |
import androidx.compose.material3.Surface | |
import androidx.compose.material3.Text | |
import androidx.compose.runtime.Composable | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.res.dimensionResource | |
import androidx.compose.ui.res.painterResource | |
import androidx.compose.ui.res.stringResource | |
import androidx.compose.ui.tooling.preview.Preview | |
import com.example.woof.data.Dog | |
import com.example.woof.data.dogs | |
import com.example.woof.ui.theme.WoofTheme | |
import androidx.compose.ui.draw.clip | |
import androidx.compose.ui.layout.ContentScale | |
import androidx.compose.material3.CenterAlignedTopAppBar | |
import androidx.compose.material3.ExperimentalMaterial3Api | |
import androidx.compose.ui.Alignment | |
class MainActivity : ComponentActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContent { | |
WoofTheme { | |
// A surface container using the 'background' color from the theme | |
Surface( | |
modifier = Modifier.fillMaxSize() | |
) { | |
WoofApp() | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Composable that displays an app bar and a list of dogs. | |
*/ | |
@Composable | |
fun WoofApp() { | |
Scaffold( | |
topBar = { | |
WoofTopAppBar() | |
} | |
) { it -> | |
LazyColumn(contentPadding = it) { | |
items(dogs) { | |
DogItem( | |
dog = it, | |
modifier = Modifier.padding(dimensionResource(R.dimen.padding_small)) | |
) | |
} | |
} | |
} | |
} | |
/** | |
* Composable that displays a list item containing a dog icon and their information. | |
* | |
* @param dog contains the data that populates the list item | |
* @param modifier modifiers to set to this composable | |
*/ | |
@Composable | |
fun DogItem( | |
dog: Dog, | |
modifier: Modifier = Modifier | |
) { | |
Card(modifier = modifier) { | |
Row( | |
modifier = Modifier | |
.fillMaxWidth() | |
.padding(dimensionResource(id = R.dimen.padding_small)) | |
) { | |
DogIcon(dog.imageResourceId) | |
DogInformation(dog.name, dog.age) | |
} | |
} | |
} | |
/** | |
* Composable that displays a photo of a dog. | |
* | |
* @param dogIcon is the resource ID for the image of the dog | |
* @param modifier modifiers to set to this composable | |
*/ | |
@Composable | |
fun DogIcon( | |
@DrawableRes dogIcon: Int, | |
modifier: Modifier = Modifier | |
) { | |
Image( | |
modifier = modifier | |
.size(dimensionResource(R.dimen.image_size)) | |
.padding(dimensionResource(R.dimen.padding_small)) | |
.clip(MaterialTheme.shapes.small), | |
contentScale = ContentScale.Crop, | |
painter = painterResource(dogIcon), | |
// Content Description is not needed here - image is decorative, and setting a null content | |
// description allows accessibility services to skip this element during navigation. | |
contentDescription = null | |
) | |
} | |
/** | |
* Composable that displays a dog's name and age. | |
* | |
* @param dogName is the resource ID for the string of the dog's name | |
* @param dogAge is the Int that represents the dog's age | |
* @param modifier modifiers to set to this composable | |
*/ | |
@Composable | |
fun DogInformation( | |
@StringRes dogName: Int, | |
dogAge: Int, | |
modifier: Modifier = Modifier | |
) { | |
Column(modifier = modifier) { | |
Text( | |
text = stringResource(dogName), | |
style = MaterialTheme.typography.displayMedium, | |
modifier = Modifier.padding(top = dimensionResource(id = R.dimen.padding_small)) | |
) | |
Text( | |
text = stringResource(R.string.years_old, dogAge), | |
style = MaterialTheme.typography.bodyLarge | |
) | |
} | |
} | |
/** | |
* Composable that displays what the UI of the app looks like in light theme in the design tab. | |
*/ | |
@Preview | |
@Composable | |
fun WoofPreview() { | |
WoofTheme(darkTheme = false) { | |
WoofApp() | |
} | |
} | |
@OptIn(ExperimentalMaterial3Api::class) | |
@Composable | |
fun WoofTopAppBar(modifier: Modifier = Modifier) { | |
CenterAlignedTopAppBar( | |
title = { | |
Row( | |
verticalAlignment = Alignment.CenterVertically | |
) { | |
Image( | |
modifier = Modifier | |
.size(dimensionResource(id = R.dimen.image_size)) | |
.padding(dimensionResource(id = R.dimen.padding_small)), | |
painter = painterResource(R.drawable.ic_woof_logo), | |
contentDescription = null | |
) | |
Text( | |
text = stringResource(R.string.app_name), | |
style = MaterialTheme.typography.displayLarge | |
) | |
} | |
}, | |
modifier = modifier | |
) | |
} |
Pada sumber kode kita membuat method WoofTopAppBar dimana merupakan header, komponen yang digunakan adalah CenterAlignedTopAppBar untuk membuat header align tengah, dengan parameter title berisi komponen header kita, yakni baris berisi gambar logo dan teks Woof. Terdapat perbedaan sumber kode dengan referensi, yakni pemanggilan OptIn sebelum pendefinisian Composable, seperti berikut:
This file contains 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
@OptIn(ExperimentalMaterial3Api::class) | |
@Composable | |
fun WoofTopAppBar(modifier: Modifier = Modifier) |
Hasil
Hasil dari aplikasi yang kita buat seperti ini:
Komentar
Posting Komentar