Jetpack Compose: IconButton from vector resource

Introduction

In modern Android development, Jetpack Compose has become a popular toolkit for building user interfaces. It simplifies the UI development process by using a declarative approach, where developers describe how UI elements should look based on the app's state. One useful feature in Jetpack Compose is the ability to use vector resources for displaying icons. In this article, we will break down an Android Kotlin example that demonstrates how to create IconButton elements from vector resources using Jetpack Compose.

This example showcases how to build a basic user interface with buttons containing icons from vector resources. When these buttons are clicked, they update a message on the screen. This project not only highlights the simplicity and flexibility of Jetpack Compose but also emphasizes how easily developers can integrate vector-based icons in their UI.

Main Components of the Code

The code begins with a typical setup for an Android activity using AppCompatActivity. In the onCreate method, Jetpack Compose is initialized with setContent, which allows developers to set the UI for the activity using composable functions. The Scaffold layout, a Jetpack Compose component, is used to provide a basic structure for the app. It includes a top app bar with a title and a section for content that contains the main composable.

The MainContent function is where the core of the UI is defined. This function contains a Column layout that vertically arranges its child elements, centering them horizontally and vertically on the screen. At the top of this column is a Text element that displays a message, which changes when the user interacts with the buttons. The message’s state is handled by the remember function and mutableStateOf, ensuring the UI reacts to state changes.

IconButton Usage

Below the message, the app uses a Row layout to arrange two IconButton elements horizontally, with spacing between them. These buttons are interactive, and when clicked, they trigger changes to the message. Each IconButton contains an Icon, which is loaded using the ImageVector.vectorResource function. This function allows the app to access vector resources—scalable images that can be resized without losing quality.

In this example, two vector resources are used: a mail icon (ic_baseline_mail_24) and a share icon (ic_baseline_share_24). These icons are referenced by their resource IDs, which are automatically generated when the vector assets are added to the project’s res/drawable directory. Each button is associated with a different click event, which updates the message displayed on the screen to either "Mail clicked" or "Share clicked," depending on the button pressed.

The Role of State Management

Jetpack Compose manages UI state automatically. In this example, the message state is defined as a mutable variable using var message by remember { mutableStateOf("") }. This ensures that whenever the state of message changes, Jetpack Compose will automatically recompose the UI to reflect the new state. This reactive nature eliminates the need for manual UI updates, making the code more efficient and easier to maintain.

State management in Compose is crucial because it allows developers to build dynamic and interactive interfaces. By coupling the state (in this case, the message) with user interactions (the button clicks), the UI remains synchronized with the underlying data.

Customizing UI with Modifiers

Modifiers in Jetpack Compose are essential tools for customizing the appearance and behavior of UI elements. In this example, several Modifier instances are used to control layout properties. For instance, Modifier.fillMaxSize() is applied to the Column to make it occupy the entire screen, while Modifier.height(24.dp) is used to add spacing between the message and the buttons. These modifiers provide a declarative way to handle layout logic, further enhancing the flexibility of Jetpack Compose.

Summary

This example demonstrates how to create a simple, interactive UI using Jetpack Compose, focusing on IconButton elements that use vector resources. The code leverages the power of Jetpack Compose’s state management system to handle user interaction dynamically, updating the UI as the app’s state changes. By utilizing vector resources, the app is able to display scalable, high-quality icons within buttons.

Jetpack Compose’s declarative approach, combined with features like modifiers and state handling, streamlines the process of building modern Android UIs. Whether you are creating a simple app or a more complex UI, Jetpack Compose offers the tools and flexibility needed to make development easier and more efficient.


MainActivity.kt

package com.cfsuman.jetpackcompose

import android.annotation.SuppressLint
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

class MainActivity : AppCompatActivity() {
    @SuppressLint("UnusedMaterialScaffoldPaddingParameter")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            Scaffold(
                topBar = { TopAppBar(
                    title = {
                        Text(text ="IconButton From Vector Resources")
                    }
                )},
                content = { MainContent() },
                backgroundColor = Color(0xFFFEFEFA)
            )
        }
    }


    @Composable
    fun MainContent(){
        var message by remember { mutableStateOf("")}

        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(
                text = message,
                fontSize = 30.sp
            )

            Spacer(modifier = Modifier.height(24.dp))

            Row(
                horizontalArrangement = Arrangement.spacedBy(16.dp)
            ){
                IconButton(onClick = { message = "Mail clicked" }) {
                    Icon(
                        imageVector = ImageVector.vectorResource(
                            id = R.drawable.ic_baseline_mail_24),
                        contentDescription = ""
                    )
                }

                IconButton(onClick = { message = "Share clicked" }) {
                    Icon(
                        imageVector = ImageVector.vectorResource(
                            id = R.drawable.ic_baseline_share_24),
                        contentDescription = ""
                    )
                }
            }
        }
    }
}
More android jetpack compose tutorials

