Android Core Components Guide - Activity, Fragment, Service, and Broadcast
Understanding Android’s core components is essential for building robust applications. This guide covers Activity lifecycle, Fragment management, Services, Broadcast receivers, and Runtime permissions.
Table of Contents
Activity Lifecycle
Lifecycle Callbacks
| Callback | Description |
|---|---|
onCreate() |
Activity created, initialize UI |
onStart() |
Activity becoming visible |
onResume() |
Activity in foreground, interactive |
onPause() |
Another activity partially covers this one |
onStop() |
Activity no longer visible |
onDestroy() |
Activity being destroyed |
Key Considerations
- Force Kill: When force killed,
onDestroy()is NOT called - onPause vs onStop:
onPause()called when partially covered,onStop()when fully covered - State Loss:
onSaveInstanceState()called beforeonStop(). Fragment commits after this cause crashes
// Avoid state loss in fragment transactions
supportFragmentManager.beginTransaction()
.replace(R.id.container, fragment)
.commitAllowingStateLoss() // Use with caution
Lifecycle Observer
Monitor lifecycle events from external components:
class MyObserver : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
// Connect to resource
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPause() {
// Disconnect from resource
}
}
// Register observer
lifecycle.addObserver(MyObserver())
// Check current state
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
// Safe to update UI
}
Activity Lifecycle Callbacks
Monitor all activities in your app:
class MyApplication : Application() {
private var activityCount = 0
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityStarted(activity: Activity) {
activityCount++
}
override fun onActivityStopped(activity: Activity) {
activityCount--
if (activityCount == 0) {
// App is in background
}
}
// ... other callbacks
})
}
fun isAppInForeground() = activityCount > 0
}
Fragment
XML Declaration
<fragment
android:name="com.example.MyFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout="@layout/fragment_my"/>
Back Stack Management
override fun onBackPressed() {
val fragmentManager = supportFragmentManager
if (fragmentManager.backStackEntryCount > 0) {
fragmentManager.popBackStack()
} else {
finish()
}
}
Common Issues
Context Null Issue
Fragment’s getContext() can return null when fragment is detached:
// BAD: May crash
val inflater = LayoutInflater.from(context)
// GOOD: Check for null
context?.let { ctx ->
val inflater = LayoutInflater.from(ctx)
}
// Or check if attached
if (isAdded) {
// Safe to use context
}
Lifecycle Awareness
// Check if fragment is added before operations
if (fragment.isAdded) {
// Safe to perform operations
}
// Retain instance across configuration changes
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true // Deprecated in favor of ViewModel
}
Fragment without Back Stack
If fragment is added via XML (not back stack), onResume() may not be invoked when returning.
Service
IntentService (Deprecated)
Simple service that runs on its own worker thread:
class MyIntentService : IntentService("MyIntentService") {
override fun onHandleIntent(intent: Intent?) {
// Runs on worker thread
// Called sequentially if multiple requests
// Stops automatically when work is done
}
}
Note: IntentService is deprecated. Use WorkManager or coroutines instead.
Service
Standard service runs on main thread:
class MyService : Service() {
private val binder = LocalBinder()
inner class LocalBinder : Binder() {
fun getService(): MyService = this@MyService
}
override fun onBind(intent: Intent): IBinder = binder
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Start background work on separate thread
thread {
// Long-running operation
}
return START_STICKY
}
}
Communication with Activity
Using Messenger:
// Activity - Send messenger to service
class MainActivity : AppCompatActivity() {
private val handler = IncomingHandler(this)
private val messenger = Messenger(handler)
fun startMyService() {
val intent = Intent(this, MyService::class.java).apply {
putExtra("messenger", messenger)
}
startService(intent)
}
class IncomingHandler(activity: MainActivity) : Handler(Looper.getMainLooper()) {
private val activityRef = WeakReference(activity)
override fun handleMessage(msg: Message) {
activityRef.get()?.let { activity ->
when (msg.what) {
1 -> {
// Handle message from service
}
}
}
}
}
}
// Service - Send message to activity
class MyService : Service() {
private var activityMessenger: Messenger? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
activityMessenger = intent?.getParcelableExtra("messenger")
return START_STICKY
}
private fun sendToActivity(messageId: Int, data: Any?) {
try {
val message = Message.obtain().apply {
what = messageId
obj = data
}
activityMessenger?.send(message)
} catch (e: RemoteException) {
Log.e("Service", "Error sending message to activity")
}
}
}
Broadcast Receiver
LocalBroadcastManager
For in-process communication only (deprecated, use LiveData or EventBus):
// Register receiver
LocalBroadcastManager.getInstance(this).registerReceiver(
messageReceiver,
IntentFilter("custom-event-name")
)
// Send broadcast
val intent = Intent("custom-event-name").apply {
putExtra("data", "value")
}
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
// Unregister
LocalBroadcastManager.getInstance(this).unregisterReceiver(messageReceiver)
Ordered Broadcast
Receive results from broadcast:
sendOrderedBroadcast(intent, null)
Implicit Broadcast Restrictions
Starting from Android 8.0 (Oreo), implicit broadcasts cannot be registered in manifest (with some exceptions):
- Use explicit broadcasts
- Register receivers dynamically in code
- Check exceptions list
Intent
Share Intent with Chooser
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, "Message to share")
}
val additionalIntent = Intent() // Additional option
val chooser = Intent.createChooser(shareIntent, "Share via").apply {
putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(additionalIntent))
}
startActivity(chooser)
LabeledIntent
Add custom label and icon to intent in chooser:
val labeledIntent = LabeledIntent(
intent,
packageName,
R.string.custom_label,
R.drawable.custom_icon
)
Runtime Permissions
Permission Levels
| Level | Description |
|---|---|
| Normal | Automatically granted at install |
| Dangerous | Requires runtime permission (Android 6.0+) |
| Signature | Granted to apps signed with same certificate |
Permission Flow
- Check if permission is granted
- If not, request permission
- Handle permission result
- If denied permanently, guide user to settings
Check and Request
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CAMERA),
CAMERA_PERMISSION_CODE
)
}
Handle Result
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
CAMERA_PERMISSION_CODE -> {
if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted
openCamera()
} else {
// Permission denied
if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
// Permanently denied - guide to settings
showSettingsDialog()
}
}
}
}
}
Permission Helper Class
class PermissionHelper(private val fragment: Fragment) {
private val requestCodeMap = mutableMapOf<Int, PermissionCallback>()
private var requestCode = 0
fun requestPermissions(
permissions: Array<String>,
callback: PermissionCallback
) {
val code = requestCode++
requestCodeMap[code] = callback
fragment.requestPermissions(permissions, code)
}
fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
val callback = requestCodeMap.remove(requestCode) ?: return
val denied = mutableListOf<String>()
var hasPermanentDenied = false
permissions.forEachIndexed { index, permission ->
if (grantResults[index] != PackageManager.PERMISSION_GRANTED) {
denied.add(permission)
if (!fragment.shouldShowRequestPermissionRationale(permission)) {
hasPermanentDenied = true
}
}
}
when {
denied.isEmpty() -> callback.onGranted()
hasPermanentDenied -> callback.onDeniedPermanently(denied)
else -> callback.onDenied(denied)
}
}
interface PermissionCallback {
fun onGranted()
fun onDenied(permissions: List<String>)
fun onDeniedPermanently(permissions: List<String>)
}
}
Usage
permissionHelper.requestPermissions(
arrayOf(Manifest.permission.CAMERA),
object : PermissionHelper.PermissionCallback {
override fun onGranted() {
openCamera()
}
override fun onDenied(permissions: List<String>) {
showRationale()
}
override fun onDeniedPermanently(permissions: List<String>) {
showSettingsGuide()
}
}
)
Hardware Feature Declaration
<uses-feature
android:name="android.hardware.camera"
android:required="false"/>
if (packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
// Device has camera
}
ADB Permission Commands
# Grant permission
adb shell pm grant com.example.app android.permission.CAMERA
# Revoke permission
adb shell pm revoke com.example.app android.permission.CAMERA
SMS Retriever API
Receive OTP SMS without SMS permission:
// Use Google's SMS Retriever API
// https://developers.google.com/identity/sms-retriever/
Best Practices
Lifecycle
- Use ViewModel to survive configuration changes
- Use LiveData for lifecycle-aware data observation
- Always check lifecycle state before UI updates
Fragments
- Use ViewModel for data sharing between fragments
- Check
isAddedbefore context operations - Use Navigation component for navigation
Services
- Use WorkManager for deferrable background work
- Use foreground service for long-running visible tasks
- Clean up resources in onDestroy
Permissions
- Request permissions just-in-time when needed
- Provide clear rationale for permission requests
- Handle all denial cases gracefully
Comments