Create a dynamic list name

Hi DW. Anyone who knows how can I achieve this? List<String> dynamic[i];
The reason why I need this is that I'm creating a dynamic list with nested lists so there's no fixed number of data the list can be, this is inside the for statement which receive the json data which is multidimention array so I want for each item in the json data create 1st the parent list then create nested list for it.

Currently it mixes data and I see that it because of this.

Android Native – How to use UseCases

Introduction

In Android development, UseCases are classes that encapsulate business logic that are often used in ViewModel classes. UseCases belong to the optional Domain layer in Android apps, so they are not required, but can reduce your ViewModels complexity and make your application easier to test.

In this tutorial, we will learn how to add UseCases into an Android app.

Goals

At the end of the tutorial, you would have learned:

  1. How to add UseCases.
  2. How to use the special invoke() operator with a UseCase.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 3.
Prerequisite Knowledge
  1. Intermediate Android.
  2. Basic MVVM architecture.
  3. StateFlow.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Replace the content of activitiy_main.xml with the code below. This simply adds three new TextViews and a Button in a vertical chain.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/button_loadNext"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/load_next_item"
            app:layout_constraintBottom_toTopOf="@+id/textView_item1"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <TextView
            android:id="@+id/textView_item1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toTopOf="@+id/textView_item2"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/button_loadNext"
            tools:text="Item 1" />
    
        <TextView
            android:id="@+id/textView_item2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toTopOf="@+id/textView_item3"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView_item1"
            tools:text="Item 2" />
    
        <TextView
            android:id="@+id/textView_item3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView_item2"
            tools:text="Item 3" />
    
     </androidx.constraintlayout.widget.ConstraintLayout>
  3. Add the <string> resource below into your strings.xml file.

     <string name="load_next_item">Load Next Item</string>
  4. Add the dependency to Lifecycle and Activity KTX below into your project.

     //Lifecycle
     implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
    
     //For convenient viewModels() extension function
     implementation "androidx.activity:activity-ktx:1.4.0"
  5. Create a new data class called MainActivitiyUiState to hold the UI state for the MainActivity.

     data class MainActivityUiState(
        val item1: String,
        val item2: String,
        val item3: String
     )
  6. Create a new class called MainActivityViewModel using the code below. This ViewModel exposes the UI state as an immutable Flow. The function loadNext() is used to ask the ViewModel to load the next source of data. To reduce complexity, the ViewModel will just return another MainActivityUiState object with randomized values. In the real world, you would most likely retrieve this data from a repository (or a UseCase, which we will learn soon).

     class MainActivityViewModel : ViewModel() {
        //Private mutable flow
        private val _stateFlow = MutableStateFlow(
            getUiState()
        )
    
        //Exposes mutable flow as immutable flow
        val stateFlow: StateFlow<MainActivityUiState> = _stateFlow
    
        //MainActivity will call this function on button click
        fun loadNext(){
            _stateFlow.value = getUiState()
        }
    
        //Convenient function to remove code duplication
        private fun getUiState() = MainActivityUiState(
            item1 = "${Random.nextInt()}",
            item2 = "${Random.nextInt()}",
            item3 = "${Random.nextInt()}"
        )
    
     }
  7. Replace the content of MainActivity with the code below. Our MainActivity class now contains a reference to the MainActivityViewModel created in the previous step. We are also consuming the Flow<MainActivityUiState> and use that to populate the Views.

     class MainActivity : AppCompatActivity() {
        private val mainActivityViewModel: MainActivityViewModel by viewModels()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            //Gets the button reference
            val button = findViewById<Button>(R.id.button_loadNext)
    
            button.setOnClickListener {
                //Calls ViewModel function after button click
                mainActivityViewModel.loadNext()
            }
    
            //Gets reference to the three TextView Views
            val item1 = findViewById<TextView>(R.id.textView_item1)
            val item2 = findViewById<TextView>(R.id.textView_item2)
            val item3 = findViewById<TextView>(R.id.textView_item3)
    
            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.STARTED){
                    mainActivityViewModel.stateFlow.collect {
                        item1.text = it.item1
                        item2.text = it.item2
                        item3.text = it.item3
                    }
                }
            }
        }
     }
Project Overview

Our app is a simple app with a single Button that triggers the ViewModel to serve new UI data. Every button click should update the TextView Views with new random integer values. Refer to the GIF below for its behavior.

DaniwebUseCase.gif

Right now, we are just faking data retrieval directly in the ViewModel, because we are not using any UseCase yet, this is similar to the ViewModel calling the repository directly. While this approach is not wrong, as your ViewModel grows, it would be better to move this logic into something else. This is where UseCases come in.

Create a UseCase

UseCase classes are not specific to Android, and sometimes they are also called Interactors. There is no pre-made Android-specific interface for your UseCase class to implement. There is no need to implement an interface unless you need to swap UseCase implementations in your ViewModel.

Each UseCase should only perform one business logic. The Google naming convention for UseCase classes is described below.

    verb in present tense + noun/what (optional) + UseCase

Based on my experience, Android developers DO follow the convention above, so we will follow this convention as well in our tutorial.

Follow the steps below to create a UseCase.

  1. Create a class called GetNextFakeBusinessDataUseCase from the code below.

     class GetNextFakeBusinessDataUseCase {
        fun getNextFakeBusinessData(): FakeBusinessData {
            return FakeBusinessData(
                item1 = "${Random.nextInt()}",
                item2 = "${Random.nextInt()}",
                item3 = "${Random.nextInt()}"
            )
        }
     }
  2. FakeBusinessData does not exist, so you will see compile errors. Create another data class called FakeBusinessData from the code below.

     data class FakeBusinessData(
        val item1: String,
        val item2: String,
        val item3: String
     )
  3. You might have noticed that FakeBusinessData looks identical to MainActivityUiState, so why cant the UseCase simply return MainActivityUiState instead? This is because the UseCase belongs to the Domain layer, and should not be aware of the UI data at all. It is the ViewModels job to transform the business data into UI data. In this simple tutorial, we can also replace MainActivityUiState with FakeBusinessData so the ViewModel will not have to do any transformation, but we will not skip that step here to emphasize the distinction between business data and UI data.

  4. Back in the ViewModel, add a new instance of GetNextFakeBusinessDataUseCase like below. You can also inject an instance using Hilt if you prefer.

     private val getNextFakeBusinessDataUseCase = GetNextFakeBusinessDataUseCase()
  5. Modifies the convenient getUiState() function to get and then transforms the business data into UI data.

     private fun getUiState(): MainActivityUiState {
        val next = getNextFakeBusinessDataUseCase.getNextFakeBusinessData()
        return MainActivityUiState(
            item1 = next.item1,
            item2 = next.item2,
            item3 = next.item3
        )
     }
  6. Run the app now to make sure that everything is still working as before.

The special invoke() function

In our ViewModel right now, the UseCase function call below seems long and unwieldy.

getNextFakeBusinessDataUseCase.getNextFakeBusinessData()

Fortunately, we can use the special operator function invoke() to improve readability a little bit more. Modify the UseCase class to the code below.

class GetNextFakeBusinessDataUseCase {
   operator fun invoke(): FakeBusinessData{
       return FakeBusinessData (
           item1 = "${Random.nextInt()}",
           item2 = "${Random.nextInt()}",
           item3 = "${Random.nextInt()}"
       )
   }
}

With this special syntax we can use any instance of our UseCase like a function call. In the ViewModel, we can now replace

val next = getNextFakeBusinessDataUseCase.getNextFakeBusinessData()

with

val next = getNextFakeBusinessDataUseCase()

Note that we also allowed to overload and return any type from invoke() as well.

Solution Code

MainActivity.kt

class MainActivity : AppCompatActivity() {
   private val mainActivityViewModel: MainActivityViewModel by viewModels()

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       //Gets the button reference
       val button = findViewById<Button>(R.id.button_loadNext)

       button.setOnClickListener {
           //Calls ViewModel function after button click
           mainActivityViewModel.loadNext()
       }

       //Gets reference to the three TextView Views
       val item1 = findViewById<TextView>(R.id.textView_item1)
       val item2 = findViewById<TextView>(R.id.textView_item2)
       val item3 = findViewById<TextView>(R.id.textView_item3)

       lifecycleScope.launch {
           repeatOnLifecycle(Lifecycle.State.STARTED){
               mainActivityViewModel.stateFlow.collect {
                   item1.text = it.item1
                   item2.text = it.item2
                   item3.text = it.item3
               }
           }
       }
   }
}

MainActivityViewModel.kt

class MainActivityViewModel : ViewModel() {
   private val getNextFakeBusinessDataUseCase = GetNextFakeBusinessDataUseCase()

   //Private mutable flow
   private val _stateFlow = MutableStateFlow(
       getUiState()
   )

   //Exposes mutable flow as immutable flow
   val stateFlow: StateFlow<MainActivityUiState> = _stateFlow

   //MainActivity will call this function on button click
   fun loadNext(){
       _stateFlow.value = getUiState()
   }

   private fun getUiState(): MainActivityUiState {
       //val next = getNextFakeBusinessDataUseCase.getNextFakeBusinessData()
       val next = getNextFakeBusinessDataUseCase()
       return MainActivityUiState(
           item1 = next.item1,
           item2 = next.item2,
           item3 = next.item3
       )
   }

}

/*
class MainActivityViewModel : ViewModel() {
   //Private mutable flow
   private val _stateFlow = MutableStateFlow(
       getUiState()
   )

   //Exposes mutable flow as immutable flow
   val stateFlow: StateFlow<MainActivityUiState> = _stateFlow

   //MainActivity will call this function on button click
   fun loadNext(){
       _stateFlow.value = getUiState()
   }

   //Convenient function to remove code duplication
   private fun getUiState() = MainActivityUiState(
       item1 = "${Random.nextInt()}",
       item2 = "${Random.nextInt()}",
       item3 = "${Random.nextInt()}"
   )

}*/

GetNextFakeBusinessData.kt

class GetNextFakeBusinessDataUseCase {
   operator fun invoke(): FakeBusinessData{
       return FakeBusinessData (
           item1 = "${Random.nextInt()}",
           item2 = "${Random.nextInt()}",
           item3 = "${Random.nextInt()}"
       )
   }
}

/*
class GetNextFakeBusinessDataUseCase {
   fun getNextFakeBusinessData(): FakeBusinessData {
       return FakeBusinessData(
           item1 = "${Random.nextInt()}",
           item2 = "${Random.nextInt()}",
           item3 = "${Random.nextInt()}"
       )
   }
}*/

FakeBusinessData.kt

data class FakeBusinessData(
   val item1: String,
   val item2: String,
   val item3: String
)