Jetpack Compose: Icon from vector resource

Introduction

Jetpack Compose, the modern toolkit for building native Android UIs, is rapidly transforming how developers approach Android app development. One of its powerful features is the ability to easily manage UI elements using composable functions. In this tutorial, we will explore a simple example that demonstrates how to create icons using vector resources within a Jetpack Compose layout. The code highlights how to effectively utilize vector assets to build clean, responsive interfaces, showcasing the flexibility of Compose in managing images and icons.

In Android development, vector graphics are widely preferred over raster images for their scalability and small file sizes. This example illustrates how to include vector-based icons directly in a Jetpack Compose project, enhancing the visual appearance and overall performance of the app. We will break down the code in detail, so both new and experienced developers can understand the key concepts at play.

Breakdown of Code: MainActivity Class Overview

The MainActivity class in this example extends AppCompatActivity, which serves as the entry point of the app. The primary responsibility of this class is to set up the user interface using the Jetpack Compose setContent method. The UI is structured within a Scaffold, which is a powerful layout component that provides basic structure such as the top bar, bottom bar, or floating action buttons in a typical Android screen.

The top bar (TopAppBar) in this example displays a title text—"Compose - Icon From Vector Resources"—using the Text composable function. The background color for the Scaffold is set to a light off-white color (Color(0xFFFEFEFA)), creating a visually clean appearance for the app's content area.

The MainContent Composable

The main user interface of the app is built within the MainContent composable function. This function is responsible for arranging the icons and managing the layout. It uses a Row composable, which organizes its child elements horizontally. The fillMaxSize() modifier ensures that the row takes up the full screen, while the horizontalArrangement and verticalAlignment parameters center the content both horizontally and vertically.

Within the Row, two Icon composable functions are used to display vector images. These icons are loaded using ImageVector.vectorResource(), which fetches the vector drawable from the resources. In this case, the icons are sourced from the ic_baseline_mail_24 and ic_baseline_share_24 vector resources, representing a mail and share icon, respectively.

Customizing Icons

The first icon, ic_baseline_mail_24, is displayed without any customizations. It is placed within the layout using default settings, and the contentDescription parameter is left empty. While it’s good practice to provide a content description for accessibility, it has been omitted in this example for simplicity.

The second icon, ic_baseline_share_24, includes a customization: a tint is applied using the tint parameter. The tint color is set to red (Color.Red), changing the appearance of the icon without modifying the underlying vector resource. A Spacer with a width of 16.dp is added between the two icons, ensuring proper spacing between the elements.

Handling Vector Resources

Vector resources are often stored in the res/drawable folder as XML files. In this example, the ImageVector.vectorResource() function is used to load these resources by their R.drawable IDs. This method allows developers to use vector assets in a scalable and efficient manner, avoiding the performance pitfalls of large bitmap images. The use of vectors ensures that icons look sharp on any screen size or resolution.

Summary

In this example, we explored how Jetpack Compose simplifies the process of incorporating vector icons into Android apps. By leveraging the ImageVector.vectorResource() function, developers can efficiently manage scalable vector graphics within a modern Compose UI framework. The use of composables like Icon, Row, and Spacer provides a declarative and flexible approach to building responsive layouts.

This approach not only results in a clean and maintainable codebase but also enhances the performance and scalability of the app. By using vector resources, developers can ensure that icons remain crisp across various devices, leading to a better user experience. As Jetpack Compose continues to evolve, mastering these fundamental components will be essential for creating polished and responsive Android applications.


MainActivity.kt

package com.cfsuman.jetpackcompose

import android.annotation.SuppressLint
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp

class MainActivity : AppCompatActivity() {
    @SuppressLint("UnusedMaterialScaffoldPaddingParameter")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            Scaffold(
                topBar = { TopAppBar(
                    title = {
                        Text(text = "Compose - Icon From Vector Resources")
                    }
                )},
                content = { MainContent() },
                backgroundColor = Color(0xFFFEFEFA)
            )
        }
    }


    @Composable
    fun MainContent(){
        Row(
            modifier = Modifier.fillMaxSize(),
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Icon(
                imageVector = ImageVector.vectorResource(
                    id = R.drawable.ic_baseline_mail_24),
                contentDescription = ""
            )

            Spacer(modifier = Modifier.width(16.dp))

            Icon(
                imageVector = ImageVector.vectorResource(
                    id = R.drawable.ic_baseline_share_24),
                contentDescription = "",
                tint = Color.Red
            )
        }
    }
}
More android jetpack compose tutorials

Compose Glance: How to Create an App Widget in Android with Kotlin

Creating an app widget in Android using Kotlin and Jetpack Compose’s Glance framework is an elegant approach to bringing interactivity and dynamic UI to a user’s home screen. This article breaks down the key components of a CounterWidget app, which demonstrates how to build an increment counter widget. We will cover how to use Glance to structure the UI, manage state using Jetpack’s DataStore, and handle interactions like button clicks. The CounterWidget example is a great starting point for understanding the fundamentals of creating app widgets using modern Android development practices.

In this explanation, we will walk through the code step-by-step, beginning with the widget’s layout and composable functions, followed by how the widget’s state is updated, and how actions such as button clicks are handled. By the end, you'll have a clear understanding of the necessary components involved in creating a fully functional app widget with Kotlin and Glance.

Structure of the CounterWidget

The CounterWidget class extends GlanceAppWidget, which is the core component responsible for rendering the widget's content. It overrides the Content() composable function to define the layout and behavior of the widget. This layout includes a text view displaying the current count and a button to increment the counter.

A Column is used to organize the layout vertically, centering the elements both horizontally and vertically. The background color for the widget is set to a light blue (0xFFD3E9FA), and the GlanceModifier.fillMaxSize() ensures the widget fills the available space. The count is displayed in the Text composable using a large font size (50.sp) and is aligned to the center. The button labeled "Up" allows the user to increment the counter when pressed. Its size is set to 100x50 dp, with a background color of 0xFFB6C0C9.

Managing State with Preferences

One of the key features of this app widget is its ability to retain the counter value across widget updates. To achieve this, Jetpack’s DataStore is used to store the counter state persistently. The countPreferenceKey is defined as a key to access the stored counter value, and it is retrieved using currentState<Preferences>(). If the key is not already present (e.g., the widget is used for the first time), a default value of 0 is provided.

The GlanceStateDefinition is overridden to use PreferencesGlanceStateDefinition, which binds the widget to DataStore’s state management. This allows the counter value to be retrieved and updated in a persistent manner without needing a backend service.

Handling Button Clicks and State Updates

The button's functionality is handled using the actionRunCallback<UpdateActionCallback>(), which triggers an action when the button is pressed. This is where the counter's value is incremented. The parameters passed to the callback include the updated count value (count + 1).

The UpdateActionCallback class implements the ActionCallback interface. The onRun() function, which gets executed when the button is pressed, retrieves the new count from the action parameters. It then updates the widget’s state by calling updateAppWidgetState(). This function writes the new counter value to DataStore by converting the preferences into a mutable form and assigning the updated count. Finally, CounterWidget().update() refreshes the widget's UI to display the new count.

Widget Configuration and Manifest Setup

The widget’s appearance and size are defined in the res/xml/counter_widget_info.xml file. Here, the minWidth and minHeight attributes determine the default size of the widget, while resizeMode allows users to resize it both horizontally and vertically. Additionally, the widget is marked as a home screen widget with android:widgetCategory="home_screen".

In the AndroidManifest.xml, the CounterWidgetReceiver class is declared as a broadcast receiver that listens for app widget update events. It specifies that the receiver handles the APPWIDGET_UPDATE action. This ensures that the system knows when to update the widget, especially after a configuration change or an action-triggered update. The receiver is also linked to the widget’s XML configuration through the <meta-data> tag.

Dependencies

Lastly, in the build.gradle file, the Glance library is added as a dependency with the following line:

gradle

implementation "androidx.glance:glance-appwidget:1.0.0-alpha03"

