Topic 3: Understanding Android Components Part-1

This is our third topic from learn android from basic to advance series

ยท

15 min read

Topic 3: Understanding Android Components Part-1

Hello devs, We have completed our previous topics, and it's time to move on to the new topic - Android components. These components are essential in shaping the user experience and functionality of an Android application. So, let's start by understanding these Android components.

Android Components

  • Activity

  • Fragment

  • Intent

  • Service

  • Broadcast Receiver

  • Content Provider

  • Manifest File

  • Layout

  • Resources and Assets

We already discuss activity and fragments on the Lifecycle topic. Now we can discuss other components of this topic

Intent

We use intent for performing navigation between one activity to another and passing data between the activities. The intent is used for launching activities, starting services, and broadcasting events within an Android app.

There are two types of intent :

  • Implicit Intent

  • Explicit intent

Implicit Intent

Intents that do not deal with a specific component, but declare an action to perform another app such as showing location, calling number, and sending e-mail.

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

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

        // Create an implicit intent to view a webpage
        val webpage: Uri = Uri.parse("https://www.example.com")
        val intent = Intent(Intent.ACTION_VIEW, webpage)

        // Verify that the intent will resolve to an activity
        if (intent.resolveActivity(packageManager) != null) {
            // Start the activity if it resolves successfully
            startActivity(intent)
        } else {
            // Handle the case where no activity can handle the intent
            // For example, show an error message to the user
        }
    }
}

In this example:

  • We create an Intent object with the action ACTION_VIEW, which is used to view the data specified by the intent's data URI.

  • We specify the URI of the webpage we want to view.

  • We use resolveActivity() method to check if there's an activity that can handle this intent. If there is, we start the activity using startActivity(intent).

  • If there's no activity to handle the intent, you can handle this case gracefully, for example, by displaying an error message to the user.

This is just one example of how you can use implicit intents in Android Kotlin. They are versatile and can be used for various purposes like sharing content, opening maps, sending emails, etc.

Explicit intent

This intent is used for passing data and navigating between activities in the same application. Suppose you have two activities: MainActivity and SecondActivity, and you want to navigate and pass data from MainActivity to SecondActivity when a button is clicked.

MainActivity.class

// MainActivity.kt

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

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

        // Set up click listener for the button
        button.setOnClickListener {
            // Get the text from EditText
            val message = editText.text.toString()

            // Create an explicit intent to start SecondActivity
            val intent = Intent(this, SecondActivity::class.java).apply {
                // Pass the message as an extra to SecondActivity
                putExtra("MESSAGE", message)
            }
            startActivity(intent)
        }
    }
}

SecondActivity.class

// SecondActivity.kt

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_second.*

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

        // Retrieve the message from the intent extra
        val message = intent.getStringExtra("MESSAGE")

        // Display the message in TextView
        textView.text = message
    }
}

In this example:

  • We retrieve the text from an EditText in MainActivity.

  • We pass this text as an extra with the key "MESSAGE" to SecondActivity using putExtra().

  • In SecondActivity, we retrieve the passed data from the intent using getStringExtra(), and then display it in a TextView.

Make sure to adjust your layout files (activity_main.xml and activity_second.xml) to include the necessary EditText, TextView, and Button views.

This demonstrates navigation and passing data between activities using explicit intents in Android Kotlin.

Services

Services are background components that perform long-running operations without a user interface. They are used for tasks such as playing music, downloading files, and interacting with content providers. Services are run on a main thread.

There are three types of services:

  • Background services

  • Foreground service

  • Bound service

Background Services

This type of service runs only when the app is running so it will get terminated when the app is terminated. This service does not notify the user about the background task. During this service user interaction is not required. We use background services when the data has to be synced to the cloud storage.

import android.app.Service
import android.content.Intent
import android.os.Handler
import android.os.IBinder
import android.util.Log

class MyBackgroundService : Service() {

    private var handler: Handler? = null
    private var isServiceRunning = false

    override fun onCreate() {
        super.onCreate()
        Log.d(TAG, "Service created")
        handler = Handler()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d(TAG, "Service started")
        isServiceRunning = true

        // Start a background thread to perform some task periodically
        handler?.postDelayed(backgroundTask, 0)

        return START_STICKY
    }