MainActivityUiState.kt

data class MainActivityUiState(
   val item1: String,
   val item2: String,
   val item3: String
)

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/button_loadNext"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/load_next_item"
       app:layout_constraintBottom_toTopOf="@+id/textView_item1"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintHorizontal_bias="0.5"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

   <TextView
       android:id="@+id/textView_item1"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toTopOf="@+id/textView_item2"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintHorizontal_bias="0.5"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/button_loadNext"
       tools:text="Item 1" />

   <TextView
       android:id="@+id/textView_item2"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toTopOf="@+id/textView_item3"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintHorizontal_bias="0.5"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/textView_item1"
       tools:text="Item 2" />

   <TextView
       android:id="@+id/textView_item3"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintHorizontal_bias="0.5"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/textView_item2"
       tools:text="Item 3" />

</androidx.constraintlayout.widget.ConstraintLayout>

string.xml

<resources>
   <string name="app_name">DaniwebAndroidNativeUseUseCases</string>
   <string name="load_next_item">Load Next Item</string>
</resources>

Module build.gradle

plugins {
   id 'com.android.application'
   id 'org.jetbrains.kotlin.android'
}

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.example.daniwebandroidnativeuseusecases"
       minSdk 21
       targetSdk 32
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

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

dependencies {
   //Lifecycle
   implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'

   //For convenient viewModels() extension function
   implementation "androidx.activity:activity-ktx:1.4.0"

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.1'
   implementation 'com.google.android.material:material:1.5.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
   testImplementation 'junit:junit:4.13.2'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
Summary

We have learned how to add UseCase class into our App. Note that, in the real world, the UseCase should call a repository to generate the fake business data instead of doing it by itself. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidNativeUseUseCases

Android Native – How to provide input data to Workers

Introduction

When working with WorkManager, it is important to know how to provide input data to your Workers. In this tutorial, we will learn how to provide basic input data to a Worker as well as when Workers are chained together.

Goals

At the end of the tutorial, you would have learned:

  1. How to provide input data to Workers.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 3.
Prerequisite Knowledge
  1. Intermedia Android.
  2. Basic WorkManager.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Add the dependency to WorkManager library below into your Module build.gradle file.

     //Kotlin Worker
     implementation "androidx.work:work-runtime-ktx:2.7.1"
  3. Replace activity_main.xml with the code below. This adds a Button to start the Workers later in the tutorial.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/button_startWorker"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/button_start_worker"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
     </androidx.constraintlayout.widget.ConstraintLayout>
  4. Add the <string> resource below into strings.xml.

     <string name="button_start_worker">Start Worker</string>
  5. Create a new class called DownloadWorker with the code below. This Worker does not do anything yet and simply returns Result.sucess() at this point.

     class DownloadWorker(appContext: Context, workerParams: WorkerParameters) :
        Worker(appContext, workerParams) {
    
        override fun doWork(): Result {
            return Result.success()
        }
    
     }
  6. In MainActivity, append the code below into the function onCreate(). This binds the Buttons OnClickListener to enqueue a Work request. In a real application, avoid using WorkManager directly in the UI because it actually belongs to the Data layer (Repository/Data Source).

     val button = findViewById<Button>(R.id.button_startWorker)
    
     val workManager = WorkManager.getInstance(applicationContext)
     val downloadWorkRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
        .build()
    
     button.setOnClickListener {
        workManager.enqueue(downloadWorkRequest)
     }
Provide Input Data to DownloadWorker

Realistically, a DownloadWorker should have access to some kind of URL to load data. We did not provide it with any data, so let us do that now.

  1. In MainActivity#onCreate(), after the workManager and before the downloadWorkRequest declarations, create a Data (androidx.work.Data) object using its Builder().

     val data = Data.Builder()
        .putString(WORKER_INPUT_KEY_URL, "daniweb.com")
        .build()
  2. WORKER_INPUT_KEY_URL is not a pre-made key, we will have to create it ourselves by declaring it at the top level in DownloadWorker.kt.

     const val WORKER_INPUT_KEY_URL = "0"
  3. Add another step to the OneTimeWorkRequestBuilder building process to put the data that we just built, using setInputData().

     val downloadWorkRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
        .setInputData(data)
        .build()
  4. Back at DownloadWorker#doWork(), access the input data using the inputData property access, and then add logic to fail the Worker if the value is null.

     override fun doWork(): Result {
        val url = inputData.getString(WORKER_INPUT_KEY_URL)
    
        return if (url != null){
            Result.success()
        } else {
            Result.failure()
        }
     }
  5. We can now run the app, but do not click on the Button yet.

  6. Click on the App Inspection tab in Android IDE -> Background Task Inspector. After clicking on the button, you should see that your DownloadWorker ran successfully.

3.png

Input Data to Chained Workers

Now we will learn how to use the inputData when Workers are chained together. Create two more Workers called ProcessDataWorker

class ProcessDataWorker (appContext: Context, workerParams: WorkerParameters) :
   Worker(appContext, workerParams){

   override fun doWork(): Result {
       val url = inputData.getString(WORKER_INPUT_KEY_URL)

       return if (url != null){
           Result.success()
       } else {
           Result.failure()
       }
   }

}

and PostProcessWorker

class PostProcessWorker (appContext: Context, workerParams: WorkerParameters) :
   Worker(appContext, workerParams){

   override fun doWork(): Result {
       val url = inputData.getString(WORKER_INPUT_KEY_URL)

       return if (url != null){
           Result.success()
       } else {
           Result.failure()
       }
   }

}

Both of these workers will fail if url is null. Modify MainActivity#onCreate() to call all three Workers in a chain like the code below.

val processDataWorkRequest = OneTimeWorkRequestBuilder<ProcessDataWorker>()
   .build()

val postProcessDataWorker = OneTimeWorkRequestBuilder<PostProcessWorker>()
   .build()

button.setOnClickListener {
   workManager
       .beginWith(downloadWorkRequest)
       .then(processDataWorkRequest)
       .then(postProcessDataWorker)
       .enqueue()
}

After running the app and clicking on the Button, we can see the Workers failing.

2.png

ProcessDataWorker failed because url is null, which also causes PostProcessWorker to fail. Apparently, the inputData that we provided to the first Worker (DownloadWorker) is only valid in DownloadWorker, but not in other Workers in the chain.

To pass the data down the chain, we can use the overloaded version of Result.success(), which optionally takes a Data object. Modify both doWork() functions of DownloadWorker and ProcessDataWorker with the code below.

override fun doWork(): Result {
   val url = inputData.getString(WORKER_INPUT_KEY_URL)

   return if (url != null){
       Result.success(inputData)
   } else {
       Result.failure()
   }
}

Now, if we run the code again and press the Button, we can see that all Workers are completed successfully. This is because inputData has been passed down the chain. You can also create a new Data object to pass down the chain if you wish.

1.png

Solution Code

MainActivity.kt

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val button = findViewById<Button>(R.id.button_startWorker)

       val workManager = WorkManager.getInstance(applicationContext)

       val data = Data.Builder()
           .putString(WORKER_INPUT_KEY_URL, "daniweb.com")
           .build()

       val downloadWorkRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
           .setInputData(data)
           .build()

       val processDataWorkRequest = OneTimeWorkRequestBuilder<ProcessDataWorker>()
           .build()

       val postProcessDataWorker = OneTimeWorkRequestBuilder<PostProcessWorker>()
           .build()

       button.setOnClickListener {
           workManager
               .beginWith(downloadWorkRequest)
               .then(processDataWorkRequest)
               .then(postProcessDataWorker)
               .enqueue()
       }
   }
}

DownloadWorker.kt

const val WORKER_INPUT_KEY_URL = "0"

class DownloadWorker(appContext: Context, workerParams: WorkerParameters) :
   Worker(appContext, workerParams) {

   override fun doWork(): Result {
       val url = inputData.getString(WORKER_INPUT_KEY_URL)

       return if (url != null){
           Result.success(inputData)
       } else {
           Result.failure()
       }
   }

}

ProcessDataWorker.kt

class ProcessDataWorker (appContext: Context, workerParams: WorkerParameters) :
   Worker(appContext, workerParams){

   override fun doWork(): Result {
       val url = inputData.getString(WORKER_INPUT_KEY_URL)

       return if (url != null){
           Result.success(inputData)
       } else {
           Result.failure()
       }
   }

}

PostProcessWorker.kt

class PostProcessWorker (appContext: Context, workerParams: WorkerParameters) :
   Worker(appContext, workerParams){

   override fun doWork(): Result {
       val url = inputData.getString(WORKER_INPUT_KEY_URL)

       return if (url != null){
           Result.success()
       } else {
           Result.failure()
       }
   }

}

activitiy_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/button_startWorker"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/button_start_worker"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Module build.gradle

plugins {
   id 'com.android.application'
   id 'org.jetbrains.kotlin.android'
}

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.example.daniwebprovideinputdatatoworkers"
       minSdk 21
       targetSdk 32
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

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

