In this post I will demonstrate how to implement communication with Android Services and Activities with Handler messages.
View the link below for a more general Android Service implementation example from a previous post.
Multiple clients can bind to the sameΒ ServiceΒ as needed. When a bound Service is implemented, the system will manage terminating the Service lifecycle.
Meaning, you would not be required to call stopSelf() in the Service to terminate it. As long as at least one client is bound to theΒ Service, it will continue running.Β
The example below I will demonstrate a bound Service which allows application components to attach to it. The ServiceConnection interface is used to monitor the state of the bound Service. Communication will happen between the Service and the Activity using a Messenger which is a Parcelable Object derived from Handlers.
Communication Between Android Services And Activities
Android Bound Service Sending Messages
Register Service In Android Manifest
First register your Service in the Android Manifest.
<service
android:name=".CustomService"
android:exported="false" />Service State Holder
I created a state holder to hold a boolean on when the Service is bound or not. This is due to accessing the boolean inside and outside the Composable.
class ServiceStateHolder {
var isBound by mutableStateOf(false)
}Create The UI And Bind To The Service
Next we will create the UI. The UI is simple with a few Buttons. When the Service is called to start with bindService(), a ServiceConnection is used to monitor the state. As soon as the Service is created, an initial message is sent for the Service to hold this activities Messenger for further communication.
Following Service creation, we are listening to messages that may be received from the Service into the activity.
class MainActivity : ComponentActivity() {
private var serviceMessenger: Messenger? = null
val replyHandler = object : Handler(Looper.getMainLooper()){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
if(msg.what == CustomService.FROM_SERVICE){
Log.i("Activity", "handleMessage: ${msg.data.getString("msg")}")
}
}
}
private val outgoingMessenger = Messenger(replyHandler)
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
serviceMessenger = Messenger(binder)
stateHolder.isBound = true
Log.i("ServiceConnection", "${stateHolder.isBound}")
//used to have service save reference to messenger for communication
val msg = Message.obtain(null, CustomService.INIT_COMM)
val bundle = Bundle().apply {
putString("msg", "Empty")
}
msg.data = bundle
msg.replyTo = outgoingMessenger
serviceMessenger?.send(msg)
}
//onServiceDisconnected only called if service crashed or has been killed by system
override fun onServiceDisconnected(name: ComponentName?) {
serviceMessenger = null
stateHolder.isBound = false
Log.i("ServiceConnection", "${stateHolder.isBound}")
}
}
override fun onStop() {
super.onStop()
if(stateHolder.isBound) {
unbindService(serviceConnection)
stateHolder.isBound = false
}
}
val stateHolder = ServiceStateHolder()
override fun onCreate(savedInstanceState: Bundle?) {
...
}
}onCreate To Build The Composable UI
Below is the basic UI used for this example. When the Service is bound, a Button will be displayed to send a static message to the Service.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val bound = remember { stateHolder }
SampleServiceTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(modifier = Modifier
.fillMaxSize()
.padding(innerPadding), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
Button(
onClick = {
Intent(this@MainActivity, CustomService::class.java).apply {
bindService(this, serviceConnection, Context.BIND_AUTO_CREATE)
}
}
) {
Text(text = "Start")
}
Button(
onClick = {
if(stateHolder.isBound) {
unbindService(serviceConnection)
stateHolder.isBound = false
}
}
) {
Text(text = "Stop")
}
if(bound.isBound) {
Button(
onClick = {
val msg = Message.obtain(null, CustomService.FROM_ACTIVITY)
val bundle = Bundle().apply {
putString("msg", "Yep this is from activity")
}
msg.data = bundle
msg.replyTo = outgoingMessenger
serviceMessenger?.send(msg)
}
) {
Text(text = "Test")
}
}
}
}
}
}
}Create The Service
Finally we create the Service.
Generally this Service example runs a continuous coroutine loop building a math function.
After the Service is created and bound, we process the initial message received from the client activity that passed the INIT_COMM message. Moreover this message is used so the Service can save the Messenger object used by the client to send messages back.
In addition to the Service running, we use a Handler to listen to more Activity messages.
class CustomService : Service() {
companion object{
val FROM_ACTIVITY : Int = 1
val FROM_SERVICE : Int = 2
val INIT_COMM : Int = 3
}
private val serviceCoroutine = CoroutineScope(Dispatchers.Default)
private var serviceJob : Job? = null
var replyMessenger : Messenger? = null
val serviceHandler = object : Handler(Looper.getMainLooper()){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
if(msg.what == INIT_COMM){
replyMessenger = msg.replyTo
}
if(msg.what == FROM_ACTIVITY){
logi("handleMessage: ${msg.data.getString("msg")}")
}
}
}
private val serviceMessenger = Messenger(serviceHandler)
override fun onBind(intent: Intent?): IBinder? {
...
}
override fun onDestroy() {
super.onDestroy()
logi("onDestroy")
serviceJob?.cancel()
serviceJob = null
}
}
fun Service.logi(message : String){
Log.i("CustomService", message)
}onBind Math Function Used To Show Service Running
Additionally, we add a simple math function to the onBind() function demonstrating the Service is running. This is a coroutine running on the Service continuously while the Service is alive. Furthermore the coroutine will send a message back to the Activity if the math calculation is over a predetermined value ( 95 ).
override fun onBind(intent: Intent?): IBinder? {
if(serviceJob == null){
serviceJob = serviceCoroutine.launch {
while(isActive) {
val first = Math.floor(Math.random() * 100)
val second = Math.floor(Math.random() * 100)
val total = first + second
if(total > 95.0 && replyMessenger != null){
val msg = Message.obtain(null, FROM_SERVICE)
val bundle = Bundle().apply {
putString("msg", "Math calc was above 95")
}
msg.data = bundle
msg.replyTo = replyMessenger
replyMessenger!!.send(msg)
}
logi("${first} + ${second} = ${total}")
delay(300)
}
}
}
return serviceMessenger.binder
}