This includes all the necessary APIs to create, manage, and update app widgets using Jetpack Compose.

Summary

In this article, we explored how to create a simple counter app widget using Kotlin and Jetpack Compose’s Glance framework. The CounterWidget uses a combination of DataStore for state persistence and Glance composables to build an interactive user interface directly on the home screen. We walked through how the UI is structured, how the state is managed and updated, and how interactions like button clicks are handled.

This example demonstrates the potential of using modern Compose tools to build efficient and functional widgets with ease. By utilizing Glance, developers can leverage the power of Jetpack Compose, even outside the app’s main interface, creating a more consistent and enjoyable user experience.


CounterWidget.kt

package com.cfsuman.widgetexamples

import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.glance.*
import androidx.glance.action.ActionParameters
import androidx.glance.action.actionParametersOf
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.glance.layout.*
import androidx.glance.state.GlanceStateDefinition
import androidx.glance.state.PreferencesGlanceStateDefinition
import androidx.glance.text.Text
import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider

private val countPreferenceKey = intPreferencesKey("count-key")
private val countParamKey = ActionParameters.Key<Int>("count-key")


class CounterWidget : GlanceAppWidget(){

    override val stateDefinition: GlanceStateDefinition<*> =
        PreferencesGlanceStateDefinition

    @Composable
    override fun Content(){
        val prefs = currentState<Preferences>()
        val count = prefs[countPreferenceKey] ?: 0

        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalAlignment = Alignment.CenterVertically,
            modifier = GlanceModifier
                .background(Color(0xFFD3E9FA))
                .fillMaxSize()
        ) {
            Text(
                text = count.toString(),
                modifier = GlanceModifier.fillMaxWidth(),
                style = TextStyle(
                    textAlign = TextAlign.Center,
                    color = ColorProvider(Color.Blue),
                    fontSize = 50.sp
                )
            )

            Spacer(modifier = GlanceModifier.padding(8.dp))

            Button(
                text = "Up",
                modifier = GlanceModifier
                    .background(Color(0xFFB6C0C9))
                    .size(100.dp,50.dp),
                onClick = actionRunCallback<UpdateActionCallback>(
                    parameters = actionParametersOf(
                        countParamKey to (count + 1)
                    )
                )
            )
        }
    }
}


class UpdateActionCallback : ActionCallback{
    override suspend fun onRun(context: Context, glanceId: GlanceId,
                               parameters: ActionParameters) {

        val count = requireNotNull(parameters[countParamKey])

        updateAppWidgetState(
            context = context,
            definition = PreferencesGlanceStateDefinition,
            glanceId = glanceId
        ){ preferences ->
            preferences.toMutablePreferences()
                .apply {
                    this[countPreferenceKey] = count
                }
        }

        CounterWidget().update(context,glanceId)
    }
}


class CounterWidgetReceiver : GlanceAppWidgetReceiver(){
    override val glanceAppWidget:GlanceAppWidget = CounterWidget()
}
res/xml/counter_widget_info.xml

<appwidget-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/app_name"
    android:minWidth="100dp"
    android:minHeight="50dp"
    android:resizeMode="horizontal|vertical"
    android:targetCellWidth="3"
    android:targetCellHeight="2"
    android:widgetCategory="home_screen"
    />
AndroidManifest.xml [Part]

<receiver
	android:name=".CounterWidgetReceiver"
	android:enabled="@bool/glance_appwidget_available"
	android:exported="false">
	<intent-filter>
		<action android:name
			="android.appwidget.action.APPWIDGET_UPDATE" />
	</intent-filter>
	<meta-data
		android:name="android.appwidget.provider"
		android:resource="@xml/counter_widget_info" />
</receiver>

build.gradle [app] [dependencies]

implementation "androidx.glance:glance-appwidget:1.0.0-alpha03"
More android jetpack compose tutorials

Jetpack compose: ViewModel Room delete clear data

Introduction

This code demonstrates a Jetpack Compose application that utilizes Room database to manage employee data. The application allows adding new employees, deleting existing ones, and clearing all data. It leverages ViewModel to manage the data state and provide a single source of truth for the UI.

Breakdown

