Android Native – How to validate Intents in Espresso tests

Introduction ##

In this tutorial, we will learn how to filter and validate Intents fired from the application under test.

Goals

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

  1. How to filter and validate Intents in Espresso tests.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Chipmunk 2021.2.1 Patch 1.
Prerequisite Knowledge
  1. Intermediate 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. Add the <string> resource below into strings.xml.

     <string name="launch_intent">Launch Intent</string>
  3. Replace the code inside activity_main.xml with the code below. This simply adds a Button.

     <?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_launchIntent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/launch_intent"
            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 dependencies below into your module build.gradle file.

     androidTestImplementation 'androidx.test:runner:1.4.0'
     androidTestImplementation 'androidx.test:rules:1.4.0'
     androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
  5. Remove all test cases from ExampleInstrumentedTest in your androidTest source set. Your ExampleInstrumentedTest file should look like the empty class below.

     @RunWith(AndroidJUnit4::class)
     class ExampleInstrumentedTest {
     }
  6. Append MainActivity#onCreate() with the code below.

     findViewById<Button>(R.id.button_launchIntent).setOnClickListener {
        val intent = Intent(ACTION_VIEW).apply {
            data = intentData
        }
    
        startActivity(intent)
     }
  7. Add the companion object below into MainActivity as well.

     companion object {
        val intentData: Uri = Uri.Builder()
            .scheme("geo")
            .query("0,0")
            .appendQueryParameter("q", "First St SE, Washington, DC, 20004")
            .build()
     }
Project Overview

The tutorial app is a super simple app with a single Button. After clicking on the Button, the app will attempt to launch an Activity that can consume the Uri scheme geo. There is only one app on my emulator that can do this, which is the default Google Maps app.

The Intent launched is an implicit Intent with data pointing to a specific location (US Capitol) on the world map. The data used for the Intent is shown below.

val intentData: Uri = Uri.Builder()
   .scheme("geo")
   .query("0,0")
   .appendQueryParameter("q", "First St SE, Washington, DC, 20004")
   .build()

Reference the animation below to get a feel of what the application does.

Map_App.gif

Basic Espresso Test

Copy and paste the code below into your ExampleInstrumentedTest class.

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

/*    @Before
   fun startCapturingIntent(){
       Intents.init()
   }*/

/*    @After
   fun clearIntentsState() {
       Intents.release()
   }*/

   @Test
   fun intentTest(){
       onView(withId(R.id.button_launchIntent))
           .perform(click())

/*        Intents.intended(allOf(
           hasAction(ACTION_VIEW),
           hasData(MainActivity.intentData)
       ))*/
   }

The test intentTest() simply opens the app and then performs a click().

Map_Test_No_Capture.gif

The test is still not aware of the Intent being fired yet, but the commented out sections of the code can help us capture and verify the Intent being fired.

The Intents class

For us to be able to capture Intents from the application under test, we can call Intents.init() before each test. Go ahead and uncomment startCapturingIntent().

@Before
fun startCapturingIntent(){
   Intents.init()
}

Because Intents#init() will modify an internal cache of Intents, it is very important that we must call Intents#release() after each test is completed to clear out this cache. Go ahead and uncomment the function clearIntentsState().

@After
fun clearIntentsState() {
   Intents.release()
}
Accessing the captured Intents

The Intents class provides a couple of methods to access the captured Intents.

  1. getIntents(): retrieve all captured Intents in a list.
  2. intented() variants: find one or more Intents and perform verifications on them.
  3. intending(): used for stubbing Intent responses.

In intentTest(), the method chosen was intended(). Go ahead and uncomment it out.

Intents.intended(allOf(
   hasAction(ACTION_VIEW),
   hasData(MainActivity.intentData)
))

To verify Intent information, we can use the convenient methods from the IntentMatchers class. hasAction() and hasData() do not exist on the vanilla Matchers class.

You can run the test now and verify whether it passes.

Map_Test_With_Capture.gif

Solution Code

ExampleInstrumentedTest.kt

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {

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

   @Before
   fun startCapturingIntent(){
       Intents.init()
   }

   @After
   fun clearIntentsState() {
       Intents.release()
   }

   @Test
   fun intentTest(){
       onView(withId(R.id.button_launchIntent))
           .perform(click())

       Intents.intended(allOf(
           hasAction(ACTION_VIEW),
           hasData(MainActivity.intentData)
       ))
   }

}

MainActivity.kt

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

       findViewById<Button>(R.id.button_launchIntent).setOnClickListener {
           val intent = Intent(ACTION_VIEW).apply {
               data = intentData
           }

           startActivity(intent)
       }
   }

   companion object {
       val intentData: Uri = Uri.Builder()
           .scheme("geo")
           .query("0,0")
           .appendQueryParameter("q", "First St SE, Washington, DC, 20004")
           .build()
   }
}

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_launchIntent"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/launch_intent"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

strings.xml

<resources>
   <string name="app_name">Daniweb Android Validate Intents</string>
   <string name="launch_intent">Launch Intent</string>
</resources>

Module build.gradle

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

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.example.daniwebandroidvalidateintents"
       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.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.1'
   implementation 'com.google.android.material:material:1.6.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
   testImplementation 'junit:junit:4.13.2'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

   androidTestImplementation 'androidx.test:runner:1.4.0'
   androidTestImplementation 'androidx.test:rules:1.4.0'
   androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
}
Summary

We have learned how to test Intents in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidValidateIntents.

Android Native – How to use CountingIdlingResource in Espresso tests

Introduction ##

When working with Espresso tests, you might have found it hard to make Espresso wait for background tasks to complete before performing other actions or assertions. Fortunately, the classes in the Espresso idling package exist to cover this use case.

In this tutorial, we will learn how to use one of those classes called CountingIdlingResource.

Goals

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

  1. How to use CountingIdlingResource in Espresso tests.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Chipmunk 2021.2.1 Patch 1.
Prerequisite Knowledge
  1. Intermediate Android.
  2. Basic Espresso.
  3. Basic understanding of async operations.
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 into your module build.gradle file.

     implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
     implementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
     androidTestImplementation 'androidx.test:runner:1.4.0'
     androidTestImplementation 'androidx.test:rules:1.4.0'
  3. Replace activity_main.xml with the code below. We simply changed the textSize and added an android:id to TextView.

     <?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">
    
        <TextView
            android:id="@+id/textView_helloWorld"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"
            android:textSize="32sp"
            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. Replace the entire class ExampleInstrumentedTest with the code below. Besides the commented out lines, it is just a simple Espresso test that performs a click on the TextView, and then verifies that the new text content is correct. You do not have to understand the commented out parts for now.

     @RunWith(AndroidJUnit4::class)
     class ExampleInstrumentedTest {
    
        //Gets the IdlingRegistry singleton
        //private val idlingResourceRegistry = IdlingRegistry.getInstance()
    
        @get:Rule
        val activityRule = ActivityScenarioRule(MainActivity::class.java)
    
        @Test
        fun doAsyncTest(){
            //Register the CountingIdlingResource before click()
            //idlingResourceRegistry.register(countingIdlingResource)
    
            onView(withId(R.id.textView_helloWorld))
                .perform(click())
                .check(matches(withText("WorldHello!")))
        }
    
     /*    @After
        fun unregisterIdlingResources(){
            //Unregisters the CountingIdlingResource
            idlingResourceRegistry.resources.forEach {
                idlingResourceRegistry.unregister(it)
            }
        }*/
     }
  5. Create a new class called HttpClient using the code below. This class contains a single function that will suspend for three seconds, and then assign a new text value to a TextView object.

     class HttpClient {
        fun doLongAsync(textView: TextView) {
            //Incrementing counter when work starts
            //countingIdlingResource.increment()
    
            CoroutineScope(Dispatchers.Main).launch {
                delay(3000)
                textView.text = "WorldHello!"
                //countingIdlingResource.decrement()
            }
        }
     }
  6. In the same file, add the singleton below. You do not need to understand it for now.

     /*
     We can use a global singleton for this project because
     We don't have more than one test using this IdlingResourceCounter
     It is recommended to add IdlingResourceCounter directly into your
     Production code
     */
     object IdlingResourceCounter {
        private const val IDLING_RESOURCE_NAME = "GlobalIdlingResourceCounter"
        val countingIdlingResource = CountingIdlingResource(IDLING_RESOURCE_NAME)
     }
  7. Finally, append the code below to MainActivity#onCreate().

     val textView = findViewById<TextView>(R.id.textView_helloWorld)
    
     val httpClient = HttpClient()
    
     textView.setOnClickListener {
        //Don't pass a View to a Service in a real app!
        httpClient.doLongAsync(it as TextView)
     }
Project Overview

The tutorial app contains a single TextView, after clicking on it and waits for 3 seconds, then its value will change to WorldHello! from Hello World!.

WorldHello.gif

Now let us look at the instrument test doAsyncTest() in ExampleInstrumentedTest.

Failed_Instrument_Test.gif

doAsyncTest() will fail because Espresso is not aware of the background task.

androidx.test.espresso.base.DefaultFailureHandler$AssertionFailedWithCauseError: 'an instance of android.widget.TextView and view.getText() with or without transformation to match: is "WorldHello!"' doesn't match the selected view.
Expected: an instance of android.widget.TextView and view.getText() with or without transformation to match: is "WorldHello!"

It tries to check for the WorldHello! value immediately after clicking on the TextView. There are only three conditions where Espresso will wait:

  1. The MessageQueue is empty.
  2. There is no running AsyncTask.
  3. There is no idling resource. CountingIdlingResource is one of such resources that we are learning about in this tutorial.
Creating the CountingIdlingResource object

The object IdlingResourceCounter contains a public field called countingIdlingResource, which is an instance of CountingIdlingResource. The only argument that the CountingIdlingResource constructor requires is a String value that explains what the instance of CountingIdlingResource is used for.

object IdlingResourceCounter {
    private const val IDLING_RESOURCE_NAME = "GlobalIdlingResourceCounter"
    val countingIdlingResource = CountingIdlingResource(IDLING_RESOURCE_NAME)
}

We are only able to use a global object in this tutorial because we only have one test. It is recommended that you provide a CountingIdlingResource for each instance of your async-worker class.

CountingIdlingResource and Service class

Your Service or Repository classes should increment() the counter when starting long-running async work and decrement() the counter when the work is complete. To achieve this, uncomment the respective lines in the HttpClient class, so it should look like the code below.

class HttpClient {
   fun doLongAsync(textView: TextView) {
       //Incrementing counter when work starts
       countingIdlingResource.increment()

       CoroutineScope(Dispatchers.Main).launch {
           delay(3000)
           textView.text = "WorldHello!"
           countingIdlingResource.decrement()
       }
   }
}
Register CountingIdlingResource to IdlingResourceRegistry

Simply having a working CountingIdlingResource is not enough. We still have to register the CountingIdlingResource instance to the IdlingResourceRegistry. IdlingResourceRegistry is a singleton. We already have the code to retrieve it in ExampleInstrumentedTest, so retrieve it by uncomment the line of code below.

private val idlingResourceRegistry = IdlingRegistry.getInstance()

Now that we have the IdlingResourceRegistry, we can register CountingIdlingResource to it by uncommenting this line of code inside doAsyncTest().

idlingResourceRegistry.register(countingIdlingResource)

We also have to unregister the idling resource after the test is done, so uncomment the function unregisterIdlingResources() as well.

@After
fun unregisterIdlingResources(){
   //Unregisters the CountingIdlingResource
   idlingResourceRegistry.resources.forEach {
       idlingResourceRegistry.unregister(it)
   }
}

We should have everything we need for a working test. If we run the test now, we can see that Espresso will wait for 3 seconds before performing the check().

Passed_Idling_Resource_Test.gif

Solution Code

ExampleInstrumentedTest.kt

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {

   //Gets the IdlingRegistry singleton
   private val idlingResourceRegistry = IdlingRegistry.getInstance()

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

   @Test
   fun doAsyncTest(){
       //Register the CountingIdlingResource before click()
       idlingResourceRegistry.register(countingIdlingResource)

       onView(withId(R.id.textView_helloWorld))
           .perform(click())
           .check(matches(withText("WorldHello!")))
   }

   @After
   fun unregisterIdlingResources(){
       //Unregisters the CountingIdlingResource
       idlingResourceRegistry.resources.forEach {
           idlingResourceRegistry.unregister(it)
       }
   }
}

HttpClient.kt

class HttpClient {
   fun doLongAsync(textView: TextView) {
       //Incrementing counter when work starts
       countingIdlingResource.increment()

       CoroutineScope(Dispatchers.Main).launch {
           delay(3000)
           textView.text = "WorldHello!"
           countingIdlingResource.decrement()
       }
   }
}

/*
We can use a global singleton for this project because
We don't have more than one test using this IdlingResourceCounter
It is recommended to add IdlingResourceCounter directly into your
Production code
*/
object IdlingResourceCounter {
   private const val IDLING_RESOURCE_NAME = "GlobalIdlingResourceCounter"
   val countingIdlingResource = CountingIdlingResource(IDLING_RESOURCE_NAME)
}

MainActivity.kt

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

       val textView = findViewById<TextView>(R.id.textView_helloWorld)

       val httpClient = HttpClient()

       textView.setOnClickListener {
           //Don't pass a View to a Service in a real app!
           httpClient.doLongAsync(it as TextView)
       }
   }
}

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

   <TextView
       android:id="@+id/textView_helloWorld"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="Hello World!"
       android:textSize="32sp"
       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.daniwebandroidcountingidlingresource"
       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.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.1'
   implementation 'com.google.android.material:material:1.6.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
   implementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
   testImplementation 'junit:junit:4.13.2'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
   androidTestImplementation 'androidx.test:runner:1.4.0'
   androidTestImplementation 'androidx.test:rules:1.4.0'
   implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'

}
Summary

We have learned how to use CountingIdlingResource to make Espresso wait in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidCountingIdlingResource.

Flutter Development job search

Proficient with Flutter
Experience with Flutter/Native hybrid development
Experience in the development of multiple Flutter projects, including the launch of Flutter projects
Email: tianxzhang@163.coma

Require pattern or password when user attempts to shutdown device from lock

Hey guys is there a way to intercept shut down to make the user enter password if the state is in the locked state? Its just that I am engineering an anti-theft app and would to disable shut down from lock screen while the device is in the locked state. Is there a way of achieving this with Android Java. I tried listening on the shut down intent but the device does not even give the code in the OnRecieve to execute

Android Native – How to launch Activity from an exact alarm

Introduction ##

Starting from Android 10 (API 29), Android places many restrictions on how apps can launch activities from the background.