    private val backgroundTask = object : Runnable {
        override fun run() {
            if (isServiceRunning) {
                // Your background task goes here
                Log.d(TAG, "Running background task...")

                // Schedule the task to run again after a delay
                handler?.postDelayed(this, DELAY_MILLIS)
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "Service destroyed")
        isServiceRunning = false
        handler?.removeCallbacks(backgroundTask)
    }

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    companion object {
        private const val TAG = "MyBackgroundService"
        private const val DELAY_MILLIS = 60000L // 1 minute delay
    }
}

In this example:

  • We create a MyBackgroundService class that extends the Service class provided by Android.

  • In the onStartCommand method, we start a background thread using a Handler to perform a task (backgroundTask) periodically.

  • The backgroundTask is a Runnable that contains the task to be performed in the background. Inside this task, you can write your own logic or code to perform any background operation.

  • We use postDelayed to schedule the task to run again after a certain delay (in this example, 1 minute). You can adjust the delay time according to your requirements.

  • The service stops itself (onDestroy) when it's no longer needed. Make sure to clean up resources and stop any ongoing background tasks here.

For the use of this service in your Android App add this line in your manifest File.

<service android:name=".MyBackgroundService" />

And if you want to start this service add this line to your activity or application context.

val serviceIntent = Intent(context, MyBackgroundService::class.java)
context.startService(serviceIntent)

Remember to handle any necessary permissions or background execution limits based on your app's requirements and Android version.

Foreground Services

This type of service runs even when the application is terminated and this service notifies the user about the operation. Users can interact with the service by notification providers about the ongoing task. We use this service for downloading files, playing music etc.

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat

class MyForegroundService : Service() {

    companion object {
        private const val TAG = "MyForegroundService"
        private const val NOTIFICATION_CHANNEL_ID = "ForegroundServiceChannel"
        private const val NOTIFICATION_ID = 101
    }

    override fun onCreate() {
        super.onCreate()
        Log.d(TAG, "Foreground service created")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d(TAG, "Foreground service started")

        // Create the notification channel (required for API 26 and above)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel()
        }

        // Build the notification
        val notification: Notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
            .setContentTitle("Foreground Service")
            .setContentText("This is a foreground service")
            .setSmallIcon(R.drawable.ic_notification)
            .build()

        // Start the service in the foreground
        startForeground(NOTIFICATION_ID, notification)

        // Perform any task here (e.g., updating data, playing music)

        return START_STICKY
    }

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "Foreground service destroyed")
    }

    private fun createNotificationChannel() {
        val channel = NotificationChannel(
            NOTIFICATION_CHANNEL_ID,
            "Foreground Service Channel",
            NotificationManager.IMPORTANCE_DEFAULT
        )
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        manager.createNotificationChannel(channel)
    }
}

In this example:

  • We create a MyForegroundService a class that extends the Service the class provided by Android.

  • In the onStartCommand method, we create and display a notification that represents the foreground service. This notification will appear in the notification bar while the service is running.

  • We use startForeground to start the service in the foreground, providing it with the notification and a unique notification ID.

  • Inside the onDestroy method, we clean up any resources used by the service.

  • The createNotificationChannel method is used to create a notification channel for the foreground service. This is required for devices running Android Oreo (API level 26) and above.

To use and start this service in your Android app add the same code as we added in the background service in your manifest file and activity or application context.

Remember to handle any necessary permissions and adjust the notification content according to your app's requirements.

Bound Services

This type of service runs only if the component is bound. This service allows other components to bind to it and communicate with it through an interface. We use this service for tasks that require interaction with another component, such as sharing data, performing calculations or handling requests.

First, create a service interface:

interface MyBoundServiceListener {
    fun onProgressUpdate(progress: Int)
    fun onTaskCompleted(result: String)
}

Then, create the bound service class:

import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import kotlinx.coroutines.*

class MyBoundService : Service() {

    private val binder = MyBinder()
    private var listener: MyBoundServiceListener? = null
    private var job: Job? = null