The code is organized into several classes:

  • MainActivity.kt: This is the main activity that sets the content of the app using a Composable function called MainContent.
  • MainContent.kt: This composable function builds the UI for the app. It retrieves data from the ViewModel, displays the number of employees, and presents a list of employees. It also provides buttons for adding new employees and clearing all data.
  • RoomSingleton.kt: This class defines a Room database named "roomdb" with a single entity - Employee. It provides a singleton instance using the getInstance method.
  • Employee.kt: This data class represents an employee with an ID (Long) and a full name (String).
  • EmployeeDao.kt: This interface defines the data access methods for the Employee table in the Room database. It includes methods for fetching all employees, inserting a single or multiple employees, updating an employee, deleting an employee, and clearing all data.
  • EmployeeViewModel.kt: This ViewModel class manages the employee data. It retrieves data from the database on initialization and provides functions for adding, updating, deleting, and clearing employees. It also exposes a StateFlow of employees to be observed by the UI.
  • build.gradle: This file specifies the dependencies required for the project, including Room and ViewModel libraries.

Summary

This code showcases a well-structured Jetpack Compose application with Room database integration. The ViewModel effectively manages the data state and interacts with the database through the defined Dao methods. The UI is built using Compose functions and reacts to changes in the data flow. Overall, the code demonstrates good practices for data management and UI updates in a Jetpack Compose application.


MainActivity.kt

package com.example.composeroomexample

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import java.util.*

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { MainContent() }
    }
}


@Composable
fun MainContent(){
    val model = viewModel<EmployeeViewModel>()
    val list = model.employees.collectAsState()
    Log.d("xapp", "Recomposition")

    Column(
        modifier = Modifier.padding(12.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(12.dp)
        ){
            Button(onClick = {
                val employees = mutableListOf<Employee>()
                for (i in 1..10){
                    employees.add(
                        Employee(
                            null,UUID.randomUUID().toString()
                        )
                    )
                }
                model.insertAll(employees)
            }) { Text(text = "Add 10 Employees") }

            Button(onClick = {
                model.clear()
            }) { Text(text = "Clear") }
        }

        Text(
            text ="${list.value.size} Employees",
            fontWeight = FontWeight.Bold
        )

        LazyColumn(
            verticalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            itemsIndexed(list.value) { index,item ->
                Card(
                    modifier = Modifier
                        .fillMaxWidth(),
                    backgroundColor = Color(0xFFC0E8D5)
                ) {
                    Row(
                        modifier = Modifier.padding(12.dp),
                        verticalAlignment = Alignment.CenterVertically,
                        horizontalArrangement = Arrangement.spacedBy(12.dp)
                    ) {
                        Box(
                            Modifier.size(48.dp).clip(CircleShape)
                                .background(Color(0xFF8DB600)),
                            contentAlignment = Alignment.Center
                        ) {
                            Text(
                                text = "${item.id}",
                                style = MaterialTheme.typography.h6
                            )
                        }

                        Spacer(modifier = Modifier.width(12.dp))
                        Text(
                            text = item.fullName.take(12),
                            style = MaterialTheme.typography.h6
                        )

                        Spacer(modifier = Modifier.weight(1F))
                        IconButton(onClick = {
                            model.delete(item)
                        }) {
                            Icon(
                                imageVector = Icons.Default.Delete,
                                contentDescription = "",
                                tint = Color(0xFFD3212D)
                            )
                        }
                    }
                }
            }
        }
    }
}
RoomSingleton.kt

package com.example.composeroomexample

import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context

@Database(entities = [Employee::class], version = 1, exportSchema = false)
abstract class RoomSingleton : RoomDatabase() {
    abstract fun employeeDao():EmployeeDao

    companion object {
        private var INSTANCE: RoomSingleton? = null
        fun getInstance(context: Context): RoomSingleton {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(
                    context,
                    RoomSingleton::class.java,
                    "roomdb")
                    .build()
            }
            return INSTANCE as RoomSingleton
        }
    }
}
RoomEntity.kt

package com.example.composeroomexample

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "employeeTbl")
data class Employee(
    @PrimaryKey
    var id:Long?,

    @ColumnInfo(name = "uuid")
    var fullName: String
)
RoomDao.kt

package com.example.composeroomexample

import androidx.room.*

@Dao
interface EmployeeDao{
    @Query("SELECT * FROM employeeTbl ORDER BY id DESC")
    fun allEmployees():List<Employee>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(employee:Employee)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(list:List<Employee>)

    @Update(onConflict = OnConflictStrategy.REPLACE)
    suspend fun update(employee:Employee)