There are a couple of exemptions, which can be found on this list. At the end of the list is the usage of the dangerous permission SYSTEM_ALERT_WINDOW. I have seen this permission recommended elsewhere, but usage of this permission is unnecessary to show an Activity for an alarm application and violates best practices for Android permission. Even the stock Clock app does not require this permission, the only install-time permission that it needs is SCHEDULE_EXACT_ALARM.

The correct way to show an Activity with an alarm is by using a notification with a full-screen Intent.

Goals

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

  1. How to schedule an exact alarm.
  2. How to show an Activity on the lock screen even if the screen is off and the Activity destroyed.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Chipmunk 2021.2.1.
Prerequisite Knowledge
  1. Intermediate Android.
  2. Notification.
  3. BroadcastReceiver.
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 <string> resource below into strings.xml.

     <string name="schedule_alarm">Schedule Alarm</string>
  3. Replace the code in activity_main.xml with the code below. This replaces the default TextView with a Button.

     <?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_scheduleAlarm"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/schedule_alarm"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
     </androidx.constraintlayout.widget.ConstraintLayout>
AlarmManager Overview

Although there are many types of alarms that you can set on Android, for this tutorial, knowing that we are only working with exact alarms is enough. Android allows other types of inexact alarms, which are not relevant for this tutorial. To set an exact alarm, you can use the method setAlarmClock() from the AlarmManager class.

You can retrieve an instance of AlarmManager inside your Activity by calling getSystemService().

Project Plan

To be able to set an exact alarm and show the Activity on the lock screen when the device is sleeping, there are quite a few steps involved. The list below provides a quick summary.

  1. The application must be ready to show a notification on high priority, so we must create a NotificationChannel with IMPORTANTCE_HIGH.
  2. The AlarmManager cannot show the Activity directly, so it must go through a BroadcastReceiver, which will then displays a Notification that can show an Activity.
    AlarmManager.jpg
  3. Install-time permissions and proper attributes for the Activity must be set in the manifest.

Now that we have a rough idea of what to do next, it is time to write some code.

Create the NotificationChannel
  1. Add the constants above on top of MainActivity.kt. These are just convenient constants for the NotificationManager.

     private const val NOTIFICATION_CHANNEL_NAME = "0"
     const val NOTIFICATION_CHANNEL_ID = "1"
  2. Add the function to create the NotificationChannel to MainActivity.

     private fun createNotificationChannel(){
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                NOTIFICATION_CHANNEL_ID,
                NOTIFICATION_CHANNEL_NAME,
                NotificationManager.IMPORTANCE_HIGH
            )
    
            with(getSystemService(NOTIFICATION_SERVICE) as NotificationManager){
                createNotificationChannel(channel)
            }
        }
     }
  3. Finally, call it in onCreate().

     createNotificationChannel()
Schedule an exact alarm

Next, we will have to bind the Button to schedule an exact alarm.

  1. Append the code below to onCreate() to find the Button and binds its onClickListener.

     findViewById<Button>(R.id.button_scheduleAlarm).setOnClickListener {
     }
  2. We will have to add FLAG_IMMUTABLE to all PendingIntent created in this tutorial, so add the constant below above MainActivity.kt

     val compatFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        FLAG_IMMUTABLE
     } else {
        0
     }
  3. Add the code below inside the setOnClickListener() callback. This code creates the explicit Intent as well as wrapping it in a PendingIntent, which is required later by AlarmClockInfo. Ignores the compile error because of the missing AlarmReceiver for now.

     val sendBroadcastIntent = Intent(this, AlarmReceiver::class.java)
     val pendingIntent = PendingIntent.getBroadcast(
        this,
        0,
        sendBroadcastIntent,
        compatFlags
     )
  4. Finally, retrieve the AlarmManager, and use setAlarmClock().

     with(getSystemService(Context.ALARM_SERVICE) as AlarmManager){
        //Alarm will trigger in 10 seconds
        //You should set the alarm by clicking the Button, and then clears the Activity
        // from the backstack by swiping up on the Recents screen.
        val triggerTime = Calendar.getInstance().apply {
            add(SECOND, 10)
        }
    
        val alarmInfo = AlarmManager.AlarmClockInfo(
            triggerTime.timeInMillis,
            pendingIntent //The alarm will fire this PendingIntent
        )
    
        setAlarmClock(
            alarmInfo,
            pendingIntent
        )
     }
Create the BroadcastReceiver

We will now create a BroadcastReceiver called AlarmReceiver.

Copy and paste the entire code below into your project. Here, we created and show a Notification with a full-screen Intent.

import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat

class AlarmReceiver : BroadcastReceiver() {
   override fun onReceive(context: Context?, intent: Intent?) {
       if (context != null){
           val fullScreenPendingIntent = PendingIntent.getActivity(
               context,
               0,
               Intent(context, MainActivity::class.java),
               compatFlags
           )

           val notificationBuilder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
               .setSmallIcon(R.drawable.ic_launcher_foreground)
               .setPriority(NotificationCompat.PRIORITY_HIGH)
               .setFullScreenIntent(fullScreenPendingIntent, true)

           with(NotificationManagerCompat.from(context)) {
               //This notification only works once until the end user dismiss the previous notification.
               //Can also make the notification auto-dismiss if desired.
               notify(1, notificationBuilder.build())
           }
       }
   }
}
Tasks in the Manifest
  1. For the app to work, we will need to declare two install-time permissions, so add to the the manifest.

     <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
     <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
  2. For MainActivity, we will need to add the showWhenLocked and turnScreenOn attributes.

     android:showWhenLocked="true"
     android:turnScreenOn="true"
  3. Finally, declare the BroadcastReceiver AlarmReceiver.

     <receiver
        android:name=".AlarmReceiver"
        android:exported="false"/>
Run the App

If you ran the app previously on your device, completely uninstall it and install it again.

To test if the app is working correctly, follow the steps below:

  1. Starts the app.
  2. Press the Button to schedule an alarm.
  3. Immediately kills the app from the Recents screen.
  4. Put the device into sleep.
  5. Wait a couple of seconds. Remember that you only have ten seconds to perform steps 2-4.
  6. Wait for the app to show on the home screen. Home screen is still locked for obvious security reasons.

You can use the animation below as reference.

Alarm.gif

Solution Code

MainActivity.kt

private const val NOTIFICATION_CHANNEL_NAME = "0"
const val NOTIFICATION_CHANNEL_ID = "1"

val compatFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
   FLAG_IMMUTABLE
} else {
   0
}

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

       createNotificationChannel()

       findViewById<Button>(R.id.button_scheduleAlarm).setOnClickListener {
           val sendBroadcastIntent = Intent(this, AlarmReceiver::class.java)
           val pendingIntent = PendingIntent.getBroadcast(
               this,
               0,
               sendBroadcastIntent,
               compatFlags
           )

           with(getSystemService(Context.ALARM_SERVICE) as AlarmManager){
               //Alarm will trigger in 10 seconds
               //You should set the alarm by clicking the Button, and then clears the Activity
               // from the backstack by swiping up on the Recents screen.
               val triggerTime = Calendar.getInstance().apply {
                   add(SECOND, 10)
               }

               val alarmInfo = AlarmManager.AlarmClockInfo(
                   triggerTime.timeInMillis,
                   pendingIntent //The alarm will fire this PendingIntent
               )

               setAlarmClock(
                   alarmInfo,
                   pendingIntent
               )
           }
       }
   }

   private fun createNotificationChannel(){
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
           val channel = NotificationChannel(
               NOTIFICATION_CHANNEL_ID,
               NOTIFICATION_CHANNEL_NAME,
               NotificationManager.IMPORTANCE_HIGH
           )

           with(getSystemService(NOTIFICATION_SERVICE) as NotificationManager){
               createNotificationChannel(channel)
           }
       }
   }
}

AlarmReceiver.kt

class AlarmReceiver : BroadcastReceiver() {
   override fun onReceive(context: Context?, intent: Intent?) {
       if (context != null){
           val fullScreenPendingIntent = PendingIntent.getActivity(
               context,
               0,
               Intent(context, MainActivity::class.java),
               compatFlags
           )

           val notificationBuilder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
               .setSmallIcon(R.drawable.ic_launcher_foreground)
               .setPriority(NotificationCompat.PRIORITY_HIGH)
               .setFullScreenIntent(fullScreenPendingIntent, true)

           with(NotificationManagerCompat.from(context)) {
               //This notification only works once until the end user dismiss the previous notification.
               //Can also make the notification auto-dismiss if desired.
               notify(1, notificationBuilder.build())
           }
       }
   }
}

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_scheduleAlarm"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/schedule_alarm"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

strings.xml

<resources>
   <string name="app_name">Daniweb Android Native AlarmManager</string>
   <string name="schedule_alarm">Schedule Alarm</string>
</resources>

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.daniwebandroidnativealarmmanager">

   <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
   <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>

   <application
       android:allowBackup="true"
       android:dataExtractionRules="@xml/data_extraction_rules"
       android:fullBackupContent="@xml/backup_rules"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/Theme.DaniwebAndroidNativeAlarmManager"
       tools:targetApi="31">
       <activity
           android:name=".MainActivity"
           android:showWhenLocked="true"
           android:turnScreenOn="true"
           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=".AlarmReceiver"
           android:exported="false"/>
   </application>

</manifest>
Summary

We have learned how to launch an Activity from an exact alarm in this tutorial. Please keep in mind that this tutorial was only tested on Android API 32. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidNativeAlarmManager.

Android Native – How to partially update a Room entity

Introduction

When working with Room, you might have run into a situation where you only want to update specific fields of an entity instead of replacing the entire row. In this tutorial, we will learn how to do this in two different ways.

Goals

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

  1. How to update individual fields of an entity.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Chipmunk 2021.2.1.
Prerequisite Knowledge
  1. Intermediate Android.
  2. Room database.
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 for Room in the module build.gradle file.

     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 data class called User.kt. This class will be used as an entity.

     @Entity
     data class User(
        @PrimaryKey val id: Int = 1,
        val name: String,
        val age: Int
     )
  5. Create a new Dao for the User entity using the code below.

     @Dao
     interface UserDao {
    
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        fun insert(vararg users: User)
    
        @Update
        fun update(vararg users: User)
    
        @Delete
        fun delete(vararg users: User)
     }
  6. Create a MyDatabase class using the code below.

     @Database(entities = [User::class], version = 1)
     abstract class MyDatabase : RoomDatabase() {
        abstract fun studentDao(): UserDao
     }
  7. Replace the content of activity_main.xml with the code below.

     <?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_update"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/update"
            app:layout_constraintBottom_toTopOf="@+id/button_partialUpdate"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <Button
            android:id="@+id/button_partialUpdate"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/partial_update"
            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/button_update" />
     </androidx.constraintlayout.widget.ConstraintLayout>
  8. Add the string resources below into strings.xml.

     <string name="update">Update</string>
     <string name="partial_update">Partial Update</string>
  9. In MainActivity#onCreate(), append the code below to insert an entity at launch. Note that the database used here is an in-memory database and will lose data after the app is killed.

     //Creates db and get a Dao instance
     val studentDao = Room.inMemoryDatabaseBuilder(
        applicationContext,
        MyDatabase::class.java
     ).build()
        .studentDao()
    
     //Insert a User on start
     lifecycleScope.launch(Dispatchers.IO) {
        val user = User(
            name = "Anna",
            age = 50
        )
    
        studentDao.insert(user)
     }
    
     val updateButton = findViewById<Button>(R.id.button_update)
     updateButton.setOnClickListener {
        //Updates the entire row where id = 1
        lifecycleScope.launch(Dispatchers.IO){
            val user = User(
                name = "Updated Anna",
                age = 51
            )
    
            studentDao.update(user)
        }
     }
Project Overview

The tutorial project contains two buttons.

image3.png

The Update button will update update the entire User row when clicked.

Entity_Full_Update.gif

The Partial Update button does nothing as of now. At the end of the tutorial, it should be able to perform a partial update to the User with an ID of 1. Besides the id column, the User entity contains two other columns, name and age. These are the columns that we want to individually update.

Partial update with @Query

The first way to update just a single field in an entity is to simply use an @Query Dao function with an UPDATE statement.

In UserDao.kt, add the function below to update just the column name using a @Query.

@Query("UPDATE user SET name=:name WHERE id=:id")
fun updateNameByQuery(id: Int, name: String)

Unless you want to, we dont really have to write any UI code to test this. You can just use the Database Inspector like in the animation below.

Update_Entity_Name_with_Query.gif

Partial update using partial entity

Personally, I believe that we should only use @Query with a non-querying statement only as a last resort. Fortunately, Rooms @Update supports partial updates as well, but some boilerplate is required because we have to create new classes that represent the partial data. Follow the steps below to add code that will update the fields name and age individually.

  1. Inside of User.kt, add two more data classes to act as partial entities UserName and UserAge. They need to have the id field declared as well which can tell Room which row to update.

     class UserName(
        val id: Int = 1,
        val name: String
     )
    
     class UserAge(
        val id: Int = 1,
        val age: Int
     )
  2. Back in UserDao.kt, add the Dao functions below. Each @Update annotation needs to pass in a value for the entity parameter, which is the Class object of the full entity. In the function parameters is where you need to declare the partial entities.

     @Update(entity = User::class)
     fun updateName(userName: UserName)
    
     @Update(entity = User::class)
     fun updateAge(userAge: UserAge)
  3. To test the new dao functions, we need to the append the code below to MainActivity#onCreate(). I have only used the partial partial entity UserName here to keep things simple.

     val updatePartialButton = findViewById<Button>(R.id.button_partialUpdate)
     updatePartialButton.setOnClickListener {
        lifecycleScope.launch(Dispatchers.IO){
            val userName = UserName(
                name = "New Anna"
            )
    
            studentDao.updateName(userName)
        }
     }

When we launch the app, we can see that only the user name is updated after clicking on Partial Update.

Partial_UserName_Update.gif

Solution Code

MainActivity.kt

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

       //Creates db and get a Dao instance
       val studentDao = Room.inMemoryDatabaseBuilder(
           applicationContext,
           MyDatabase::class.java
       ).build()
           .studentDao()

       //Insert a User on start
       lifecycleScope.launch(Dispatchers.IO) {
           val user = User(
               name = "Anna",
               age = 50
           )

           studentDao.insert(user)
       }

       val updateButton = findViewById<Button>(R.id.button_update)
       updateButton.setOnClickListener {
           //Updates the entire row where id = 1
           lifecycleScope.launch(Dispatchers.IO){
               val user = User(
                   name = "Updated Anna",
                   age = 51
               )

               studentDao.update(user)
           }
       }

       val updatePartialButton = findViewById<Button>(R.id.button_partialUpdate)
       updatePartialButton.setOnClickListener {
           lifecycleScope.launch(Dispatchers.IO){
               val userName = UserName(
                   name = "New Anna"
               )

               studentDao.updateName(userName)
           }
       }
   }
}

