Handle incoming messages on the device with Flutter.
Hey, Flutter devs!! It’s been about 2 years since I started working with flutter and by far I’ve come across various great challenges which expanded my knowledge in Flutter. One such use case was reading incoming messages on the user’s device.
Before we ramble into the code-verse lemme explain how I came across this challenge. I’ve been working on a project that involves user authentication by the means of an OTP. The challenge was to automatically read the OTP from the device and update the UI.
Many of you must be aware of some packages that provide this functionality like sms_autofill, but their implementation is very narrow and only applicable only in certain cases.
I myself started with a package to achieve this functionality but for this to work, you need the app signature to be included in the message and in my case, it was not possible. What if your use-case was to read the messages that include financial transactions or something else, In such cases, these packages don’t work. If you’ve ever stumbled upon such challenges, then this article is for you, Read along😊
Overview🧐
In this article, we’ll learn how to read incoming messages on a user’s device. We will be using platform channels to communicate with the native side and write some platform-specific code for Android in Kotlin.
Prerequisite:- Minimal knowledge of flutter/Dart would suffice for you to survive through this article.
Base Project
You can refer to the link given here for the base project. The base UI of the project is given below:-
Whenever any SMS is received by the device we will update it on the device. Also, refer to the Github repo for code reference of the complete project.
Now let’s get started🚀
Platform channels, Method calls, and much more
According to the official Flutter documentation, Platform channels are used to pass messages between client and host platform (Android or IoS). In simpler words, it provides a bridge between dart code and platform-specific code.
On the client side,
MethodChannel
enables sending messages that correspond to method calls. On the platform side,MethodChannel
on Android (MethodChannelAndroid
) andFlutterMethodChannel
on iOS (MethodChanneliOS
) enable receiving method calls and sending back a result.
Method calls are basically functions that perform some calculation on the host side and return a result to the client(again a message).
We will be focussing only on the Android part for this article, not IoS.
How do we trigger a method call?🤔
Triggering method calls are done asynchronously in Flutter. We do something like:
Now the result returned by the method call can be a future or a Broadcast Stream depending upon the Platform channel used. There are three types of platform channels available. We will be using mainly two of them, they’re:-
- MethodChannel (returns future)
- EventChannel (returns BroadcastStream)
Summarizing things
We make platform channels on the client-side (Flutter) and the host side (android/ios). Then we can use that channel to invoke a method call asynchronously. This method call executes a block of platform-specific code on Android/ios. After successful completion of the method call, it returns the result back to the client-side.
Let’s dive into some code👨💻
Let’s create a separate file in the lib folder. In this file, all the code related to our platform channel will live. Create a new class and declare a method channel for receiving SMS.
class PlatformChannel {
static const _channel = MethodChannel("com.example.app/sms")
}
This is how you declare a method channel. MethodChannel
the class takes in a string as a required parameter that defines its name. Now we’ll have to declare a method channel with the same name on the android side.
I’d recommend using Android studio for writing Kotlin code if you’ve no prior experience with Kotlin like me😅. Open the android folder of your flutter application in android studio and navigate to MainActivity.kt
.
In MainActivity.kt
inside the MainActivity class, we’ll override the configureFlutterEngine
function. Inside this function, we’ll initialize our method channel and call setMethodCallHandler
the method provided in MethodChannel class.
The method channel on the android side takes in a binary messenger provided by the flutter engine which is used to pass the messages to the client-side. setMethodCallHandler
gives us two things — call and result.
call is used to identify which method call has been triggered from the dart code. result is used to return back a response to the dart code, it may be successful/unsuccessful/exception. In the same block it can be achieved as follows:-
Now let’s switch to the flutter side for a moment to trigger this method call and understand the bigger picture more clearly.
In the PlatformChannel
class, create a new function to receive the SMS.
We call invokeMethod
method on _channel which returns a result asynchronously. As you can see the string “receive_sms”
passed in invokeMethod
determines the method call on both sides. Now all that's left is to write the logic to read SMS in Kotlin. So let’s do that quickly!
Reading SMS ✉
In Android, all the data related to phone operation, specifically SMS and MMS messages can be accessed by using the Telephony
class. An incoming message can be read from the device through a BroadcastReceiver
whenever SMS_RECEIVED_ACTION
or a DATA_SMS_RECEIVED_ACTION
intent is received.
Now, what are BroadcastReceiver and an intent?
Let's start with intent, According to the Android docs, Intents are messaging objects. It has three fundamental use-cases and in our case whenever any system event occurs an intent carrying the data about it is passed to our BroadcastReceiver. Specifically, In our case, we accept only those intents which are either SMS_RECEIVED_ACTION
or a DATA_SMS_RECEIVED_ACTION.
You would’ve guessed by now that a BroadcastReceiver
is just a handler for system events.
Let's implement it!
In the methodCallHandler
for the method “receive_sms
” declare the smsReceiver
object of type BroadcastReceiver as follows:-
The onReceive
function handles all the broadcast messages from system events. Now we have to get the message from these intents. To do this, We have a method inside the Telephony.Sms.Intents
class called getMessageFromIntent
which takes in intent as a parameter. It returns an array of SmsMessage
class.
The SmsMessage
class provides a method displayMessageBody
that provides the body of the SMS received. To get this, Inside the onReceive
function we can do something like:-
You would’ve got the basic idea from the code about what we’re actually trying to do. result.success()
passes the sms.displayMessageBody
back to the dart code.
Before it works, We need to register the BroadcastReceiver and unregister it when we’ve successfully received an SMS. To do this we have a function called registerReceiver
which takes in the broadcast receiver i.e smsReceiver
and an intent filter which will be — “android.provider.Telephony.SMS_RECEIVED”
Anddd, We’re not done yet🌚
Finally, we need to add the required permissions to the manifest and also ask the user for SMS permission.
Add the following line above the application tag in the android manifest file.
<uses-permission android:name="android.permission.RECEIVE_SMS" />
Add the permission_handler plugin to request SMS permission from the user. we can create a function to request SMS permission.
Then inside the initState
of our app do this:-
Anddd… We’re finally done. Run the app and send an SMS to the device it is running on.
On an emulator, you can send SMS by clicking on the three dots given at the bottom of the panel on the right. Click on the phone section and there you can simulate sending SMS.
Well, We’ve successfully accomplished our objective But what if you wanted to keep reading SMS continuously?. In that case, we need something like a stream of SMS. Don’t worry Flutter has you covered by giving you EventChannels.
Setting up a stream of SMS
Instead of a method channel, we need to set up event channels on both sides to set up a stream of messages. In the PlatformChannel class let’s recreate our channel as given below.
static const _channel = EventChannel("com.example.app/smsStream");
We can now create a method that returns the stream of messages. To to that flutter provides you with a method called receiveBroadcastStream
on an event channel.
In the main.dart
file, inside our initState
we will change the function to update SMS as follows:-
That’s all on the flutter side. Now, all we need to do is set up an event channel on the Kotlin side and somehow create a stream of messages.
In Kotlin
navigate to MainActivity.kt
and inside the configureFlutterEngine
function create an EventChannel
and call setStreamHandler
method on it.
Now, setStreamHandler
the method takes a parameter of the type EventChannel.StreamHandler.
So, we need to make a few changes to our smsReceiver object. We need it to implement the EventChannel.StreamHandler
interface as one would do in Kotlin.
We can do it as follows:-
Now apart from onReceive
function, We have onListen
and onCancel
functions as well. These are provided by the EventChannel.StreamHandler
interface. It’s the same as when you need to override functions of a parent abstract class by a child class that implements it.
Now you would notice that we do not have any call/result kind of thing as we did in handling the method call earlier. Instead, we get something called an EventSink to communicate messages to the dart code in form of a stream.
Above the onListen
function, we’ll declare an EventSink.
var eventSink: EventChannel.EventSink? = null
Inside the onListen
function, Initialize it to the value of the variable events
provided by the function itself.
eventSink = events
In the onCancel
function, we can make ur eventSink
null
eventSink = null
Finally, to emit messages t the dart code, we have a method called success
on the eventSink
just like we had on the result
object.
eventSink?.success(sms.displayMessageBody)
Now the smsReceiver
object looks like the following piece of code:-
Again we need to register our smsReceiver to make it work. But this time we do not unregister it.
registerReceiver(smsReceiver,IntentFilter("android.provider.Telephony.SMS_RECEIVED"))
And that’s it. Now run your app and it will read messages continuously from the device.
Concluding things
In this article, we learned to read an incoming SMS from the device in a very basic way on Android. You can follow up on the official flutter docs to implement it in ios as well and you’ll need to look up how to read SMS in swift as well.
Platform channels let us achieve services and functionalities that are currently not possible in Flutter. You can extend this knowledge in your own way, in your own challenge that you’re facing currently. Like someone would want to read these SMS in the background.
Well, I’ll be ending this article here. I hope you learned something new today😊. You can reach me on LinkedIn in case of any query. I’ll be happy to help.