Topic 3: Understanding Android Components Part-1
This is our third topic from learn android from basic to advance series
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 actionACTION_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 usingstartActivity(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
usingputExtra()
.In
SecondActivity
, we retrieve the passed data from the intent usinggetStringExtra()
, 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 theService
class provided by Android.In the
onStartCommand
method, we start a background thread using aHandler
to perform a task (backgroundTask
) periodically.The
backgroundTask
is aRunnable
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 theService
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 theService
the class provided by Android.Inside the service class, we define a binder class (
MyBinder
) that extendsBinder
and provides a method to get the service instance.We implement the
onBind
method to return the binder instance.We define methods (
setListener
andstartTask
) 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 usingbindService
.We implement an
ServiceConnection
object to handle the connection and disconnection events.We call methods of the bound service (
setListener
andstartTask
) 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:
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() } }
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.
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" } }
- 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")
}
}
}
- 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:
Content Provider Contract (
MyContentProviderContract
): Defines the contract for the content provider, including the authority, content URI, and column names.Content Provider (
MyContentProvider
): Implements theContentProvider
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.Database Helper (
MyDatabaseHelper
): ExtendsSQLiteOpenHelper
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! ๐