dependencies {
   //Kotlin Worker
   implementation "androidx.work:work-runtime-ktx:2.7.1"

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.1'
   implementation 'com.google.android.material:material:1.5.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
   testImplementation 'junit:junit:4.13.2'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

strings.xml

<resources>
   <string name="app_name">Daniweb Provide Input Data To Workers</string>
   <string name="button_start_worker">Start Worker</string>
</resources>
Summary

We have learned how to provide input data to Workers in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebProvideInputDataToWorkers.

When to Use keyExtractor Prop in React Native’s FlatList

In React Native, the FlatList component works well to render a long list of data. It renders only the items shown on the screen in a scrolling list and not all the data items at once.

To render a scrollable list of items using FlatList, you need to pass the required data prop to the component. The data prop accepts an array of items. Each item in the array represents a single item in the list. Another required prop is renderItem, which takes an item from the data and renders it on the list. This prop accepts a function that returns the JSX to be rendered.

Android Native – How to inject dependencies into a Worker with Hilt

Introduction

When using a Worker (from the WorkManager library), you might have wondered how to inject dependencies with Hilt. In this tutorial, we will learn how to inject dependencies into our Worker.

Goals

At the end of the tutorial, you would have learned:

  1. How to inject dependencies into a Worker.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 2.
Prerequisite Knowledge
  1. Intermediate Android.
  2. WorkManager library.
  3. Hilt.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Add the plugins below into the plugins{} block of the module build.gradle file.

     id 'kotlin-kapt'
     id 'dagger.hilt.android.plugin'
  3. Add the dependencies below into the module build.gradle file. These are Hilt and WorkManager dependencies.

     //Work Manager
     implementation 'androidx.work:work-runtime-ktx:2.7.1'
     //Hilt
     implementation 'com.google.dagger:hilt-android:2.41'
     implementation 'androidx.hilt:hilt-work:1.0.0'
     kapt 'com.google.dagger:hilt-compiler:2.41'
     kapt 'androidx.hilt:hilt-compiler:1.0.0'
  4. Add the Hilt Gradle plugin dependency to the Project build.gradle file.

     buildscript {
        repositories {
            google()
            mavenCentral()
        }
        dependencies {
            classpath 'com.google.dagger:hilt-android-gradle-plugin:2.41'
        }
     }
  5. Create an empty class called ExampleDependency in a file called ExampleDependency.kt. This will act as the dependency that we would later inject into our Worker.

     class ExampleDependency @Inject constructor()
  6. Create a class called MyApplication that extends Application, and then annotate it with @HiltAndroidApp.

     @HiltAndroidApp
     class MyApplication: Application() {
     }
  7. Add the Application class name into your manifests <application>.

     android:name=".MyApplication"
  8. Create a class called ExampleWorker using the code below. This is the Worker that we will inject ExampleDependency into later.

     class ExampleWorker constructor(
        context: Context,
        workerParams: WorkerParameters
     ) : Worker(context, workerParams) {
        override fun doWork(): Result {
            return Result.success()
        }
     }
Project Overview

Our project is quite simple. We only have the bare minimum needed for Hilt to work with a Worker. The ExampleWorker class has two dependencies declared in its constructor, but they are only dependencies required by the super class Worker and not the ExampleDependency that we are trying to inject.

class ExampleWorker constructor(
   context: Context,
   workerParams: WorkerParameters
)

There is a little bit of boilerplate code that we must write in the next few steps to be able to inject ExampleDependency into our ExampleWorker.

Apply the @HiltWorker Annotation

The first thing that we need to do is to apply @HiltWorker to ExampleWorker.

@HiltWorker
class ExampleWorker constructor(
   context: Context,
   workerParams: WorkerParameters
) : Worker(context, workerParams) {
   override fun doWork(): Result {
       return Result.success()
   }
}

Here are a few important things to note about this annotation:

  1. Your Worker is now available to be created by HiltWorkerFactory.
  2. Only dependencies scoped to SingletonComponent can be injected into your Worker.
Assisted Injection

Next, we need to apply the annotation @AssistedInject to the constructor of ExampleWorker.

@HiltWorker
class ExampleWorker @AssistedInject constructor(
   context: Context,
   workerParams: WorkerParameters
) : Worker(context, workerParams) {
   override fun doWork(): Result {
       return Result.success()
   }
}

Adding @AssistedInject tells Hilt that some dependencies will be provided with a custom factory. We do not have to create our own factory because Hilt already provides one for us (HiltWorkerFactory). HiltWorkerFactory will assist Hilt in injecting certain dependencies to ExampleWorker. The dependencies that HiltWorkerFactory should assist with are context and workerParams, and they must be annotated with @Assisted.

@HiltWorker
class ExampleWorker @AssistedInject constructor(
   @Assisted context: Context,
   @Assisted workerParams: WorkerParameters
) : Worker(context, workerParams) {
   override fun doWork(): Result {
       return Result.success()
   }
}

For the dependencies that HiltWorkerFactory cannot help with, such as our custom class ExampleDependency, we can simply list them for Hilt to inject.

@HiltWorker
class ExampleWorker @AssistedInject constructor(
   @Assisted context: Context,
   @Assisted workerParams: WorkerParameters,
   exampleDependency: ExampleDependency
) : Worker(context, workerParams) {
   override fun doWork(): Result {
       return Result.success()
   }
}
Configuring Worker Initialization

Next, we will need to inject an instance of HiltWorkerFactory and modifies Worker initialization.

  1. In MyApplication, implements Configuration.Provider.

     @HiltAndroidApp
     class MyApplication: Application(), Configuration.Provider {
    
     }
  2. Inject an instance of HiltWorkerFactory into MyApplication with field injection.

     @HiltAndroidApp
     class MyApplication: Application(), Configuration.Provider {
        @Inject lateinit var workerFactory: HiltWorkerFactory
    
     }
  3. Overrides getWorkManagerConfiguration().

     @HiltAndroidApp
     class MyApplication: Application(), Configuration.Provider {
        @Inject lateinit var workerFactory: HiltWorkerFactory
    
        override fun getWorkManagerConfiguration() =
            Configuration.Builder()
                .setWorkerFactory(workerFactory)
                .build()
     }
  4. Add the code below into the manifest, under <application>. This disables the default Worker initializer.

      <provider
         android:name="androidx.startup.InitializationProvider"
         android:authorities="${applicationId}.androidx-startup"
         android:exported="false"
         tools:node="merge">
         <meta-data
             android:name="androidx.work.WorkManagerInitializer"
             android:value="androidx.startup"
             tools:node="remove" />
      </provider>
  5. If your code shows a compile error because of the missing tools namespace, add the tools namespace to your manifest.

     xmlns:tools="http://schemas.android.com/tools"
Run the App

We are now ready to run the app. Add some code to queue up your Worker in MainActivitys onCreate() to check if our Worker can be instantiated by Hilt.

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)

   val workRequest = OneTimeWorkRequestBuilder<ExampleWorker>().build()
   WorkManager.getInstance(applicationContext).enqueue(workRequest)
}

After running the App, you should see a log entry similar to the one below, which means that our ExampleWorker has been created by Hilt successfully.

I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=84076b7b-5247-4d82-9ca4-7171e68c1aee, tags={ com.example.daniwebandroidnativeinjectworkerdepswithhilt.ExampleWorker } ]
Solution Code

MainActivity.kt

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val workRequest = OneTimeWorkRequestBuilder<ExampleWorker>().build()
       WorkManager.getInstance(applicationContext).enqueue(workRequest)
   }
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   package="com.example.daniwebandroidnativeinjectworkerdepswithhilt">

   <application
       android:name=".MyApplication"
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/Theme.DaniwebAndroidNativeInjectWorkerDepsWithHilt">
       <provider
           android:name="androidx.startup.InitializationProvider"
           android:authorities="${applicationId}.androidx-startup"
           android:exported="false"
           tools:node="merge">
           <meta-data
               android:name="androidx.work.WorkManagerInitializer"
               android:value="androidx.startup"
               tools:node="remove" />
       </provider>

       <activity
           android:name=".MainActivity"
           android:exported="true">
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>
       </activity>
   </application>

</manifest>

Project build.gradle

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
   repositories {
       google()
       mavenCentral()
   }
   dependencies {
       classpath 'com.google.dagger:hilt-android-gradle-plugin:2.41'
   }
}

plugins {
   id 'com.android.application' version '7.1.2' apply false
   id 'com.android.library' version '7.1.2' apply false
   id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
}

task clean(type: Delete) {
   delete rootProject.buildDir
}


Module **build.gradle**

plugins {
   id 'com.android.application'
   id 'org.jetbrains.kotlin.android'
   id 'kotlin-kapt'
   id 'dagger.hilt.android.plugin'
}

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.example.daniwebandroidnativeinjectworkerdepswithhilt"
       minSdk 21
       targetSdk 32
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

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

dependencies {
   //Work Manager
   implementation 'androidx.work:work-runtime-ktx:2.7.1'
   //Hilt
   implementation 'com.google.dagger:hilt-android:2.41'
   implementation 'androidx.hilt:hilt-work:1.0.0'
   kapt 'com.google.dagger:hilt-compiler:2.41'
   kapt 'androidx.hilt:hilt-compiler:1.0.0'

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.1'
   implementation 'com.google.android.material:material:1.5.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
   testImplementation 'junit:junit:4.13.2'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

ExampleWorker.kt

@HiltWorker
class ExampleWorker @AssistedInject constructor(
   @Assisted context: Context,
   @Assisted workerParams: WorkerParameters,
   exampleDependency: ExampleDependency
) : Worker(context, workerParams) {
   override fun doWork(): Result {
       return Result.success()
   }
}

ExampleDependency.kt

class ExampleDependency @Inject constructor()

MyApplication.kt

@HiltAndroidApp
class MyApplication: Application(), Configuration.Provider {
   @Inject lateinit var workerFactory: HiltWorkerFactory

   override fun getWorkManagerConfiguration() =
       Configuration.Builder()
           .setWorkerFactory(workerFactory)
           .build()
}
Summary

We have learned how to inject dependencies into a Worker in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidNativeInjectWorkerDepsWithHilt

Android Native – How to match sibling in Espresso tests

Introduction

Finding a View in Espresso tests can be quite confusing because there are so many matchers available. In this tutorial, we will learn how to find a View based on its sibling contents.

Goals

At the end of the tutorial, you would have learned:

  1. How to match against siblings in Espresso tests.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 2.
Prerequisite Knowledge
  1. Basic Android.
  2. Basic Espresso.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Replace the default activity_main.xml with the code below. This replaces the default TextView with a RecyclerView, constrains it, and assign it an id.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView_myRecycler"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
     </androidx.constraintlayout.widget.ConstraintLayout>
  3. Create a new sample layout called sample_viewholder.xml to act as the View for each RecyclerView item. This layout includes 3 TextView objects placed in a horizontal chain.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/layout_viewHolder"
        android:layout_marginVertical="16dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    
        <TextView
            android:id="@+id/textView_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/textView_title"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="Name" />
    
        <TextView
            android:id="@+id/textView_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/textView_age"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/textView_name"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="Title" />
    
        <TextView
            android:id="@+id/textView_age"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/textView_title"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="Age" />
     </androidx.constraintlayout.widget.ConstraintLayout>
  4. Create a new data class called SampleViewHolderUiState to hold the UI state for the sample_viewholder.xml.

     data class SampleViewHolderUiState(
        val name: String,
        val title: String,
        val age: Int
     )
  5. Create a new MyAdapter to adapt to SampleViewHolderUiState.

     class MyAdapter(private val dataSet: List<SampleViewHolderUiState>)
        : RecyclerView.Adapter<MyAdapter.SampleViewHolder>() {
    
        class SampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
            val textViewName: TextView = itemView.findViewById(R.id.textView_name)
            val textViewTitle: TextView = itemView.findViewById(R.id.textView_title)
            val textViewAge: TextView = itemView.findViewById(R.id.textView_age)
        }
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SampleViewHolder {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.sample_viewholder, parent, false)
    
            return SampleViewHolder(view)
        }
    
        override fun onBindViewHolder(holder: SampleViewHolder, position: Int) {
            holder.textViewName.text = dataSet[position].name
            holder.textViewTitle.text = dataSet[position].title
            holder.textViewAge.text = "${dataSet[position].age}"
        }
    
        override fun getItemCount() = dataSet.size
     }
  6. Append the code below to initialize the RecyclerView in MainActivity#onCreate().

     val recyclerView = findViewById<RecyclerView>(R.id.recyclerView_myRecycler)
    
     val sampleDataSet = listOf(
        SampleViewHolderUiState("John", "Student", 17),
        SampleViewHolderUiState("Mary", "Teacher", 28)
     )
    
     recyclerView.adapter = MyAdapter(sampleDataSet)
  7. It is a good idea to run the App now to see if everything was set up correctly. It should look the same as the screenshot below.
    1.png

  8. To keep things simple, we will just reuse the pre-created ExampleInstrumentedTest file. Update it with the content below.

     @RunWith(AndroidJUnit4::class)
     class ExampleInstrumentedTest {
    
        @get:Rule
        val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)
    
        @Test
        fun test_matchSibling() {
            onView(withId(R.id.layout_viewHolder))
                .check(matches(isDisplayed()))
        }
     }