User.kt

@Entity
data class User(
   @PrimaryKey val id: Int = 1,
   val name: String,
   val age: Int
)

class UserName(
   val id: Int = 1,
   val name: String
)

class UserAge(
   val id: Int = 1,
   val age: Int
)

UserDao.kt

@Dao
interface UserDao {

   @Insert(onConflict = OnConflictStrategy.IGNORE)
   fun insert(vararg users: User)

   @Update
   fun update(vararg users: User)

   @Delete
   fun delete(vararg users: User)

   @Query("UPDATE user SET name=:name WHERE id=:id")
   fun updateNameByQuery(id: Int, name: String)

   @Update(entity = User::class)
   fun updateName(userName: UserName)

   @Update(entity = User::class)
   fun updateAge(userAge: UserAge)
}

MyDatabase.kt

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

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_update"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/update"
       app:layout_constraintBottom_toTopOf="@+id/button_partialUpdate"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintHorizontal_bias="0.5"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

   <Button
       android:id="@+id/button_partialUpdate"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/partial_update"
       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/button_update" />
</androidx.constraintlayout.widget.ConstraintLayout>

strings.xml

<resources>
   <string name="app_name">Daniweb Android Partial Room Entity Update</string>
   <string name="update">Update</string>
   <string name="partial_update">Partial Update</string>
</resources>

Module build.gradle

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

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.example.daniwebandroidpartialroomentityupdate"
       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.6.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 perform a partial update on an entity in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidPartialRoomEntityUpdate.

Android Native – How to create a homescreen Widget

Introduction

On the android platform, widgets provide a quick way to interact with your Apps. They are extremely useful at providing quick information without the user launching the app Activities themselves. Common use cases for homescreen widgets are weather, email, or banking information.

In this tutorial, we will learn how to create a widget for our Android app.

Goals

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

  1. How to create a homescreen widget.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Chipmunk 2021.2.1.
Prerequisite Knowledge
  1. Intermediate Android.
Project Setup

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

  1. Create a new Android project with the default Empty Activity.
Steps to create a Widget

There are three basic things that we would need to create a widget:

  1. An AppWidgetProviderInfo object. We can just declare the configuration for this object in an XML file.
  2. An XML file that contains the layout for the widget. A layout for a widget is a lot different than a layout for Activity/Fragment because you are only able to use the View and ViewGroups defined in RemoteViews[https://developer.android.com/reference/android/widget/RemoteViews].
  3. An implementation of AppWidgetProvider. An AppWidgetProvider is just a BroadcastReceiver, so you can intercept Intents in this class. We will also need to define this implementation in the manifest.

The Android Studio IDE supports creating all of these files, add string resources, Material 3 styles AND declaring the widget in the manifest in one button click. We will learn how to create the essential files manually in this tutorial. After you know how to create widgets manually, then it would just be faster to let the IDE generate most of the boilerplate code by right-clicking on res/xml -> New -> Widget -> AppWidget.

Create the AppWidgetProviderInfo XML
  1. Create a new XML file called widget_provider_info under res/xml. The root element must be <appwidget-provider>.
    Screenshot_2022-05-21_002706.png

  2. After the file has been created, copy and paste the code below into widget_provider_info.xml. These attributes are enough for this tutorial, but more attributes can be found here(https://developer.android.com/reference/android/appwidget/AppWidgetProviderInfo).

     <?xml version="1.0" encoding="utf-8"?>
     <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
        android:minWidth="50dp"
        android:minHeight="50dp"
        android:initialLayout="@layout/widget"
        android:resizeMode="horizontal|vertical">
     </appwidget-provider>
Create the Widget layout

The file above will display a compile error because the file widget.xml is missing from the layout directory. Create the widget.xml in the layout directory with RelativeLayout> as the root element.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:background="@color/white">

   <TextView
       android:id="@+id/widget_textView"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_alignParentStart="true"
       android:text="@string/widget_textView"
       android:textSize="32sp" />
</RelativeLayout>

This layout contains exactly one element, which is a TextView. Remember that all of the elements used here are supported by RemoteViews.

Make sure to add the string resource below to strings.xml as well.

<string name="widget_textView">Hello World!</string>
Implements AppWidgetProvider

Next, we will have to create an implementation of AppWidgetProvider. Create a new class called Widget, which overrides AppWidgetProvider.

class Widget : AppWidgetProvider() {
   override fun onReceive(context: Context?, intent: Intent?) {
       super.onReceive(context, intent)
   }

   override fun onUpdate(
       context: Context?,
       appWidgetManager: AppWidgetManager?,
       appWidgetIds: IntArray?
   ) {
       super.onUpdate(context, appWidgetManager, appWidgetIds)
   }

   override fun onAppWidgetOptionsChanged(
       context: Context?,
       appWidgetManager: AppWidgetManager?,
       appWidgetId: Int,
       newOptions: Bundle?
   ) {
       super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
   }

   override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
       super.onDeleted(context, appWidgetIds)
   }

   override fun onEnabled(context: Context?) {
       super.onEnabled(context)
   }

   override fun onDisabled(context: Context?) {
       super.onDisabled(context)
   }

   override fun onRestored(context: Context?, oldWidgetIds: IntArray?, newWidgetIds: IntArray?) {
       super.onRestored(context, oldWidgetIds, newWidgetIds)
   }
}

There are no abstract functions that we need to override at all. Our Widget still receives a broadcast if one matches the Intent declared in the manifest (later on in the tutorial), but it is not taking any action yet, unless you want to do something with it. We will not cover how to respond to broadcasts in this tutorial as that topic is quite lengthy and deserves an entire tutorial by itself. One of the most important thing to keep in mind for the base class AppWidgetProvider is that only the function onReceive() contains any code, and its main functionality is to filter out the Intent received and call its appropriate onX functions (as shown in code). All of the other functions in AppWidgetProvider are empty.

Since AppWidgetProvider is also a BroadcastReceiver, we will also need to declare it in the manifest.

<receiver android:name=".Widget"
   android:exported="false">
   <intent-filter>
       <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
   </intent-filter>
   <meta-data android:name="android.appwidget.provider"
       android:resource="@xml/widget_provider_info" />
</receiver>
Run the App

We should have everything we need to run the App and create our own widget. The app should behave similar to the animation below.

Android_Widget.gif

Solution Code

Widget.kt

class Widget : AppWidgetProvider() {
   override fun onReceive(context: Context?, intent: Intent?) {
       super.onReceive(context, intent)
   }

   override fun onUpdate(
       context: Context?,
       appWidgetManager: AppWidgetManager?,
       appWidgetIds: IntArray?
   ) {
       super.onUpdate(context, appWidgetManager, appWidgetIds)
   }

   override fun onAppWidgetOptionsChanged(
       context: Context?,
       appWidgetManager: AppWidgetManager?,
       appWidgetId: Int,
       newOptions: Bundle?
   ) {
       super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
   }

   override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
       super.onDeleted(context, appWidgetIds)
   }

   override fun onEnabled(context: Context?) {
       super.onEnabled(context)
   }

   override fun onDisabled(context: Context?) {
       super.onDisabled(context)
   }

   override fun onRestored(context: Context?, oldWidgetIds: IntArray?, newWidgetIds: IntArray?) {
       super.onRestored(context, oldWidgetIds, newWidgetIds)
   }
}

widget.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:background="@color/white">

   <TextView
       android:id="@+id/widget_textView"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_alignParentStart="true"
       android:text="@string/widget_textView"
       android:textSize="32sp" />
</RelativeLayout>

strings.xml

<resources>
   <string name="app_name">Daniweb Android Create Launcher Widget</string>
   <string name="widget_textView">Hello World!</string>
</resources>

widget_provider_info.xml

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
   android:minWidth="50dp"
   android:minHeight="50dp"
   android:initialLayout="@layout/widget"
   android:resizeMode="horizontal|vertical">
</appwidget-provider>

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.daniwebandroidcreatelauncherwidget">

   <application
       android:allowBackup="true"
       android:dataExtractionRules="@xml/data_extraction_rules"
       android:fullBackupContent="@xml/backup_rules"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/Theme.DaniwebAndroidCreateLauncherWidget"
       tools:targetApi="31">

       <receiver android:name=".Widget"
           android:exported="false">
           <intent-filter>
               <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
           </intent-filter>
           <meta-data android:name="android.appwidget.provider"
               android:resource="@xml/widget_provider_info" />
       </receiver>

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

We have learned how to create widgets in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidCreateLauncherWidget.

Android Native – How to use RecyclerView ConcatAdapter

Introduction

In this tutorial, we will learn how to use ConcatAdapter. It is a great choice for displaying header or footer at the beginning or at the end of a RecyclerView.

Goals

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

  1. How to use ConcatAdapter to combine Adapters.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Chipmunk 2021.2.1.
rerequisite Knowledge
  1. Basic Android.
  2. RecyclerView.
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 activity_main.xml with the code below. This removes the default Hello World TextView and add a RecyclerView that is constrained to the parent.

     <?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/recycler"
            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 layout resource called text_holder.xml to contain the view for the first ViewHolder.

     <?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="wrap_content">
    
        <TextView
            android:id="@+id/textView_holder"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="Text Holder Content" />
     </androidx.constraintlayout.widget.ConstraintLayout>
  4. Add the vector image below as a drawable resource using the name ic_android_black_24dp.

     <vector android:height="24dp" android:tint="#000000"
        android:viewportHeight="24" android:viewportWidth="24"
        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
        <path android:fillColor="#FF000000" android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z"/>
     </vector>
  5. Create another layout resource file called image_holder.xml for the second ViewHolder.

     <?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"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    
        <ImageView
            android:id="@+id/imageView_holder"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:src="@drawable/ic_android_black_24dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
     </androidx.constraintlayout.widget.ConstraintLayout>
  6. Create a TextAdapter class to display text_holder_xml.

     class TextAdapter(private val dataSet: List<String>) : RecyclerView.Adapter<TextAdapter.TextViewHolder>() {
    
        class TextViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
            val textView: TextView = itemView.findViewById(R.id.textView_holder)
        }
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextViewHolder {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.text_holder, parent, false)
    
            return TextViewHolder(view)
        }
    
        override fun onBindViewHolder(holder: TextViewHolder, position: Int) {
            holder.textView.text = dataSet[position]
        }
    
        override fun getItemCount() = dataSet.size
     }
  7. Create an ImageHolder class to display image_holder.xml.

     class ImageAdapter : RecyclerView.Adapter<ImageAdapter.ImageViewHolder>() {
    
        class ImageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.image_holder, parent, false)
    
            return ImageViewHolder(view)
        }
    
        override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {}
        override fun getItemCount() = 1
     }
  8. Replace the code in MainActivity with the code below. This implementation of onCreate() creates data for the separate Adapters and assign them to the RecyclerView. Only one Adapter can be assigned to a RecyclerView, so the last one that is assigned wins.

     class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            //list of strings
            val textDataSet = alphabet
                .map { "$it" }
    
            val recycler = findViewById<RecyclerView>(R.id.recycler)
            recycler.adapter = TextAdapter(textDataSet)
            recycler.adapter = ImageAdapter()
        }
    
        companion object {
            private val alphabet = CharRange('A', 'Z')
        }
     }
  9. Add the dependency for the latest version of RecyclerView to your project build.gradle file. ConcatAdapter is only included from version 1.2.0-alpha04 (it used to be called MergeAdapter).

     implementation "androidx.recyclerview:recyclerview:1.2.1"
Project Overview

As of right now, there are two lines of code that create an assign an Adapter to the RecyclerView. The app will look different depending on which Adapter you decide to use.

If you comment out the line below,

recycler.adapter = ImageAdapter(imageDataSet)

then your RecyclerView will use the TextAdapter.

Screenshot_20220513_163352.png

If you just leave the code as-is, letting the ImageAdapter wins, then the RecyclerView will just use the ImageAdapter as its adapter. The ImageAdapter will only display one item.

Screenshot_20220513_163440.png

Basic ConcatAdapter Usage

ConcatAdapter is typically used for displaying multiple Adapters one after another, this does not mean that the Adapters are interweaving into one another.

  1. Comment out the current assignments to Adapter.

     // recycler.adapter = TextAdapter(textDataSet)
     // recycler.adapter = ImageAdapter()
  2. Using a ConcatAdapter is fairly simple, you can just add the other Adapters into the ConcatAdapter constructor.

     recycler.adapter = ConcatAdapter(
        TextAdapter(textDataSet),
        ImageAdapter()
     )

If we run the App now, it will look like the image below.

Screenshot_20220513_164613.png

Dynamically Add and Remove Adapters from ConcatAdapter

If you want to dynamically add or remove the child Adapters, you can also instantiate an empty ConcatAdapter, and then call addAdapter() or removeAdapter() as needed. You can also add another Adapter of the same type again if your use case needs it.

//Dynamically add or more Adapters
val concatAdapter = ConcatAdapter()
   .apply {
       addAdapter(TextAdapter(textDataSet))
       addAdapter(ImageAdapter())
       addAdapter(TextAdapter(textDataSet))
       addAdapter(ImageAdapter())
   }

recycler.adapter = concatAdapter

Screenshot_20220513_170038.png

Solution Code

MainActivity.kt

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

       //list of strings
       val textDataSet = alphabet
           .map { "$it" }

       val recycler = findViewById<RecyclerView>(R.id.recycler)
//        recycler.adapter = TextAdapter(textDataSet)
//        recycler.adapter = ImageAdapter()

       //Use ConcatAdapter constructor
/*        recycler.adapter = ConcatAdapter(
           TextAdapter(textDataSet),
           ImageAdapter()
       )*/

       //Dynamically add or more Adapters
       val concatAdapter = ConcatAdapter()
           .apply {
               addAdapter(TextAdapter(textDataSet))
               addAdapter(ImageAdapter())
               addAdapter(TextAdapter(textDataSet))
               addAdapter(ImageAdapter())
           }

       recycler.adapter = concatAdapter
   }

   companion object {
       private val alphabet = CharRange('A', 'Z')
   }
}

ImageAdapter.kt

class ImageAdapter : RecyclerView.Adapter<ImageAdapter.ImageViewHolder>() {

   class ImageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

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

       return ImageViewHolder(view)
   }

   override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {}
   override fun getItemCount() = 1
}

TextAdapter.kt

class TextAdapter(private val dataSet: List<String>) : RecyclerView.Adapter<TextAdapter.TextViewHolder>() {

   class TextViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
       val textView: TextView = itemView.findViewById(R.id.textView_holder)
   }

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

       return TextViewHolder(view)
   }

   override fun onBindViewHolder(holder: TextViewHolder, position: Int) {
       holder.textView.text = dataSet[position]
   }

   override fun getItemCount() = dataSet.size
}

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/recycler"
       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>

