Press "Enter" to skip to content

Using MQTT With Flutter to Build a Location Sharing Mobile App

In this article, we’re gonna work on a location sharing application that uses MQTT as its communication protocol. In real life, we might need this application to create an uber driver like app experience. In essence, all this app does is it fetches location continuously from the device and publish them to an MQTT topic to be consumed later by other clients or this app itself.

As usual, if you want to dive directly to the code, you can see it on this GitHub repository below:

https://github.com/blackmenthor/flutter-mqtt-location-example

Alright moving on. The app that we’re trying to make today will look like this.

So basically what this app does is, it fetches the device location continuously, publish them to an MQTT topic, subscribes to that exact topic for location updates that were sent before, and draw a marker for every location updates that were received. If you think about it, in a nutshell, this looks like an Uber app where the driver would send their location updates periodically, and you as a user will receive them and see if there’s any driver near you by seeing the markers on the map.

Pretty cool right? Well although it’s not exactly what those advanced tech company uses, I think it’d pretty much mimick their application and the behaviors.

I’ll try to explain how to make this app as systematic as possible, so I’ll divide the explanation by creating a few big sections so hopefully, it’d be easier to understand this.

Disclaimer: I only test and work on the Android native side of this app. I haven’t tested and work on the iOS side of this app, maybe I’ll do that later. And, I’m assuming that you already had a foundation knowledge on MQTT and how it works. If you want to know more about MQTT, I suggest you to read this article.


1. Dependencies

To be able to run all the functionality in this app, we’re gonna need three dependencies. Those dependencies are:

We need this dependency to be able to connect, subscribe, and publish to MQTT and its topic so we’re able to communicate with the server through them. The package is quite mature and currently on version 5.6.2.

