# Jetpack Compose Crash Note ![compose](https://hackmd.io/_uploads/BJ0LDhIcyl.jpg) - `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. ![image](https://hackmd.io/_uploads/HJZF9t2wlx.png) ## 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) ![app shortcut](https://developer.android.com/static/images/guide/topics/ui/shortcuts/pinned-shortcuts.png) :::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 ![image](https://hackmd.io/_uploads/SydH7-ncJx.png) :::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 ![image](https://hackmd.io/_uploads/BJt5fIT2kx.png) ::: ## 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 ![image](https://hackmd.io/_uploads/S1tZkvCjye.png) :::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) ``` ![image](https://hackmd.io/_uploads/SkfbGkysJx.png) ```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 ![image](https://hackmd.io/_uploads/Sygsp9G3Jx.png) ```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 ![image](https://hackmd.io/_uploads/SyVGj289kg.png) ```kotlin AlertDialog( icon = {/* icon */}, title = {/* title text */}, text = {/* content */}, onDismissRequest = {/* function when try to dismiss */}, confirmButton = {/* button */}, dismissButton = {/* button */} ) ``` ## DropDown Menu ![MinimalDropdownMenu](https://hackmd.io/_uploads/BJGqctp5kx.png) ```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 ![image](https://hackmd.io/_uploads/HyRwrbS2kl.png) ![image](https://hackmd.io/_uploads/B1CaN-Hhyx.png) ```kotlin Badge(content = { Text("12") }) ``` ## Segmented buttons ![image](https://hackmd.io/_uploads/BJ3HYbBnJg.png) ```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) } ) } ```