image_holder.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"
   android:layout_width="match_parent"
   android:layout_height="wrap_content">

   <ImageView
       android:id="@+id/imageView_holder"
       android:layout_width="match_parent"
       android:layout_height="50dp"
       android:src="@drawable/ic_android_black_24dp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

text_holder.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="wrap_content">

   <TextView
       android:id="@+id/textView_holder"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       tools:text="Text Holder Content" />
</androidx.constraintlayout.widget.ConstraintLayout>

Project build.gradle

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

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.example.daniwebandroidconcatadapter"
       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.recyclerview:recyclerview:1.2.1"
   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.1'
   implementation 'com.google.android.material:material:1.6.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 ConcatAdapter in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidConcatAdapter.

Android Native – Modify button appearance using State List Resources

Introduction

Rather than modifying the button widgets appearance in code, it is possible to do this in an XML resource instead. In this tutorial, we will learn how to create two different types of specialized XML resource files for changing the Drawable and the color.

Goals

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

  1. How to use a Color State List resource.
  2. How to use a Drawable State List resource.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Chipmunk 2021.2.1.
Prerequisite Knowledge
  1. Basic Android.
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 vector resource below as ic_baseline_arrow_24.xml into drawable.

     <vector android:height="24dp" android:tint="#000000"
        android:viewportHeight="24" android:viewportWidth="24"
        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
        <path android:fillColor="@android:color/white" android:pathData="M8,5v14l11,-7z"/>
     </vector>
  3. Replace the code inside of activity_main.xml with the code below. The screen will now contain a single ImageButton for us to play with.

     <?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">
    
        <ImageButton
            android:id="@+id/imageButton"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:backgroundTint="#00FFFFFF"
            android:scaleType="fitCenter"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/ic_baseline_play_arrow_24" />
     </androidx.constraintlayout.widget.ConstraintLayout>
  4. If we run the app now, it should look like the screenshot below.

Screenshot_2022-05-13_211656.png

Creating a Color State List Resource

Conceptually, a state list resource is an XML file that tells Android what to do when a widget is in a certain state. For Button-type widgets, the common states are focused, pressed, and none.

Let us create the XML file first and dissect the syntax later.

  1. Right-click on res -> New -> Android Resource File.

  2. Select the *Resource type as Color. The root element must be selector. Finally use color as the Directory name**.
    image2.png

  3. Copy and paste the code below into the button_colors.xml file.

     <?xml version="1.0" encoding="utf-8"?>
     <selector xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:color="@color/purple_700"
            android:state_pressed="true"/>
        <item android:color="@color/purple_700"
            android:state_focused="true" />
        <item android:color="@color/black" />
     </selector>
  4. Now that we have a resource file, it is time to discuss the syntax. There are only a couple of simple rules.
    a. The root element of a state list resource file must be <selector>.
    b. Every child item is an <item>.
    c. Every <item> must have an android:color attribute.
    d. The android:state_X attributes define whether you want this color to be active when the button is in a specific state. e. Here, we configure for purple_700 to be active when the button is either in the focused or pressed states. When it is not focused or pressed, it takes on the color black.

To apply these color states to our Play button, we can add an app:tint attribute directly to the ImageButton widget in the activity_main.xml. The value for this attribute is the path to the color state list button_colors.xml.

app:tint="@color/button_colors"

If we run the app and test the button, it should change its color on click.

Button_Color_States.gif

Modify Button Drawables using a State List

Besides a Color State List, Android provides of state list that is called Drawable State List. The concept is very similar to the Color State List. You define an XML resource that tells Android which Drawable to use for the widget.

  1. To create a Drawable state list, follow the steps above to create an XML resource, but select Drawable as the Resource type instead. The root element remains <selector>. The directory name should be drawable.
    image3.png

  2. Add another vector resource called ic_baseline_pause_24.xml for this step.

     <vector android:height="24dp" android:tint="#000000"
        android:viewportHeight="24" android:viewportWidth="24"
        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
        <path android:fillColor="@android:color/white" android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
     </vector>
  3. Replace the content of button_drawables.xml with the code below. This will load the vector image created previously when the button is pressed.

     <?xml version="1.0" encoding="utf-8"?>
     <selector xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:drawable="@drawable/ic_baseline_pause_24"
            android:state_pressed="true" />
        <item android:drawable="@drawable/ic_baseline_play_arrow_24" />
     </selector>
  4. Back in activity_main.xml, replace the value of app:srcCompat to reference the new Drawable State List instead of the direct vector image.

     app:srcCompat="@drawable/button_drawables"

If we run the app now, we can see that both the color and Drawable are changed when the button is pressed.

Button_Drawable_States.gif

Solution Code

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

   <ImageButton
       android:id="@+id/imageButton"
       android:layout_width="200dp"
       android:layout_height="200dp"
       android:backgroundTint="#00FFFFFF"
       android:scaleType="fitCenter"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       app:srcCompat="@drawable/button_drawables"
       app:tint="@color/button_colors" />
</androidx.constraintlayout.widget.ConstraintLayout>

button_drawables.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
   <item android:drawable="@drawable/ic_baseline_pause_24"
       android:state_pressed="true" />
   <item android:drawable="@drawable/ic_baseline_play_arrow_24" />
</selector>

button_colors.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
   <item android:color="@color/purple_700"
       android:state_pressed="true"/>
   <item android:color="@color/purple_700"
       android:state_focused="true" />
   <item android:color="@color/black" />
</selector>

ic_baseline_pause_24.xml

<vector android:height="24dp" android:tint="#000000"
   android:viewportHeight="24" android:viewportWidth="24"
   android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
   <path android:fillColor="@android:color/white" android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
</vector>

ic_baseline_play_arrow_24

<vector android:height="24dp" android:tint="#000000"
   android:viewportHeight="24" android:viewportWidth="24"
   android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
   <path android:fillColor="@android:color/white" android:pathData="M8,5v14l11,-7z"/>
</vector>
Summary

We have learned how to change button color and drawables using state lists in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebModifyButtonColorUsingColorStateListResource.

Android Native – How to create an Options Menu

Introduction

The Android platform provides many different types of menus to be added to an Android app. In this tutorial, we will learn how to add the most common type of menu, an Options Menu.

An Options Menu is one that appears at the right corner of the Action Bar/Toolbar. By default, all items in the menu is contained inside the overflow action.

options_menu_1.png

Goals

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

  1. How to add an Options Menu to an Android app.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 3.
Prerequisite Knowledge
  1. Basic Android.
  2. Action Bar/Toolbar.
Project Setup

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

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

  2. Give the default TextView an android:id of hello.

  3. Change its android:textSize to 32sp.

     <?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">
    
        <TextView
            android:id="@+id/hello"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"
            android:textSize="32sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
     </androidx.constraintlayout.widget.ConstraintLayout>
Add an Options Menu using XML

The easiest way to add an Options Menu to an Action Bar is by declaring the menu in an XML file and then inflating it in the Activitys onCreateOptionsMenu() function.

We will start with creating a simple menu with three items in it:

  • Refresh.
  • Help.
  • Settings.
  1. Right-click on res -> New -> Android Resource File.

  2. Name the file options.

  3. Change the Resource type to Menu.
    options_menu_2.png

  4. Select OK.

  5. Add these string resources into strings.xml.

     <string name="refresh">Refresh</string>
     <string name="help">Help</string>
     <string name="settings">Settings</string>
  6. Now that we have the XML file with <menu> as the root element, we can either build the menu in the Code View by ourselves, or using the Design Surface. Regardless of how you choose to build your menu, add three Menu Item/<item> that corresponds to the three options Refresh, Help, and Settings.

  7. Make sure that your Menu Items are using the string resources created in step 5 as the value for their android:title attributes.

  8. Your menu should look like the screenshot below if using the Design Surface.
    options_menu_3.png

  9. In Code View, they should look like the code below.

     <?xml version="1.0" encoding="utf-8"?>
     <menu xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:title="@string/refresh" />
        <item android:title="@string/help" />
        <item android:title="@string/settings" />
     </menu>
  10. We are now done with the XML file for now, but the app will not make use of the menu resource file yet. We still have to inflate the XML resource into the Action Bar. To do this, override MainActivitys onCreateOptionsMenu(), inflate the XML resource using the built-in menuInflater object, and return true.

     override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.options, menu)
        return true
     }

Run the app now to check whether the menu is working. It should behave similarly to the animation below.

OptionsMenu1.gif

Handle Click Events for Menu Items

The menu that we have created looks nice, but it does not perform any action when clicked. To respond to click events on each menu item, we will have to do a couple more things.

  1. When an item in the menu is clicked, MainActivitys onOptionsItemSelected() will be called, so we will have to override this function if we want to respond to click events.

  2. We should also give the menu items android:id attributes to make it easy to filter them out in onOptionsItemSelected().

  3. Modify options.xml to add android:ids to each <item>.

     <?xml version="1.0" encoding="utf-8"?>
     <menu xmlns:android="http://schemas.android.com/apk/res/android">
        <item
            android:title="@string/refresh"
            android:id="@+id/refresh"/>
        <item
            android:title="@string/help"
            android:id="@+id/help"/>
        <item android:title="@string/settings"
            android:id="@+id/settings"/>
     </menu>
  4. Override onOptionsItemSelected() in MainActivity using the code below. The app will now change the value of the default TextView to match the menu items title.

     override fun onOptionsItemSelected(item: MenuItem): Boolean {
        val textView = findViewById<TextView>(R.id.hello)
    
        return when(item.itemId){
            R.id.refresh,
            R.id.help,
            R.id.settings -> {
                textView.text = item.title
                true
            }
            else -> false
        }
     }

When we run the app now, we can see that clicks on any menu item will modify the TextView like the animation below.

OptionsMenu2.gif

Solution Code

MainActivity.kt

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

   override fun onCreateOptionsMenu(menu: Menu?): Boolean {
       menuInflater.inflate(R.menu.options, menu)
       return true
   }

   override fun onOptionsItemSelected(item: MenuItem): Boolean {
       val textView = findViewById<TextView>(R.id.hello)

       return when(item.itemId){
           R.id.refresh,
           R.id.help,
           R.id.settings -> {
               textView.text = item.title
               true
           }
           else -> false
       }
   }
}

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

   <TextView
       android:id="@+id/hello"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="Hello World!"
       android:textSize="32sp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

options.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
   <item
       android:title="@string/refresh"
       android:id="@+id/refresh"/>
   <item
       android:title="@string/help"
       android:id="@+id/help"/>
   <item android:title="@string/settings"
       android:id="@+id/settings"/>
</menu>

strings.xml

<resources>
   <string name="app_name">Daniweb Android Options Menu</string>
   <string name="refresh">Refresh</string>
   <string name="help">Help</string>
   <string name="settings">Settings</string>
</resources>
Summary

Congratulations, we have learned how to add an Options menu into our apps Action Bar. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidOptionsMenu.

Android Native – How to create Contextual Menus

Introduction

The Android platform provides many different types of menus to be added to an Android app. In this tutorial, we will learn how to add Contextual Menus to our app.

Goals

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

  1. How to add a floating contextual menu.
  2. How to add a contextual menu in action mode.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 3.
Prerequisite Knowledge
  1. Basic Android.
  2. Menu XML resource.
Project Setup

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

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

  2. Give the default TextView an android:id of hello and android:textSize of 32sp.

  3. Your activity_main.xml should look like the code below.

     <?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">
    
        <TextView
            android:id="@+id/hello"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/hello_world"
            android:textSize="32sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
     </androidx.constraintlayout.widget.ConstraintLayout>
  4. Add the string resources below into your strings.xml file.

     <string name="shuffle">Shuffle</string>
     <string name="reset">Reset</string>
     <string name="insert_random">Insert Random</string>
     <string name="hello_world">Hello World!</string>
  5. Add a menu XML resource called contextual.xml under res/menu. This will represent a menu with 3 options, Shuffle, Reset, and Insert Random. Later on, these actions will be performed on the default TextView.

     <?xml version="1.0" encoding="utf-8"?>
     <menu xmlns:android="http://schemas.android.com/apk/res/android">
    
        <item
            android:id="@+id/shuffle"
            android:title="@string/shuffle" />
        <item
            android:id="@+id/reset"
            android:title="@string/reset" />
        <item
            android:id="@+id/insert_random"
            android:title="@string/insert_random" />
     </menu>
Contextual Menu modes

A Contextual Menu can operate in two different modes:

  1. floating contextual menu: a floating menu appears on the screen after long-clicking a View. The floating menu uses this View as an anchor.
    floating_menu_1.png

  2. contextual action mode menu: the menu appears in the Action Bar instead, showing menu items as actions.

contextual_action.png

Add a floating contextual menu

To add a floating contextual menu, follow the steps below:

  1. In MainActivitys onCreate(), retrieve the default TextView.

     val textView = findViewById<TextView>(R.id.hello)
  2. Next, we will have to register this View for a contextual menu by calling registerForContextMenu() and passing in this same View.

     //Contextual floating menu
     registerForContextMenu(textView)
  3. Now, we need to override MainActivitys onCreateContextMenu() method, and inflate the menu resource file contextual.xml here.

     override fun onCreateContextMenu(
        menu: ContextMenu?,
        v: View?,
        menuInfo: ContextMenu.ContextMenuInfo?
     ) {
        super.onCreateContextMenu(menu, v, menuInfo)
        menuInflater.inflate(R.menu.contextual, menu)
     }
  4. The floating contextual menu should be working if you run the app now, but let us add some behaviors to the menu items to make them useful. We can listen for the floating contextual menus item clicks in onContextItemSelected().

     override fun onContextItemSelected(item: MenuItem): Boolean {
        super.onContextItemSelected(item)
        return handleContextMenuItem(item)
     }
  5. Because we will reuse the logic to handle the menu items later on in the tutorial, we will extract this logic into a different function called handleContextMenuItem(). This function will shuffle, reset, or insert a random alphabet character at a random index to Textview.

     private fun handleContextMenuItem(item: MenuItem): Boolean {
        val textView = findViewById<TextView>(R.id.hello)
        val text = textView.text
    
        return when(item.itemId){
            R.id.shuffle -> {
                textView.text = text
                    .asSequence()
                    .shuffled()
                    .joinToString(separator = "") {
                        "$it"
                    }
                true
            }
            R.id.reset -> {
                textView.text = getString(R.string.hello_world)
                true
            }
            R.id.insert_random -> {
                textView.text = StringBuilder(text)
                    .insert(
                        text.indices.random(),
                        alphabet.random()
                    )
                true
            }
            else -> false
        }
     }
  6. Also, add the companion object below for the alphabet constant.

     companion object {
        //Don't want chars 91->96
        private val alphabet = CharRange('A', 'Z').plus(CharRange('a', 'z'))
     }