The Problem

If you attempt to run the test now, then it will fail with the error message below.

androidx.test.espresso.AmbiguousViewMatcherException: 'view.getId() is <2131230949/com.example.daniwebandroidnativematchsiblinginespresso:id/layout_viewHolder>' matches multiple views in the hierarchy.

This is because there are multiple views with the id of layout_viewHolder. Our RecyclerView has two different ViewHolders after all. Fortunately, they both contain different text values for all three TextView (name, title, age). To be able to find a specific View, then we will have to dig deeper into the view hierarchy and filter against those inner values.

Match against sibling children

It is possible to find the first layout_viewHolder View by matching against a sibling and its children. To match against a sibling, we need to use the hasSibling matcher. Replace the content of test_matchSibling with the code below.

@Test
fun test_matchSibling() {
   onView(allOf(
       withId(R.id.layout_viewHolder),
       hasSibling(allOf(
           withId(R.id.layout_viewHolder),
           withChild(allOf(
               withId(R.id.textView_name),
               withText("Mary"),
               hasSibling(allOf(
                   withId(R.id.textView_title),
                   withText("Teacher"),
                   hasSibling(allOf(
                       withId(R.id.textView_age),
                       withText("28")
                   ))
               ))
           ))
       ))
   ))
}

The code above is totally overkill because we actually do not have to go too deep into the siblings hierarchy nor match against the siblings id at all. I just wanted to demonstrate how nested hasSibling looks like when we need to use it. The code below can also find the same View in only one line.

onView(hasSibling(withChild(withText("Mary"))))
Match against own child siblings

We can also match the View against its own children hierarchy. The code below demonstrates this.

@Test
fun test_matchChildrenSiblings(){
   onView(allOf(
       withId(R.id.layout_viewHolder),
       withChild(allOf(
           withId(R.id.textView_name),
           withText("John"),
           hasSibling(allOf(
               withId(R.id.textView_title),
               withText("Student"),
               hasSibling(allOf(
                   withId(R.id.textView_age),
                   withText("17")
               ))
           ))
       ))
   )).check(matches(isDisplayed()))

   //One liner
   //onView(withChild(withText("John")))
}
Solution Code

ExampleInstrumentedTest.kt

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {

   @get:Rule
   val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)

   @Test
   fun test_matchSibling() {
       onView(allOf(
           withId(R.id.layout_viewHolder),
           hasSibling(allOf(
               withId(R.id.layout_viewHolder),
               withChild(allOf(
                   withId(R.id.textView_name),
                   withText("Mary"),
                   hasSibling(allOf(
                       withId(R.id.textView_title),
                       withText("Teacher"),
                       hasSibling(allOf(
                           withId(R.id.textView_age),
                           withText("28")
                       ))
                   ))
               ))
           ))
       ))

       //One liner
       //onView(hasSibling(withChild(withText("Mary"))))
   }

   @Test
   fun test_matchChildrenSiblings(){
       onView(allOf(
           withId(R.id.layout_viewHolder),
           withChild(allOf(
               withId(R.id.textView_name),
               withText("John"),
               hasSibling(allOf(
                   withId(R.id.textView_title),
                   withText("Student"),
                   hasSibling(allOf(
                       withId(R.id.textView_age),
                       withText("17")
                   ))
               ))
           ))
       )).check(matches(isDisplayed()))

       //One liner
       //onView(withChild(withText("John")))
   }
}

MainActivity.kt

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val recyclerView = findViewById<RecyclerView>(R.id.recyclerView_myRecycler)

       val sampleDataSet = listOf(
           SampleViewHolderUiState("John", "Student", 17),
           SampleViewHolderUiState("Mary", "Teacher", 28)
       )

       recyclerView.adapter = MyAdapter(sampleDataSet)
   }
}

MyAdapter.kt

class MyAdapter(private val dataSet: List<SampleViewHolderUiState>)
   : RecyclerView.Adapter<MyAdapter.SampleViewHolder>() {

   class SampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
       val textViewName: TextView = itemView.findViewById(R.id.textView_name)
       val textViewTitle: TextView = itemView.findViewById(R.id.textView_title)
       val textViewAge: TextView = itemView.findViewById(R.id.textView_age)
   }

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SampleViewHolder {
       val view = LayoutInflater.from(parent.context)
           .inflate(R.layout.sample_viewholder, parent, false)

       return SampleViewHolder(view)
   }

   override fun onBindViewHolder(holder: SampleViewHolder, position: Int) {
       holder.textViewName.text = dataSet[position].name
       holder.textViewTitle.text = dataSet[position].title
       holder.textViewAge.text = "${dataSet[position].age}"
   }

   override fun getItemCount() = dataSet.size
}

SampleViewHolderUiState

data class SampleViewHolderUiState(
   val name: String,
   val title: String,
   val age: Int
)

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recyclerView_myRecycler"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

sample_viewholder.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/layout_viewHolder"
   android:layout_marginVertical="16dp"
   android:layout_width="match_parent"
   android:layout_height="wrap_content">

   <TextView
       android:id="@+id/textView_name"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toStartOf="@+id/textView_title"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       tools:text="Name" />

   <TextView
       android:id="@+id/textView_title"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toStartOf="@+id/textView_age"
       app:layout_constraintHorizontal_bias="0.5"
       app:layout_constraintStart_toEndOf="@+id/textView_name"
       app:layout_constraintTop_toTopOf="parent"
       tools:text="Title" />

   <TextView
       android:id="@+id/textView_age"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toEndOf="@+id/textView_title"
       app:layout_constraintTop_toTopOf="parent"
       tools:text="Age" />
</androidx.constraintlayout.widget.ConstraintLayout>
Summary

Congratulations, you have learned how to match against siblings in Espresso tests. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidNativeMatchSiblingInEspresso

Android Native – Run multiple statements in a Room transaction

Introduction

When working with Room, you might have wondered how to run multiple statements in a single transaction. Running multiple statements in one transaction has two main benefits:

  1. Your statements can reuse the same connection.
  2. They can all fail together if something goes wrong.

Regarding the first benefit, it is not well documented whether Room keeps the connection alive or closes them after every statement. Developers normally do not have to worry about manually closing database connections when using Room. Upon close inspection of the RoomDatabase source code, it appears that RoomDatabase does have a property called mAutoCloser that is used to run transactions with, at least when it is not null.

public void beginTransaction() {
    assertNotMainThread();
    if (mAutoCloser == null) {
        internalBeginTransaction();
    } else {
        mAutoCloser.executeRefCountingFunction(db -> {
            internalBeginTransaction();
            return null;
        });
    }
}

From the AutoCloser source code, this class is described as:

AutoCloser is responsible for automatically opening (using delegateOpenHelper) and closing (on a timer started when there are no remaining references) a SupportSqliteDatabase.

Because we now know that AutoCloser exists, we are going to assume that Room opens and closes the database connection for every transaction (maybe not immediately, but with Handler#postDelayed()), and this opening and closing can be expensive. If the situation applies, we should wrap multiple statements into a single transaction.

There are two different ways, that I am aware of, to run multiple statements in a transaction, using the Dao or the convenient methods from the RoomDatabase class. In this tutorial, we will learn how to use both.

Goals

At the end of the tutorial, you would have learned:

  1. How to run multiple statements in a Room transaction.
Tools Required

Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 2.

Prerequisite Knowledge
  1. Intermediate Android.
  2. SQL.
  3. Basic Room database.
  4. Kotlin coroutines.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Add the dependencies below for Room into the Module build.gradle.

     def room_version = "2.4.2"
      implementation "androidx.room:room-runtime:$room_version"
      kapt "androidx.room:room-compiler:$room_version"
      implementation "androidx.room:room-ktx:$room_version"
      implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
  3. In the same file, add the kapt plugin under plugins

     id 'kotlin-kapt'
  4. Create a new Entity called Student using the code below.

     @Entity(tableName = "student")
     data class Student(
        @PrimaryKey(autoGenerate = true) val id: Long = 0,
        val name: String,
        val age: Int,
     )
  5. Create a new Dao for the Student entity using the code below.

     @Dao
     interface StudentDao {
     @Insert(onConflict = OnConflictStrategy.REPLACE)
     abstract fun insert(student: Student)
    
     @Update(onConflict = OnConflictStrategy.REPLACE)
     abstract fun update(student: Student)
    
     @Delete
     abstract fun delete(student: Student)
     }
  6. Create a MyDatabase class using the code below.

     @Database(entities = [Student::class], version = 1)
     abstract class MyDatabase : RoomDatabase() {
        abstract fun studentDao(): StudentDao
     }
Running statements in a transaction using @Transaction

The first way that we are going to learn is with the annotation @Transaction. This annotation can be applied to a Dao method, which will run all of the contained statements in one single transaction.

Our StudentDao contains three different methods, @Insert, @Update, and @Delete, and we would like to run all three in a transaction. Follow the steps below to do this.

  1. Create a new concrete function called insertUpdateDelete() in StudentDao using the code below.

     suspend fun insertUpdateDelete(student: Student){
    
     }
  2. Call all three functions insert(), update(), and delete() like the code below.

     suspend fun insertUpdateDelete(student: Student){
        insert(student)
        update(student)
        delete(student)
     }
  3. Add the @Transaction annotation to insertUpdateDelete() like the code below.

     @Transaction
     suspend fun insertUpdateDelete(student: Student){
        insert(student)
        update(student)
        delete(student)
     }

And that is it. We can append the code below into MainActivity#onCreate() to see if it works. This code just creates an instance of MyDatabase and executes the @Transaction insertUpdateDelete() function.

    val db = Room.databaseBuilder(
       applicationContext,
       MyDatabase::class.java, "my-database"
    ).build()

    lifecycleScope.launch(Dispatchers.IO) {
       val student = Student(
           id = 1,
           name = "John",
           age = 6
       )
       db.studentDao().insertUpdateDelete(student)
    }
Running statements in a transaction using convenient methods

Another way to run multiple statements in Room would be to use the convenient methods below from the RoomDatabase class. Your options depend on whether you are using Java or Kotlin.

For Java, you have two options:

  1. runInTransaction (Callable<V> body)
  2. runInTransaction (Runnable body)

For Kotlin, in addition to the two Java methods above, you can also use the extension function below (requires androidx.room:room-ktx):

  1. withTransaction(block: suspend () -> R)

I cannot think of a use case where the Java methods are preferred over the Kotlin extension, so I will only showcase the Kotlin extension function. The only two important things that you need to know if you are suing the Kotlin withTransaction() are:

  1. The Dao functions called inside of the suspend block should be suspending functions themselves.
  2. withTransaction() must be used inside of a coroutine scope.

Follow the steps below to use withTransaction().

  1. Make the 3 Dao functions in StudentDao suspend.

     @Insert(onConflict = OnConflictStrategy.REPLACE)
     abstract suspend fun insert(student: Student)
    
     @Update(onConflict = OnConflictStrategy.REPLACE)
     abstract suspend fun update(student: Student)
    
     @Delete
     abstract suspend fun delete(student: Student)
  2. In onCreate(), comment out the insertUpdateDelete() call because we do not need it anymore.
    Create a reference called studentDao to improve readability a little bit.

     val studentDao = db.studentDao()
  3. Inside of the launch(), call withTransaction() with an empty body.

     db.withTransaction {
    
     }
  4. Add all 3 Dao functions to the withTransaction block.

     db.withTransaction {
        studentDao.insert(student)
        studentDao.update(student)
        studentDao.delete(student)
     }

And we are done. If we run the app, the student table should be empty because we inserted, updated, but also deleted the same entity.

Solution Code

MainActivity.kt

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val db = Room.databaseBuilder(
           applicationContext,
           MyDatabase::class.java, "my-database"
       ).build()

       lifecycleScope.launch(Dispatchers.IO) {
           val student = Student(
               id = 1,
               name = "John",
               age = 6
           )
           //db.studentDao().insertUpdateDelete(student)

           val studentDao = db.studentDao()

           db.withTransaction {
               studentDao.insert(student)
               studentDao.update(student)
               studentDao.delete(student)
           }
       }
   }
}