    override fun onBind(intent: Intent?): IBinder? {
        return binder
    }

    inner class MyBinder : Binder() {
        fun getService(): MyBoundService = this@MyBoundService
    }

    fun setListener(listener: MyBoundServiceListener) {
        this.listener = listener
    }

    fun startTask() {
        job = CoroutineScope(Dispatchers.IO).launch {
            // Simulate a long-running task
            for (i in 0..100) {
                delay(100) // Simulate progress updates every 100 milliseconds
                listener?.onProgressUpdate(i)
            }
            listener?.onTaskCompleted("Task completed")
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        job?.cancel()
    }
}

In this example:

  • We create a MyBoundService a class that extends the Service the class provided by Android.

  • Inside the service class, we define a binder class (MyBinder) that extends Binder and provides a method to get the service instance.

  • We implement the onBind method to return the binder instance.

  • We define methods (setListener and startTask) that the client can call to interact with the service.

  • The startTask method simulates a long-running task and notifies the client about the progress and completion of the task using the listener interface.

  • We use coroutines to perform asynchronous operations in a non-blocking manner.

To use this bound service in your Android app add this service in your manifest file.

Now Bind to the service from your activity:

class MyActivity : AppCompatActivity() {

    private lateinit var service: MyBoundService
    private var bound: Boolean = false

    private val connection = object : ServiceConnection {
        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            val binder = service as MyBoundService.MyBinder
            this@MyActivity.service = binder.getService()
            bound = true
        }

        override fun onServiceDisconnected(className: ComponentName) {
            bound = false
        }
    }

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

        val intent = Intent(this, MyBoundService::class.java)
        bindService(intent, connection, Context.BIND_AUTO_CREATE)
    }

    override fun onDestroy() {
        super.onDestroy()
        if (bound) {
            unbindService(connection)
            bound = false
        }
    }

    fun onStartTaskClicked(view: View) {
        service.setListener(object : MyBoundServiceListener {
            override fun onProgressUpdate(progress: Int) {
                // Update UI with progress
            }

            override fun onTaskCompleted(result: String) {
                // Handle task completion
            }
        })
        service.startTask()
    }
}

In this example:

  • We bind to the service in the onCreate method of the activity using bindService.

  • We implement an ServiceConnection object to handle the connection and disconnection events.

  • We call methods of the bound service (setListener and startTask) to interact with it.

  • When the activity is destroyed, we unbind from the service using unbindService.

  • Inside the onStartTaskClicked method, we set a listener on the service and start a task.

Remember to handle any necessary permissions and adjust the service and activity code according to your app's requirements.

Did you know that Services have their lifecycle? Let's discuss the service lifecycle.

Service Lifecycle

  • onCreate()

  • onStartCommand()

  • onBind()

  • onUnbind()

  • onDestroy()

onCreate(): Called when the service is first called

onStartCommand(): Called each time the service is started with startService()

onBind(): Called when the client binds to the service using bindService()

onUnbind(): Called when all clients unbind from the service

onDestroy(): Called when the service is no longer needed

Broadcast receiver

Broadcast Receiver is an Android component that allows you to send or receive messages from other applications or other application systems itself. This message can be event or intent. Android system sends broadcasts when system events occur such as system boots up, device starts charging, connectivity changing, date changing, and low battery.