We are now done with the floating contextual menu. The app should behave similarly to the animation below.

ContextualMenu1.gif

Add a contextual menu in action mode

In this section, we will learn how to add a contextual menu in action mode.

  1. Comment out registerForContextMenu(textView) in onCreate() as we will not be using the floating action menu anymore.

  2. Implement the ActionMode.Callback interface. This callback allows you to inflate, listen to click and destroy events, etc.

     private val actionModeCallback = object : ActionMode.Callback {
        override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
            menuInflater.inflate(R.menu.contextual, menu)
            return true
        }
    
        override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
            return handleContextMenuItem(item)
        }
    
        override fun onPrepareActionMode(mode: ActionMode, menu: Menu) = false
        override fun onDestroyActionMode(mode: ActionMode) {}
     }
  3. In onCreateActionMode(), we inflate the menu XML resource as done previously in onCreateContextMenu. Because actionModeCallback is declared inside the MainActivity scope, we have access to the menuInflater in the callback as well. If your implementation of ActionMode.Callback is outside of MainActivity, then you can access a MenuInflater from the ActionMode parameter instead.

  4. Back in onCreate(), we can bind the TextViews OnLongClickListener to invoke a function called startActionMode().

     //Contextual action menu
     textView.setOnLongClickListener {
        startActionMode(actionModeCallback)
        true
     }

When we run the app now, we should get a contextual menu in Action Bar instead of a floating contextual menu.

ContextualMenu2.gif

Solution Code

MainActivity.kt

class MainActivity : AppCompatActivity() {

   private val actionModeCallback = object : ActionMode.Callback {
       override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
           menuInflater.inflate(R.menu.contextual, menu)
           return true
       }

       override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
           return handleContextMenuItem(item)
       }

       override fun onPrepareActionMode(mode: ActionMode, menu: Menu) = false
       override fun onDestroyActionMode(mode: ActionMode) {}
   }

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

       val textView = findViewById<TextView>(R.id.hello)

       //Contextual floating menu
       //registerForContextMenu(textView)

       //Contextual action menu
       textView.setOnLongClickListener {
           startActionMode(actionModeCallback)
           true
       }
   }

   override fun onCreateContextMenu(
       menu: ContextMenu?,
       v: View?,
       menuInfo: ContextMenu.ContextMenuInfo?
   ) {
       super.onCreateContextMenu(menu, v, menuInfo)
       menuInflater.inflate(R.menu.contextual, menu)
   }

   override fun onContextItemSelected(item: MenuItem): Boolean {
       super.onContextItemSelected(item)
       return handleContextMenuItem(item)
   }

   private fun handleContextMenuItem(item: MenuItem): Boolean {
       val textView = findViewById<TextView>(R.id.hello)
       val text = textView.text

       return when(item.itemId){
           R.id.shuffle -> {
               textView.text = text
                   .asSequence()
                   .shuffled()
                   .joinToString(separator = "") {
                       "$it"
                   }
               true
           }
           R.id.reset -> {
               textView.text = getString(R.string.hello_world)
               true
           }
           R.id.insert_random -> {
               textView.text = StringBuilder(text)
                   .insert(
                       text.indices.random(),
                       alphabet.random()
                   )
               true
           }
           else -> false
       }
   }

   companion object {
       //Don't want chars 91->96
       private val alphabet = CharRange('A', 'Z').plus(CharRange('a', 'z'))
   }

}

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

   <TextView
       android:id="@+id/hello"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/hello_world"
       android:textSize="32sp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

contextual.xml

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

   <item
       android:id="@+id/shuffle"
       android:title="@string/shuffle" />
   <item
       android:id="@+id/reset"
       android:title="@string/reset" />
   <item
       android:id="@+id/insert_random"
       android:title="@string/insert_random" />
</menu>

**strings.xml**

<resources>
   <string name="app_name">Daniweb Android Contextual Menu</string>
   <string name="shuffle">Shuffle</string>
   <string name="reset">Reset</string>
   <string name="insert_random">Insert Random</string>
   <string name="hello_world">Hello World!</string>
</resources>
Summary

We have learned how to create a floating contextual menu as well as a contextual menu in action mode. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidContextualMenus.

Android Native – How to use PagingSource

Introduction

Among all of the Android libraries, Paging 3 is perhaps one of the hardest to understand.

Personally, I think the problem with Paging 3 are:

  1. Developers need to learn a lot of custom classes before they can understand how Paging 3 works under the hood and how to use the library to its fullest potential. The list of classes is long. It includes:

     CombinedLoadStates
     ConcatAdapter
     DiffUtil
     LoadState
     LoadStateAdapter
     LoadStates
     LoadType
     Pager
     PagingConfig
     PagingData
     PagingDataAdapter
     PagingSource
     PagingSource.LoadResult
     PagingState
     RemoteMediator
     RemoteMediator.MediatorResult
  2. You will have to dig deep into Pager/PagingDataAdapter source code to understand how PagingSource and PagingData are invalidated. Because you are not in control of creating PagingData yourself, how the PagingSource/PagingData pair is created and invalidated can be too magical for comfort.

  3. Unlike other libraries that only provide functionality for a specific layer, Paging 3 library classes span across multiple layers of your App: UI, ViewModel, Repository, Database.

On the bright side The final result is not a lot of code, and using this pre-made library would still most likely be simpler than rolling your own.

Paging 3 can operate in two different modes. The first mode only displays paged data from a network without an offline database as a cache. In the second mode, you can enlist a local Room database to use as your cache, for offline support.

In this tutorial, we will learn how to use Paging 3 without an offline database.

Goals

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

  1. How to use PagingSource to display data from the network.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 3.
Prerequisite Knowledge
  1. Intermediate Android.
  2. MVVM.
  3. Hilt.
  4. Kotlin Flow.
  5. Retrofit.
  6. Moshi.
  7. ViewBinding.
  8. DataBinding.
Project Setup

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

  1. Clone the sample app called CatFacts from https://github.com/dmitrilc/CatFacts.git
  2. Switch to the No_RemoteMediator branch.
Project Overview

The project is already completed, but we will walk through the built app to learn what happens at each step.

In the screenshot of the project structure below, we are only concerned about the classes in green. It is okay to ignore the packages and classes in red because they are not really interesting to the current topic; they are just Hilt and Retrofit classes if you are curious.

app_structure.png

You can run the app now. It is just a RecyclerView that displays cat facts received from the Cat Facts API. As you reach the end of the list, it will reach out to the network to get more data. You can also verify this behavior by using the Network Inspector. Refer to the animation below as a reference.

NoRemoteMediator.gif

The App also has a button to trigger a refresh, which causes Pager to reload data at a specified location.

App Architecture

To understand the apps architecture, we can refer to the diagram below.

NoRemoteMediatorArch.png

In the diagram above, how PagingDataAdapter loads another item is mostly hidden from us, so we can just call it implementation details. Because implementation details are subject to change, It is really not worth it to divulge them in this tutorial. Though if you are still confused and wants to know what happens, you can start with debugging backwards at the beginning of the CatFactPagingSource#load() function.

The classes PagingDataAdapter, MainActivity, CatFactsViewModel, CatFactRepo, Pager, and WebService all exist within the project code. This is what the app does:

  1. At first launch, the Repository prefetches enough data to display on the screen. It might load one or two pages, depending on the height of the screen and how many facts can fit.

  2. As the user scrolls to the bottom of the RecyclerView, PagingDataAdapter signals the Pager to load more items, invalidating the current generation (PagingSource/PagingData pair) as well as generating a new generation.

  3. After the new pair of PagingSource/PagingData is generated, a new PagingData object is pushed to the PagingDataAdapter.

  4. At the top of the App, there is a Button to to manually trigger a refresh() action. This app always start from the beginning because I am trying to avoid business logic overload in this tutorial, which can lead to unnecessary complexity. Just keep in mind that this refresh feature is very situational and not all Apps will need it, but we are still required to override it.

  5. I have also injected a callback into CatFactPagingSource, which can be used to update the UI of the last loaded page. This is just to confirm that the refresh is working correctly. This step is entirely optional.

Implementing PagingSource

When using Paging 3 without an offline database, you must first define a custom implementation of PagingSource yourself. The only two functions that you need to override are load() and getRefreshKey().

In CatFactPagingSource.kt, our implementation of load() is as below.

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CatFact> {
   try {
       // Start refresh at page 1 if undefined.
       val position = params.key ?: CAT_FACTS_STARTING_PAGE_INDEX
       pageNumberUpdater(position)

       val response = webService.getCatFactPage(position)
       val catFacts = response.data

       val prevKey = if (position == CAT_FACTS_STARTING_PAGE_INDEX) {
           null //if current position is the same as the starting position
       } else {
           position - 1
       }

       val nextKey = if (catFacts.isEmpty()){
           null //if there is no more data to load in the current direction
       } else {
           position + 1
       }

       return LoadResult.Page(
           data = catFacts,
           prevKey = prevKey,
           nextKey = nextKey
       )
   } catch (exception: IOException) {
       return LoadResult.Error(exception)
   } catch (exception: HttpException) {
       return LoadResult.Error(exception)
   }
}

Let us go through this code line by line.

  1. The function load() has a parameter of type LoadParams. An internal implementation will provide the argument when it calls load(). The most important properties of LoadParams would be its public properties key and loadSize.

  2. Typically, the key usually come from 4 different places:
    A. The previous or the next key that you have provided to the previous generation, depending on the LoadType (APPEND or PREPEND).
    B. The key from the getRefreshKey() function.
    C. The initialKey property when constructing the Pager.
    D. A custom key defined by you. For example, in this App, we used the constant CAT_FACTS_STARTING_PAGE_INDEX as the initial key.

  3. The other property of LoadParams, loadSize indicates how many items to load. This is the number pageSize passed to the PagingConfig in the CatFactRepo.kt file. The load() function is not required to use this property if it does want to.

  4. You are in control of how you want to calculate the previous and next keys. This heavily depends on the remote API that you are fetching from. For this App, we simply add or subtract one to the current key depending on the scrolling direction.

  5. Lastly, we must return a LoadResult object, which can be a LoadResult.Page on success or LoadResult.Error/LoadResult.Invalid if something went wrong. All three classes Page, Error, and Invaliad are subclasses of LoadResult.

Next, we will have to override getRefreshKey() as well. Our current implementation is as below.

override fun getRefreshKey(state: PagingState<Int, CatFact>): Int? {
   /*
   This will always start loading from the beginning.
   Same effect as return AT_FACTS_STARTING_PAGE_INDEX.
   */
   return null
   //If you want the list to resume at page 5, return 5, etc..
}

We are simply returning null, which the load() function will receive and then resets the key back into CAT_FACTS_STARTING_PAGE_INDEX, which will starts loading page one or page two as well, depending on whether the loaded items can fill the viewport.

Creating a Pager

To connect the UI and the Data layers, we would need to use a Pager object. The code below illustrates how to create a Pager.

fun getCatFactPagerWithoutRemoteMediator(): Flow<PagingData<CatFact>> =
   Pager(
       config = PagingConfig(pageSize = 1)
   ) {
       CatFactPagingSource(webService){ page ->
           _currentPage.value = "$page"
       }
   }
       .flow

After creation, its only public property is flow, which can be collected in other layers. There exists an extension function that exposes this flow as a LiveData as well.

Creating the PagingDataAdapter

To consume the Flow<PagingData<T>>, we must use a special RecycleView Adapter called PagingDataAdapter.

class PagingCatFactAdapter : PagingDataAdapter<CatFact, PagingCatFactAdapter.CatFactViewHolder>(DiffCallback) {

   class CatFactViewHolder(val binding: CatFactBinding) : RecyclerView.ViewHolder(binding.root){}

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
       CatFactViewHolder(
           CatFactBinding.inflate(
               LayoutInflater.from(parent.context),
               parent,
               false
           )
       )

   //Binds data to a viewholder
   override fun onBindViewHolder(holder: CatFactViewHolder, position: Int) {
       val item = getItem(position)
       if (item != null) {
           holder.binding.catFact.text = item.fact
       }
   }

   object DiffCallback : DiffUtil.ItemCallback<CatFact>() {
       override fun areItemsTheSame(old: CatFact, new: CatFact) = old == new
       override fun areContentsTheSame(old: CatFact, new: CatFact) = old.fact == new.fact
   }

}

The implementation is very similar to a normal adapter, there are only 3 main differences.

  1. You must subclass PagingDataAdapter.
  2. PagingDataAdapter requires a DiffUtil.ItemCallback in its constructor, which you must implement as well. Your implementation of DiffUtil.ItemCallback must know how to compare the objects and its contents.
  3. You do not have to implement getItemCount() yourself.
Connecting Pager to PagingDataAdapter

Lastly, we must collect the Flow from the Pager, and then submit it to PagingDataAdapter in the Activity or Fragment.

lifecycleScope.launch {
   viewModel.pagerNoRemoteMediator.collectLatest {
       adapter.submitData(it)
   }
}
Run the App

Now that we have understood everything, it is time to run the App again to see how the refresh functionality works.

NoRemoteMediator2.gif

Summary

Congratulations! We have learned how to use Paging 3 without offline support in this tutorial. If you are interested in learning how to use Paging 3 with offline support as well, check out the RemoteMediator tutorial here. The full project code can be found at https://github.com/dmitrilc/CatFacts/tree/No_RemoteMediator

Android Native – How to use RemoteMediator

Introduction

The Android Paging 3 library can operate in two modes, with an offline Room database as a source of truth or without one. In this tutorial, we will look at how to use Paging 3 with a local Room database.

This tutorial expands on the previously published Paging 3 tutorial without an offline database here. Paging 3 is somewhat complex, so if you are new to the library, I would recommend checking out the tutorial without RemoteMediator first.

Goals

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

  1. How to use RemoteMediator with Paging 3.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 3.
Prerequisite Knowledge
  1. Intermediate Android.
  2. MVVM.
  3. Hilt.
  4. Kotlin Flow.
  5. Retrofit.
  6. Moshi.
  7. ViewBinding.
  8. DataBinding.
  9. Room.
  10. Basic SQL.
Project Setup

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

  1. Clone the sample app called CatFacts from https://github.com/dmitrilc/CatFacts.git
  2. Switches to the With_RemoteMediator branch.
Project Overview

The project has been completed, but we will walk through the important classes to understand how RemoteMediator is used.

In the project structure below, the classes and packages in red can be ignored, while the ones in green are important to the topic.

app_structure_with_RemoteMediator.png

The App is very simple. It only loads Cat Facts from the Cat Facts API and displays them in a RecyclerView.

There are also some TextViews at the top of the screen to show statistics about the pages. This is only for demonstration purposes.