StudentDao.kt

@Dao
interface StudentDao {
   @Insert(onConflict = OnConflictStrategy.REPLACE)
   abstract suspend fun insert(student: Student)

   @Update(onConflict = OnConflictStrategy.REPLACE)
   abstract suspend fun update(student: Student)

   @Delete
   abstract suspend fun delete(student: Student)

   @Transaction
   suspend fun insertUpdateDelete(student: Student){
       insert(student)
       update(student)
       delete(student)
   }
}

Student.kt

@Entity(tableName = "student")
data class Student(
   @PrimaryKey(autoGenerate = true) val id: Long = 0,
   val name: String,
   val age: Int,
)

MyDatabase.kt

@Database(entities = [Student::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
   abstract fun studentDao(): StudentDao
}

Module build.gradle

plugins {
   id 'com.android.application'
   id 'org.jetbrains.kotlin.android'
   id 'kotlin-kapt'
}

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.example.daniwebandroidroomtransaction"
       minSdk 21
       targetSdk 32
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

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

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

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.1'
   implementation 'com.google.android.material:material:1.5.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
   testImplementation 'junit:junit:4.13.2'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
Summary

We have learned how to use Room transactions in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidRoomTransaction

Android Native – Define one-to-many relationship in Room

Introduction

When working with Room, you might have wondered how to describe one-to-many relationships between entities. In this tutorial, we will learn how to do just that.

Goals

At the end of the tutorial, you would have learned:

  1. How to define one-to-many relationship for entities in Room.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 2.
Prerequisite Knowledge
  1. Intermediate Android.
  2. SQL.
  3. Basic Room database.
  4. Kotlin coroutines.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Add the dependencies below for Room into the Module build.gradle.

     def room_version = "2.4.2"
      implementation "androidx.room:room-runtime:$room_version"
      kapt "androidx.room:room-compiler:$room_version"
      implementation "androidx.room:room-ktx:$room_version"
      implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
  3. In the same file, add the kapt plugin under plugins

     id 'kotlin-kapt'
  4. Create a ClassRoom entity using the code below.

     @Entity(tableName = "class_room")
     data class ClassRoom(
        @PrimaryKey(autoGenerate = true)
        @ColumnInfo(name = "class_room_id")
        val classRoomId: Long = 0
     )
  5. Create a new Student entity using the code below.

     @Entity(tableName = "student")
     data class Student(
        @PrimaryKey(autoGenerate = true)
        @ColumnInfo(name = "student_id")
        val studentId: Long = 0,
        val name: String,
        val age: Int
     )
  6. Create a new StudentDao using the code below.

     @Dao
     interface StudentDao {
        @Insert
        suspend fun insertStudents(vararg students: Student)
     }
  7. Create a new ClassRoomDao using the code below.

     @Dao
     interface ClassRoomDao {
        @Insert
        suspend fun insertClassRoom(classRoom: ClassRoom)
     }
  8. Create a MyDatabase class using the code below.

     @Database(entities = [ClassRoom::class, Student::class], version = 1)
     abstract class MyDatabase : RoomDatabase() {
        abstract fun classRoomDao(): ClassRoomDao
        abstract fun studentDao(): StudentDao
     }
Project Overview

Our project so far only contains two entities, Student and ClassRoom. One crucial step for our tutorial is that we must specify which entity is the parent and which is the child. For simplicity, we will choose ClassRoom as the parent and Student as the child; this means that one ClassRoom can contain many students.

Defining one-to-many relationship

To define a one-to-many relationship in Room, there is a little bit of boilerplate involved. Follow the steps below to define a one-ClassRoom-to-many-Students relationship for our Project.

  1. The child must reference the primary key of the parent as a property. In this case, the primary key of ClassRoom would just be classRoomId. Add the classRoomId to Student with the code below.

     @Entity(tableName = "student")
     data class Student(
        @PrimaryKey(autoGenerate = true)
        @ColumnInfo(name = "student_id")
        val studentId: Long = 0,
        val name: String,
        val age: Int,
        @ColumnInfo(name = "class_room_id") val classRoomId: Long
     )
  2. Optionally, you can add a foreign key here, if it makes sense, to improve data integrity, since each Student must belong to an existing ClassRoom anyways.

  3. In order to map this relationship, we will need to create a new data class to act as a glue between ClassRoom and List<Student>. Create a new data class ClassRoomWithStudent using the code below.

     data class ClassRoomWithStudent(
        @Embedded val classRoom: ClassRoom,
        val students: List<Student>
     )
  4. If you are not sure what @Embedded does, you can check out the tutorial on it here.
    Annotate the child students with @Relation and fill out its parentColumn and entityColumn parameters.

     data class ClassRoomWithStudent(
        @Embedded val classRoom: ClassRoom,
        @Relation(
            parentColumn = "class_room_id",
            entityColumn = "class_room_id"
        )
        val students: List<Student>
     )

And that is it for the relationship mapper, but there is still more to do if you want to make use of the relationship wrapper class.

Interact with the wrapper relationship class

To use the wrapper class, follow the steps below:

  1. Add the function getAllClassRoomWithStudent() to ClassRoomDao using the code below. The most important part of this function is its return type. It must return the relationship wrapper class.

     suspend fun getAllClassRoomWithStudent(): List<ClassRoomWithStudent>
  2. This is a query, so add a @Query annotation to it. Notice that we are simply querying the class_room table here, not class_room_with_student (does not exist) or student.

     @Query("SELECT * FROM class_room")
     suspend fun getAllClassRoomWithStudent(): List<ClassRoomWithStudent>
  3. Because the individual fields of the relationship wrapper will be queried individually, we need to add @Transaction to it.

     @Transaction
     @Query("SELECT * FROM class_room")
     suspend fun getAllClassRoomWithStudent(): List<ClassRoomWithStudent>
  4. Append the code below to MainActivity#onCreate() to create the database instance.

     val db = Room.databaseBuilder(
        applicationContext,
        MyDatabase::class.java, "my-database"
     ).build()
  5. Now, launch a new coroutine with the code below.

     lifecycleScope.launch(Dispatchers.IO) {
    
     }
  6. Inside of the coroutine, add the code below to create sample Student and ClassRoom objects.

     val classRoomId = 1L
     val classRoom = ClassRoom(classRoomId)
    
     val studentA = Student(
        name = "John",
        age = 6,
        classRoomId = classRoomId
     )
     val studentB = studentA.copy(
        name = "Mary",
        age = 7
     )
  7. Run the Dao functions in the order below. We obviously must wait for the Student and ClassRoom to be persisted first before we can query them.

     db.withTransaction {
        db.classRoomDao().insertClassRoom(classRoom)
        db.studentDao().insertStudents(studentA, studentB)
        val result = db.classRoomDao().getAllClassRoomWithStudent()
        Log.d(TAG, "$result")
     }
  8. Upon running the app, we should see the ClassRoomWithStudent object printed to the console.

     2022-03-22 19:52:20.953 7980-8016/com.example.daniwebandroidroomonetomany D/MAIN_ACTIVITY: [ClassRoomWithStudent(classRoom=ClassRoom(classRoomId=1), students=[Student(studentId=1, name=John, age=6, classRoomId=1), Student(studentId=2, name=Mary, age=7, classRoomId=1)])]
Solution Code

MainActivity.kt

private const val TAG = "MAIN_ACTIVITY"

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val db = Room.databaseBuilder(
           applicationContext,
           MyDatabase::class.java, "my-database"
       ).build()

       lifecycleScope.launch(Dispatchers.IO) {
           val classRoomId = 1L
           val classRoom = ClassRoom(classRoomId)

           val studentA = Student(
               name = "John",
               age = 6,
               classRoomId = classRoomId
           )
           val studentB = studentA.copy(
               name = "Mary",
               age = 7
           )

           db.withTransaction {
               db.classRoomDao().insertClassRoom(classRoom)
               db.studentDao().insertStudents(studentA, studentB)
               val result = db.classRoomDao().getAllClassRoomWithStudent()
               Log.d(TAG, "$result")
           }
       }
   }
}

ClassRoom.kt

@Entity(tableName = "class_room")
data class ClassRoom(
   @PrimaryKey(autoGenerate = true)
   @ColumnInfo(name = "class_room_id")
   val classRoomId: Long = 0
)

ClassRoomDao.kt