We need this dependency to get the current location of the device. It’s already mature enough and currently on version 2.3.5. Although its maturity, it currently doesn’t work when our app is on background, there’s an experimental feature about that but it’s not production-ready yet I guess. You can read about it 

  • Google Maps Flutter
  • We need this dependency to show Google Maps to our user and draws the marker that points to the user’s current location. It’s still a rather new library on pub.dev, but I’m pretty confident about this library since it’s an official Flutter library. Currently on version 0.5.21+15.

    2. Setting up the MQTT

    Alright, the first thing we want to do is creating a wrapper for the MQTT client so it’ll be ready to be called by other files. A lot of code on this section was copy-pasted from the dart’s MQTT client library page, so if you need something from that lib that I didn’t cover on this article, chances are you could find them on the official page.

    First, create the class, we’ll call it MQTTClientWrapper. And the first attribute that this class has would be an MQTTClient instance, a class from the official library. So it’ll look like this.

    class MQTTClientWrapper {  MqttClient client;}

    But, we haven’t initialized the instance yet. We’ll create a function to do so. Based on the docs, the first thing we need to do in order to connect to the MQTT server is to instantiate and setup the instance with few parameters based on our own options. For this app, we’re gonna use a test server provided by void _setupMqttClient() {
    client = MqttClient.withPort('test.mosquitto.org', '#', 1883);
    client.logging(on: false);
    client.keepAlivePeriod = 20;
    client.onDisconnected = _onDisconnected;
    client.onConnected = _onConnected;
    client.onSubscribed = _onSubscribed;
    }

    You’re gonna see a few errors there because you haven’t implemented all the needed callbacks, but don’t worry because we’re gonna get there soon.

    After we’re done with the credentials and the parameters set up to the instance, the next step is to connect the instance to the server that we’re going to communicate with. But before going there, our plan is to make the current connection and subscription state available on the wrapper. Why? because sometimes we needed that information to be able to decide what we’re going to draw on the screen or if a button should show up or not based on current condition, etc.

    So, we’re going to define the models for this MQTT connectivity and subscription. I define this model as an enum that represents current connectivity and subscription status. The models would look like this.

    enum MqttCurrentConnectionState {
    IDLE,
    CONNECTING,
    CONNECTED,
    DISCONNECTED,
    ERROR_WHEN_CONNECTING
    }
    enum MqttSubscriptionState {
    IDLE,
    SUBSCRIBED
    }

    You must note here that I didn’t include all the MQTT connectivity state here. I just include all the important things for this sample. Moving on.

    After the models were defined, the next step is to create instances of those models in our MqttClientWrapper class. So our wrapper class would look like this.

    class MQTTClientWrapper {MqttClient client;MqttCurrentConnectionState connectionState =                                    MqttCurrentConnectionState.IDLE;
    MqttSubscriptionState subscriptionState = MqttSubscriptionState.IDLE;
    }

    And as you can guess, the default value of those attributes would be IDLE. After that, we’re going to connect our MqttClient instance to the server. To do so, what we had to do is just call connect. But, the tricky part here is we need to wrap the call in a try-catch block to intercept if the connecting flow fails. And if it fails, we need to call disconnect from the client instance. So our code would look like this.

    Future<void> _connectClient() async {
    try {
    print('MQTTClientWrapper::Mosquitto client connecting....');
    connectionState = MqttCurrentConnectionState.CONNECTING;
    await client.connect();
    } on Exception catch (e) {
    print('MQTTClientWrapper::client exception - $e');
    connectionState = MqttCurrentConnectionState.ERROR_WHEN_CONNECTING;
    client.disconnect();
    }
    if (client.connectionStatus.state == MqttConnectionState.connected) {
    connectionState = MqttCurrentConnectionState.CONNECTED;
    print('MQTTClientWrapper::Mosquitto client connected');
    } else {
    print(
    'MQTTClientWrapper::ERROR Mosquitto client connection failed - disconnecting, status is ${client.connectionStatus}');
    connectionState = MqttCurrentConnectionState.ERROR_WHEN_CONNECTING;
    client.disconnect();
    }
    }

    One thing worth noting here is that we need to make this function returns Future<void> and also an async function. The thing is, the connect function is an async function so we had to make sure that no other MQTT call would be made before this function completed, either with success or failure.

    Next step. After we’re finally connected to the server (yay!), we’re gonna need to subscribe to a topic so we can listen to our current position that was sent back to us after we publish it to the same topic. Fortunately, Mosquitto already created a topic that will do exactly this for us, which is receiving a message from the client and sending it back to that exact topic. The topic name that we’re going to use is Dart/Mqtt_client/testtopic.

    Before doing so, we should add a new onMessageReceived callback attribute to the wrapper so the caller would get notifications when there is a new message received from a particular topic. The attribute would look like this.

    final Function(String) onMessageReceived;

    What we’re gonna do here is we’re going to subscribe to that exact topic, and after that, we’re going to listen for any messages that’ll come through that topic. Don’t forget that we also need to call the onMessageReceived callback that we just defined. The code would look like this.

    void _subscribeToTopic(String topicName) {
    print('MQTTClientWrapper::Subscribing to the $topicName topic');
    client.subscribe(topicName, MqttQos.atMostOnce);
    client.updates.listen((List<MqttReceivedMessage<MqttMessage>> c) {
    final MqttPublishMessage recMess = c[0].payload;
    final String newLocationJson =
    MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
    print("MQTTClientWrapper::GOT A NEW MESSAGE $newLocationJson");
    });
    onMessageReceived(newLocationJson);
    }

    One thing worth noting here is that the message that we’re receiving is in a form of bytes. So in order to make that human or parser readable, we need to convert that to a String. To do that, we could just call this function;

    MqttPublishPayload.bytesToStringAsString(messageBytes);

    Alright, so next up. Remember that callbacks that we haven’t take care of from the instance? We’re going to do that now.

    So there are three callbacks that we need to create, onConnected, onDisconnected, and onSubscribed.

    • onConnected

    This function will be called once the client has successfully made a connection to the server. We can do anything on this callback, and in this example, we’re also going to pass that responsibility to the wrapper. So, the caller of this wrapper would also get a callback if it’s already connected.

    So we’re going to add one more attribute to the wrapper, which is a VoidCallback. It’ll look like this.

    final VoidCallback onConnectedCallback;

    After that, we’re going to define the onCallback function inside our wrapper. Its responsibility is just to change the current connection state to CONNECTED. It’ll look like this.

    void _onConnected() {
    connectionState = MqttCurrentConnectionState.CONNECTED;
    print(
    'MQTTClientWrapper::OnConnected client callback - Client connection was sucessful');
    onConnectedCallback();
    }
    • onDisconnected

    And for the onDisconnected function. We’re just going to change the current connection state to DISCONNECTED.

    void _onDisconnected() {
    print('MQTTClientWrapper::OnDisconnected client callback - Client disconnection');
    if (client.connectionStatus.returnCode == MqttConnectReturnCode.solicited) {
    print('MQTTClientWrapper::OnDisconnected callback is solicited, this is correct');
    }
    connectionState = MqttCurrentConnectionState.DISCONNECTED;
    }
    • onSubscribed

    And for the last one. We’re just going to change the current subscription status to SUBSCRIBED.

    void _onSubscribed(String topic) {
    print('MQTTClientWrapper::Subscription confirmed for topic $topic');
    subscriptionState = MqttSubscriptionState.SUBSCRIBED;
    }

    So the last thing of the connect and subscribe flow is to create a public method so the outside world would be able to start the connect and subscribe flow. We’ll call that function prepareMqttClient. It’ll look like this.

    void prepareMqttClient() async {
    _setupMqttClient();
    await _connectClient();
    _subscribeToTopic('Dart/Mqtt_client/testtopic');
    }

    Notice that async-await identifier was there so the wrapper won’t start the subscription process if the connecting process wasn’t finished yet.

    Next, we need to define a function to publish a message to a topic. To do so, we need to create a MqttClientPayload that will be used later to publish to the topic. The code would look like this.

    void publishMessage(String message) {
    final MqttClientPayloadBuilder builder = MqttClientPayloadBuilder();
    builder.addString(message);

    print('MQTTClientWrapper::Publishing message $message to topic ${Constants.topicName}');
    client.publishMessage('Dart/Mqtt_client/testtopic', MqttQos.exactlyOnce, builder.payload);
    }

    It’s quite simple really how we could publish a message through a topic. We’d just need to create a MqttClientPayload using a builder and add a simple string to it. Then, we could just call the publishMessage function inside the MqttClient. And for the QoS, I suggest you read the article that explains about MQTT in-depth above.

    Alright. Now that the MqttClientWrapper is done. Now we could instantiate the MqttClientWrapper, connect, subscribe, and publish to the server. To do so, we need to call a code similar to this.

    MQTTClientWrapper mqttClientWrapper;void setup() {
    mqttClientWrapper = MQTTClientWrapper(
    () => whatToDoAfterConnect(),
    (newMessage) => gotNewMessage(newMessage)
    );
    mqttClientWrapper.prepareMqttClient();
    }
    void publishMessage(String msg) {
    mqttClientWrapper.publishMessage(msg);
    }

    Horray! I guess the most complex part of this application is done. Now, we could start to connect, subscribe, and publish to the MQTT server using this wrapper and this wrapper would take care of it. Moving on to the next part that is also exciting, the location service!

    3. Setting up the Location Service

    This part should be much much simpler than the previous one. In this section, we’re going to create a wrapper for the location service and start the location monitoring when a function is called.

    Alright, the first thing is we need to define the attributes for the wrapper. There are two attributes that we need, first is a Location instance from the library, and the second is an onLocationChanged callback that was supplied by the wrapper caller. So, the class would look like this.

    class LocationWrapper {var location = new Location();
    final Function(LocationData) onLocationChanged;
    LocationWrapper(this.onLocationChanged);}

    Next, we’re going to create an initialize method to the wrapper so it could start to monitor the device’s current location periodically.

    One thing worth noting here is, the location library would only work if the user already allows this app to monitor its location. So the flow of this initialization would be, check if the location permission is already granted, if not, try to request permission to the user, and if it’s granted then we can start to monitor the user’s location. And, don’t forget to call onLocationChanged callback that was registered on the constructor. So with that being said, our final code for the location wrapper would look like this.

    class LocationWrapper {var location = new Location();
    final Function(LocationData) onLocationChanged;
    LocationWrapper(this.onLocationChanged);void prepareLocationMonitoring() {
    location.hasPermission().then((bool hasPermission) {
    if (!hasPermission) {
    location.requestPermission().then((bool permissionGranted) {
    if (permissionGranted) {
    _subscribeToLocation();
    }
    });
    } else {
    _subscribeToLocation();
    }
    });
    }
    void _subscribeToLocation() {
    location.onLocationChanged().listen((LocationData newLocation) {
    onLocationChanged(newLocation);
    });
    }
    }

    Well, that’s all. Much much simpler than the MQTT part right? So right now, we could initialize the location middleware class and start the location monitoring process from that instance. One thing worth noting here is if the user didn’t allow the permission when the app starts, the location monitoring won’t be started at all. But, I didn’t take this case into account because let’s face it, it’s a sample after all. Moving on.

    4. Setting up the Google Map and tie it all up together

    Next up, we’re almost there! Now we’re going to set up the Google Map instance and tie it up with the MQTT and Location wrapper that we just made.

    Now we can create the main.dart as the entry point to our app. And, I assume you had created the default MyHomePage and _MyHomePageState that Flutter creates for us on creation.

    We’re gonna have 4 attributes to the _MyHomePageState, which is an MQTTClientWrapper, LocationWrapper, LocationData, and a GoogleMapController. So right now, our _MyHomePageState looks like this:

    class _MyHomePageState extends State<MyHomePage> {MQTTClientWrapper mqttClientWrapper;
    LocationWrapper locationWrapper;
    LocationData currentLocation;GoogleMapController _controller;@override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text(widget.title),
    ),
    body: Container(),
    );
    }
    }

    After done with the attributes, the next step is to create a setup function. In this function, we’re going to do two things which are:

    • Prepare and initialize LocationWrapper so it can start to monitor the user’s location.
    • Prepare and initialize MQTTClientWrapper so it can start to connect to the MQTT server and also start subscribing to the topic we defined earlier.

    For the LocationWrapper, we need to define a callback which will be called once the LocationWrapper acquires the user’s new location. In this case, what we want to do is, we want to publish that location to the MQTT topic we defined earlier. But, the updates that we received is in a form of LocationData, so we need to convert that first to a JSON string. Given that, so the code would look like this:

    locationWrapper = LocationWrapper((newLocation) {
    String newLocationJson = "{\"latitude\":${input.latitude},\"longitude\":${input.longitude}}";
    mqttClientWrapper.publishLocation(newLocationJson));
    }

    Next, for the MQTTClientWrapper, we need to define two callbacks. The first one is the onConnected callback, which will be called once this app successfully connects to the MQTT server. And the latter is the onMessageReceived callback, which will be called once the app receives a message from the MQTT server’s topic that we defined earlier. After that, we should call the prepareMqttClient function so the wrapper would start connecting to the server and do its job. Given that, the code would look like this:

    mqttClientWrapper = MQTTClientWrapper(
    () => locationWrapper.prepareLocationMonitoring(),
    (newLocationJson) => gotNewLocation(newLocationJson)
    );
    mqttClientWrapper.prepareMqttClient();

    Right now, you should see an error that says gotNewLocation function isn’t defined. Now we fill that in, create a gotNewLocationFunction that receives a string message we received from the MQTT server’s topic. But before we can use that string, we need to convert that to a LocationData so the google maps could recognize them. After that, the code would look like this:

    void gotNewLocation(String newLocationData) {
    Map<String, dynamic> jsonInput = jsonDecode(input);
    LocationData newLocation = LocationData.fromMap({
    'latitude':jsonInput['latitude'],
    'longitude':jsonInput['longitude'],
    });
    setState(() {
    this.currentLocation = newLocation;
    });
    animateCameraToNewLocation(newLocation);
    }

    Hold the phone. Now there’s an error saying that animateCameraToNewLocation isn’t defined. Well, now we’re going to define that. What this function does is, it would animate the camera on our google maps instance once it receives a new location update. On Flutter, we could do this using the GoogleMapController that binds to our GoogleMap instance. So the code would look like this:

    void animateCameraToNewLocation(LocationData newLocation) {
    _controller?.animateCamera(CameraUpdate.newCameraPosition(CameraPosition(
    target: LatLng(
    newLocation.latitude,
    newLocation.longitude
    ),
    zoom: 15.8746
    )));
    }

    After that, we could define the main setup function that will call everything that we defined above, and call that function inside our initState function. So with that being said, our setup and initState function would now look like this:

    void setup() {
    locationWrapper = LocationWrapper((newLocation) => mqttClientWrapper.publishLocation(newLocation));
    mqttClientWrapper = MQTTClientWrapper(
    () => locationWrapper.prepareLocationMonitoring(),
    (newLocationJson) => gotNewLocation(newLocationJson)
    );
    mqttClientWrapper.prepareMqttClient();
    }
    @override
    void initState() {
    super.initState();
    setup();
    }

    Now, we get to the last thing on our to-do-list, which is to create the widget that will contain our maps and draw markers of our current location. On this widget, we’re going to show a text that says “Connecting to MQTT…” if the MQTTClientWrapper is still connecting to the server, and will start showing the map once it’s connected. So our main Scaffold and the loading screen code would look like this:

    Widget loadingText() {
    return Center(
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    crossAxisAlignment: CrossAxisAlignment.center,
    children: <Widget>[
    Text("CONNECTING TO MQTT..."),
    Padding(
    padding: const EdgeInsets.only(top: 8.0),
    child: CircularProgressIndicator(),
    ),
    ],
    ),
    );
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text(widget.title),
    ),
    body: mqttClientWrapper.connectionState != MqttCurrentConnectionState.CONNECTED ?
    loadingText() : googleMapWidget(),
    );
    }

    Last thing! we need to define the googleMapWidget function that will return the Google Map widget with its markers. For this one, I’ll show the code first and will try to explain what it means after.

    Widget googleMapWidget() {
    return GoogleMap(
    initialCameraPosition: CameraPosition(
    target: LatLng(-6.1753871, 106.8249641),
    zoom: 10.8746
    ),
    markers: currentLocation == null ? Set() : [
    Marker(
    markerId: MarkerId("1"),
    position: LatLng(currentLocation.latitude, currentLocation.longitude)
    )
    ].toSet(),
    onMapCreated: (GoogleMapController controller) {
    setState(() {
    this._controller = controller;
    });
    },
    );
    }

    Alright so first thing first, we need to define an InitialCameraPosition that currently points to the very center of Jakarta (Hello Jakartans!), it could be anywhere in the world. All you had to do is just define the Latitude, Longitude, and the Zoom value you want it to be.

    Next, we should draw the markers if we already received a location update from the MQTT server, or draw nothing if we didn’t (by declaring an empty Set). To do so, we should create a MarkerId which is a simple string, it could be any string but remember that there can’t be any duplicates here! After defining the map inside a List of one item, we then convert that List to a Set so Google Map can start consuming it.

    The last thing, remember how we call the GoogleMapController above to animate the camera’s map? Well, now we had the onMapCreated callback that comes with the GoogleMapController instance that ready for us to use later. Now we could set this new value to our _controller so it can be used later.


    Well, that’s all! Congratulatulations, you just created an app that connects MQTT, Location Service, and Google Map using Flutter. The next interesting step is to create a test case for all the things we’ve just written above. Or maybe, create a Flutter app that acts as a client to this app and consume its data.

    I’m sorry if any of my explanations weren’t that perfect, but I hope you could use this article and the project I made on Github.

    See you in the next article!

    Thank you, and have a good rest of your day!


    Original Source: https://medium.com/swlh/using-mqtt-with-flutter-to-build-a-location-sharing-app-24e7307b21d3

    Be First to Comment

    Leave a Reply

    Your email address will not be published.