Try   HackMD

Jetpack Compose Crash Note

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

  • 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:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
  • Manually ReCompose
var count by remember { mutableStateOf(0) }
key(count){
    // recompose when the var count change
}

Effect Handlers

snapshotFlow

kind of like LaunchEffect but allow to use in non-composable function

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

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            /* composable function gose here... */
        }
    }
}

Get DeviceConfig

  • Get the screen width
val configuration = LocalConfiguration.current
val screenWidthDp = configuration.screenWidthDp

Text To Speech (TTS)

Setup a speecher

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

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

Glance Widget

  • Dependencies
implementation("androidx.glance:glance-appwidget:1.1.0")
implementation("androidx.glance:glance-material3:1.1.0")
  • Create a Receiver
class MyReceiver : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget = MyGlanceWidget()
}
  • Create widget UI
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 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

HINT: android.appwidget.*
the widget is appwidget

<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

BarWidget is a GlanceAppWidget() in this example

Method 1: Via Application Class

In Application Class
Declare a scope to run suspend action

private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

Run the update widget method to update the widget

appScope.launch {
    BarWidget().updateAll(applicationContext)
}

Remember to bind the application class in AndroidManifest.xml
and add the widget update method to onCreate() to update the widget when app launch

Method 2: Via MainActivity

Under MainAcvtivity

suspend fun refreshWidget(context: Context) {
    BarWidget().updateAll(context)
}

In Composable

CoroutineScope(Dispatchers.Main).launch {
    refreshWidget(this@MainActivity)
}

Permission Check/Request

image

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)
val hasPermission = ContextCompat.checkSelfPermission(
    context,
    Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
  • Create a launcher to show the system dialog for requesting user permission
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)
if (!hasPermission) launcher.launch(Manifest.permission.POST_NOTIFICATIONS)

Notification

  • Add permission
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
  • Create a notification manager
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
  • Create a notification channel
val channel =
    NotificationChannel(
        "channel_id",
        "channel_name",
        NotificationManager.IMPORTANCE_HIGH
    )
    
notificationManager.createNotificationChannel(channel)
  • Create a notification
val notification = 
    NotificationCompat.Builder(context, channelId)
        .setContentTitle("hello")
        .setContentText("hello")
        .setSmallIcon(R.drawable.play)
        .build()
  • Push the notification
notificationManager.notify(<notifiacationId<Int>>, notification)

Alarm Manager

  • Here is a example of how to send a notification at a custom time via Alarm Manager

Permission required

<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

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
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
@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

@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

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

@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

Raw

  • Data class
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

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
if(data.isNotEmpty()){
    LazyColumn {
        items(data) {
            Text(it.title())
        }
    }
}

Retrofit

  • Dependencies
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
  • Example data
data class Music(
    val title: String,
    val url: String
)
  • Create a interface for api action
interface Api {
    @GET("/music")
    suspend fun getMusic(): List<Music>
}
  • Create a Retrofit Instance for doing request
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
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
@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

  • Dependencies
implementation("com.squareup.okhttp3:okhttp:4.9.3")
  • Create a client
val client = OkHttpClient()
  • use the withContext(Dispatchers.IO) to perform the request in background
  • create a request
val request = Request.Builder().url("<url>").build()
  • get the response
val response = client.newCall(request).execute()
val body = call.body?.string()?: return@withContext emptyList<MusicList>()
  • prase to json
val gson = Gson()
val listType = object : TypeToken<List<MusicList>>() {}.type
gson.fromJson(res, listType)

Example (simple)

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)

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

ExoPlayer

  • Dependencies
implementation("androidx.media3:media3-exoplayer:1.5.1")
implementation("androidx.media3:media3-common:1.5.1")
  • Create a player
val player = remember { ExoPlayer.Builder(context).build() }
  • Add media source with URI and preare for playing
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
DisposableEffect(Unit) {
    onDispose {
        ExoPlayer.release()
    }
}

PlayList

the playlist in exoplayer is just a set of mediaitem

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

player.setMediaItems(mediaItems)

Use a player listener to retrive realtime feel meta data from player

  • Player listener
player.addListener(object : Player.Listener {
    override fun onEvents(player: Player, events: Player.Events) {
        // something to do
        super.onEvents(player, events)
    }
})
  • Get / Adjust volume
