Open In App

Android Jetpack Compose – Store Information using State Class and Data Class

Improve
Improve
Like Article
Like
Save
Share
Report

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:

  1. Familiar with Kotlin and OOP Concepts as well
  2. Basic understanding of Jetpack Compose
  3. What is State Management in android?
  4. 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"?>
    xmlns:tools="http://schemas.android.com/tools">
  
    <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.



Last Updated : 31 Dec, 2022
Like Article
Save Article
Previous
Next
Share your thoughts in the comments
Similar Reads