Android Jetpack Compose – Store Information using State Class and Data Class
Jetpack Compose is a new UI toolkit from Google used to create native Android UI. It speeds up and simplifies UI development using less code, Kotlin APIs, and powerful tools.
Prerequisites:
- Familiar with Kotlin and OOP Concepts as well
- Basic understanding of Jetpack Compose
- What is State Management in android?
- Android Studio Canary Version
A sample video is given below to get an idea about what we will do in this article.
Step by Step Implementation
Step 1: Create a New Project in Android Studio
To create a new project in Android Studio using Jetpack Compose please refer to:- How to Create a New Project in Android Studio Canary Version with Jetpack Compose.
Step 2: Let’s first review the build.gradle(module level)
Remember to double-check this file that everything is included. If something is missing add those blocks from the below snippet.
plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' } android { namespace 'com.example.store_state_info' compileSdk 33 defaultConfig { applicationId "com.example.store_state_info" minSdk 21 targetSdk 33 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary true } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion compose_version } packagingOptions { resources { excludes += '/META-INF/{AL2.0,LGPL2.1}' } } } dependencies { implementation 'androidx.core:core-ktx:1.9.0' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' implementation 'androidx.activity:activity-compose:1.6.1' implementation 'androidx.appcompat:appcompat:1.5.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.4' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0-RC" implementation'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0' }
Step 3: Now let’s review the build.gradle(project level)
Remember to double-check this file that everything is included. If something is missing add those blocks from the below snippet.
buildscript { ext { kotlin_version = '1.0.1-2' compose_version = '1.1.0-rc01' } // Requirements repositories { google() mavenCentral() } dependencies { // dependency classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" } }// Top-level build file where you can add configuration options // common to all sub-projects/modules. plugins { id 'com.android.application' version '7.3.0-alpha01' apply false id 'com.android.library' version '7.3.0-alpha01' apply false id 'org.jetbrains.kotlin.android' version '1.6.0' apply false }
Step 4: Now rename MainActivity.kt to StateActivity.kt
We can put the same code to MainActivity.kt as well, but it’s a good idea to create or rename the file to reflect its role. Once you change this we also need to modify the AndroidManifest.xml activity tag to the renamed file since the default is MainActivity. You can refer to the below snippet of AndroidManifest.xml.
XML
<? xml version = "1.0" encoding = "utf-8" ?> < application android:allowBackup = "true" android:dataExtractionRules = "@xml/data_extraction_rules" android:fullBackupContent = "@xml/backup_rules" android:icon = "@mipmap/ic_launcher" android:label = "@string/app_name" android:roundIcon = "@mipmap/ic_launcher_round" android:supportsRtl = "true" android:theme = "@style/Theme.Store_State_Info" tools:targetApi = "31" > < activity android:name = ".StateActivity" android:exported = "true" android:label = "@string/app_name" android:theme = "@style/Theme.AppCompat" > < intent-filter > < action android:name = "android.intent.action.MAIN" /> < category android:name = "android.intent.category.LAUNCHER" /> </ intent-filter > </ activity > </ application > </ manifest > |
Step 5: Importing necessary modules
It’s good practice to import only the necessary modules rather than importing all the modules and using only a few.
Kotlin
// Code for storing state information // using state class and // data class in android jetpack compose // Please replace the name of package // with your project name package com.example.store_state_info import android.os.Bundle import androidx.activity.compose.setContent import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Divider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.unit.dp |
Step 6: Implement AppCompatActivity() to class StateActivity
Kotlin
class StateActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) // This sets the @Composable function as // the root view of the activity. // This is meant to replace the .xml // file that we would typically // set using the setContent(R.id.xml_file) method. setContent { Column(modifier = Modifier.fillMaxWidth()) { StateComponent() // A pre-defined composable that renders a // thin line on the screen that makes it // easy to group contents Divider() ModelComponent() } } } } |
Step 7: Create a composable function stateComponent for storing the state information
Before we go, there are a few things you should be aware of:
- Composable annotations: The @Composable annotation is used to indicate a Composable function. Composable functions can only be invoked from other composable functions. Consider composable functions to be comparable to Lego blocks in that each composable function is constructed up of smaller composable functions
- Row: The row is composable and places its children horizontally. It is comparable to a LinearLayout in that it is horizontally oriented. Additionally, we add a modifier to the row
- Modifier: Modifiers serve as examples of the decorator pattern and are used to alter the composable to which they are applied. In this case, we use the Modifier to set the Row up to take up the whole available width using the Modifier.fillMaxWidth() modifier
- elevation: Elevation is used for giving some shadow effects to the button
Kotlin
@Composable fun StateComponent() { var counter by remember { mutableStateOf( 0 ) } Text( "Example using state class to store state" ) Row(modifier = Modifier.fillMaxWidth()) { // This Row consists of two buttons. // We wanted to ensure that both these buttons occupy // equal amount of width. We do that by using the // LayoutWeight modifier and passing equal // weight to both the buttons. This is similar to // how we used layout_weight with LinearLayouts // in the old Android UI Toolkit. Button( modifier = Modifier.padding( 16 .dp).weight(1f), elevation = ButtonDefaults.elevation( 5 .dp), // We increment the counter every time // this button is clicked. onClick = { counter++ }) { Text(text = "Increment" , modifier = Modifier.padding( 16 .dp)) } Button( modifier = Modifier.padding( 16 .dp).weight(1f), elevation = ButtonDefaults.elevation( 5 .dp), onClick = { counter = 0 }) { Text(text = "Reset" , modifier = Modifier.padding( 16 .dp)) } } // This text composable is just used to // display the current value of the counter. Text(text = "Counter value is $counter" , modifier = Modifier.padding( 16 .dp)) } |
Step 8: Alternatively, we can use a dataclass for storing the state information
In this example, the state we want our composable to observe is stored in a data class called [CounterState]. The only difference between this step 8 and the previous step 7 is that it uses a data class to hold the value of our state. Additionally, we mutate the values of a data class using the copy() method
Kotlin
@Composable fun ModelComponent() { var counterState by remember { mutableStateOf(CounterState()) } Text( "Example using Model class to store state" ) // In this example, we assign add a modifier to the Row // and ask it to extend the full width available to it. // Alternatively, we could've assigned a fixed width to // this row using Modifier.width(val width: Dp). Row(modifier = Modifier.fillMaxWidth()) { // This Row consists of two buttons. // We wanted to ensure that both these buttons occupy // equal amount of width. We do that by using the // LayoutWeight modifier and passing equal // weight to both the buttons. This is similar to // how we used layout_weight with LinearLayouts // in the old Android UI Toolkit. Button( modifier = Modifier.padding( 16 .dp).weight(1f), elevation = ButtonDefaults.elevation( 5 .dp), onClick = { counterState = counterState.copy(counter = counterState.counter + 1 ) }) { Text(text = "Increment" , modifier = Modifier.padding( 16 .dp)) } Button( modifier = Modifier.padding( 16 .dp).weight(1f), elevation = ButtonDefaults.elevation( 5 .dp), onClick = { counterState = counterState.copy(counter = 0 ) }) { Text(text = "Reset" , modifier = Modifier.padding( 16 .dp)) } } // This text composable is just used to // the current value of the counter. Text(text = "Counter value is ${counterState.counter}" , modifier = Modifier.padding( 16 .dp)) } data class CounterState(val counter: Int = 0 ) |
Step 9: If you want to preview your AnimateColorComponent then continue else you can skip it
Significance of @preview and composable annotations:
- Instead of downloading the app to an Android device or emulator, Android Studio allows you to preview your composable functions within the IDE itself. This is an excellent feature since it allows you to preview every one of your components—or composable functions—right inside the IDE
- The composable function cannot accept any parameters, which is the fundamental constraint. You may just include your component within another composable function that doesn’t take any arguments and calls your composable function with the necessary parameters
- Also, don’t forget to annotate it with @Preview & @Composable annotations
Kotlin
@Preview ( "Example using state delegate" ) @Composable fun StateComponentPreview() { Column(modifier = Modifier.fillMaxHeight()) { StateComponent() } } @Preview ( "Example using Model annotation" ) @Composable fun ModelComponentPreview() { Column(modifier = Modifier.fillMaxHeight()) { ModelComponent() } } |
Step 10: Complete code snippet
Kotlin
// Code for storing state information // using state class and // data class in android jetpack compose // Please replace the name of package // with your project name package com.example.store_state_info import android.os.Bundle import androidx.activity.compose.setContent import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Divider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.unit.dp class StateActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) // This sets the @Composable function // as the root view of the activity. // This is meant to replace the .xml // file that we would typically // set using the setContent(R.id.xml_file) method. setContent { Column(modifier = Modifier.fillMaxWidth()) { StateComponent() // A pre-defined composable that renders a // thin line on the screen that makes it // easy to group contents Divider() ModelComponent() } } } } @Composable fun StateComponent() { var counter by remember { mutableStateOf( 0 ) } Text( "Example using state class to store state" ) Row(modifier = Modifier.fillMaxWidth()) { // This Row consists of two buttons. // We wanted to ensure that both these buttons occupy // equal amount of width. We do that by using the // LayoutWeight modifier and passing equal // weight to both the buttons. This is similar to // how we used layout_weight with LinearLayouts // in the old Android UI Toolkit. Button( modifier = Modifier.padding( 16 .dp).weight(1f), elevation = ButtonDefaults.elevation( 5 .dp), // We increment the counter every time // this button is clicked. onClick = { counter++ }) { Text(text = "Increment" , modifier = Modifier.padding( 16 .dp)) } Button( modifier = Modifier.padding( 16 .dp).weight(1f), elevation = ButtonDefaults.elevation( 5 .dp), onClick = { counter = 0 }) { Text(text = "Reset" , modifier = Modifier.padding( 16 .dp)) } } // This text composable is just used to // display the current value of the counter. Text(text = "Counter value is $counter" , modifier = Modifier.padding( 16 .dp)) } /** * In this example, the state that we want our composables * to observe is stored in a data class called [CounterState]. * The only difference between this step 8 and the previous * step 7 is that it uses a data class to hold the value * of our state. Additionally, we mutate the values of a * data class using the copy() method. */ @Composable fun ModelComponent() { var counterState by remember { mutableStateOf(CounterState()) } Text( "Example using Model class to store state" ) // In this example, we assign add a modifier to the Row // and ask it to extend the full width available to it. // Alternatively, we could've assigned a fixed width to // this row using Modifier.width(val width: Dp). Row(modifier = Modifier.fillMaxWidth()) { // This Row consists of two buttons. // We wanted to ensure that both these buttons occupy // equal amount of width. We do that by using the // LayoutWeight modifier and passing equal // weight to both the buttons. This is similar to // how we used layout_weight with LinearLayouts // in the old Android UI Toolkit. Button( modifier = Modifier.padding( 16 .dp).weight(1f), elevation = ButtonDefaults.elevation( 5 .dp), onClick = { counterState = counterState.copy(counter = counterState.counter + 1 ) }) { Text(text = "Increment" , modifier = Modifier.padding( 16 .dp)) } Button( modifier = Modifier.padding( 16 .dp).weight(1f), elevation = ButtonDefaults.elevation( 5 .dp), onClick = { counterState = counterState.copy(counter = 0 ) }) { Text(text = "Reset" , modifier = Modifier.padding( 16 .dp)) } } // This text composable is just used to // the current value of the counter. Text(text = "Counter value is ${counterState.counter}" , modifier = Modifier.padding( 16 .dp)) } data class CounterState(val counter: Int = 0 ) @Preview ( "Example using state delegate" ) @Composable fun StateComponentPreview() { Column(modifier = Modifier.fillMaxHeight()) { StateComponent() } } @Preview ( "Example using Model annotation" ) @Composable fun ModelComponentPreview() { Column(modifier = Modifier.fillMaxHeight()) { ModelComponent() } } |
Output:
As we can see with the help of jetpack compose we stored state information using state class and data class.
Please Login to comment...