The app can receive broadcasts in two ways:

  1. Manifest-declared receiver (Statically)

    Static receivers are declared in the AndroidManifest.xml file and can receive system-wide broadcasts even when the app is not running. Here's how you can implement a static broadcast receiver:

    First, declare the receiver in your AndroidManifest.xml:

     <receiver android:name=".MyStaticReceiver">
         <intent-filter>
             <action android:name="com.example.myapp.MY_STATIC_ACTION" />
         </intent-filter>
     </receiver>
    

    Then, create the MyStaticReceiver class:

     class MyStaticReceiver : BroadcastReceiver() {
         override fun onReceive(context: Context?, intent: Intent?) {
             // Handle the broadcast message here
             Toast.makeText(context, "Static Receiver triggered", Toast.LENGTH_SHORT).show()
         }
     }
    
  2. Context-registered receiver (Dynamically)

    Dynamic receivers are registered and unregistered programmatically within an activity or a service. They are useful for receiving broadcasts only when the component is active. Here's how you can implement a dynamic broadcast receiver:

     class MyDynamicReceiver : BroadcastReceiver() {
         override fun onReceive(context: Context?, intent: Intent?) {
             // Handle the broadcast message here
             Toast.makeText(context, "Dynamic Receiver triggered", Toast.LENGTH_SHORT).show()
         }
     }
    

    In your activity or service, register and unregister the dynamic receiver:

     class MainActivity : AppCompatActivity() {
         private val dynamicReceiver = MyDynamicReceiver()
    
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
             setContentView(R.layout.activity_main)
    
             val filter = IntentFilter().apply {
                 addAction("com.example.myapp.MY_DYNAMIC_ACTION")
             }
             registerReceiver(dynamicReceiver, filter)
         }
    
         override fun onDestroy() {
             super.onDestroy()
             unregisterReceiver(dynamicReceiver)
         }
     }
    

Remember to declare the necessary permissions and handle any required permissions for the broadcast receivers in your AndroidManifest.xml file.

Content Provider

A Content Provider component supplies data from one application to others on request. Content Provider can be used to enable data sharing between different apps, such as sharing contacts or calendar events. Content Provider support the four basic operations, thatโ€™s called CRUD operations. Here is one example.

  1. Define the Content Provider Contract:

     import android.net.Uri
     import android.provider.BaseColumns
    
     object MyContentProviderContract {
         const val AUTHORITY = "com.example.myapp.provider"
         val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/data")
    
         object DataEntry : BaseColumns {
             const val TABLE_NAME = "data"
             const val COLUMN_NAME = "name"
             const val COLUMN_VALUE = "value"
         }
     }
    
    1. Create the Content Provider:
    import android.content.ContentProvider
    import android.content.ContentValues
    import android.content.UriMatcher
    import android.database.Cursor
    import android.database.sqlite.SQLiteDatabase
    import android.net.Uri

    class MyContentProvider : ContentProvider() {

        private lateinit var dbHelper: MyDatabaseHelper

        companion object {
            private const val DATA = 1
            private const val DATA_ID = 2
            private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)

            init {
                uriMatcher.addURI(MyContentProviderContract.AUTHORITY, "data", DATA)
                uriMatcher.addURI(MyContentProviderContract.AUTHORITY, "data/#", DATA_ID)
            }
        }

        override fun onCreate(): Boolean {
            dbHelper = MyDatabaseHelper(context!!)
            return true
        }

        override fun insert(uri: Uri, values: ContentValues?): Uri? {
            val db = dbHelper.writableDatabase
            val rowId = db.insert(MyContentProviderContract.DataEntry.TABLE_NAME, null, values)
            context?.contentResolver?.notifyChange(uri, null)
            return Uri.withAppendedPath(MyContentProviderContract.CONTENT_URI, rowId.toString())
        }

        override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
            val db = dbHelper.readableDatabase
            val cursor = when (uriMatcher.match(uri)) {
                DATA -> db.query(MyContentProviderContract.DataEntry.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder)
                DATA_ID -> {
                    val id = uri.lastPathSegment
                    db.query(MyContentProviderContract.DataEntry.TABLE_NAME, projection, "_ID = ?", arrayOf(id), null, null, sortOrder)
                }
                else -> throw IllegalArgumentException("Unknown URI: $uri")
            }
            cursor.setNotificationUri(context?.contentResolver, uri)
            return cursor
        }

        override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
            val db = dbHelper.writableDatabase
            val count = when (uriMatcher.match(uri)) {
                DATA -> db.update(MyContentProviderContract.DataEntry.TABLE_NAME, values, selection, selectionArgs)
                DATA_ID -> {
                    val id = uri.lastPathSegment
                    db.update(MyContentProviderContract.DataEntry.TABLE_NAME, values, "_ID = ?", arrayOf(id))
                }
                else -> throw IllegalArgumentException("Unknown URI: $uri")
            }
            context?.contentResolver?.notifyChange(uri, null)
            return count
        }

        override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
            val db = dbHelper.writableDatabase
            val count = when (uriMatcher.match(uri)) {
                DATA -> db.delete(MyContentProviderContract.DataEntry.TABLE_NAME, selection, selectionArgs)
                DATA_ID -> {
                    val id = uri.lastPathSegment
                    db.delete(MyContentProviderContract.DataEntry.TABLE_NAME, "_ID = ?", arrayOf(id))
                }
                else -> throw IllegalArgumentException("Unknown URI: $uri")
            }
            context?.contentResolver?.notifyChange(uri, null)
            return count
        }

        override fun getType(uri: Uri): String? {
            return when (uriMatcher.match(uri)) {
                DATA -> "vnd.android.cursor.dir/vnd.$MyContentProviderContract.AUTHORITY.data"
                DATA_ID -> "vnd.android.cursor.item/vnd.$MyContentProviderContract.AUTHORITY.data"
                else -> throw IllegalArgumentException("Unknown URI: $uri")
            }
        }
    }
  1. Create a Database Helper:
    import android.content.Context
    import android.database.sqlite.SQLiteDatabase
    import android.database.sqlite.SQLiteOpenHelper

    class MyDatabaseHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {

        companion object {
            const val DATABASE_NAME = "myapp.db"
            const val DATABASE_VERSION = 1
            private const val SQL_CREATE_ENTRIES = "CREATE TABLE ${MyContentProviderContract.DataEntry.TABLE_NAME} (" +
                    "${MyContentProviderContract.DataEntry._ID} INTEGER PRIMARY KEY," +
                    "${MyContentProviderContract.DataEntry.COLUMN_NAME} TEXT," +
                    "${MyContentProviderContract.DataEntry.COLUMN_VALUE} TEXT)"

            private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${MyContentProviderContract.DataEntry.TABLE_NAME}"
        }

        override fun onCreate(db: SQLiteDatabase) {
            db.execSQL(SQL_CREATE_ENTRIES)
        }

        override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
            db.execSQL(SQL_DELETE_ENTRIES)
            onCreate(db)
        }
    }