// 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
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
var currentTime by remember { mutableStateOf("") }

LaunchedEffect(Unit) {
    while (true) {
        currentTime = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date())
        delay(1000L)
    }
}
  • DateTimeForamtter
LocalDateTime.now().format(
    DateTimeFormatter.ofPattern(
        "yyyy-MM-dd HH:mm:ss"
    )
)

Image

Access Image from assets/

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

@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

@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

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

  • External Root application file directory
    /storage/emulated/0/Android/data/your.package.name/files/
    Big File (No Limited Space & Required permission)
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)
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

Folder

  • null in here mean get the root dir path
context.getExternalFilesDir(null)
  • To get the public view asset folders (Example: Download Folder)
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS

File/Folder Operation

  • Check whether a file exist
var myFile = File(context.getExternalFilesDir(null), "ocean.mp3")
myFile.exists()
  • Create a folder under root dir
val customDir = File(context.getExternalFilesDir(null), "customFolder")

if (!customDir.exists()) {
    customDir.mkdir()
}
  • Get all files (via list)
fun getFiles(context: Context): List<File> {
    val dir = context.getExternalFilesDir(null)
    return dir?.listFiles()?.toList() ?: emptyList()
}
  • Write a file
    With FileWriter
withContext(Dispatchers.IO) {  
    val file = File(context.getExternalFilesDir(null), "hello.json")  
    FileWriter(file).use { writer ->  
        writer.write(Gson().toJson(api))  
    }  
}
  • With File
File(context.getExternalFilesDir(null), "customFolder")
    .outputStream().use { output ->
    <Your-InputStream>.copyTo(output)
}
  • Rename/Move file
val oldFile = File(context.getExternalFilesDir(null), "ocean.mp3")
val newFile = File(context.getExternalFilesDir(null), "new_ocean.mp3")

oldFile.renameTo(newFile)
  • Delete a file
File(context.getExternalFilesDir(null), "customFolder").delete()
  • Get URI
File(context.getExternalFilesDir(null), "ocean.mp3").toUri()

Check format

  • Email Return a boolean
android.util.Patterns.EMAIL_ADDRESS.matcher(email.value).matches()

Clipbaord

clipboardManager.setText("Hello, clipboard")

Vibrate feedback

val haptic = LocalHapticFeedback.current
haptic.performHapticFeedback(HapticFeedbackType.LongPress)

Navigation

with navHost

  • Dependencies
implementation("androidx.navigation:navigation-compose:2.5.2")
  • Base code
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

  • navigate() navigate to a screen
  • popBackStack() navigate to the last screen

Example:

Button(
    onClick = { 
        navController.navigate("about") 
    }
) { 
    Text("Go to About") 
}

Button(
    onClick = { 
        navController.popBackStack() 
    }
) { 
    Text("Go Back") 
}

Restrict user popBack

navController.navigate("signin") {
    // Clear the back stack to prevent the user from navigating back to the home screen
    popUpTo("home") { inclusive = true }
}

With Enum

enum class Screen {
    Home, List
}
var navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.Home.name) {
    composable(Screen.Home.name) {
        HomeScreen()
    }
    composable(Screen.List.name) {
        ListScreen()
    }
}

With Class

sealed class Screen(val route: String) {
    object Home : Screen("home")
    object List : Screen("list")
}
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
val uriHandler = LocalUriHandler.current
uriHandler.openUri("https://eliaschen.dev")

ViewModel

image

  • Dependencies
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.0") 

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

Install the dependencies will extend the life time of the viewModel

  • Usage

Add a class that inherit the viewModel

class userAuth : ViewModel() {
    var starterScreen = mutableStateOf("signin")

    fun changeStarterScreen(screen: String) {
        starterScreen.value = screen
    }
}

Call it in composable function

@Composable
fun Main(viewModel: userAuth = viewModel()) {
	
}

or

  • Create a value
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
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
val viewModel = remember { MyViewModel() }
  • Create in MainActivity
val viewModel: MyViewModel by viewModels()

Foreground Service (Background work)

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

RoomDB

  • Dependencies (viewModel Required)
  • add kapt plugin at plugins section
plugins {
    id("kotlin-kapt") 
}
implementation("androidx.room:room-ktx:2.6.1")
kapt("androidx.room:room-compiler:2.6.1")