    @Delete
    suspend fun delete(employee: Employee)

    @Query("DELETE FROM employeeTbl")
    suspend fun clear()
}
EmployeeViewModel.kt

package com.example.composeroomexample

import android.app.Application
import android.util.Log
import androidx.lifecycle.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch


class EmployeeViewModel(application:Application)
    : AndroidViewModel(application){

    private val db:RoomSingleton = RoomSingleton
        .getInstance(application)


    private val _Employees = MutableStateFlow(emptyList<Employee>())
    var employees: StateFlow<List<Employee>> = _Employees

    init {
        loadEmployees()
    }

    private fun loadEmployees() =  effect {
        _Employees.value = db.employeeDao().allEmployees()
        Log.d("xapp", "Loading employees")
    }

    fun insert(employee: Employee){
        viewModelScope.launch(Dispatchers.IO) {
            db.employeeDao().insert(employee)
            loadEmployees()
        }
    }

    fun insertAll(list:List<Employee>){
        viewModelScope.launch (Dispatchers.IO){
            db.employeeDao().insertAll(list)
            loadEmployees()
        }
    }

    fun update(employee: Employee){
        viewModelScope.launch(Dispatchers.IO){
            db.employeeDao().update(employee)
            loadEmployees()
        }
    }

    fun delete(employee: Employee){
        viewModelScope.launch(Dispatchers.IO) {
            db.employeeDao().delete(employee)
            loadEmployees()
        }
    }

    fun clear(){
        viewModelScope.launch(Dispatchers.IO) {
            db.employeeDao().clear()
            loadEmployees()
        }
    }

    private fun effect(block: suspend () -> Unit) {
        viewModelScope.launch(Dispatchers.IO) { block() }
    }
}
build.gradle [dependencies]

apply plugin: 'kotlin-kapt'

dependencies {
    def room_version = "2.4.2"
    implementation "androidx.room:room-runtime:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
    kapt "androidx.room:room-compiler:$room_version"

    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0-alpha06"
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha06'
}
More android jetpack compose tutorials

Jetpack compose: ViewModel Room edit update data

Introduction

This code demonstrates an Android application built with Jetpack Compose and Room Persistence Library. The application manages employee data, allowing users to add, update, and clear employee records. The code utilizes several key components:

  • Jetpack Compose: A modern UI framework for building declarative and reactive user interfaces.
  • Room: A persistence library that provides an abstraction layer for SQLite databases.
  • ViewModel: A lifecycle-aware class that manages data for a UI component.

Breakdown of the Code

The code is divided into several files:

  • MainActivity.kt: The main activity of the application. It sets up the content of the screen using setContent and utilizes EmployeeViewModel to manage employee data.
  • RoomSingleton.kt: Defines a singleton class for accessing the Room database instance.
  • RoomEntity.kt: Defines the data structure for an employee (Employee) with fields for ID and full name.
  • RoomDao.kt: Defines the Data Access Object (DAO) interface for interacting with the employee table in the database.
  • EmployeeViewModel.kt: The ViewModel class responsible for managing employee data. It interacts with the DAO to perform database operations and exposes functions to add, update, clear, and load employees.
  • build.gradle: Defines the necessary dependencies for the project, including Room and ViewModel libraries.

Summary

The code showcases a practical example of using Jetpack Compose with Room for data persistence. The ViewModel acts as a central point for data management, ensuring data consistency and providing a reactive way to update the UI based on changes in the database. The application demonstrates basic CRUD (Create, Read, Update, Delete) operations for employee data.


MainActivity.kt

package com.example.composeroomexample

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import java.util.*

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { MainContent() }
    }
}