This example demonstrates the implementation of a simple content provider in an Android app using Kotlin. Content providers are used to manage and share structured data between applications.

Here's a brief overview of each component:

  1. Content Provider Contract (MyContentProviderContract): Defines the contract for the content provider, including the authority, content URI, and column names.

  2. Content Provider (MyContentProvider): Implements the ContentProvider class and provides methods for performing CRUD operations (insert, query, update, delete) on data stored in a SQLite database. It also handles URI matching and notification of data changes.

  3. Database Helper (MyDatabaseHelper): Extends SQLiteOpenHelper and provides methods for creating and upgrading the SQLite database used by the content provider.

The content provider manages a simple "data" table with two columns: "name" and "value". It exposes CRUD operations through a content URI (content://com.example.myapp.provider/data).

This example demonstrates both the static declaration of a content provider in the AndroidManifest.xml file and the dynamic creation of database tables and registration of content URIs within the content provider class.

Additionally, you would interact with this content provider using a ContentResolver from your app's components, such as activities or services, to perform operations on the data stored by the content provider.

To use this content provider in your app, you need to:

  • Register the content provider in your AndroidManifest.xml.

  • Request appropriate permissions if necessary.

  • Implement CRUD (create, read, update, delete) operations using the content provider's URI.

  • Handle data using ContentResolver from your app components such as activities or services.

I think that is enough for the day. We discussed so many things in this blog and it's time to wrap up the blog. Don't worry about the remaining components that we did not catch up on in this blog but yeah I wrote another blog for these remaining components and on that blog, we explore these Android components. See you on the next blog devs.


Connect with Me:

Hey there! If you enjoyed reading this blog and found it informative, why not connect with me on LinkedIn? ๐Ÿ˜Š You can also follow my Instagram page for more mobile development-related content. ๐Ÿ“ฒ๐Ÿ‘จโ€๐Ÿ’ป Letโ€™s stay connected, share knowledge and have some fun in the exciting world of app development! ๐ŸŒŸ

Check out my Instagram page

Check out my LinkedIn

ย