Specifications
I was tasked with adding a ‘Panic Alert’ functionality to an existing Flutter (mobile) application. Here’s the breakdown:
- Add a way for a user to initiate a Panic Alert easily, but without being able to trigger it by accident.
- Get the user’s location in GPS coordinates, send it to a middle-man server to be processed and displayed for security providers.
- Display a map on their screen, and center their location on it, so they can be at ease while in a stressful situation.
- Implement a ‘Test Mode’, which lets the user start a panic, without actually sending the alert all the way through to the security provider.
Plug-in Breakdown
When I first started, I had no idea which plug-ins would be useful, but over weeks of researching (googling), I found these three that finally clicked everything into place:
- Google Maps Flutter - This plug-in lets you render a map of the user, as well as giving you controls to programmatically move the virtual camera. Very useful, though a bit finicky.
- Location - Lets you get an accurate location, down to the address, as GPS coordinates. Incredibly easy to use, but needs permissions on the Android/iOS level.
- get_it - An incredibly easy way to access backend data from the frontend, it effectively packages your classes as Javascript objects and makes them accessible from anywhere, supports singletons as well. (In our implementation, each time you see locator<ServiceName>, that’s this plug-in.)
Implementing The Button
Button Building (Expand for Code)
I had two challenges with this button. The custom long-press cooldown, not natively supported by Flutter, and the state change when the app is in 'Test Mode'.
I tried a lot of solutions, until I came across the Inkwell component. I could define a Timer at the top of my class like so: Timer alertTimer;. Then, upon your finger touching the button, it’d start the timer with the onTapDown function, and cancel it if your finger lifts before it finishes, with the onTapUp function.
Next up is updating the UI for the ‘Test Mode’, but also handling test mode when it runs out, as it only lasts for thirty seconds. I found the ValueListenableBuilder to be extremely useful here, as it makes use of Flutter in-built ValueNotifier, which lets you listen to variable changes from anywhere. The ValueListenableBuilder listens for the current state of the ValueNotifier variable, and builds a separate state below it. Whenever that variable changes, the ValueListenableBuilder automatically rebuilds its state without needing a custom function, taking a lot of state management off my hands. All I had to do now was implement the ValueNotifier variable in one of my services and update it via a switch in the settings menu, more on that later.
Getting the User’s Location
Sixty Second Loop (Expand for Code)
Getting the Location (Expand for Code)
Now that we can start the Panic Alert, we need a loop that runs every sixty seconds to continually send the user’s location, to track if they’re moving or not. Doing so is fairly easy, just have a while loop that runs while the alert is active, get the location, send the data.
To get the location, I use the Location plug-in, which gets called from the top of the loop. The first challenge I faced here was the permissions. A previous developer already implemented some of the permissions, but I had to extend them to include getting the location from the background, which both Apple and Google are extremely sensitive about, which is good.
The Location plug-in already has the functions needed to request permission from the user, but we need to manually add the permissions we’re allowed to ask in the Android Manifest, which is a way of declaring ‘This is what we might need and will ask for’ before we’re actually asking for it. The file is called AndroidManifest.xml, and can be found in the /android/app/src/main folder path.
Android Manifest XML (Expand for XML)
I’ll begin with the access permissions, thankfully they’re fairly straightforward. Fine Location is equivalent to getting someone’s address, obviously important for a Panic Alert app, but also for something like a taxi-service or food-delivery app. Coarse Location lets you find roughly in which city someone is, not ideal, but useful if you can’t get the fine location for whatever reason. Background location is the one that lets you retrieve location data even when the app isn’t active on the screen at the moment, whether by simply pressing the home button or locking the screen.
To launch an app that has Background Location permissions, you need to submit a Youtube video to the Play Store detailing why exactly it’s needed, and showcasing its use-case; they're very strict about it.
Lastly, the Foreground Service permission, which is the odd-one-out. I noticed with my phone, the battery saving techniques are obscenely harsh, killing the app after a minute even if it’s actively processing data. You can force the operating system to keep the app alive by designating it as a foreground service, which creates a little notification in the control center (when you swipe the notification bar down), which prevents it from being closed automatically.
With the permissions set-up, simply request the location. In this case, the previous developer inserted a for loop that only breaks when accuracy is 100%, improving the average accuracy you get, though there are more improvements and optimizations you could make.
Displaying the Map to the User
Laying down the Map Framework (Expand for Code)
Building the Map (Expand for Code)
Now that we have the location in a backend service, all we have to do is render the map, and then move the camera so it centers on the location we got. For this app, I made it take up the entire screen by using MediaQuery.of(context).size.height, but any (reasonable) height works fine. I gave it a default _center position in the middle of South Africa by getting the latitude and longitude from Google Maps, and a 6.0 zoom level, which gives you a view of the whole country before you zoom into the calculated location. As an aside, I also specified a minMaxZoomLevel of 0 to 16, with 16 being the furthest the user is allowed to zoom in. At the time of adding this feature, zooming in any further seemingly crashed the plug-in, it might be fixed by now or sometime in the future.
Finally, to center the map whenever the backend location service finds the user’s location, we can put a listener on that ValueNotifier in the initState, which calls the updateMap function, which translates the map to those specific coordinates, as well as zooming in to the max 16 level, giving the user a clear view of where they are, and the area surrounding them.
That’s all you need to do to render a map, the google_maps_flutter plugin does all the rest.
The Test Mode Switch
In the Service (Expand for Code)
In the Settings View (Expand for Code)
Lastly, the ‘Test Mode’, we want a simple switch that activates a timed test mode. In the Home Service, I just defined a timer, and the testMode ValueNotifier boolean, much like the location variable. Then, implemented a switch that has a value based on the current testMode boolean thanks to the ValueListenableBuilder. (For Android devices, we use the standard Switch component, but for iOS/Apple devices, the Cupertino one, which is the same component but with Apple’s style.)
Finally, the start and cancel functions simply flip the variable, while starting or canceling the timer to set the testMode boolean to false. No other logic is required, as ValueNotifier and all its listeners automatically handle value changes.