# Jetpack Compose Crash Note

- `sp` (match the user font preference) **For Text**
- `dp` (match the user screen pixel 1dp=1px) **For Non-Text**
- `Color = Color.Blue` or using HEX code `color = Color(0xFF[hex-code])`
- To use the `by` keyword in compose to import:
```kotlin
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
```
- Manually ReCompose
```kotlin
var count by remember { mutableStateOf(0) }
key(count){
// recompose when the var count change
}
```
# InputStream? OutputStream? wtf
`InputStream` -> Read data
`OutputStram` -> Write data
<div>
<img src="https://hackmd.io/_uploads/SJEZHzxcgg.png" style="background:white;"/>
</div>
# Content Provider
The Content Provider functions as an intermediary, facilitating access to the private data or files of your application by other applications.

## Path provider
Share file with other apps
- Setup a provider
```xml
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/paths" />
</provider>
```
- Gave other apps permission to access app private external storage
```xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path name="external_dir" path="." />
</paths>
```
- Get uri to share with other apps
```kotlin
val cameraCaptureUri = FileProvider.getUriForFile(
context,
"${context.packageName}.provider",
File(context.getExternalFilesDir("image"), "image_${System.currentTimeMillis()}.png")
)
```
:::warning
Make sure the requested path is defined in the `FILE_PROVIDER_PATHS` resource, or else you'll get a security exception.
:::
# App shortcut
[app shortcut doc](https://developer.android.com/develop/ui/views/launch/shortcuts/creating-shortcuts)

:::info
The maximum action for a app is 5
:::
## Static shortcut
Can't be modify after build
- In android manifest add meta data inside `activity`
```xml
<meta-data android:name="android.app.shortcuts" android:resource="@xml/action" />
```
- add resource for setup action
## Dynamic shortcut
# Effect Handlers
## snapshotFlow
kind of like `LaunchEffect` but allow to use in non-composable function
```kotlin
snapshotFlow { searchQuery }
.filter { it.length > 2 } // Only emit if query is longer than 2 characters
.debounce(500) // Wait 500ms after last change
.collect { query ->
println("Search query: $query")
// Example: Trigger a network search
}
```
# Compose MainActivity base code
```kotlin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
/* composable function gose here... */
}
}
}
```
# Get DeviceConfig
- Get the screen width
```kotlin
val configuration = LocalConfiguration.current
val screenWidthDp = configuration.screenWidthDp
```
# Text To Speech (TTS)
Setup a speecher
```kotlin
var tts by remember { mutableStateOf<TextToSpeech?>(null) }
var ttsReady by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
tts = TextToSpeech(context) { status ->
if (status == TextToSpeech.SUCCESS) {
tts?.language = Locale.US
ttsReady = true
}
}
}
DisposableEffect(Unit) {
onDispose {
tts?.stop()
tts?.shutdown()
}
}
```
Speak the text
```kotlin
tts?.speak(text, TextToSpeech.QUEUE_FLUSH, null, null)
```
In the above example the second paramter decided the TTS engine behavior when there's a new text to speak, there are mainly two option.
| Usage | Description |
| -------------------------- | -------------------------------------------- |
| `TextToSpeech.QUEUE_FLUSH` | Speek immediately |
| `TextToSpeech.QUEUE_ADD` | Speek right after the current speech is done |
# Repeate Background
```kotlin
@Composable
fun RepeatedBackgroundExample() {
val context = LocalContext.current
val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.your_repeatable_image) // Replace with your image resource ID
val imageShader = ImageShader(bitmap.asImageBitmap(), TileMode.Repeated, TileMode.Repeated)
val shaderBrush = remember { ShaderBrush(imageShader) }
Box(
modifier = Modifier
.fillMaxSize()
.background(shaderBrush)
) {
// Your content goes here
}
}
```
# Glance Widget
:::info
- Dependencies
```kotlion
implementation("androidx.glance:glance-appwidget:1.1.0")
implementation("androidx.glance:glance-material3:1.1.0")
```
:::
- Create a Receiver
```kotlin
class MyReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = MyGlanceWidget()
}
```
- Create widget UI
```kotlin
class MyGlanceWidget : GlanceAppWidget() {
@SuppressLint("RestrictedApi")
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
/* composable function here... */
}
}
}
```
- Add appwidget provider for meta data \
create a `xml` file under `res/xml/` with
```xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_name"
android:minWidth="200dp"
android:minHeight="100dp"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="1000" />
```
- In `AndroidManifest.xml`
:::warning
HINT: `android.appwidget.*`
the widget is **appwidget**
:::
```xml
<receiver
android:name=".MyReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/music_widget_info" />
</receiver>
```
### Auto update widget when rebuild
:::success
> `BarWidget` is a `GlanceAppWidget()` in this example
#### Method 1: Via Application Class
In Application Class
Declare a scope to run suspend action
```kotlin
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
```
Run the update widget method to update the widget
```kotlin
appScope.launch {
BarWidget().updateAll(applicationContext)
}
```
:::warning
Remember to bind the application class in `AndroidManifest.xml`
and add the widget update method to `onCreate()` to update the widget when app launch
:::
:::success
#### Method 2: Via MainActivity
Under `MainAcvtivity`
```kotlin
suspend fun refreshWidget(context: Context) {
BarWidget().updateAll(context)
}
```
In Composable
```kotlin
CoroutineScope(Dispatchers.Main).launch {
refreshWidget(this@MainActivity)
}
```
:::
# Permission Check/Request

:::warning
**Require android studio version above `TIRAMISU`** `@RequiresApi(Build.VERSION_CODES.TIRAMISU)`
:::
- Check permission (Ex: Whether user allow APP to push notification, if so the `hasPermission` will be `true`)
```kotlin
val hasPermission = ContextCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
```
- Create a launcher to show the system dialog for requesting user permission
```kotlin
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) {
Toast.makeText(context, "A permission request has done", Toast.LENGTH_SHORT).show()
}
```
- If the user didn't accept the permission show the dialog (Image above)
```kotlin
if (!hasPermission) launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
```
# Notification
- Add permission
```xml
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
```
- Create a notification manager
```kotlin
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
```
- Create a notification channel
```kotlin
val channel =
NotificationChannel(
"channel_id",
"channel_name",
NotificationManager.IMPORTANCE_HIGH
)
notificationManager.createNotificationChannel(channel)
```
- Create a notification
```kotlin
val notification =
NotificationCompat.Builder(context, channelId)
.setContentTitle("hello")
.setContentText("hello")
.setSmallIcon(R.drawable.play)
.build()
```
- Push the notification
```kotlin
notificationManager.notify(<notifiacationId<Int>>, notification)
```
# Alarm Manager
- Here is a example of how to send a notification at a custom time via Alarm Manager
:::warning
Permission required
```xml
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
```
**Remember to register the receier in the manifest**
:::
:::info
In this example `android.permission.RECEIVE_BOOT_COMPLETED` is not necessary because we don't active the alarm when the device boot up
:::
- Create a brodcastReceiver for pushing notification
```kotlin
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
sendNotification(context)
}
private fun sendNotification(context: Context) {
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notificationId = (100..200).random()
val channelId = "hello_background"
val channel =
NotificationChannel(
channelId,
"hello_background",
NotificationManager.IMPORTANCE_HIGH
)
notificationManager.createNotificationChannel(channel)
val notification = NotificationCompat.Builder(context, channelId)
.setContentTitle("It's time to die")
.setContentText("A test message!!!")
.setSmallIcon(R.drawable.play)
.build()
notificationManager.notify(notificationId, notification)
}
}
```
- Create a alarm manager for calling from `MainActivity` and set the alarm via alarm manager
```kotlin
@SuppressLint("ScheduleExactAlarm")
fun setExactAlarm(context: Context, triggerTime: Long) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, AlarmReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent)
}
```
## Repeating Alarm
```kotlin
@RequiresApi(Build.VERSION_CODES.O)
class NotificationReceiver : BroadcastReceiver() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onReceive(context: Context, intent: Intent) {
sendNotification(context, intent)
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private fun sendNotification(context: Context, intent: Intent) {
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification =
NotificationCompat.Builder(context, alarm_channel).setContentTitle("Hello")
.setContentText("Hello World").setSmallIcon(R.drawable.ic_launcher_foreground)
.build()
manager.notify(1, notification)
setExactAlarm(context, intent.getLongExtra("time", AlarmManager.INTERVAL_DAY))
}
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@SuppressLint("ScheduleExactAlarm")
fun setExactAlarm(context: Context, triggerTime: Long, cancel: Boolean = false) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, NotificationReceiver::class.java).apply {
putExtra("time", triggerTime)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
if (!cancel) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
} else {
alarmManager.cancel(pendingIntent)
}
}
```
# Calendar
Calendar is a method for java to convert readable time into a calendar that can be convert to muti format
[Article about calendar & alarm](https://ithelp.ithome.com.tw/articles/10206960)
```kotlin
val calendar = Calendar.getInstance().apply {
timeInMillis = System.currentTimeMillis()
set(Calendar.HOUR_OF_DAY, 7)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
if (timeInMillis <= System.currentTimeMillis()) {
add(Calendar.DAY_OF_MONTH, 1)
}
}
```
# Time Picker
```kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Material3AlarmTimePicker() {
val timePickerState =
rememberTimePickerState(initialHour = 7, initialMinute = 0, is24Hour = true)
var showPicker by remember { mutableStateOf(false) }
Text("${timePickerState.hour}:${timePickerState.minute}")
FilledTonalButton(onClick = { showPicker = !showPicker }) {
Text("Set Alarm Time")
}
Text(text = "Alarm Time: ${timePickerState.hour}:${timePickerState.minute.toString().padStart(2, '0')}")
if (showPicker) {
Dialog(
onDismissRequest = { showPicker = false }
) {
Column(
Modifier
.clip(RoundedCornerShape(20.dp))
.background(Color.White)
.padding(10.dp)
) {
TimePicker(state = timePickerState)
FilledTonalButton(onClick = {
showPicker = false
}) {
Text("Confirm")
}
}
}
}
}
```
# API Fetching
## HTTP Connection via JSON Object
:::info
For 55th National
:::
### GET
```kotlin
data class Files(
val name: String,
val url: String
)
fun getFiles(): Flow<List<Files>> = flow {
val files: List<Files> = withContext(Dispatchers.IO) {
val url = URL("$host/api/files").openConnection() as HttpURLConnection
url.requestMethod = "GET"
val jsonText = url.inputStream.bufferedReader().use { it.readText() }
val jsonFilesObj = JSONObject(jsonText).getJSONArray("files")
return@withContext List(jsonFilesObj.length()) {
val file = jsonFilesObj.getJSONObject(it)
Files(
file.getString("name"),
file.getString("url")
)
}
}
emit(files)
}.catch {
Log.e("getFiles", it.toString())
emit(emptyList())
}
```
## Raw via GSON
- Data class
```kotlin
data class Music(
val cover: String,
val title: String,
val url: String
) {
fun title(): String = title.replace(".mp3", "")
}
```
- Fetch API
> `object : TypeToken<List<Music>>() {}.type` (provide by gson) is for non-generic type
```kotlin
var data by remember { mutableStateOf<List<Music>>(emptyList()) }
withContext(Dispatchers.IO) {
val jsonString =
URL("https://skills-music-api.eliaschen.dev/music").openConnection().let {
BufferedReader(InputStreamReader(it.getInputStream()))
}.use { it.readText() }
val gson = Gson()
val jsonData = object : TypeToken<List<Music>>() {}.type
data = gson.fromJson(jsonString, jsonData)
}
```
- Display the data
```kotlin
if(data.isNotEmpty()){
LazyColumn {
items(data) {
Text(it.title())
}
}
}
```
## Retrofit
:::info
- Dependencies
```kotlin
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
```
:::
- Example data
```kotlin
data class Music(
val title: String,
val url: String
)
```
- Create a interface for api action
```kotlin
interface Api {
@GET("/music")
suspend fun getMusic(): List<Music>
}
```
- Create a Retrofit Instance for doing request
```kotlin
object RetrofitInstance {
val api: Api by lazy {
Retrofit.Builder()
.baseUrl("https://skills-music-api.eliaschen.dev") // Example host
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(Api::class.java)
}
}
```
- Use a ViewModel to retrive data from composable function
```kotlin
class MainViewModel : ViewModel() {
private val _musicList = MutableStateFlow<List<Music>>(emptyList())
val musicList: StateFlow<List<Music>> get() = _musicList
// handle error
private val _failed = MutableStateFlow(false)
val failed: StateFlow<Boolean> get() = _failed
init {
fetchMusicList()
}
private fun fetchMusicList() {
viewModelScope.launch {
try {
val musicList = RetrofitInstance.api.getMusic()
_musicList.value = musicList
} catch (e: Exception) {
_failed.value = true
}
}
}
}
```
- Retrive data in composable function
```kotlin
@Composable
fun MusicListScreen(viewModel: MainViewModel = viewModel()) {
val musicList by viewModel.musicList.collectAsState()
val failed by viewModel.failed.collectAsState()
if (!failed) {
LazyColumn {
items(musicList) { music ->
Column {
Text(text = music.title)
Text(text = music.url)
}
}
}
} else {
Text("Failed Request!!!")
}
}
```
## OkHttp
:::info
- Dependencies
```kotlin
implementation("com.squareup.okhttp3:okhttp:4.9.3")
```
:::
- Create a client
```kotlin
val client = OkHttpClient()
```
- use the **`withContext(Dispatchers.IO)`** to perform the request in background
- create a request
```kotlin
val request = Request.Builder().url("<url>").build()
```
- get the response
```kotlin
val response = client.newCall(request).execute()
val body = call.body?.string()?: return@withContext emptyList<MusicList>()
```
- prase to json
```kotlin
val gson = Gson()
val listType = object : TypeToken<List<MusicList>>() {}.type
gson.fromJson(res, listType)
```
### Example (simple)
```kotlin
var data by remember { mutableStateOf<List<Music>>(emptyList()) }
withContext(Dispatchers.IO) {
val client = OkHttpClient()
val request = Request.Builder()
.url("https://skills-music-api.eliaschen.dev/music")
.build()
data = client.newCall(request).execute().use { response ->
Gson().fromJson(response.body?.string(), object : TypeToken<List<Music>>() {}.type)
}
}
```
### Example (handle exception)
```kotlin
data class MusicList(
val title: String,
val url: String
)
val gson = Gson()
val client = OkHttpClient()
suspend fun fetchUrl(): List<MusicList> {
return withContext(Dispatchers.IO) {
try {
val request = Request.Builder().url("$host/music").build()
val call = client.newCall(request).execute()
if (call.isSuccessful) {
val res = call.body?.string() ?: return@withContext emptyList<MusicList>()
val listType = object : TypeToken<List<MusicList>>() {}.type
gson.fromJson(res, listType)
} else {
emptyList<MusicList>()
}
} catch (e: Exception) {
emptyList<MusicList>()
}
}
}
```
# Audio
How to do with mediaPlayBack Service Check out the prepare note
[Skills 55th 08 regional Android](/Vur5L4tbTI6DYibWz7wyUA)
## ExoPlayer
:::info
- Dependencies
```kotlin
implementation("androidx.media3:media3-exoplayer:1.5.1")
implementation("androidx.media3:media3-common:1.5.1")
```
:::
- Create a player
```kotlin
val player = remember { ExoPlayer.Builder(context).build() }
```
- Add media source with URI and preare for playing
```kotlin
player.setMediaItem(MediaItem.fromUri("<uri>"))
player.prepare()
```
- player action
- `play()` play the media
- `pause()` pause the media
- `stop()` end the media
- `release()` unload player
- Ex: `release()` when UI unmount
```kotlin
DisposableEffect(Unit) {
onDispose {
ExoPlayer.release()
}
}
```
### PlayList
the playlist in exoplayer is just a set of mediaitem
```kotlin
MediaItem.Builder()
.setUri(host + music.url)
.setMediaMetadata( // set some metadata
MediaMetadata.Builder()
.setTitle(music.title())
.setDescription(music.cover)
.build()
)
.build()
```
Add the media items array to the player
```kotlin
player.setMediaItems(mediaItems)
```
Use a player listener to retrive realtime feel meta data from player
- Player listener
```kotlin
player.addListener(object : Player.Listener {
override fun onEvents(player: Player, events: Player.Events) {
// something to do
super.onEvents(player, events)
}
})
```
- Get / Adjust volume
```kotlin
// use `player.volume` to get/set volume, sooo easy~
fun adjustVolume(target: Float) {
player.volume.coerceIn(0.0f, 1.0f)
player.volume = target
}
```
- Repeat Mode
```kotlin
player.repeatMode // get
player.repeatMode = Player.REPEAT_MODE_OFF // no repeat
player.repeatMode = Player.REPEAT_MODE_ONE // for current media item
player.repeatMode = Player.REPEAT_MODE_ALL // for the whole playlist
```
# Current Time
- `SimpleDateFormat`
```kotlin
var currentTime by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
while (true) {
currentTime = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date())
delay(1000L)
}
}
```
- `DateTimeForamtter`
```kotlin
LocalDateTime.now().format(
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss"
)
)
```
# Image
## Access Image from `assets/`
```kotlin
val context = LocalContext.current
val bitmap = remember {
val file = context.assets.open(<filePath>)
BitmapFactory.decodeStream(file)
}
Image(
bitmap = bitmap.asImageBitmap(), contentDescription = <filePath>
)
```
## Network Image
### Raw
```kotlin
@Composable
fun NetworkImage() {
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
try {
bitmap = URL("https://skills-music-api.eliaschen.dev/image/ocean.jpg").openStream()
.use { BitmapFactory.decodeStream(it) } ?: null
} catch (e: Exception) {
bitmap = null
}
}
}
Column {
bitmap?.asImageBitmap()?.let { Image(bitmap = it, contentDescription = "") }
}
}
```
### Okhttp
```kotlin
@Composable
fun NetworkImage(url: String) {
val bitmap = remember { mutableStateOf<android.graphics.Bitmap?>(null) }
LaunchedEffect(url) {
bitmap.value = withContext(Dispatchers.IO) {
OkHttpClient()
.newCall(Request.Builder().url(url).build())
.execute()
.use { response ->
response.body?.bytes()?.let { android.graphics.BitmapFactory.decodeByteArray(it, 0, it.size) }
}
}
}
bitmap.value?.let {
Image(bitmap = it.asImageBitmap(), contentDescription = "Network image")
}
}
```
# File Download
## OkHttp
```kotlin
withContext(Dispatchers.IO) {
val client = OkHttpClient()
val request = Request.Builder()
.url("https://skills-music-api-v2.eliaschen.dev/audio/ocean.mp3")
.build()
val response = client.newCall(request).execute()
response.use {
it.body?.byteStream()?.use { input ->
File(context.getExternalFilesDir(null), "hello.mp3").outputStream()
.use { output ->
input.copyTo(output)
}
}
}
}
```
# File Manage
:::info
- External Root application file directory
`/storage/emulated/0/Android/data/your.package.name/files/`
**Big File (No Limited Space & Required permission)**
```kotlin
context.getExternalFilesDir(null) // use this to access External dir
```
- Internal Root application file directory
`/data/user/0/your.package.name/files/`
**Small File (Limited Space & No permission require)**
```kotlin
context.filesDir // use this to access Internal dir
```
we will call the External Root application file directory `root dir`
also you can use the `Device Explore` in android studio to review edit all the file inside the android elmulator

:::
## Folder
- `null` in here mean get the root dir path
```kotlin
context.getExternalFilesDir(null)
```
- To get the public view asset folders (Example: Download Folder)
```kotlin
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS
```
## File/Folder Operation
- Check whether a file exist
```kotlin
var myFile = File(context.getExternalFilesDir(null), "ocean.mp3")
myFile.exists()
```
- Create a folder under root dir
```kotlin
val customDir = File(context.getExternalFilesDir(null), "customFolder")
if (!customDir.exists()) {
customDir.mkdir()
}
```
- Get all files (via list)
```kotlin
fun getFiles(context: Context): List<File> {
val dir = context.getExternalFilesDir(null)
return dir?.listFiles()?.toList() ?: emptyList()
}
```
- Write a file
With FileWriter
```kotlin
withContext(Dispatchers.IO) {
val file = File(context.getExternalFilesDir(null), "hello.json")
FileWriter(file).use { writer ->
writer.write(Gson().toJson(api))
}
}
```
- With File
```kotlin
File(context.getExternalFilesDir(null), "customFolder")
.outputStream().use { output ->
<Your-InputStream>.copyTo(output)
}
```
- Rename/Move file
```kotlin
val oldFile = File(context.getExternalFilesDir(null), "ocean.mp3")
val newFile = File(context.getExternalFilesDir(null), "new_ocean.mp3")
oldFile.renameTo(newFile)
```
- Delete a file
```kotlin
File(context.getExternalFilesDir(null), "customFolder").delete()
```
- Get URI
```kotlin
File(context.getExternalFilesDir(null), "ocean.mp3").toUri()
```
# Check format
- Email Return a `boolean`
```kotlin
android.util.Patterns.EMAIL_ADDRESS.matcher(email.value).matches()
```
# Clipbaord
```kotlin
clipboardManager.setText("Hello, clipboard")
```
# Vibrate feedback
```kotlin
val haptic = LocalHapticFeedback.current
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
```
# Navigation
## with navHost
- Dependencies
```kotlin
implementation("androidx.navigation:navigation-compose:2.5.2")
```
- Base code
```kotlin
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home"){
composable("home"){
Home()
}
composable("about"){
About()
}
composable("new"){
New()
}
}
```
the page set in `startDestination` will be displayed when the NavHost Component being render
### Nav between screen
- `navigate()` navigate to a screen
- `popBackStack()` navigate to the last screen
Example:
```kotlin
Button(
onClick = {
navController.navigate("about")
}
) {
Text("Go to About")
}
Button(
onClick = {
navController.popBackStack()
}
) {
Text("Go Back")
}
```
#### Nav Arguments
- Set up NavHost with arguments
```kotlin
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
composable("home") {
HomeScreen(navController)
}
composable(
route = "detail/{itemId}",
arguments = listOf(navArgument("itemId") { type = NavType.StringType })
) { backStackEntry ->
DetailScreen(itemId = backStackEntry.arguments?.getString("itemId"))
}
}
}
```
- Navigate with argument
```kotlin
@Composable
fun HomeScreen(navController: NavController) {
Button(onClick = { navController.navigate("detail/123") }) {
Text("Go to Detail")
}
}
```
- Receive argument
```kotlin
@Composable
fun DetailScreen(itemId: String?) {
Text("Item ID: $itemId")
}
```
#### Restrict user popBack
```kotlin
navController.navigate("signin") {
// Clear the back stack to prevent the user from navigating back to the home screen
popUpTo("home") { inclusive = true }
}
```
### Nav with TypeSafe
#### With Enum
```kotlin
enum class Screen {
Home, List
}
```
```kotlin
var navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.Home.name) {
composable(Screen.Home.name) {
HomeScreen()
}
composable(Screen.List.name) {
ListScreen()
}
}
```
#### With Class
```kotlin
sealed class Screen(val route: String) {
object Home : Screen("home")
object List : Screen("list")
}
```
```kotlin
var navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.Home.route) {
composable(Screen.Home.route) {
HomeScreen()
}
composable(Screen.List.route) {
ListScreen()
}
}
```
## Open URI
- create a uri handler
```kotlin
val uriHandler = LocalUriHandler.current
uriHandler.openUri("https://eliaschen.dev")
```
# ViewModel

:::info
- Dependencies
```kotlin
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.0")
```
:::
:::info
You can use the `ViewModel` without any dependencies, but get the limit lifetime (the ViewModel will be destory every time the composables where it declared being recompose or destory), so you have to place the viewModel at the `MainActivity` where it won't destory or recompose during the run time
:::warning
**Install the dependencies will extend the life time of the viewModel**
:::
- Usage
Add a class that inherit the `viewModel`
```kotlin
class userAuth : ViewModel() {
var starterScreen = mutableStateOf("signin")
fun changeStarterScreen(screen: String) {
starterScreen.value = screen
}
}
```
Call it in `composable` function
```kotlin
@Composable
fun Main(viewModel: userAuth = viewModel()) {
}
```
or
- Create a value
```kotlin
val viewModel = userAuth()
```
## ViewModel Factory
Use ViewModel Factory when you need to pass a value into a ViewModel
- In this example we can passing `context` to PlayerModel via viewmodel factory
```kotlin
class PlayerViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(PlayerModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return PlayerModel(context) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
```
## ViewModel without plugin
- use `remember`
```kotlin
val viewModel = remember { MyViewModel() }
```
- Create in `MainActivity`
```kotlin
val viewModel: MyViewModel by viewModels()
```
# Foreground Service (Background work)
<img src="https://hackmd.io/_uploads/HJ7DJD0s1l.png" style="background:white;"/>
# RoomDB
:::info
- Dependencies **(viewModel Required)**
- **add `kapt` plugin at plugins section**
```kotlin
plugins {
id("kotlin-kapt")
}
```
```kotlin
implementation("androidx.room:room-ktx:2.6.1")
kapt("androidx.room:room-compiler:2.6.1")
```
:::
## Schema (`@Entity`)
```kotlin
@Entity(tableName = "user")
data class User(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val name: String, // default column name will be "name"
// Set some custom props with @ColumnInfo like custom ColumnName or DefaultValue
@ColumnInfo(name = "user_name", defaultValue = "Unknown", collate = ColumnInfo.NOCASE) val name: String = "EliasChen",
val age: Int,
val hobby: String
)
```
- To set the default value to a column just do `val name: String = "EliasChen"` then the default value of name will be "EliasChen"
## DB action (`@Dao`)
- Example of a CRUD Operation using RoomDB
```kotlin
@Dao
interface UserDao {
@Insert
suspend fun insert(user: User)
@Query("SELECT * FROM user")
suspend fun getAllUsers(): List<User>
// use flow to auto get updated data
@Query("SELECT * FROM user")
fun getAllUsers(): Flow<List<User>>
@Delete
suspend fun delete(user: User)
@Query("DELETE FROM user")
suspend fun deleteAll()
@Query("SELECT COUNT(*) FROM user WHERE name = :name")
suspend fun checkNameExists(name: String): Int
}
```
- parse `fun` ’s data to `@query` by adding `:` at the start of the string. EX: `:name`
## Database Entry point (`@Database`)
```kotlin
@Database(entities = [<Schema-name>::class,<Schema-name>::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
```
## Build Database (require passing `context` from parent)
```kotlin
fun getDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(
context.applicationContext, AppDatabase::class.java, "app_database"
)
.fallbackToDestructiveMigration()
.build()
}
```
- Use `.fallbackToDestructiveMigration()` to drop old schema when DB migrate
### Delete the database file
```kotlin
context.deleteDatabase("db")
```
## `viewModel` for table
### via flow auto update
- Different schema
```kotlin
class TodoViewModel(private val db: AppDB) : ViewModel() {
val allTodo: Flow<List<Todo>> = db.todoDao().select()
fun insert(todo: Todo) = viewModelScope.launch {
db.todoDao().insert(todo)
}
fun deleteTodo(id: Int) = viewModelScope.launch {
db.todoDao().delete(id)
}
fun updateName(newName: String, id: Int) = viewModelScope.launch {
db.todoDao().updateName(newName, id)
}
fun updateDone(done: Boolean, id: Int) = viewModelScope.launch {
db.todoDao().doneTodo(done, id)
}
}
```
### via manually update data
```kotlin
class UserViewModel(private val database: AppDatabase) : ViewModel() {
val users = mutableStateListOf<User>() // for storing data from DB
init {
updateUsers()
}
fun updateUsers() {
viewModelScope.launch {
users.clear()
users.addAll(database.userDao().getAllUsers())
}
}
fun addUser(name: String) {
viewModelScope.launch {
database.userDao().insert(User(name = name))
updateUsers()
}
}
fun deleteUser(user: User) {
viewModelScope.launch {
database.userDao().delete(user)
updateUsers()
}
}
fun deleteAllUsers() {
viewModelScope.launch {
database.userDao().deleteAll()
updateUsers()
}
}
suspend fun checkNameExists(name: String): Boolean {
return database.userDao().checkNameExists(name) > 0
}
}
```
- `NOTE:` All the DB action (`database` must be use in a `suspend function` or a `viewModelScope`)
## Setup `context` for database
```kotlin
val database = getDatabase(this)
val userViewModel = UserViewModel(database)
```
- Create `database`from `MainActivity` and passing `database` to the `viewModel` that **interact with the `database`**
# Shared Preferences
Save small data
```kotlin
// create a preferences
val sharedPref = context.getSharedPreferences("test", Context.MODE_PRIVATE)
// get data
var countSaved = sharedPref.getInt("count", 0)
// set data
sharedPref.edit().putInt("count", <set_value>).apply()
```
# JSON Parsing
## Org.json (Raw Method)
```kotlin
import org.json.*
```
- `JSONArray` `JSONObject`
### Parsing JSON
- Pass a string with validate JSON format to `JSONArray` or `JSONObject`
- Get a value of key with `jsonObject[<index>]` or `jsonObject.get<DataType>`
- To get a jsonObject in <mark>jsonObject</mark> use `.getJSONObject(<key>)`
- To get a jsonObject in <mark>jsonArray</mark> use `.getJSONObject(<index>)`
#### Get data from a json object
```kotlin
val jsonString = """
{
"name": "Elias",
"birthday": "2010-05-15",
"favorite": "Coding"
}
""".trimIndent()
val jsonObject = JSONObject(jsonString)
val name = jsonObject.getString("name")
val birthday = jsonObject.getString("birthday")
val favorite = jsonObject.getString("favorite")
println("Name: $name")
println("Birthday: $birthday")
println("Favorite: $favorite")
```
#### Get data from a json array
```kotlin
val jsonString = """
{
"name": "Elias",
"hobbies": ["Coding", "Reading", "Writing"]
}
""".trimIndent()
val jsonObject = JSONObject(jsonString)
val name = jsonObject.getString("name")
val hobbiesArray = jsonObject.getJSONArray("hobbies")
println("Name: $name")
println("Hobbies:")
for (i in 0 until hobbiesArray.length()) {
val hobby = hobbiesArray.getString(i)
println("- $hobby")
}
```
#### Create json object or array
```kotlin
val name = "Elias"
val age = 14
val hobbies = listOf("Coding", "Reading", "Gaming")
// Create a JSON array from the hobbies list
val hobbiesJsonArray = JSONArray()
for (hobby in hobbies) {
hobbiesJsonArray.put(hobby)
}
// Create the JSON object and put data into it
val jsonObject = JSONObject()
jsonObject.put("name", name)
jsonObject.put("age", age)
jsonObject.put("hobbies", hobbiesJsonArray)
// Output the final JSON string
println(jsonObject.toString(2)) // Pretty print with 2 spaces
```
## Gson
- Dependencies - gson
```kotlin
implementation("com.google.code.gson:gson:2.8.9")
```
## Local json file
Ex: the `data.json` locale in `assets/`
`data.json`
```json
{
"cities": [
{
"name": "Taipei",
"population": 2646204
},
{
"name": "Kaohsiung",
"population": 2773496
},
{
"name": "Taichung",
"population": 2815100
}
]
}
```
## Create a dataclass
```kotlin
data class City(val name: String, val population: Int)
data class CityList(val cities: List<City>)
```
- Use `@SerializedName` to custom the syntax gson will parsing from json
```kotlin
data class Todo(
@SerializedName("user_id") val userId: Int,
@SerializedName("todo_id") val id: Int,
@SerializedName("task") val title: String,
@SerializedName("done") val completed: Boolean
)
```
- Use function under data calss to get modified data
```kotlin
data class Music(
val cover: String,
val title: String,
val url: String
) {
fun title() {
title.replace(".mp3", "")
}
}
```
## Parsing data into a list
```kotlin
val inputStream = context.assets.open("data.json").BufferReader.use {it.readText()}
val gson = Gson()
gson.fromJson(reader, CityList::class.java)
```
# XML Parsing
```kotlin
val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
val inputStream = context.assets.open("sitemap.xml")
// ^if the source is `String` use `byteInputStream`
// EX: val `inputStream = aStringData.byteInputStream()
val document = builder.parse(inputStream)
val elements = document.documentElement.getElementsByTagName("loc")
// ^return a list of all tag matched
```
- use `.textContent` to get the value in the tag
```kotlin
elements.item(0).textContent
```
# Uri
## Content URI (Accessing Content Providers):
- `content://contacts/people/1` (accesses a specific contact with ID 1)
- `content://media/external/audio/media/123` (points to an audio file with ID 123 in external storage)
- `content://com.android.calendar/events` (accesses calendar events)
## File URI (Accessing Files):
- `file:///storage/emulated/0/Download/sample.pdf` (points to a PDF file in the Download folder)
- `file:///android_asset/index.html` (accesses a file in the app’s assets folder)
Intent URI (Triggering Actions):
- `tel:5551234567` (initiates a phone call to the specified number)
- `mailto:user@example.com` (opens an email client with the specified address)
- `geo:37.7749,-122.4194` (opens a map at the specified coordinates)
## Custom URI (App-Specific):
`myapp://profile/user123` (a custom scheme to open a specific user profile in a custom app)
# WebView
:::info
- Dependencies
```kotlin
implementation("androidx.webkit:webkit:1.8.0")
```
:::
- Usage
- View for local HTML file
In project explorer under `main/assets` add a HTML file, than use `loadUrl("file:///android_asset/...")` to locate it.
**NOTE:** `file:///android_asset` is point to `main/assets`
```kotlin
AndroidView(factory = {
WebView(it).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, // for style width and height
ViewGroup.LayoutParams.MATCH_PARENT
)
}
}, update = {
it.loadUrl(<file-path or web url(permission for access internet is required)>)
})
```
- View for web URL
Though we need access to the internet, we have to enable the permission connect to the internet in `AndroidManifest.xml` by adding:
```xml
<uses-permission android:name="android.permission.INTERNET" />
```
# Pager
- Create a pager state
```kotlin
var pager_state = rememberPagerState { 2 } // <- the length of pager
HorizontalPager(
state = pager_state,
modifier = Modifier
.fillMaxWidth()
.height(220.dp),
) { page ->
Image(
painter = painterResource(pager_images[page]),
modifier = Modifier.fillMaxSize(),
contentDescription = "Image of page $page"
)
}
```
- `pager_state.currentPage` get current page
- `pager_state.animateScrollToPage(<Index>)` scroll to page **(Place in `coroutine scope`)**
# Unit Convert
the `toDp()` method is only available in Local
```kotli
with(LocalDensity.current) {
offestX.value.toDp()
}
```
# Animation
## Animate ContentSize
```kotlin
Modifier.animateContentSize()
```
## Infinite Transition
```kotlin
val offset by infiniteTransition.animateFloat(
label = "Example infinite transition"
initialValue = -textWidth,
targetValue = screenWidth,
animationSpec = infiniteRepeatable(
tween(textMovingSpeedSlider.toInt(), easing = LinearEasing),
RepeatMode.Restart
)
)
```
## Animatable
**`Animatable` is a value based animation method**
- Start by createing a variable with `Animatable()` inside (Also set a inital `float` value)
```kotlin
val offsetX = remember { Animatable(0f) }
```
- `animateTo()` suspend method to animated value to the target value
```kotlin
offsetX.animateTo(
targetValue = maxWidth.toFloat(),
animationSpec = tween(durationMillis = 4000, easing = LinearEasing)
)
```
- `snapTo()` set the value to the target instantly without any animation
```kotlin
offsetX.snapTo(0f)
```
## Muti Animation
```kotlin
var toggle: Boolean by remember { mutableStateOf(false) }
val transition = updateTransition(targetState = toggle, label = "")
val color by transition.animateColor(label = "") { s ->
when (s) {
true -> Color.Red
false -> Color.Yellow
}
}
val size by transition.animateDp(label = "") { s ->
when (s) {
true -> 200.dp
false -> 100.dp
}
}
Column {
Button(onClick = {
toggle = !toggle
}) { Text("Click to change Color & Size") }
Box(
Modifier
.background(color)
.size(size)
)
}
```
## Fade
```kotlin
var toggle by remember { mutableStateOf(false) }
Column {
Button(onClick = { toggle = !toggle }) {
Text("Toggle to see CrossFade")
}
Crossfade(toggle) { toggle ->
when (toggle) {
true -> Icon(Icons.Default.Add, contentDescription = "")
false -> Icon(Icons.Default.Call, contentDescription = "")
}
}
}
```
## Visbility
```kotlin
var toggle: Boolean by remember { mutableStateOf(false) }
Column {
Button(onClick = {
toggle = !toggle
}) { Text("Click to Show & Hide") }
AnimatedVisibility(toggle) {
Box(
Modifier
.background(Color.Red)
.size(100.dp)
)
}
}
```
## Animated value
```kotlin
animateFloatAsState(
targetValue = if (it + 1 == pager.currentPage) 1f else 0.2f,
label = "alpha" // <- just for differentiate from other animations (optional)
)
```
# Modifier
## Whether to use a modifier via condition
```kotlin
fun Modifier.conditional(condition: Boolean, modifier: Modifier.() -> Modifier): Modifier {
return if (condition) then(modifier(Modifier)) else this
}
```
## onGloballyPositioned
Retrive widget layout (width, height)
```kotlin
Modifier.onGloballyPositioned {
textWidth = it.size.width // Int
}
```
## Scrollable Column or Row
Use `.verticalScroll()` or `.horizontalScroll()` to do that
```kotlin
val state = rememberScrollState()
Column(Modifier.verticalScroll(state)){
}
```
After binding the `state` use `.animateScrollTo()` to scroll to a location with animation
## BoxWithConstraints
## size & requiredSize
| `requiredSize` | `size` |
| -------- | -------- |
| force to set the size no matter what | respect parent layout size if overflow then fit in the parent layout |
```kotlin
Box(modifier = Modifier.size(100.dp))
Box(modifier = Modifier.requiredSize(100.dp))
```
## zIndex
same as `z-index` in css, it allow you to chagne the layer of the composables
```kotlin
Modifier.zIndex(1f)
Modifier.zIndex(2f) // <- higher layer
```
## Weight
`weight()` take the remaining space of the composables
```kotlin
Modifier.weight(1f)
```

```kotlin
@Composable
fun WeightExample() {
Column(modifier = Modifier.fillMaxHeight().systemBarsPadding()) {
Row(modifier = Modifier.fillMaxWidth().height(50.dp)) {
Text(text = "Weight 2",
modifier = Modifier.weight(2f)
.background(Color.Red).padding(8.dp))
Text(text = "Weight 2",
modifier = Modifier.weight(2f)
.background(Color.Green).padding(8.dp))
Text(text = "Weight 3",
modifier = Modifier.weight(3f)
.background(Color.Blue).padding(8.dp))
}
Row(modifier = Modifier.fillMaxWidth().height(50.dp)) {
Text(text = "Weight 2",
modifier = Modifier.weight(2f)
.background(Color.Yellow).padding(8.dp))
Text(text = "Weight 1",
modifier = Modifier.weight(1f)
.background(Color.Cyan).padding(8.dp))
}
}
}
```
## Border
```kotlin
Modifier.border(2.dp, Color.Green, CircleShape)
```
## Safe area
- for all bars (bottom nav controller & topbar)
```kotlin
Modifier.windowInsetsPadding(WindowInsets.systemBars)
// ..or
Modifier.systemBarsPadding()
```
- for only top bar
```kotlin
Modifier.windowInsetsPadding(WindowInsets.statusBars)
```
## Rounded
- For Object
```jsx
Modifier.clip(RoundedCornerShape(20))
```
The `RoundedCornerShape()` also accept something like `CircleShape` (pretty useful)
- For Border
```kotlin
Modifier.border(1.dp, Color.Red,RoundedCornerShape(20))
```
## Gradient
- for background
```kotlin
Brush.linearGradient(listOf(Color.Red, Color.White))
```
- Text Color Gradient

```kotlin
Text(
text = "Hello Gradient",
style = TextStyle(
brush = Brush.linearGradient(
colors = listOf(Color.Red, Color.Blue),
),
fontSize = 50.sp
)
)
```
gradient for different direction
| Method|Direction|
|---------|-------|
|`linearGradient`|topLeft -> bottomRight|
|`horizontalGradient`|Left -> Right|
|`verticalGradient`|Top -> Bottom|
## Shadow
```kotlin
Box(
modifier = Modifier
.padding(top = 50.dp)
.size(200.dp)
.shadow(
elevation = 8.dp, // the height of the shadow
shape = RoundedCornerShape(16.dp), // border radius of the box
clip = false // whether not the
)
.background(
Color.White
)
) {
Text("Hello Shadow", modifier = Modifier.align(Alignment.Center))
}
```
# Composables
## Alert Dialog

```kotlin
AlertDialog(
icon = {/* icon */},
title = {/* title text */},
text = {/* content */},
onDismissRequest = {/* function when try to dismiss */},
confirmButton = {/* button */},
dismissButton = {/* button */}
)
```
## DropDown Menu

```kotlin
var expanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.padding(16.dp)
) {
IconButton(onClick = { expanded = !expanded }) {
Icon(Icons.Default.MoreVert, contentDescription = "More options")
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text("Option 1") },
onClick = { /* Do something... */ }
)
DropdownMenuItem(
text = { Text("Option 2") },
onClick = { /* Do something... */ }
)
}
}
```
## BoxWithConstraints
- Exmaple
```kotlin
BoxWithConstraints {
Image(
painter = painterResource(R.drawable.ocean_cover),
contentDescription = "ocean cover", modifier = Modifier
.size(maxWidth)
.clip(RoundedCornerShape(15.dp))
)
}
```
Available syntax `minWidth` `minHeight` `maxWidth` `maxHeight`
## Button
- `ButtonDefaults` allow you to custom the button color without rewrite all the color settings
```kotlin
Button(onClick = {}, colors = ButtonDefaults.buttonColors(containerColor = Color.Red)) { }
```
## LazyColumn
:::warning
`stickyHeader` -> `@OptIn(ExperimentalFoundationApi::class)`
:::
```kotlin
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp), // Outer padding
verticalArrangement = Arrangement.spacedBy(8.dp) // Space between items
) {
stickyHeader {
Text(
text = "Sticky Header",
modifier = Modifier
.background(Color.Gray)
.padding(16.dp)
.fillMaxWidth()
)
}
items(items) { item ->
Column {
Text(text = item)
Divider() // Separator between items
}
}
}
```
## LazyVerticalGrid
- Same as the LazyColumn or LazyRow but `columns` is required
```kotlin
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 200.dp)) {
items(data[0].schedule) { schedule ->
Text(schedule.destination)
}
}
```
## Snack bar
<img src="https://hackmd.io/_uploads/r1pWeZS2Je.png" width="250px" />
Must calling snackBar in a coroutineScope
```kotlin
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
floatingActionButton = {
ExtendedFloatingActionButton(
text = { Text("Show snackbar") },
icon = { Icon(Icons.Filled.Image, contentDescription = "") },
onClick = {
scope.launch {
snackbarHostState.showSnackbar("Snackbar")
}
}
)
}
) { contentPadding ->
// Screen content
}
```
Add `action` or `duration`
```kotlin
val result = snackbarHostState
.showSnackbar(
message = "Snackbar",
actionLabel = "Action",
// Defaults to SnackbarDuration.Short
duration = SnackbarDuration.Indefinite
)
when (result) {
SnackbarResult.ActionPerformed -> {
/* Handle snackbar action performed */
}
SnackbarResult.Dismissed -> {
/* Handle snackbar dismissed */
}
}
```
## Bottom Bar
<img src="https://hackmd.io/_uploads/SyPG0xHnJl.png" width="250px" />
:::warning
Combind with <a href="#Navigation">NavHost</a>
But in this example we will focus on the BottomBar
:::
To create a BottomBar for our app we need to create it in a `Scaffold`
```kotlin
Scaffold(modifier = Modifier.fillMaxSize(), bottomBar = {
NavigationBar {
// NavigationBarItem in here ...
}
})
```
:::success
Suggestion: Create a list for stroing navitems data then recall in `NavigationBar` for more simple and effective code
:::
### NavgationBarItem
```kotlin
NavigationBarItem(
selected = currentScreen == index,
onClick = {
currentScreen = index
nav.navigate(item.title)
},
label = { Text(item.title) },
icon = {
BadgedBox(badge = {
// Bad
}) {
Icon(
imageVector = if (currentScreen == index) item.selectedIcon else item.unselectedIcon,
contentDescription = "image"
)
}
}
)
```
### Badge


```kotlin
Badge(content = { Text("12") })
```
## Segmented buttons

```kotlin
var selectedItem by remember { mutableStateOf(0) }
var items = listOf("Kitty", "Squirrel", "Dog")
SingleChoiceSegmentedButtonRow {
items.forEachIndexed { index, item ->
SegmentedButton(
selected = index == selectedItem,
onClick = { selectedItem = index },
shape = SegmentedButtonDefaults.itemShape(index = index, count = items.size)
) {
Text(item)
}
}
}
```
:::info
So what the hell does shape do?
The segmented buttons only have rounded borders on the left for the first button and on the right for the last button. This requires you to provide the index and count so it knows which shape to render.
:::
## TimePicker
return a `LocalTime` object
```kotlin
@Composable
fun TimePickerExample() {
var showTimePicker by remember { mutableStateOf(false) }
var selectedTime by remember { mutableStateOf(LocalTime.now()) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Selected Time: ${selectedTime.format(DateTimeFormatter.ofPattern("HH:mm"))}",
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { showTimePicker = true }) {
Text("Pick a Time")
}
if (showTimePicker) {
TimePickerDialog(
onDismissRequest = { showTimePicker = false },
onTimeSelected = { time ->
selectedTime = time
showTimePicker = false
}
)
}
}
}
```
```kotlin
@Composable
fun TimePickerDialog(
onDismissRequest: () -> Unit,
onTimeSelected: (LocalTime) -> Unit
) {
val timePickerState = rememberTimePickerState(
initialHour = LocalTime.now().hour,
initialMinute = LocalTime.now().minute,
is24Hour = true
)
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = {
onTimeSelected(LocalTime.of(timePickerState.hour, timePickerState.minute))
}) {
Text("OK")
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text("Cancel")
}
},
text = {
TimePicker(state = timePickerState)
}
)
}
```