WithRemoteMediator.gif

App Architecture

The diagram PagingSource With RemoteMediator below attempts to explain the relationships among the classes in the current App. I have also included the diagram without RemoteMediator to make it easier for you to see the differences between the two.

WithAndWithoutRemoteMediator.png

RemoteMediator is required if we want to use Paging 3 with a local database. It has a couple of responsibilities:

  1. Determine what to do based on the LoadType of APPEND, PREPEND, or REFRESH.
  2. Queries more data from the WebService.
  3. Commit CRUDs on this new data to the local Room database. How Room talks to the Pager object is mostly implementation details.

The Pager only reaches out to the RemoteMediator if there is no more data to load from the database. You might have also noticed that we do not have to implement our own PagingSource in this case. This is because Room provides its own implementation for us, with both load() and getRefreshKey() implemented.

Implementing RemoteMediator

First, let us walk through the CatFactRemoteMediator class to see how it was implemented. When you provide an implementation of RemoteMediator, you are required to override two functions, load() and initialize().

In our App, the implementation of load() is as below.

override suspend fun load(
   loadType: LoadType,
   state: PagingState<Int, CatFact>
): MediatorResult {
   return try {
       val loadKey = when (loadType) {
           LoadType.REFRESH -> {
               catFactDao.deleteAll()
               remoteKeyDao.deleteAll()
               CAT_FACTS_STARTING_PAGE_INDEX
           }
           LoadType.PREPEND -> return MediatorResult.Success(true)
           LoadType.APPEND -> {
               val remoteKey = remoteKeyDao.get()!!

               if (remoteKey.currentPage == remoteKey.lastPage){
                   return MediatorResult.Success(true)
               }

               remoteKey.currentPage.plus(1)
           }
       }

       val response = webService.getCatFactPage(loadKey)

       database.withTransaction {
           remoteKeyDao.insertOrReplace(
               RemoteKey(
                   currentPage = response.current_page,
                   lastPage = response.last_page
               ))

           catFactDao.insertAll(response.data)
       }

       MediatorResult.Success(false)
   } catch (e: IOException) {
       MediatorResult.Error(e)
   } catch (e: HttpException) {
       MediatorResult.Error(e)
   }
}

In the function above, the most important things that we need to understand are:

  1. Because the load() function has so many responsibilities, it is often easy to get lost if you are looking at someone elses implementation because their app logic, business logic, and remote endpoints are different from yours. It is in your best interest to only understand how each function argument can be used to return the appropriate return value for your app.

  2. The LoadType parameter: this is just an enum with 3 values, APPEND, PREPEND, and REFRESH. APPEND and PREPEND represent the direction in which the user is scrolling, so you can handle this however you want based on your App behavior, such as loading the previous page or the next page. REFRESH is a little bit different, but you are in control of this as well; you can start loading from the beginning or resuming at a specific page. In this sample app, we clear everything and start from the beginning for REFRESH. For PREPEND, we return MediatorResult.Success with endOfPaginationReached set to true; this is akin to saying you have reached the end of pagination in this direction. For APPENDING, we are just incrementing the key by one.

  3. If the function has not been short-circuited by one of the return statements in the when expressions body, then we will request new data from the API and insert them into the Room database.

  4. We did not make use of the PagingState parameter at all. This is the same object that is passed to PagingSource#getRefreshKey(). It includes useful information such as the anchorPosition and the loaded pages. Again, you do not have to use it if it does not provide any useful information for your use case.

  5. We must return a MediatorResult object in the load() function as well. Do not confuse it with the LoadResult object from the PagingSources load() function. MediatorResult is a sealed class, so you cannot instantiate it directly. You must instantiate its subclasses Error and Success instead. If you are returning an Error, then you need to provide it with a Throwable object. If you are returning a Success, then you need to provide it with a boolean, indicating whether the end of paging has been reached for the current LoadType.

Next, we will need to implement initialize().

override suspend fun initialize(): InitializeAction {
   val remoteKey = remoteKeyDao.get()
   return if (remoteKey == null){
       InitializeAction.LAUNCH_INITIAL_REFRESH
   } else {
       InitializeAction.SKIP_INITIAL_REFRESH
   }
}

In this function, you can decide whether to perform an initial load or not. Fortunately, InitializeAction is just a simple enum with two constants. You do not have any dependency passed to initialize(), so you will have to rely on the WebService or the database objects in your own RemoteMediator class to decide whether you would like to refresh.

Creating the Pager

Creating a Pager when using a RemoteMediator is almost the same as without one, the only extra step that you need to do is to provide a RemoteMediator object to the Pagers remoteMediator parameter.

fun getCatFactPagerWithRemoteMediator(): Flow<PagingData<CatFact>> =
   Pager(
       PagingConfig(1),
       remoteMediator = catFactRemoteMediator
   ) {
       catFactDao.pagingSource()
   }
       .flow
Returning PagingSource<K, V> from Room

Another very important step that you need to do is to return a PagingSource from your DAO query. Our query is located in the CatFactDao.kt file.

@Query("SELECT * FROM cat_fact")
fun pagingSource(): PagingSource<Int, CatFact>

This part is a bit weird because we are not in control of how PagingSource is created by the generated DAO. It is a bit clearer when we look at the generated source code though.

@Override
public PagingSource<Integer, CatFact> pagingSource() {
 final String _sql = "SELECT * FROM cat_fact";
 final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
 return new LimitOffsetPagingSource<CatFact>(_statement, __db, "cat_fact") {
   @Override
   protected List<CatFact> convertRows(Cursor cursor) {
     final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(cursor, "id");
     final int _cursorIndexOfFact = CursorUtil.getColumnIndexOrThrow(cursor, "fact");
     final int _cursorIndexOfLength = CursorUtil.getColumnIndexOrThrow(cursor, "length");
     final List<CatFact> _result = new ArrayList<CatFact>(cursor.getCount());
     while(cursor.moveToNext()) {
       final CatFact _item;
       final int _tmpId;
       _tmpId = cursor.getInt(_cursorIndexOfId);
       final String _tmpFact;
       if (cursor.isNull(_cursorIndexOfFact)) {
         _tmpFact = null;
       } else {
         _tmpFact = cursor.getString(_cursorIndexOfFact);
       }
       final int _tmpLength;
       _tmpLength = cursor.getInt(_cursorIndexOfLength);
       _item = new CatFact(_tmpId,_tmpFact,_tmpLength);
       _result.add(_item);
     }
     return _result;
   }
 };
}

The code above is generated (Java), so it is a bit hard to read, but not that hard. After investigating, I have broken it down to several steps:

  1. It provides a concrete implementation of LimitOffsetPagingSource, overriding the method convertRows(Cursor cursor).
  2. LimitOffsetPagingSource is a subclass of PagingSource.
  3. The Cursor is looped through, creating CatFact objects and then place everything inside an ArrayList.
Managing the remote key

When using RemoteMediator with an offline database, Google recommends developers to create another entity for managing remote keys. How you build your key is highly dependent on what the remote data APIs return.

For the Cat Facts API specifically, the prev and next keys are provided with a paginated query, but they also provided a last_page key, and I have decided use that for simplicity instead of the prev and next keys, which are in nullable string form and changes for every query. You can check out the files RemoteKey and RemoteKeyDao to see how the keys are stored.

Unlike RemoteMediator, the Paging library is not at all aware of the key table, so you are in control of storing and using them however you want. Because the data is stored locally, it makes a lot of sense to create another table for managing the keys as well. The benefit with this approach is that you can apply data integration features that Room provides (foreign key, indexes, constraints, relationships, etc).

Consuming in Activity

To consume the Flow exposed from the Repository, we would consume it the same way when not using RemoteMediator as well.

lifecycleScope.launch {
   viewModel.pagerWithRemoteMediator.collectLatest {
       adapter.submitData(it)
   }
}
Summary

Congratulations! We have learned how to use Paging 3 with a RemoteMediator in this tutorial. The full project code can be found at https://github.com/dmitrilc/CatFacts/tree/With_RemoteMediator.

Best Language for Speech Control

I am contemplating writing a speech-controlled music player app for android. I am experienced in writing PC applications, and wrote a rather sharp speech controlled MP3 player using VB in the 1990s. I have also used c, c++, and several other languages in the past, but I have zero experience with mobile app development.