@Dao
interface ClassRoomDao {
   @Insert
   suspend fun insertClassRoom(classRoom: ClassRoom)

   @Transaction
   @Query("SELECT * FROM class_room")
   suspend fun getAllClassRoomWithStudent(): List<ClassRoomWithStudent>
}

ClassRoomWithStudent.kt

data class ClassRoomWithStudent(
   @Embedded val classRoom: ClassRoom,
   @Relation(
       parentColumn = "class_room_id",
       entityColumn = "class_room_id"
   )
   val students: List<Student>
)

MyDatabase.kt

@Database(entities = [ClassRoom::class, Student::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
   abstract fun classRoomDao(): ClassRoomDao
   abstract fun studentDao(): StudentDao
}

Student.kt

@Entity(tableName = "student")
data class Student(
   @PrimaryKey(autoGenerate = true)
   @ColumnInfo(name = "student_id")
   val studentId: Long = 0,
   val name: String,
   val age: Int,
   @ColumnInfo(name = "class_room_id") val classRoomId: Long
)

StudentDao.kt

@Dao
interface StudentDao {
   @Insert
   suspend fun insertStudents(vararg students: Student)
}

Module build.gradle

plugins {
   id 'com.android.application'
   id 'org.jetbrains.kotlin.android'
   id 'kotlin-kapt'
}

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.example.daniwebandroidroomonetomany"
       minSdk 21
       targetSdk 32
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

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

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

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.1'
   implementation 'com.google.android.material:material:1.5.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
   testImplementation 'junit:junit:4.13.2'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
Summary

We have learned how to map one-to-many relationships in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidRoomOneToMany.

Android Native – How to embed an Entity inside of another Entity

Introduction

There are many ways to describe relationships between Entities in Room, one of which is to embed an entity inside of another. When embedding an Entity, the columns of the embedded Entity are extracted as member columns of the enclosing entity.

In this tutorial, we will learn how to embed an Entity in a Room database.

Goals

At the end of the tutorial, you would have learned:

  1. How to embed an Entity inside of another Entity.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 2.
Prerequisite Knowledge
  1. Intermediate Android.
  2. SQL.
  3. Basic Room database.
  4. Kotlin coroutines.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Add the dependencies below for Room into the Module build.gradle.

     def room_version = "2.4.2"
     implementation "androidx.room:room-runtime:$room_version"
     kapt "androidx.room:room-compiler:$room_version"
     implementation "androidx.room:room-ktx:$room_version"
     implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
  3. In the same file, add the kapt plugin under plugins

     id 'kotlin-kapt'
  4. Create a new Kotlin data class for an Entity called SoccerTeam using the code below.

     @Entity(tableName = "soccer_team")
     data class SoccerTeam(
        @PrimaryKey(autoGenerate = true) val id: Long
     )
  5. Create a new Kotlin data class for an Entity called HeadCoach using the code below.

     @Entity(tableName = "head_coach")
     data class HeadCoach(
        @PrimaryKey(autoGenerate = true)  val id: Long,
        val name: String,
        val age: Int,
     )
  6. Create a new Dao for SoccerTeam using the code below.

     @Dao
     interface SoccerTeamDao {
        @Insert
        suspend fun insert(team: SoccerTeam)
    
        @Query("SELECT * FROM soccer_team")
        suspend fun getAll(): List<SoccerTeam>
    
     }
  7. Create a new Dao for HeadCoach using the code below.

     @Dao
     interface HeadCoachDao {
        @Insert
        suspend fun insert(coach: HeadCoach)
    
        @Query("SELECT * FROM head_coach")
        suspend fun getAll(): List<HeadCoach>
    
     }
  8. Create the abstract class MyDatabase using the code below.

     @Database(entities = [SoccerTeam::class, HeadCoach::class], version = 1)
     abstract class MyDatabase : RoomDatabase() {
        abstract fun soccerTeamDao(): SoccerTeamDao
        abstract fun headCoachDao(): HeadCoachDao
     }
  9. Append the code below to MainActivity onCreate(). This creates an instance of the database and then attempts to perform a query on the empty database.

     val db = Room.databaseBuilder(
        applicationContext,
        MyDatabase::class.java, "my-database"
     ).build()
    
     lifecycleScope.launch(Dispatchers.IO) {
        db.soccerTeamDao().getAll()
     }
Project Overview

For this tutorial, we will only be working with the database, so we are not concerned about the frontend at all.

Currently, we have two entities, SoccerTem and HeadCoach, that are not related in any way. In the real world, every soccer team should have one head coach, so we would need to add a HeadCoach as a member of the team somehow. There are a couple of ways to describe this relationship:

  1. One-to-one (probably too tightly coupled because the head coach can be replaced)).
  2. Include a nullable ID of the head_coach in the soccer_team table, and vice versa, with a Foreign Key constraint to soccer_team.
  3. Embed a head_coach inside the soccer_team. This approach should work for our simple use case. Note that HeadCoach does not have to be an Entity itself to be embedded.

At the end of the tutorial, we should have written code to embed HeadCoach inside of SoccerTeam.

How to embed an Entity

Embedding an Entity is quite straight forward. We only need a few things:

  1. The parent Entity. It is aware of the child (embedded) Entity. It needs to have a member with the same type as the embedded Entity.
  2. The child Entity. It is not required to know of the parent.
  3. The @Embedded annotation. You will have to apply this to the member field in the parent Entity. You might be familiar with this if you have worked with JPA before.
  4. Be aware of the columns in both Entities, you might want to prefix columns of the embedded entities by passing a value to the prefix parameter of @Embedded. This prevents duplicate column names from clashing.

Follow the steps below to embed HeadCoach inside of SoccerTeam.

  1. Open SoccerTeam.kt.

  2. Add a headCoach member to it (typed to HeadCoach). And then annotate it with @Embedded.

  3. Because both HeadCoach and SoccerTeam have an identical id column, pass a value of head_coach_ to prefix for the @Embedded annotation. Do not forget the underscore (_) at the end of the prefix string; we want our column to look like head_coach_id instead of head_coachid.

  4. Your SoccerTeam Entity should now look like the code below.

     @Entity(tableName = "soccer_team")
     data class SoccerTeam(
        @PrimaryKey(autoGenerate = true) val id: Long,
        @Embedded(prefix = "head_coach_") val headCoach: HeadCoach
     )
Launch the App

Upon starting the app and using the Database Inspector, we can see that the database looks like below.

database.jpg

One thing that you will have to be aware of is that, If we insert a row in soccer_team with the command below,

    INSERT INTO soccer_team VALUES (1, 1, "John", "Doe")

head_coach would still be empty.

Solution Code

MainActivity.kt

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val db = Room.databaseBuilder(
           applicationContext,
           MyDatabase::class.java, "my-database"
       ).build()

       lifecycleScope.launch(Dispatchers.IO) {
           db.soccerTeamDao().getAll()
       }

   }
}

HeadCoach.kt

@Entity(tableName = "head_coach")
data class HeadCoach(
   @PrimaryKey(autoGenerate = true)  val id: Long,
   val name: String,
   val age: Int,
)

HeadCoachDao.kt

@Dao
interface HeadCoachDao {
   @Insert
   suspend fun insert(coach: HeadCoach)

   @Query("SELECT * FROM head_coach")
   suspend fun getAll(): List<HeadCoach>

}

MyDatabase.kt

@Database(entities = [SoccerTeam::class, HeadCoach::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
   abstract fun soccerTeamDao(): SoccerTeamDao
   abstract fun headCoachDao(): HeadCoachDao
}

SoccerTeam.kt

@Entity(tableName = "soccer_team")
data class SoccerTeam(
   @PrimaryKey(autoGenerate = true) val id: Long,
   @Embedded(prefix = "head_coach_") val headCoach: HeadCoach
)

SoccerTeamDao.kt

@Dao
interface SoccerTeamDao {
   @Insert
   suspend fun insert(team: SoccerTeam)

   @Query("SELECT * FROM soccer_team")
   suspend fun getAll(): List<SoccerTeam>

}

build.gradle

plugins {
   id 'com.android.application'
   id 'org.jetbrains.kotlin.android'
   id 'kotlin-kapt'
}

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.example.daniwebembedentity"
       minSdk 21
       targetSdk 32
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

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

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


   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.3.0'
   implementation 'com.google.android.material:material:1.4.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
   testImplementation 'junit:junit:4.13.2'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
Summary

We have learned how to embed an Entity inside of another Entity in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebEmbedEntity

Android Native – How to implement LifecycleOwner in a Service

Introduction

When working with Services on Android, you might have ran into an issue where you would like to:

  1. Use a concrete implementation of a Service (from a built-in or third party library).
  2. Make the Service lifecycle-aware so that you can use coroutines with it. Many built-in Service classes are in Java and were introduced pre-Kotlin era.

As of right now, the only built-in Service class from Android that implements LifecycleOwner is LifecycleService. But LifecycleService is a concrete implementation, so you cannot extend both from it and another concrete Service.

We can opt in to use composition by having a LifecycleService as a member of our own Service, but there are couple of downsides to this approach:

  1. We will have to be very careful to pass the correct lifecycle calls to the member LifecycleService.
  2. Other components are not aware that our Service is now lifecycle-aware.
  3. I have not seen this pattern mentioned anywhere, so this approach is not well documented and is most likely anti-pattern.
  4. There is a better approach, which is to implement LifecycleOwner directly. This option is officially supported by Android.

In this tutorial, we will learn how to implement LifecycleOwner in our own Service.

Goals

At the end of the tutorial, you would have learned:

  1. How to implement LifecycleOwner in a Service.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 2.
Prerequisite Knowledge
  1. Intermediate Android.
  2. Kotlin.
  3. Android Services.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Add the dependencies below to your Module build.gradle.

     implementation 'androidx.lifecycle:lifecycle-service:2.4.1'
     implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
  3. Create a CustomLibraryService class and extend Service.

     //Making this open to simulate a library class
     open class CustomLibraryService : Service() {
        //return null for tutorial only.
        override fun onBind(p0: Intent?): IBinder? = null
     }
  4. Create a new MyLifecycleService class from the code below, extending another Service and implementing LifecycleOwner interface as well. Ignore compile errors for now.

     class MyLifecycleService: CustomLibraryService(), LifecycleOwner {
    
     }
ServiceLifecycleDispatcher

To be able to implement LifecycleOwner, we will use a class called ServiceLifecycleDispatcher. All of the methods provided by this class are important. The methods declared in this class are:

  1. getLifecycle()
  2. onServicePreSuperOnBind()
  3. onServicePreSuperOnCreate()
  4. onServicePreSuperOnDestroy()
  5. onServicePreSuperOnStart()

All four of the onServicePre*() methods must be called BEFORE the super.*() calls in MyLifecycleService. The corresponding methods that we must override in MyLifecycleService are:

  1. getLifecycle() (from LifecycleOwner interface, not Service).
  2. onBind().
  3. onCreate().
  4. onStart() (Deprecated on newer Android versions).
  5. onStartCommand().
  6. onDestroy.

Later on, after implementing LifecycleOwner within our MyLifecycleService, the compiler will only prompt us to override getLifecycle(), but that is not enough. If we want our coroutines to work properly, we must also override onBind(), onCreate(), onStart(), onStartCommand(), and onDestroy().

Add a ServiceLifecycleDispatcher as a member of MyLifecycleService using the code below. Ignore compile errors.

private val mServiceLifecycleDispatcher = ServiceLifecycleDispatcher(this)
Implement LifecycleOwner

LifecyleOwner only has one required method to override, getLifecycle(), and we will deploy the helper class ServiceLifecycleDispatcher for this. Copy and paste the code below to implement getLifecycle().

override fun getLifecycle() = mServiceLifecycleDispatcher.lifecycle
Override the rest of the methods

Now, we will need to override all four on*() methods that MyLifecycleService inherited from Service() (via CustomLibraryService). Follow the steps below. Note that we call according mServiceLifecycleDispatcher.onServicePre*() methods before super.on*() for all of them.

  1. Override onBind().

     override fun onBind(p0: Intent?): IBinder? {
        mServiceLifecycleDispatcher.onServicePreSuperOnBind()
        return super.onBind(p0)
     }
  2. Override onCreate().

     override fun onCreate() {
        mServiceLifecycleDispatcher.onServicePreSuperOnCreate()
        super.onCreate()
     }
  3. Override onStart().

     //Deprecated, but you might need to add this if targeting really old API.
     override fun onStart(intent: Intent?, startId: Int) {
        mServiceLifecycleDispatcher.onServicePreSuperOnStart()
        super.onStart(intent, startId)
     }
  4. Override onStartCommand().

     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        mServiceLifecycleDispatcher.onServicePreSuperOnStart()
        return super.onStartCommand(intent, flags, startId)
     }
  5. Override onDestroy().

     override fun onDestroy() {
        mServiceLifecycleDispatcher.onServicePreSuperOnDestroy()
        super.onDestroy()
     }

