Implementing SmsManager in a modern Android app using Kotlin and Jetpack Compose requires a clear separation between the background telephony APIs and the declarative user interface.
Below is a complete guide to configuring permissions, setting up the runtime request workflow in Compose, and safely sending messages. 1. Declare Permissions in AndroidManifest.xml
Before executing telephony code, you must explicitly declare the SEND_SMS permission in your AndroidManifest.xml file.
Use code with caution. 2. Implement the SMS Helper Utility
The method for instantiating SmsManager differs based on the user’s Android operating system version. For API level 31 (Android 12) and above, you must retrieve it via the system service. Below is a modern wrapper utility:
import android.content.Context import android.os.Build import android.telephony.SmsManager import android.widget.Toast fun sendSmsMessage(context: Context, phoneNumber: String, message: String) { if (phoneNumber.isBlank() || message.isBlank()) { Toast.makeText(context, “Fields cannot be empty”, Toast.LENGTH_SHORT).show() return } try { // Correct initialization for different API levels val smsManager: SmsManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { context.getSystemService(SmsManager::class.java) } else { @Suppress(“DEPRECATION”) SmsManager.getDefault() } // Send the text message smsManager.sendTextMessage(phoneNumber, null, message, null, null) Toast.makeText(context, “SMS Sent Successfully!”, Toast.LENGTH_SHORT).show() } catch (e: Exception) { Toast.makeText(context, “Failed to send SMS: ${e.localizedMessage}”, Toast.LENGTH_LONG).show() e.printStackTrace() } } Use code with caution. 3. Build the Jetpack Compose UI with Permission Handling
To follow standard Android guidelines, the app must dynamically request runtime permissions before firing the SmsManager logic. This implementation leverages Google’s Accompanist Permission library syntax or native Compose activity launchers.
Make sure to add the dependency if you use Accompanist, or use the native RememberLauncherForActivityResult strategy as seen below:
import android.Manifest import android.content.pm.PackageManager import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardType import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat @Composable fun SmsSenderScreen() { val context = LocalContext.current // State management for form inputs var phoneNumber by remember { mutableStateOf(“”) } var messageContent by remember { mutableStateOf(“”) } // Tracks if the runtime permission is granted var isPermissionGranted by remember { mutableStateOf( ContextCompat.checkSelfPermission(context, Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_GRANTED ) } // Permission launcher block val permissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission() ) { granted -> isPermissionGranted = granted if (!granted) { Toast.makeText(context, “Permission Denied! Cannot send SMS.”, Toast.LENGTH_SHORT).show() } } Column( modifier = Modifier .fillMaxSize() .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = “Compose SMS Dashboard”, style = MaterialTheme.typography.headlineMedium, modifier = Modifier.padding(bottom = 24.dp) ) // Phone Number input field OutlinedTextField( value = phoneNumber, onValueChange = { phoneNumber = it }, label = { Text(“Recipient Phone Number”) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(16.dp)) // Message text input field OutlinedTextField( value = messageContent, onValueChange = { messageContent = it }, label = { Text(“Message Body”) }, modifier = Modifier.fillMaxWidth(), minLines = 3 ) Spacer(modifier = Modifier.height(24.dp)) // Dynamic action button Button( onClick = { if (isPermissionGranted) { sendSmsMessage(context, phoneNumber, messageContent) } else { permissionLauncher.launch(Manifest.permission.SEND_SMS) } }, modifier = Modifier.fillMaxWidth() ) { Text(text = if (isPermissionGranted) “Send SMS via App” else “Grant SMS Permission”) } } } Use code with caution. 4. Architecture and Technical Best Practices Implementation Detail Implementation Strategy Long Text Messages
Handles message body inputs exceeding 160 characters without truncation.
Switch from sendTextMessage to sendMultipartTextMessage. Use smsManager.divideMessage(text) to chunk it safely. Delivery Tracking
Monitors whether an SMS safely reached the recipient’s phone.
Pass explicit PendingIntent objects into the final two parameters of sendTextMessage linked to a custom BroadcastReceiver. Play Store Policies
Compliance with strict privacy restrictions on the SEND_SMS permission.
Use a standard Intent-based fallback (Intent.ACTION_SENDTO) to route users to the native messaging client if your app isn’t a default SMS app handler.
If you plan to scale this into a production framework, would you like to see how to build a BroadcastReceiver to handle sent and delivered statuses, or do you prefer an example of the Intent-fallback strategy to avoid strict Google Play console permission requests? SmsManager in Android using Jetpack Compose
Leave a Reply