@Composable
fun MainContent(){
    val model = viewModel<EmployeeViewModel>()
    val list = model.employees.collectAsState()
    Log.d("xapp", "Recomposition")

    Column(
        modifier = Modifier.padding(12.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(12.dp)
        ){
            Button(onClick = {
                val employees = mutableListOf<Employee>()
                for (i in 1..10){
                    employees.add(
                        Employee(
                            null,UUID.randomUUID().toString()
                        )
                    )
                }
                model.insertAll(employees)
            }) { Text(text = "Add 10 Employees") }

            Button(onClick = {
                model.clear()
            }) { Text(text = "Clear") }
        }

        Text(
            text ="${list.value.size} Employees",
            fontWeight = FontWeight.Bold
        )

        LazyColumn(
            verticalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            itemsIndexed(list.value) { index,item ->
                Card(
                    modifier = Modifier.fillMaxWidth(),
                    backgroundColor = Color(0xFFF1E9D2)
                ) {
                    Row(
                        modifier = Modifier.padding(12.dp),
                        verticalAlignment = Alignment.CenterVertically,
                        horizontalArrangement = Arrangement.spacedBy(12.dp)
                    ) {
                        Box(
                            Modifier.size(48.dp).clip(CircleShape)
                                .background(Color(0xFFF6ADC6)),
                            contentAlignment = Alignment.Center
                        ) {
                            Text(
                                text = "${item.id}",
                                style = MaterialTheme.typography.h6
                            )
                        }

                        Spacer(modifier = Modifier.width(12.dp))
                        Text(
                            text = item.fullName.take(12),
                            style = MaterialTheme.typography.h6
                        )

                        Spacer(modifier = Modifier.weight(1F))
                        IconButton(onClick = {
                            val updatedItem = Employee(
                                item.id,UUID.randomUUID().toString()
                            )
                            model.update(updatedItem)
                        }) {
                            Icon(
                                imageVector = Icons.Default.Edit,
                                contentDescription = ""
                            )
                        }
                    }
                }
            }
        }
    }
}
RoomSingleton.kt

package com.example.composeroomexample

import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context

@Database(entities = [Employee::class], version = 1, exportSchema = false)
abstract class RoomSingleton : RoomDatabase() {
    abstract fun employeeDao():EmployeeDao

    companion object {
        private var INSTANCE: RoomSingleton? = null
        fun getInstance(context: Context): RoomSingleton {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(
                    context,
                    RoomSingleton::class.java,
                    "roomdb")
                    .build()
            }
            return INSTANCE as RoomSingleton
        }
    }
}
RoomEntity.kt

package com.example.composeroomexample

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "employeeTbl")
data class Employee(
    @PrimaryKey
    var id:Long?,

    @ColumnInfo(name = "uuid")
    var fullName: String
)
RoomDao.kt

package com.example.composeroomexample

import androidx.room.*

@Dao
interface EmployeeDao{
    @Query("SELECT * FROM employeeTbl ORDER BY id DESC")
    fun allEmployees():List<Employee>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(employee:Employee)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(list:List<Employee>)

    @Update(onConflict = OnConflictStrategy.REPLACE)
    suspend fun update(employee:Employee)

    @Query("DELETE FROM employeeTbl")
    suspend fun clear()
}
EmployeeViewModel.kt

package com.example.composeroomexample

import android.app.Application
import android.util.Log
import androidx.lifecycle.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch


class EmployeeViewModel(application:Application)
    : AndroidViewModel(application){

    private val db:RoomSingleton = RoomSingleton
        .getInstance(application)


    private val _Employees = MutableStateFlow(emptyList<Employee>())
    var employees: StateFlow<List<Employee>> = _Employees

    init {
        loadEmployees()
    }

    fun loadEmployees() =  effect {
        _Employees.value = db.employeeDao().allEmployees()
        Log.d("xapp", "Loading employees")
    }

    fun insert(employee: Employee){
        viewModelScope.launch(Dispatchers.IO) {
            db.employeeDao().insert(employee)
            loadEmployees()
        }
    }

    fun insertAll(list:List<Employee>){
        viewModelScope.launch (Dispatchers.IO){
            db.employeeDao().insertAll(list)
            loadEmployees()
        }
    }

    fun update(employee: Employee){
        viewModelScope.launch(Dispatchers.IO){
            db.employeeDao().update(employee)
            loadEmployees()
        }
    }

    fun clear(){
        viewModelScope.launch(Dispatchers.IO) {
            db.employeeDao().clear()
            loadEmployees()
        }
    }

    private fun effect(block: suspend () -> Unit) {
        viewModelScope.launch(Dispatchers.IO) { block() }
    }
}
build.gradle [dependencies]

apply plugin: 'kotlin-kapt'

dependencies {
    def room_version = "2.4.2"
    implementation "androidx.room:room-runtime:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
    kapt "androidx.room:room-compiler:$room_version"

    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0-alpha06"
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha06'
}
More android jetpack compose tutorials