Schema (@Entity)

@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
@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)

@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)

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

context.deleteDatabase("db")

viewModel for table

via flow auto update

  • Different schema
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

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

val database = getDatabase(this)
val userViewModel = UserViewModel(database)
  • Create databasefrom MainActivity and passing database to the viewModel that interact with the database

Shared Preferences

Save small data

// 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)

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 jsonObject use .getJSONObject(<key>)
  • To get a jsonObject in jsonArray use .getJSONObject(<index>)

Get data from a json object

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

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

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
implementation("com.google.code.gson:gson:2.8.9")

Local json file

Ex: the data.json locale in assets/

data.json

{
  "cities": [
    {
      "name": "Taipei",
      "population": 2646204
    },
    {
      "name": "Kaohsiung",
      "population": 2773496
    },
    {
      "name": "Taichung",
      "population": 2815100
    }
  ]
}

Create a dataclass

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
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
data class Music(
    val cover: String,
    val title: String,
    val url: String
) {
    fun title() {
        title.replace(".mp3", "")
    }
}

Parsing data into a list

val inputStream = context.assets.open("data.json").BufferReader.use {it.readText()}
val gson = Gson()
gson.fromJson(reader, CityList::class.java)

XML Parsing

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
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

  • Dependencies
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

    ​​​​    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:

    ​​​​<uses-permission android:name="android.permission.INTERNET" />
    

Pager

  • Create a pager state

    ​​​​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

with(LocalDensity.current) {
    offestX.value.toDp()
}

Animation

Animate ContentSize

Modifier.animateContentSize()

Infinite Transition

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)
val offsetX = remember { Animatable(0f) }
  • animateTo() suspend method to animated value to the target value
offsetX.animateTo(
    targetValue = maxWidth.toFloat(),
    animationSpec = tween(durationMillis = 4000, easing = LinearEasing)
)
  • snapTo() set the value to the target instantly without any animation
offsetX.snapTo(0f)

Muti Animation

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

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

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

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

fun Modifier.conditional(condition: Boolean, modifier: Modifier.() -> Modifier): Modifier {
    return if (condition) then(modifier(Modifier)) else this
}

onGloballyPositioned

Retrive widget layout (width, height)

Modifier.onGloballyPositioned {
    textWidth = it.size.width // Int
}

Scrollable Column or Row

Use .verticalScroll() or .horizontalScroll() to do that

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
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

Modifier.zIndex(1f)
Modifier.zIndex(2f) // <- higher layer

Weight

weight() take the remaining space of the composables

Modifier.weight(1f)

image

@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

Modifier.border(2.dp, Color.Green, CircleShape)

Safe area

  • for all bars (bottom nav controller & topbar)
Modifier.windowInsetsPadding(WindowInsets.systemBars)
// ..or
Modifier.systemBarsPadding()
  • for only top bar
Modifier.windowInsetsPadding(WindowInsets.statusBars)

Rounded

  • For Object
Modifier.clip(RoundedCornerShape(20))

The RoundedCornerShape() also accept something like CircleShape (pretty useful)

  • For Border
Modifier.border(1.dp, Color.Red,RoundedCornerShape(20))

Gradient

  • for background
Brush.linearGradient(listOf(Color.Red, Color.White))
  • Text Color Gradient
    image
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

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

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

MinimalDropdownMenu

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
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
Button(onClick = {}, colors = ButtonDefaults.buttonColors(containerColor = Color.Red)) { }

LazyColumn

stickyHeader -> @OptIn(ExperimentalFoundationApi::class)

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
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 200.dp)) {
    items(data[0].schedule) { schedule ->
        Text(schedule.destination)
    }
}

Snack bar

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Must calling snackBar in a coroutineScope

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

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

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Combind with NavHost
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

Scaffold(modifier = Modifier.fillMaxSize(), bottomBar = {
    NavigationBar {
        // NavigationBarItem in here ...
    }
})

Suggestion: Create a list for stroing navitems data then recall in NavigationBar for more simple and effective code

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

image

Badge(content = { Text("12") })

Segmented buttons

image

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)
        }
    }
}

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

@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
                }
            )
        }
    }
}
@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)
        }
    )
}