And that is it. We have successfully implemented both LifecycleOwner and extended another concrete Service. As you can see, we can use coroutine and observe from LiveData as well. Here is a sample.

   override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
       mServiceLifecycleDispatcher.onServicePreSuperOnStart()

       lifecycleScope.launch {  }

/*        Sample LiveData Usage
       data.observe(this){
           //Do work
       }*/

       return super.onStartCommand(intent, flags, startId)
   }
Solution Code

CustomLibraryService.kt

package com.example.daniwebimplementlifecycleownerinservice

import android.app.Service
import android.content.Intent
import android.os.IBinder

//Making this open to simulate a library class
open class CustomLibraryService : Service() {
   //return null for tutorial only.
   override fun onBind(p0: Intent?): IBinder? = null
}

MyLifecycleService.kt

package com.example.daniwebimplementlifecycleownerinservice

import android.content.Intent

import android.os.IBinder
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ServiceLifecycleDispatcher
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch

class MyLifecycleService: CustomLibraryService(), LifecycleOwner {
   private val mServiceLifecycleDispatcher = ServiceLifecycleDispatcher(this)

   override fun getLifecycle() = mServiceLifecycleDispatcher.lifecycle

   override fun onBind(p0: Intent?): IBinder? {
       mServiceLifecycleDispatcher.onServicePreSuperOnBind()
       return super.onBind(p0)
   }

   override fun onCreate() {
       mServiceLifecycleDispatcher.onServicePreSuperOnCreate()
       super.onCreate()
   }

   //Deprecated, but you might need to add this if targeting really old API.
   override fun onStart(intent: Intent?, startId: Int) {
       mServiceLifecycleDispatcher.onServicePreSuperOnStart()
       super.onStart(intent, startId)
   }

   override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
       mServiceLifecycleDispatcher.onServicePreSuperOnStart()

       lifecycleScope.launch {  }

/*        Sample LiveData Usage
       data.observe(this){
           //Do work
       }*/

       return super.onStartCommand(intent, flags, startId)
   }

   override fun onDestroy() {
       mServiceLifecycleDispatcher.onServicePreSuperOnDestroy()
       super.onDestroy()
   }

}

Module build.gradle

plugins {
   id 'com.android.application'
   id 'org.jetbrains.kotlin.android'
}

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.example.daniwebimplementlifecycleownerinservice"
       minSdk 21
       targetSdk 32
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

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

dependencies {
   implementation 'androidx.lifecycle:lifecycle-service:2.4.1'
   implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.3.0'
   implementation 'com.google.android.material:material:1.4.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
   testImplementation 'junit:junit:4.13.2'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
Summary

The full project code can be found at https://github.com/dmitrilc/DaniwebImplementLifecycleOwnerInService

How to set broadcast receiver attributes programmatically in android studio

I'm broadcasting an intent in my app and receiving it with a broadcast receiver. I can handle the broadcasting and receiving. No problem with that. However, I want to register the receiver completely programmatically instead of doing it in the manifest file. Notice, that in the manifest file, there are two attributes of the receiver android:enabled="true" and android:exported="false". I need to know, how do I specifically set these two attributes when I register the receiver programmatically?

My AndroidManifest.xml file:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.mybroadcastapplication">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyBroadcastApplication">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <receiver
            android:name=".MyBroadcastReceiver"
            android:enabled="true"
            android:exported="false">
        </receiver>
    </application>

</manifest>

My MainActivity.java file:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    MyBroadcastReceiver myReceiver;
    IntentFilter intentFilter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myReceiver = new MyBroadcastReceiver();
        intentFilter = new IntentFilter();
        intentFilter.addAction("com.example.mybroadcastapplication.EXPLICIT_INTENT");
        findViewById(R.id.button1).setOnClickListener(this);
    }

    public void broadcastIntent() {
        Intent intent = new Intent();
        intent.setAction("com.example.mybroadcastapplication.EXPLICIT_INTENT");
        getApplicationContext().sendBroadcast(intent);
    }

    @Override
    protected void onPostResume() {
        super.onPostResume();
        registerReceiver(myReceiver, intentFilter);
    }

    @Override
    protected void onStop() {
        super.onStop();
        unregisterReceiver(myReceiver);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button1:
                broadcastIntent();
                break;
            default:
        }
    }
}

My MyBroadcastReceiver.java file:

public class MyBroadcastReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction() != null && intent.getAction().equals("com.example.mybroadcastapplication.EXPLICIT_INTENT"))
            Toast.makeText(context, "Explicit intent received.", Toast.LENGTH_LONG).show();
    }
}

Regards

Android Native – How to add Foreign Keys to Room entities

Introduction

When working with Room, there might come a situation during bulk CRUD operations that it would be useful to add a Foreign Key to one of our tables. This provides two main benefits: cascading delete (must be configured manually) and enforcing data integrity on linked entities.

In this tutorial, we will learn how to add Foreign Keys into Room entities.

Goals

At the end of the tutorial, you would have learned:

  1. How to add Foreign Keys to Room entities.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 2.
Prerequisite Knowledge
  1. Intermediate Android.
  2. SQL.
  3. Basic Room database.
  4. Kotlin coroutines.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Add the dependencies below for Room into the Module build.gradle.

     def room_version = "2.4.2"
     implementation "androidx.room:room-runtime:$room_version"
     kapt "androidx.room:room-compiler:$room_version"
     implementation "androidx.room:room-ktx:$room_version"
     implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
  3. In the same file, add the kapt plugin under plugins

     id 'kotlin-kapt'
  4. Create a new @Entity called Student with the code below.

     @Entity
     data class Student(
        @PrimaryKey(autoGenerate = true) val id: Long = 0,
        @ColumnInfo(name = "first_name") val firstName: String,
        @ColumnInfo(name = "last_name") val lastName: String
     )
  5. Create another @Entity called ReportCard with the code below.

     @Entity(tableName = "report_card")
     data class ReportCard(
        @PrimaryKey(autoGenerate = true) val id: Long = 0,
        @ColumnInfo(name = "student_id") val studentId: Long
     )
  6. Create a new empty DAO for the Student entity.

     @Dao
     interface StudentDao {
    
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        suspend fun insertStudent(student: Student): Long
    
        @Query("SELECT * FROM student WHERE id=:id")
        suspend fun getStudentById(id: Long): Student?
    
     }
  7. Create the abstract class MyRoomDB with the code below.

     @Database(entities = [Student::class, ReportCard::class], version = 1)
     abstract class MyRoomDB : RoomDatabase() {
        abstract fun studentDao(): StudentDao
     }
  8. Append the code below to MainActivity onCreate(). This creates an instance of the database and then attempts to perform a query on the empty database.

     val db = Room.databaseBuilder(
        applicationContext,
        MyRoomDB::class.java, "my-room-db"
     ).build()
    
     lifecycleScope.launch {
        db.studentDao().getStudentById(1)
     }
  9. For most of the tutorial, we will interact with the database via the Database Inspector, and not in code. The DAO query above performs a dummy connection so that the Database Inspector can keep the database connection open in debugging mode. Run the app in Debug mode and then switch to the App Inspection window. Verify that the database connection stays open. Later on, we can interact with the database directly using a Query tab.

1.png

Project Overview

For this tutorial, we are completely ignoring the frontend. We will only focus on the interaction with the database via Room.

We currently have two entities, Student and ReportCard. The Student entity is not aware of the ReportCard entity, but the ReportCard entity is dependent on the Student entity. Each ReportCard contains its own ID as well as the associated Student ID.

There is no foreign key constraint to Student in ReportCard, so it is possible that a ReportCard might be referencing a Student who does not exist in the student table. At the end of the tutorial, we should have created a Foreign Key for the ReportCard entity; this way we can ensure that each ReportCard can only reference a valid Student.

The Problem

First, in order to have a better understanding of the problems that a Foreign Key can solve, let us walk through an example where data integrity is violated.

The current student table is empty, but we can INSERT new ReportCard into report_card just fine, referencing non-existent students.

INSERT INTO report_card (id, student_id) VALUES
(1, 30),
(2, 2)

2.png

If we query the join for the two tables, we would receive nothing because those student_id do not exist in the student table.

SELECT * FROM report_card INNER JOIN student ON report_card.student_id=student.id

3.png

Foreign Key

Fortunately for us, we can create a Foreign Key to an entity so that the database can throw errors when we try to violate this constraint. One thing to keep in mind is that this does not prevent developers from writing code that violates the constraint; only via runtime testing that data inconsistencies will show up with a SQL exception.