My priorities are a fast, memory-efficient app with good command-&-control and dictation abilities (iPhone compatibility would be a plus, but I'm willing to write a separate app for iPhone if the android app goes well).

What language/framework would you recommend?

Android Natve – Extending a Framework View

Introduction

Among the many ways to create your own View, extending an existing View is the simplest option. This is because you are inheriting pre-made attributes, drawing, and accessibility features.

In this tutorial, we will learn how to create our own custom View by extending an existing View class from the Android framework.

Goals

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

  1. How to extend an Android View.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 3.
Prerequisite Knowledge
  1. Basic Android.
Project Setup

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

  1. Create a new Android project with the default Empty Activity.
  2. Remove the default TextView from activity_main.xml.
Extend a View in Kotlin/Java

The first thing that we need to do in order to extend a framework View is to declare it in source code.

The class that we want to extend for this tutorial is the Switch class (android.widget.Switch), so follow the steps below to extend Switch.

  1. Create a new class called MySwitch using the code below.

     class MySwitch(context: Context) : Switch(context)
  2. The compiler will complain that you should use SwitchCompat instead, which is what you should actually do in real code. It does not really matter which Switch class to use for this tutorial, so you can simply suppress the warning by adding the @SuppressLint("UseSwitchCompatOrMaterialCode") annotation to MySwitch.

     @SuppressLint("UseSwitchCompatOrMaterialCode")
     class MySwitch(context: Context) : Switch(context)
  3. The code may look correct for now, but if we try to use MySwitch now, the program will crash. This is because at the minimum, we need to call the super(Switch) constructor that takes both a Context and an AttributeSet. Modify MySwitch to call the super constructor with both context and attrs (AttributeSet).

     @SuppressLint("UseSwitchCompatOrMaterialCode")
     class MySwitch(context: Context, attrs: AttributeSet) : Switch(context, attrs)
Use MySwitch in a layout file

To be able to use MySwitch in an xml file, we can use its FQCN as the XML element tag. There are two ways to add MySwitch to an XML, creating it by hand or with the Palette in the Design surface. Assuming that we want to add MySwitch via the Palette, follow the steps below.

  1. Open activity_main.xml in the Design surface.

  2. Under Palette, find Project.

  3. Under Project, drag MySwitch under ConstraintLayout.
    1.png

  4. And then simply constraint MySwitch to the center of the screen.

  5. activity_main.xml should now look like the code below.

     <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">
    
        <com.example.daniwebandroidnativeextendingframeworkview.MySwitch
            android:id="@+id/mySwitch"
            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_toTopOf="parent" />
     </androidx.constraintlayout.widget.ConstraintLayout>

Even though we have not provided any custom behavior for the switch, this is basically all you need to do to extend a framework View.

Add Custom Attributes

Now that we have our custom View, we can also add custom attributes and access them as well. Follow the steps below to add a custom attribute.

  1. Create a new file called attrs.xml under values.

  2. Add a <declare-stylable> element inside of <resources> with the name of MySwitch.

     <?xml version="1.0" encoding="utf-8"?>
     <resources>
        <declare-styleable name="MySwitch">
        </declare-styleable>
     </resources>
  3. Your custom attributes will go inside of the <declare-stylable> element.

  4. Each custom attribute is created using the <attr> element. You can provide it any name you like. A name is required.
    Add a new customAttribute like the code below.

     <attr name="customAttribute" format="string"/>
  5. There does not seem to be any documentation available for the type of the format, but you should be fine to use any of the suggested types in the screenshot below.

2.png

Use the custom attribute in XML

To use the custom attribute in XML, we can just use the xmlns:app="http://schemas.android.com/apk/res-auto" namespace. In the MySwitch element in activity_main.xml, add the customAttribute like below.

<com.example.daniwebandroidnativeextendingframeworkview.MySwitch
   android:id="@+id/mySwitch"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   app:customAttribute="test"
   app:layout_constraintBottom_toBottomOf="parent"
   app:layout_constraintEnd_toEndOf="parent"
   app:layout_constraintHorizontal_bias="0.5"
   app:layout_constraintStart_toStartOf="parent"
   app:layout_constraintTop_toTopOf="parent" />

Now, you can access the value of the attribute in the MySwitch like below.

init {
   context.theme.obtainStyledAttributes(
       attrs,
       R.styleable.MySwitch,
       0, 0
   ).use {
       it.getString(R.styleable.MySwitch_customAttribute)
   }
}

Keep in mind that the TypedArray object must be closed after accessing. Fortunately, it implements AutoClosable, so we can just open it with the use function.

Solution Code

MySwitch.kt

@SuppressLint("UseSwitchCompatOrMaterialCode")
class MySwitch(context: Context, attrs: AttributeSet) : Switch(context, attrs){
   init {
       context.theme.obtainStyledAttributes(
           attrs,
           R.styleable.MySwitch,
           0, 0
       ).use {
           it.getString(R.styleable.MySwitch_customAttribute)
       }
   }
}

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

   <com.example.daniwebandroidnativeextendingframeworkview.MySwitch
       android:id="@+id/mySwitch"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:customAttribute="test"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintHorizontal_bias="0.5"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <declare-styleable name="MySwitch">
       <attr name="customAttribute" format="string"/>
   </declare-styleable>
</resources>
Summary

We have learned how to extend an existing Android View in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidNativeExtendingFrameworkView

Android Native – Extending a Framework View

Introduction

Among the many ways to create your own View, extending an existing View is the simplest option. This is because you are inheriting pre-made attributes, drawing, and accessibility features.

In this tutorial, we will learn how to create our own custom View by extending an existing View class from the Android framework.

Goals

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

  1. How to extend an Android View.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 3.
Prerequisite Knowledge
  1. Basic Android.
Project Setup

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

  1. Create a new Android project with the default Empty Activity.
  2. Remove the default TextView from activity_main.xml.
Extend a View in Kotlin/Java

The first thing that we need to do in order to extend a framework View is to declare it in source code.

The class that we want to extend for this tutorial is the Switch class (android.widget.Switch), so follow the steps below to extend Switch.

  1. Create a new class called MySwitch using the code below.

     class MySwitch(context: Context) : Switch(context)
  2. The compiler will complain that you should use SwitchCompat instead, which is what you should actually do in real code. It does not really matter which Switch class to use for this tutorial, so you can simply suppress the warning by adding the @SuppressLint("UseSwitchCompatOrMaterialCode") annotation to MySwitch.

     @SuppressLint("UseSwitchCompatOrMaterialCode")
     class MySwitch(context: Context) : Switch(context)
  3. The code may look correct for now, but if we try to use MySwitch now, the program will crash. This is because at the minimum, we need to call the super(Switch) constructor that takes both a Context and an AttributeSet. Modify MySwitch to call the super constructor with both context and attrs (AttributeSet).

     @SuppressLint("UseSwitchCompatOrMaterialCode")
     class MySwitch(context: Context, attrs: AttributeSet) : Switch(context, attrs)
Use MySwitch in a layout file

To be able to use MySwitch in an xml file, we can use its FQCN as the XML element tag. There are two ways to add MySwitch to an XML, creating it by hand or with the Palette in the Design surface. Assuming that we want to add MySwitch via the Palette, follow the steps below.

  1. Open activity_main.xml in the Design surface.

  2. Under Palette, find Project.

  3. Under Project, drag MySwitch under ConstraintLayout.
    1.png

  4. And then simply constraint MySwitch to the center of the screen.

  5. activity_main.xml should now look like the code below.

     <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">
    
        <com.example.daniwebandroidnativeextendingframeworkview.MySwitch
            android:id="@+id/mySwitch"
            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_toTopOf="parent" />
     </androidx.constraintlayout.widget.ConstraintLayout>

Even though we have not provided any custom behavior for the switch, this is basically all you need to do to extend a framework View.

Add Custom Attributes

Now that we have our custom View, we can also add custom attributes and access them as well. Follow the steps below to add a custom attribute.

  1. Create a new file called attrs.xml under values.

  2. Add a <declare-stylable> element inside of <resources> with the name of MySwitch.

     <?xml version="1.0" encoding="utf-8"?>
     <resources>
        <declare-styleable name="MySwitch">
        </declare-styleable>
     </resources>
  3. Your custom attributes will go inside of the <declare-stylable> element.

  4. Each custom attribute is created using the <attr> element. You can provide it any name you like. A name is required.
    Add a new customAttribute like the code below.

     <attr name="customAttribute" format="string"/>
  5. There does not seem to be any documentation available for the type of the format, but you should be fine to use any of the suggested types in the screenshot below.

2.png

Use the custom attribute in XML

To use the custom attribute in XML, we can just use the xmlns:app="http://schemas.android.com/apk/res-auto" namespace. In the MySwitch element in activity_main.xml, add the customAttribute like below.

<com.example.daniwebandroidnativeextendingframeworkview.MySwitch
   android:id="@+id/mySwitch"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   app:customAttribute="test"
   app:layout_constraintBottom_toBottomOf="parent"
   app:layout_constraintEnd_toEndOf="parent"
   app:layout_constraintHorizontal_bias="0.5"
   app:layout_constraintStart_toStartOf="parent"
   app:layout_constraintTop_toTopOf="parent" />

Now, you can access the value of the attribute in the MySwitch like below.

init {
   context.theme.obtainStyledAttributes(
       attrs,
       R.styleable.MySwitch,
       0, 0
   ).use {
       it.getString(R.styleable.MySwitch_customAttribute)
   }
}

Keep in mind that the TypedArray object must be closed after accessing. Fortunately, it implements AutoClosable, so we can just open it with the use function.

Solution Code

MySwitch.kt

@SuppressLint("UseSwitchCompatOrMaterialCode")
class MySwitch(context: Context, attrs: AttributeSet) : Switch(context, attrs){
   init {
       context.theme.obtainStyledAttributes(
           attrs,
           R.styleable.MySwitch,
           0, 0
       ).use {
           it.getString(R.styleable.MySwitch_customAttribute)
       }
   }
}

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

   <com.example.daniwebandroidnativeextendingframeworkview.MySwitch
       android:id="@+id/mySwitch"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:customAttribute="test"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintHorizontal_bias="0.5"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <declare-styleable name="MySwitch">
       <attr name="customAttribute" format="string"/>
   </declare-styleable>
</resources>
Summary

We have learned how to extend an existing Android View in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidNativeExtendingFrameworkView

Android Native – Use Material 3 Colors in Your App

Introduction

In this tutorial, we will learn how to apply Material 3 colors and Dynamic Colors to our App.

Goals

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

  1. How to apply Material 3 colors.
  2. How to enable Dynamic Colors.
Tools Required
  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 3.
Prerequisite Knowledge
  1. Basic Android.
Project Setup

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

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

  2. Remove the default TextView.

  3. Copy and paste the code below into activity_main.xml. This will add a couple of random widgets mainly for us to see the visual effects. The code inside is not really important.

     <?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"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button"
            app:layout_constraintBottom_toTopOf="@+id/switch1"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <androidx.appcompat.widget.SwitchCompat
            android:id="@+id/switch1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Switch"
            app:layout_constraintBottom_toTopOf="@+id/radioGroup"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/button" />
    
        <RadioGroup
            android:id="@+id/radioGroup"
            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/switch1">
    
            <RadioButton
                android:id="@+id/radioButton"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="RadioButton" />
    
            <RadioButton
                android:id="@+id/radioButton2"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="RadioButton" />
    
        </RadioGroup>
     </androidx.constraintlayout.widget.ConstraintLayout>
  4. Inside of the Module build.gradle, change the version of com.google.android.material:material to 1.6.0-rc01.

Change theme to Material 3

When the project was first created, Android Studio automatically chose the Theme.MaterialComponents.DayNight.DarkActionBar theme for us. Themes starting with Theme.MaterialComponents.* actually belongs to Material 2, so in order to use a Material 3 theme, we must inherit from a Theme.Material3.* theme instead.

Fortunately, there is a web tool called Material 3 Theme Builder that will generate the code for us, so changing the theme is as simple as copying and pasting. Follow the steps below to generate our own Material 3 theme.

  1. Follow the link https://material-foundation.github.io/material-theme-builder/#/custom and navigate to the Custom tab, if not already selected. You can ignore the Dynamic tab for now.

  2. On the color generator, you are really only required to choose your own primary color. After selecting the primary color, the tool will automatically update your secondary and tertiary colors.
    1000020100000123000000546BEC7D4DC58A207C.png

  3. For this tutorial, select rgb(255, 0, 128) (#ff0080) as the primary color and leave everything else as default.

  4. At the top left corner, select Export -> as Android Views (XML).
    1000020100000128000000480F02BE09707CC6DF.png

  5. Extract the downloaded material-theme.zip file. You will then 2 folders, values and values-night.

  6. Replace entire <resource> block in your projects colors.xml with the the content of colors.xml in the downloaded values folder.

  7. Your new colors.xml file should look like the code below.

     <?xml version="1.0" encoding="utf-8"?>
     <resources>
        <color name="md_theme_light_primary">#BB005A</color>
        <color name="md_theme_light_onPrimary">#FFFFFF</color>
        <color name="md_theme_light_primaryContainer">#FFD9E2</color>
        <color name="md_theme_light_onPrimaryContainer">#3F001A</color>
        <color name="md_theme_light_secondary">#74565D</color>
        <color name="md_theme_light_onSecondary">#FFFFFF</color>
        <color name="md_theme_light_secondaryContainer">#FFD9E1</color>
        <color name="md_theme_light_onSecondaryContainer">#2B151B</color>
        <color name="md_theme_light_tertiary">#7B5734</color>
        <color name="md_theme_light_onTertiary">#FFFFFF</color>
        <color name="md_theme_light_tertiaryContainer">#FFDCBD</color>
        <color name="md_theme_light_onTertiaryContainer">#2D1600</color>
        <color name="md_theme_light_error">#BA1B1B</color>
        <color name="md_theme_light_errorContainer">#FFDAD4</color>
        <color name="md_theme_light_onError">#FFFFFF</color>
        <color name="md_theme_light_onErrorContainer">#410001</color>
        <color name="md_theme_light_background">#FCFCFC</color>
        <color name="md_theme_light_onBackground">#201A1B</color>
        <color name="md_theme_light_surface">#FCFCFC</color>
        <color name="md_theme_light_onSurface">#201A1B</color>
        <color name="md_theme_light_surfaceVariant">#F3DEE1</color>
        <color name="md_theme_light_onSurfaceVariant">#514346</color>
        <color name="md_theme_light_outline">#837376</color>
        <color name="md_theme_light_inverseOnSurface">#FAEEEF</color>
        <color name="md_theme_light_inverseSurface">#352F30</color>
        <color name="md_theme_light_inversePrimary">#FFB0C6</color>
        <color name="md_theme_light_shadow">#000000</color>
        <color name="md_theme_light_primaryInverse">#FFB0C6</color>
        <color name="md_theme_dark_primary">#FFB0C6</color>
        <color name="md_theme_dark_onPrimary">#65002D</color>
        <color name="md_theme_dark_primaryContainer">#8F0043</color>
        <color name="md_theme_dark_onPrimaryContainer">#FFD9E2</color>
        <color name="md_theme_dark_secondary">#E3BDC5</color>
        <color name="md_theme_dark_onSecondary">#432930</color>
        <color name="md_theme_dark_secondaryContainer">#5B3F46</color>
        <color name="md_theme_dark_onSecondaryContainer">#FFD9E1</color>
        <color name="md_theme_dark_tertiary">#EEBD92</color>
        <color name="md_theme_dark_onTertiary">#472A0A</color>
        <color name="md_theme_dark_tertiaryContainer">#61401F</color>
        <color name="md_theme_dark_onTertiaryContainer">#FFDCBD</color>
        <color name="md_theme_dark_error">#FFB4A9</color>
        <color name="md_theme_dark_errorContainer">#930006</color>
        <color name="md_theme_dark_onError">#680003</color>
        <color name="md_theme_dark_onErrorContainer">#FFDAD4</color>
        <color name="md_theme_dark_background">#201A1B</color>
        <color name="md_theme_dark_onBackground">#ECE0E1</color>
        <color name="md_theme_dark_surface">#201A1B</color>
        <color name="md_theme_dark_onSurface">#ECE0E1</color>
        <color name="md_theme_dark_surfaceVariant">#514346</color>
        <color name="md_theme_dark_onSurfaceVariant">#D6C2C5</color>
        <color name="md_theme_dark_outline">#9E8D90</color>
        <color name="md_theme_dark_inverseOnSurface">#201A1B</color>
        <color name="md_theme_dark_inverseSurface">#ECE0E1</color>
        <color name="md_theme_dark_inversePrimary">#BB005A</color>
        <color name="md_theme_dark_shadow">#000000</color>
        <color name="md_theme_dark_primaryInverse">#BB005A</color>
     </resources>
  8. Similarly, your project files themes.xml and themes.xml (night) should be updated as well with the two themes.xml files in the downloaded zip file. When you are copying and pasting these files over, make sure to preserve your own apps theme name and change the parent to Theme.Material3.DayNight since this matches our current theme. Theme.MaterialComponents.DayNight.DarkActionBar is not available in Material 3, so the closest thing would be Theme.Material3.DayNight.

  9. Your themes.xml should look similar to the code below.

     <resources>
        <style name="Theme.DaniwebAndroidNativeMaterial3Colors" parent="Theme.Material3.DayNight">
            <item name="colorPrimary">@color/md_theme_light_primary</item>
            <item name="colorOnPrimary">@color/md_theme_light_onPrimary</item>
            <item name="colorPrimaryContainer">@color/md_theme_light_primaryContainer</item>
            <item name="colorOnPrimaryContainer">@color/md_theme_light_onPrimaryContainer</item>
            <item name="colorSecondary">@color/md_theme_light_secondary</item>
            <item name="colorOnSecondary">@color/md_theme_light_onSecondary</item>
            <item name="colorSecondaryContainer">@color/md_theme_light_secondaryContainer</item>
            <item name="colorOnSecondaryContainer">@color/md_theme_light_onSecondaryContainer</item>
            <item name="colorTertiary">@color/md_theme_light_tertiary</item>
            <item name="colorOnTertiary">@color/md_theme_light_onTertiary</item>
            <item name="colorTertiaryContainer">@color/md_theme_light_tertiaryContainer</item>
            <item name="colorOnTertiaryContainer">@color/md_theme_light_onTertiaryContainer</item>
            <item name="colorError">@color/md_theme_light_error</item>
            <item name="colorErrorContainer">@color/md_theme_light_errorContainer</item>
            <item name="colorOnError">@color/md_theme_light_onError</item>
            <item name="colorOnErrorContainer">@color/md_theme_light_onErrorContainer</item>
            <item name="android:colorBackground">@color/md_theme_light_background</item>
            <item name="colorOnBackground">@color/md_theme_light_onBackground</item>
            <item name="colorSurface">@color/md_theme_light_surface</item>
            <item name="colorOnSurface">@color/md_theme_light_onSurface</item>
            <item name="colorSurfaceVariant">@color/md_theme_light_surfaceVariant</item>
            <item name="colorOnSurfaceVariant">@color/md_theme_light_onSurfaceVariant</item>
            <item name="colorOutline">@color/md_theme_light_outline</item>
            <item name="colorOnSurfaceInverse">@color/md_theme_light_inverseOnSurface</item>
            <item name="colorSurfaceInverse">@color/md_theme_light_inverseSurface</item>
     <!--        <item name="colorInversePrimary">@color/md_theme_light_inversePrimary</item>
            <item name="colorShadow">@color/md_theme_light_shadow</item>-->
            <item name="colorPrimaryInverse">@color/md_theme_light_primaryInverse</item>
        </style>
     </resources>
  10. Your themes.xml (night) should look similar to the code below.

     <resources>
        <style name="Theme.DaniwebAndroidNativeMaterial3Colors" parent="Theme.Material3.DayNight">
            <item name="colorPrimary">@color/md_theme_dark_primary</item>
            <item name="colorOnPrimary">@color/md_theme_dark_onPrimary</item>
            <item name="colorPrimaryContainer">@color/md_theme_dark_primaryContainer</item>
            <item name="colorOnPrimaryContainer">@color/md_theme_dark_onPrimaryContainer</item>
            <item name="colorSecondary">@color/md_theme_dark_secondary</item>
            <item name="colorOnSecondary">@color/md_theme_dark_onSecondary</item>
            <item name="colorSecondaryContainer">@color/md_theme_dark_secondaryContainer</item>
            <item name="colorOnSecondaryContainer">@color/md_theme_dark_onSecondaryContainer</item>
            <item name="colorTertiary">@color/md_theme_dark_tertiary</item>
            <item name="colorOnTertiary">@color/md_theme_dark_onTertiary</item>
            <item name="colorTertiaryContainer">@color/md_theme_dark_tertiaryContainer</item>
            <item name="colorOnTertiaryContainer">@color/md_theme_dark_onTertiaryContainer</item>
            <item name="colorError">@color/md_theme_dark_error</item>
            <item name="colorErrorContainer">@color/md_theme_dark_errorContainer</item>
            <item name="colorOnError">@color/md_theme_dark_onError</item>
            <item name="colorOnErrorContainer">@color/md_theme_dark_onErrorContainer</item>
            <item name="android:colorBackground">@color/md_theme_dark_background</item>
            <item name="colorOnBackground">@color/md_theme_dark_onBackground</item>
            <item name="colorSurface">@color/md_theme_dark_surface</item>
            <item name="colorOnSurface">@color/md_theme_dark_onSurface</item>
            <item name="colorSurfaceVariant">@color/md_theme_dark_surfaceVariant</item>
            <item name="colorOnSurfaceVariant">@color/md_theme_dark_onSurfaceVariant</item>
            <item name="colorOutline">@color/md_theme_dark_outline</item>
            <item name="colorOnSurfaceInverse">@color/md_theme_dark_inverseOnSurface</item>
            <item name="colorSurfaceInverse">@color/md_theme_dark_inverseSurface</item>
     <!--        <item name="colorInversePrimary">@color/md_theme_dark_inversePrimary</item>
            <item name="colorShadow">@color/md_theme_dark_shadow</item>-->
            <item name="colorPrimaryInverse">@color/md_theme_dark_primaryInverse</item>
        </style>
     </resources>
  11. Notice that we have commented out colorInversePrimary and colorShadow. They are generated by the Theme Builder, but they cause compile errors in our code. These color roles are not listed in the official docs. Since the Theme Builder is a closed source project and I did not find any obvious error reports, I am going to assume that it is safe to comment them out for now.

Before and After

Before Material 3 was applied, our app looks like this

100002010000018E000003443E3F976F17EAD72A.png

After applying Material 3, our app looks like this

100002010000018D0000034B04ECF9D79740DA84.png

The main differences are that the Button takes on a more rounded shape and the primary color is a dark pinkish color as opposed to the bright blue previously.

Dynamic Color

Material 3 also introduced the Dynamic Color feature, which lets your app change color based on the users current wallpaper. Enabling it on top of the existing Material 3 theme is super easy. Follow the steps below:

  1. Create a new class called MyApplication that extends Application.

  2. Override its onCreate() function to apply Dynamic Colors to the entire app with the code below.

     class MyApplication : Application() {
    
        override fun onCreate() {
            super.onCreate()
            DynamicColors.applyToActivitiesIfAvailable(this)
        }
    
     }
  3. Add the android:name attribute to the <application> element in the manifest file.

     android:name=".MyApplication" 

If you start our app now, then you will see it using a different theme compared to before because the theme changes dynamically based on the current background wallpaper. Reference the pictures below to see the wallpaper/app theme pairs.

100002010000032000000345D48FFD89D6F8114A.png

1000020100000320000003412677C8A478FE2CF8.png

Solution Code

MyApplication.kt

    class MyApplication : Application() {

       override fun onCreate() {
           super.onCreate()
           DynamicColors.applyToActivitiesIfAvailable(this)
       }

    }

AndroidManifest.xml

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

       <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.DaniwebAndroidNativeMaterial3Colors">
           <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>

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"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="Button"
           app:layout_constraintBottom_toTopOf="@+id/switch1"
           app:layout_constraintEnd_toEndOf="parent"
           app:layout_constraintHorizontal_bias="0.5"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintTop_toTopOf="parent" />

       <androidx.appcompat.widget.SwitchCompat
           android:id="@+id/switch1"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="Switch"
           app:layout_constraintBottom_toTopOf="@+id/radioGroup"
           app:layout_constraintEnd_toEndOf="parent"
           app:layout_constraintHorizontal_bias="0.5"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintTop_toBottomOf="@+id/button" />

       <RadioGroup
           android:id="@+id/radioGroup"
           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/switch1">

           <RadioButton
               android:id="@+id/radioButton"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:text="RadioButton" />

           <RadioButton
               android:id="@+id/radioButton2"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:text="RadioButton" />

       </RadioGroup>
    </androidx.constraintlayout.widget.ConstraintLayout>

colors.xml

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
       <color name="md_theme_light_primary">#BB005A</color>
       <color name="md_theme_light_onPrimary">#FFFFFF</color>
       <color name="md_theme_light_primaryContainer">#FFD9E2</color>
       <color name="md_theme_light_onPrimaryContainer">#3F001A</color>
       <color name="md_theme_light_secondary">#74565D</color>
       <color name="md_theme_light_onSecondary">#FFFFFF</color>
       <color name="md_theme_light_secondaryContainer">#FFD9E1</color>
       <color name="md_theme_light_onSecondaryContainer">#2B151B</color>
       <color name="md_theme_light_tertiary">#7B5734</color>
       <color name="md_theme_light_onTertiary">#FFFFFF</color>
       <color name="md_theme_light_tertiaryContainer">#FFDCBD</color>
       <color name="md_theme_light_onTertiaryContainer">#2D1600</color>
       <color name="md_theme_light_error">#BA1B1B</color>
       <color name="md_theme_light_errorContainer">#FFDAD4</color>
       <color name="md_theme_light_onError">#FFFFFF</color>
       <color name="md_theme_light_onErrorContainer">#410001</color>
       <color name="md_theme_light_background">#FCFCFC</color>
       <color name="md_theme_light_onBackground">#201A1B</color>
       <color name="md_theme_light_surface">#FCFCFC</color>
       <color name="md_theme_light_onSurface">#201A1B</color>
       <color name="md_theme_light_surfaceVariant">#F3DEE1</color>
       <color name="md_theme_light_onSurfaceVariant">#514346</color>
       <color name="md_theme_light_outline">#837376</color>
       <color name="md_theme_light_inverseOnSurface">#FAEEEF</color>
       <color name="md_theme_light_inverseSurface">#352F30</color>
       <color name="md_theme_light_inversePrimary">#FFB0C6</color>
       <color name="md_theme_light_shadow">#000000</color>
       <color name="md_theme_light_primaryInverse">#FFB0C6</color>
       <color name="md_theme_dark_primary">#FFB0C6</color>
       <color name="md_theme_dark_onPrimary">#65002D</color>
       <color name="md_theme_dark_primaryContainer">#8F0043</color>
       <color name="md_theme_dark_onPrimaryContainer">#FFD9E2</color>
       <color name="md_theme_dark_secondary">#E3BDC5</color>
       <color name="md_theme_dark_onSecondary">#432930</color>
       <color name="md_theme_dark_secondaryContainer">#5B3F46</color>
       <color name="md_theme_dark_onSecondaryContainer">#FFD9E1</color>
       <color name="md_theme_dark_tertiary">#EEBD92</color>
       <color name="md_theme_dark_onTertiary">#472A0A</color>
       <color name="md_theme_dark_tertiaryContainer">#61401F</color>
       <color name="md_theme_dark_onTertiaryContainer">#FFDCBD</color>
       <color name="md_theme_dark_error">#FFB4A9</color>
       <color name="md_theme_dark_errorContainer">#930006</color>
       <color name="md_theme_dark_onError">#680003</color>
       <color name="md_theme_dark_onErrorContainer">#FFDAD4</color>
       <color name="md_theme_dark_background">#201A1B</color>
       <color name="md_theme_dark_onBackground">#ECE0E1</color>
       <color name="md_theme_dark_surface">#201A1B</color>
       <color name="md_theme_dark_onSurface">#ECE0E1</color>
       <color name="md_theme_dark_surfaceVariant">#514346</color>
       <color name="md_theme_dark_onSurfaceVariant">#D6C2C5</color>
       <color name="md_theme_dark_outline">#9E8D90</color>
       <color name="md_theme_dark_inverseOnSurface">#201A1B</color>
       <color name="md_theme_dark_inverseSurface">#ECE0E1</color>
       <color name="md_theme_dark_inversePrimary">#BB005A</color>
       <color name="md_theme_dark_shadow">#000000</color>
       <color name="md_theme_dark_primaryInverse">#BB005A</color>
    </resources>

themes.xml

    <resources>
       <style name="Theme.DaniwebAndroidNativeMaterial3Colors" parent="Theme.Material3.DayNight">
           <item name="colorPrimary">@color/md_theme_light_primary</item>
           <item name="colorOnPrimary">@color/md_theme_light_onPrimary</item>
           <item name="colorPrimaryContainer">@color/md_theme_light_primaryContainer</item>
           <item name="colorOnPrimaryContainer">@color/md_theme_light_onPrimaryContainer</item>
           <item name="colorSecondary">@color/md_theme_light_secondary</item>
           <item name="colorOnSecondary">@color/md_theme_light_onSecondary</item>
           <item name="colorSecondaryContainer">@color/md_theme_light_secondaryContainer</item>
           <item name="colorOnSecondaryContainer">@color/md_theme_light_onSecondaryContainer</item>
           <item name="colorTertiary">@color/md_theme_light_tertiary</item>
           <item name="colorOnTertiary">@color/md_theme_light_onTertiary</item>
           <item name="colorTertiaryContainer">@color/md_theme_light_tertiaryContainer</item>
           <item name="colorOnTertiaryContainer">@color/md_theme_light_onTertiaryContainer</item>
           <item name="colorError">@color/md_theme_light_error</item>
           <item name="colorErrorContainer">@color/md_theme_light_errorContainer</item>
           <item name="colorOnError">@color/md_theme_light_onError</item>
           <item name="colorOnErrorContainer">@color/md_theme_light_onErrorContainer</item>
           <item name="android:colorBackground">@color/md_theme_light_background</item>
           <item name="colorOnBackground">@color/md_theme_light_onBackground</item>
           <item name="colorSurface">@color/md_theme_light_surface</item>
           <item name="colorOnSurface">@color/md_theme_light_onSurface</item>
           <item name="colorSurfaceVariant">@color/md_theme_light_surfaceVariant</item>
           <item name="colorOnSurfaceVariant">@color/md_theme_light_onSurfaceVariant</item>
           <item name="colorOutline">@color/md_theme_light_outline</item>
           <item name="colorOnSurfaceInverse">@color/md_theme_light_inverseOnSurface</item>
           <item name="colorSurfaceInverse">@color/md_theme_light_inverseSurface</item>
           <!--        <item name="colorInversePrimary">@color/md_theme_light_inversePrimary</item>
                   <item name="colorShadow">@color/md_theme_light_shadow</item>-->
           <item name="colorPrimaryInverse">@color/md_theme_light_primaryInverse</item>
       </style>
    </resources>

themes.xml (night)

    <resources>
       <style name="Theme.DaniwebAndroidNativeMaterial3Colors" parent="Theme.Material3.DayNight">
           <item name="colorPrimary">@color/md_theme_dark_primary</item>
           <item name="colorOnPrimary">@color/md_theme_dark_onPrimary</item>
           <item name="colorPrimaryContainer">@color/md_theme_dark_primaryContainer</item>
           <item name="colorOnPrimaryContainer">@color/md_theme_dark_onPrimaryContainer</item>
           <item name="colorSecondary">@color/md_theme_dark_secondary</item>
           <item name="colorOnSecondary">@color/md_theme_dark_onSecondary</item>
           <item name="colorSecondaryContainer">@color/md_theme_dark_secondaryContainer</item>
           <item name="colorOnSecondaryContainer">@color/md_theme_dark_onSecondaryContainer</item>
           <item name="colorTertiary">@color/md_theme_dark_tertiary</item>
           <item name="colorOnTertiary">@color/md_theme_dark_onTertiary</item>
           <item name="colorTertiaryContainer">@color/md_theme_dark_tertiaryContainer</item>
           <item name="colorOnTertiaryContainer">@color/md_theme_dark_onTertiaryContainer</item>
           <item name="colorError">@color/md_theme_dark_error</item>
           <item name="colorErrorContainer">@color/md_theme_dark_errorContainer</item>
           <item name="colorOnError">@color/md_theme_dark_onError</item>
           <item name="colorOnErrorContainer">@color/md_theme_dark_onErrorContainer</item>
           <item name="android:colorBackground">@color/md_theme_dark_background</item>
           <item name="colorOnBackground">@color/md_theme_dark_onBackground</item>
           <item name="colorSurface">@color/md_theme_dark_surface</item>
           <item name="colorOnSurface">@color/md_theme_dark_onSurface</item>
           <item name="colorSurfaceVariant">@color/md_theme_dark_surfaceVariant</item>
           <item name="colorOnSurfaceVariant">@color/md_theme_dark_onSurfaceVariant</item>
           <item name="colorOutline">@color/md_theme_dark_outline</item>
           <item name="colorOnSurfaceInverse">@color/md_theme_dark_inverseOnSurface</item>
           <item name="colorSurfaceInverse">@color/md_theme_dark_inverseSurface</item>
    <!--        <item name="colorInversePrimary">@color/md_theme_dark_inversePrimary</item>
           <item name="colorShadow">@color/md_theme_dark_shadow</item>-->
           <item name="colorPrimaryInverse">@color/md_theme_dark_primaryInverse</item>
       </style>
    </resources>

Module build.gradle

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

    android {
       compileSdk 32

       defaultConfig {
           applicationId "com.example.daniwebandroidnativematerial3colors"
           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.core:core-ktx:1.7.0'
       implementation 'androidx.appcompat:appcompat:1.4.1'
       implementation 'com.google.android.material:material:1.6.0-rc01'
       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

Congratulations! You have learned how to add Material 3 colors into an Android app as well as how to add Dynamic Colors. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidNativeMaterial3Colors

How to add firebase database to Xamarin android app

Hey guys, I am trying to upload text entered by a user of my app in an edit text to a google firebase application. Tried to find some reference online on how to do this, the article suggested I add Firebase.FirebaseDatabae.net which I added to the project via the Nuget package manager. I logged into my Google Firebase Console and got the Firebase url string for use with the client. I instantiated the client like below

protected string firebase_link = "https://xxxxxx-default-rtdb.firebaseio.com/";
FirebaseClient myclient;

//create a new instance of the firebase client
protected override void OnCreate(Bundle savedInstanceState){
 myclient = new FirebaseClient(firebase_link);
//define the buton for uploading the details in the edit text to the server
Button upload = FindViewById<Button>(Resource.Id.upload);
//subscribe to the event listener
upload.Click+=Upload_Click;
}
//method to upload text to the firebase app
private void Upload_Click(object sender, EventArgs e){
               try {
                if(myclient.Child("Users").PostAsync("Timothy").IsCompleted){
                    Snackbar.Make(email, "Firebase call completed succesfully", Snackbar.LengthLong).Show();
                }
                else
                {
                    Snackbar.Make(email, "An exception ocurred, try again in a little bit", Snackbar.LengthLong).Show();
                }
            }
            catch (Exception j)
            {
                Toast.MakeText(this,j.Message, ToastLength.Long).Show();    
            }
}

When I run the app, it is not making changes to the firebase database online, neither is it throwing an exception, it is just returning false for the if conditional structure inside the try catch block, please help.