Notes: 21. Challenge - Communication Between Components
The student materials have been reviewed and are updated as of SEPTEMBER 2022.
To really understand Services and BroadcastReceivers, and the communication between such components, you have to solve a small challenge! :]
In this challenge, you have to build an UploadService, to replace the Workers. You also have to build a BroacastReceiver, which will get a broadcast from the Service, to tell the user the image was uploaded.
That’s it, now pause the video, and solve the challenge, then once you’re done, unpause the video, and compare the two solutions! :] Good luck!
You’ll start with the BroadcastReceiver, so create a new class called the UploadImageReceiver, and add the following code:
const val ACTION_IMAGE_UPLOAD = "image_upload"
class UploadImageReceiver(
private inline val onImageUploaded: (Boolean) -> Unit) : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == ACTION_IMAGE_UPLOAD) {
val isUploaded = intent.getBooleanExtra("is_uploaded", false)
onImageUploaded(isUploaded)
}
}
}
Similar to how you’ve built previous receivers, this one takes in a lambda parameter, to notify the UI once the upload is complete. It will try to fetch if the image has been uploaded or not, and then pass that to the UI.
It also defines a special intent action string, for the image upload.
Now, for the second part, create a new class named UploadService:
class UploadService : JobIntentService() {
private val remoteApi by lazy { App.remoteApi }
}
To start off with the class, you extend the JobIntentService class, and add the remoteApi property, as you’ll use it to upload the image. Because this is a new service, head over to the AndroidManifest.xml file, and declare it like so:
<service
android:name=".service.UploadService"
android:permission="android.permission.BIND_JOB_SERVICE" />
Good, now return to the Service, and add a way to start the service:
class UploadService : JobIntentService() {
private val remoteApi by lazy { App.remoteApi }
companion object {
private const val JOB_ID = 52
fun startWork(context: Context, intent: Intent) {
enqueueWork(context, UploadService::class.java, JOB_ID, intent)
}
}
...
}
Just like the previous JobIntentService you’ve used, using a companion object and the startWork() function, you can access the internal API, and queue the upload work.
Then the Android system will decide if it should use an IntentService, or a Job, depending on the Android version you have installed. To start uploading the image, override the onHandleWork() function, and add the following code:
class UploadService : JobIntentService() {
...
override fun onHandleWork(intent: Intent) {
val filePath = intent.getStringExtra("image_path")
if (filePath != null) {
uploadImage(filePath)
}
}
...
}
This is similar, as you’re just calling the uploadImage function, if there is a valid image path. Complete the implementation by adding the uploadImage() function, with the following code:
class UploadService : JobIntentService() {
...
private fun uploadImage(filePath: String) {
GlobalScope.launch {
val result = remoteApi.uploadImage(File(filePath))
val intent = Intent()
intent.putExtra("is_uploaded", result.message == "Success!")
intent.action = ACTION_IMAGE_UPLOAD
sendBroadcast(intent)
}
}
...
}
You have to launch a coroutine, because the remoteApi uses suspend functions. After doing so, you get the result from the upload, and send a broadcast to the receiver, with the result.
Now all that’s left to do is register the receivers, and start the service! Open the MainActivity.kt file, and add another receiver to the top of the class, and rename the previous one:
private val synchronizeImagesReceiver by lazy {
SynchronizeImagesReceiver {
toast("Images synchronized!")
}
}
private val uploadImageReceiver by lazy {
UploadImageReceiver { isUploaded ->
toast(if (isUploaded) "Image uploaded!" else "Upload failed! :[")
}
}
This way you know which receiver is for which feature of the app. The upload receiver will either show a successful message, or a failed message, depending on whether the upload finished or not. You also have to register the receiver, so do that next:
registerReceiver(synchronizeImagesReceiver, IntentFilter().apply {
addAction(ACTION_IMAGES_SYNCHRONIZED)
})
registerReceiver(uploadImageReceiver, IntentFilter().apply {
addAction(ACTION_IMAGE_UPLOAD)
})
Notice how you used a different action, that you created specifically for this receiver. This is used to differentiate between receivers and their specific intents! And unregister this receiver:
override fun onStop() {
...
unregisterReceiver(uploadImageReceiver)
super.onStop()
}
You have most of the setup done! All that’s left now is to start the service when you need to upload an image. Head over to the SettingsFragment, and upload the image using the service:
val intent = Intent().apply { putExtra("image_path", fileUri) }
UploadService.startWork(requireContext(), intent)
That’s it! :] Now run the project, and upload an image! You’ll see the message pop up, once the upload finishes! :]