To apply a Foreign Key, we can simply pass in ForeignKey objects to Entitys foreignKey parameter. entity, childColumns, and parentColumns are required by ForeignKey.

@Entity(tableName = "report_card",
   foreignKeys = [ForeignKey(
       entity = Student::class,
       childColumns = ["student_id"],
       parentColumns = ["id"]
)])
data class ReportCard(
   @PrimaryKey(autoGenerate = true) val id: Long = 0,
   @ColumnInfo(name = "student_id") val studentId: Long
)

After adding the Foreign Key, you can re-install the App so Room can make use of the new constraint.

If we attempt to run the same INSERT into report_card again, Room will not allow us and throw an exception instead.

E/SQLiteQuery: exception: FOREIGN KEY constraint failed (code 787 SQLITE_CONSTRAINT_FOREIGNKEY); query: INSERT INTO report_card (id, student_id) VALUES
    (1, 30),
     (2, 2)

In order for the INSERT into report_card to work, valid students must exist. INSERT new Students with the statement below.

INSERT INTO student VALUES
(2, "Mary", "Anne"),
(30, "John", "Doe")

After this, you can INSERT into report_card for the valid student IDs.

INSERT INTO report_card (id, student_id) VALUES
(1, 30),
(2, 2)

If we run the JOIN query again, we can see that it returns all of the data correctly.

4.png

Solution Code

MainActivity.kt

package com.codelab.daniwebandroidroomforeignkey

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import androidx.room.Room
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val db = Room.databaseBuilder(
           applicationContext,
           MyRoomDB::class.java, "my-room-db"
       ).build()

       lifecycleScope.launch {
           db.studentDao().getStudentById(1)
       }

   }
}

MyRoomDB.kt

package com.codelab.daniwebandroidroomforeignkey

import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [Student::class, ReportCard::class], version = 1)
abstract class MyRoomDB : RoomDatabase() {
   abstract fun studentDao(): StudentDao
   abstract fun reportCardDao(): ReportCardDao
}

ReportCard.kt

package com.codelab.daniwebandroidroomforeignkey

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

@Entity(tableName = "report_card",
   foreignKeys = [ForeignKey(
       entity = Student::class,
       childColumns = ["student_id"],
       parentColumns = ["id"]
)])
data class ReportCard(
   @PrimaryKey(autoGenerate = true) val id: Long = 0,
   @ColumnInfo(name = "student_id") val studentId: Long
)

Student.kt

package com.codelab.daniwebandroidroomforeignkey

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

@Entity(tableName = "student")
data class Student(
   @PrimaryKey(autoGenerate = true) val id: Long = 0,
   @ColumnInfo(name = "first_name") val firstName: String,
   @ColumnInfo(name = "last_name") val lastName: String
)

StudentDao.kt

package com.codelab.daniwebandroidroomforeignkey

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface StudentDao {

   @Insert(onConflict = OnConflictStrategy.IGNORE)
   suspend fun insertStudent(student: Student): Long

   @Query("SELECT * FROM student WHERE id=:id")
   suspend fun getStudentById(id: Long): Student?

}

Module build.gradle

plugins {
   id 'com.android.application'
   id 'org.jetbrains.kotlin.android'
   id 'kotlin-kapt'
}

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.codelab.daniwebandroidroomforeignkey"
       minSdk 21
       targetSdk 32
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

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

dependencies {
   //Room deps
   def room_version = "2.4.2"
   implementation "androidx.room:room-runtime:$room_version"
   kapt "androidx.room:room-compiler:$room_version"
   implementation "androidx.room:room-ktx:$room_version"
   implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.1'
   implementation 'com.google.android.material:material:1.5.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
   testImplementation 'junit:junit:4.13.2'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
Summary

We have learned how to create Foreign Keys on Room Entities. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidRoomForeignKey

Android Native – How to prepopulate a Room database

Introduction

If your Android Application uses Room, then you might have wondered how to prepopulate a Room database. In this tutorial, we will learn how to prepopulate a Room database.

Goals

At the end of the tutorial, you would have learned:

  1. How to prepopulate a Room database.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 2.
Prerequisite Knowledge
  1. Basic Android.
  2. Basic Room.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Add the dependencies below for Room into the Module build.gradle.

     def room_version = "2.4.2"
     implementation "androidx.room:room-runtime:$room_version"
     kapt "androidx.room:room-compiler:$room_version"
     implementation "androidx.room:room-ktx:$room_version"
     implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
  3. In the same file, add the kapt plugin under plugins.

     id 'kotlin-kapt'
  4. Add a new Person Entity with the code below.

     @Entity(tableName = "person")
     data class Person(
        @PrimaryKey(autoGenerate = true) val id: Long,
        @ColumnInfo(name = "first_name") val firstName: String,
        @ColumnInfo(name = "last_name") val lastName: String
     )
  5. Add a new Person DAO with the code below.

     @Dao
     interface PersonDao {
        @Query("SELECT * FROM person WHERE id=:id")
        suspend fun getPersonById(id: Long): Person?
     }
  6. Create a new Room database with the code below.

     @Database(entities = [Person::class], version = 1)
     abstract class MyDatabase : RoomDatabase(){
        abstract fun personDao(): PersonDao
     }
  7. In MainActivity#onCreate(), append the code below. This creates a dummy connection to the database at launch so that we can have access to it in debug mode.

     val db = Room.databaseBuilder(
        applicationContext,
        MyDatabase::class.java, "my-database"
     ).build()
    
     lifecycleScope.launch {
        db.personDao().getPersonById(1)
     }
Project Overview

For this tutorial, we are not concerned about the UI at all and only focus on the Room database itself.

There is only one simple Student Entity in our App. At the end of the tutorial, we should be able to prepopulate our database with data on boot.

Options for prepopulating data

There are three different ways to prepopulate a database.

  1. The first option involves using prepackaged database files located under a special directory called assets/.
  2. The second option allows you to load the prepackaged files from the file system itself.
  3. The third option includes loading the database files from an InputStream.

Because loading pre-packaged databases are all the same conceptually for all three options, we will only need to know how to do it with the assets/ resource directory. Loading a pre-packaged database from File or InputStream is only different depending on the location of the file.

All options require you to export your pre-built database, so we will learn how to do that first.

Export an Existing Database

To be able to export a database, we will need to build it first on a test device. Follow the steps below to create some data and export the database.

  1. Select the top-most app directory of in the Android Project View.

  2. File > New > Folder > Assets Folder.

  3. Start the App in Debug mode.

  4. Open the App Inspection tab to start the Database Inspector.

  5. Run the statement below to add some Person into the database.

     INSERT INTO person VALUES
     (1, "Terence", "Landon"),
     (2, "Danielle", "Alger"),
     (3, "Arnold", "Chandler"),
     (4, "Mariah", "Blake"),
     (5, "Randal", " Maria ")
  6. Now, highlight the my-database and select Export as File. Save it as a .db file.
    5.png

  7. You can export directly into the assets directory created previously.

  8. Optionally, save another copy of the same database as the SQL file extension, and then open the file with a text editor. With the human-readable SQL file, we now understand exactly which statements are run against the underlying SQLite database.

     PRAGMA foreign_keys=OFF;
     BEGIN TRANSACTION;
     CREATE TABLE android_metadata (locale TEXT);
     INSERT INTO android_metadata VALUES('en_US');
     CREATE TABLE `person` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_name` TEXT NOT NULL, `last_name` TEXT NOT NULL);
     INSERT INTO person VALUES(1,'Terence','Landon');
     INSERT INTO person VALUES(2,'Danielle','Alger');
     INSERT INTO person VALUES(3,'Arnold','Chandler');
     INSERT INTO person VALUES(4,'Mariah','Blake');
     INSERT INTO person VALUES(5,'Randal',' Maria ');
     CREATE TABLE room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT);
     INSERT INTO room_master_table VALUES(42,'cd4ee16aca420ae15eab14b1baa5cdcf');
     DELETE FROM sqlite_sequence;
     INSERT INTO sqlite_sequence VALUES('person',5);
     COMMIT;
Load prepackaged database from app Asset

Now that we have the exported database, it is quite simple to load it into our database on the first run. All we have to do is to call the function createFromAsset() from the Room.Builder class on our database creation call. The builder step to add is:

.createFromAsset("my-database.db")

so that the previous database builder becomes this:

val db = Room.databaseBuilder(
   applicationContext,
   MyDatabase::class.java,
   "my-database"
)
   .createFromAsset("my-database.db")
   .build()

If you reinstall the app, then you can see that the database now has data upon first boot.

6.png

Solution Code

MainActivity.kt

package com.codelab.daniwebandroidprepopulateroomdatabase

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import androidx.room.Room
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val db = Room.databaseBuilder(
           applicationContext,
           MyDatabase::class.java,
           "my-database"
       )
           .createFromAsset("my-database.db")
           .build()

       lifecycleScope.launch {
           db.personDao().getPersonById(1)
       }
   }
}

MyDatabase.kt

package com.codelab.daniwebandroidprepopulateroomdatabase

import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [Person::class], version = 1)
abstract class MyDatabase : RoomDatabase(){
   abstract fun personDao(): PersonDao
}

Person.kt

package com.codelab.daniwebandroidprepopulateroomdatabase

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

@Entity(tableName = "person")
data class Person(
   @PrimaryKey(autoGenerate = true) val id: Long,
   @ColumnInfo(name = "first_name") val firstName: String,
   @ColumnInfo(name = "last_name") val lastName: String
)

PersonDao

package com.codelab.daniwebandroidprepopulateroomdatabase

import androidx.room.Dao
import androidx.room.Query

@Dao
interface PersonDao {
   @Query("SELECT * FROM person WHERE id=:id")
   suspend fun getPersonById(id: Long): Person?
}

Module build.gradle

plugins {
   id 'com.android.application'
   id 'org.jetbrains.kotlin.android'
   id 'kotlin-kapt'
}

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.codelab.daniwebandroidprepopulateroomdatabase"
       minSdk 21
       targetSdk 32
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

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

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

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.1'
   implementation 'com.google.android.material:material:1.5.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
   testImplementation 'junit:junit:4.13.2'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
Summary

We have learned how to prepopulate a Room database in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidPrepopulateRoomDatabase

What are the best ways to develop a cost-efficient mobile app?

Hello there. I'd like to develop a mobile app with cutting-edge functionality. A rider app, a passenger app, and an admin portal are all included. It is necessary to register and edit your profile. Can someone provide me with a cost estimate for app development? or else any other way to get a cost-efficient mobile app for my taxi business?. Is anyone is aware of the development field please help me with it.