Building a killswitch package for Flutter apps

Posted February 22, 2024 β€’ 7 min read
Bionic reading mode (b)
banner image

PreludeπŸ”—

Table of contents

Links

MotivationπŸ”—

I was scrolling through Reddit and came across a post titled: What could go wrong not paying your web developer. To summarize, it told the tale of a web developer who wasn't paid for their work on their client's website, so, he subsequently edited it to appear like this:

reddit web dev not getting paid post

I thought this was hilarious!

So, I searched and found a myriad of other posts detailing similar things. Here are some other examples:

This made me want to build a tool that could be used to prevent cases such as these; at least, in relation to Flutter apps.

My idealized killswitchπŸ”—

Below is a small glimpse into my brainstorming:

  • Easy to use.
    • Solid default settings.
      • Smallest required lines of code for common use cases.
    • Optional advanced configuration.
  • Can be triggered remotely.
  • No downsides.
    • No unnecessary HTTPS requests.
    • Won't slow down the app's initialization.
    • Can't backfire.
      • No "false positive" killswitch triggers.

The implementationπŸ”—

First, I ensured the name I wanted (killswitch) was available on the Flutter/Dart package manager.

Second, I created a package in that name by running:

flutter create --template=package killswitch # <-- note the name

Next, inside /lib, I created the following structure to keep the package clean:

/lib

β”œβ”€β”€ killswitch.dart # my export file
└── src
	β”œβ”€β”€ constants.dart # package-wide constants
	β”œβ”€β”€ killed_page.dart # a page widget routed to upon the app being "killed"
	└── killswitch.dart # the widget containing all the killing/whitelisting logic

I then created a simple killed_page.dart that looks like:

killed page

The page is not meant to be extravagant. It is simply intended to show a customizable message and then optionally provides a VoidCallback upon the text being clicked (obviously with an animation, too) so that a developer can open a URL, copy their email address, etc. so the client can reach them.

Next, I created the Killswitch class. As shown below, it has many fields for advanced customization:

class  Killswitch  extends  StatefulWidget {
	final  String  killedAppText;
	final  VoidCallback?  killedAppTextClicked;
	final  VoidCallback?  onKill;
	final  VoidCallback?  onWhitelist;
	final  int  killStatusCode;
	final  int  whitelistStatusCode;
	final int doNothingStatusCode;
	final  String  killWhitelistAndIgnoreSourceUrl;
	final  Widget  child;
	final  bool  preventPushToKilledPage;
	final  String  uniqueKillswitchWhitelistPrefsKey;
	final  String  uniqueKillswitchWhitelistFailureConnectPrefsKey;
	final  int  failuresToConnectToSourceBeforeWhitelist;
	final  bool  suppressErrors;
	...

However, because of its defaults, for common use cases, it can be used as simply as:

Killswitch(
	killWhitelistAndIgnoreSourceUrl:  "https://example.com", // <-- your URL
	child: ...
);

The way it works is as follows:

On app open, asynchronously (so it doesn't slow down initialization), the Killswitch polls your provided URL. If the URL returns a killStatusCode status code, it routes the app to the killed page and triggers the onKill callback. If the URL returns a whitelistStatusCode status code, it locally saves this info to shared_preferences that the app is "whitelisted" and thus, it will never poll the URL again. Moreover, it triggers the onWhitelist callback.

Moreover, in the case where the URL is polled n times (default = 10) and is able to connect to the URL but doesn't get either a killStatusCode or whitelistStatusCode response each time, it whitelists the app. Additionally, if there is a server error (httperror != http.ClientException) n times, it also whitelists the app. This is to prevent extenuating circumstance. We don't want to think an error means the app should trigger the killswitch. We always err on the side of safety. If anything, an error should whitelist the app.

Finally, here is the killswitch in-use with a more advanced configuration (still a subset of all the possible options):

Killswitch(
	killWhitelistAndIgnoreSourceUrl:  "https://example.com/killswitch",
	suppressErrors:  false,
	killedAppText: "Hi! This is the developer speaking. I killed the app. Please contact me for help by tapping this message.",
	killedAppTextClicked: () => print("User tapped the killed app text"),
	killStatusCode:  403,
	whitelistStatusCode:  202,
	onKill: () => print("App was killed"),
	onWhitelist: () => print("App was whitelisted"),
	uniqueKillswitchWhitelistFailureConnectPrefsKey: "THIS IS MY UNIQUE PREFS KEY BECAUSE I USE THE SAME KEY AS THEIRS ELSEWHERE IN MY APP",
	failuresToConnectToSourceBeforeWhitelist:  25,
	child: ...
);

It should be used high-up in your widget tree to be effective. Likely, this means directly under your MaterialApp in your main.dart's build method.

Example use caseπŸ”—

Let's say I build an app for a client and they promise to pay me when it's done. However, I've heard from others they are unreliable and tend to steal software. So, I add the Killswitch widget to my app as a contingency and set its killWhitelistAndIgnoreSourceUrl field to https://matthewtrent.me/killswitch (a domain in which I personally control unrelated to the client). As the forever-owner of this domain, I can make it return anything I want.

Now, upon the app opening, three things can happen after the killswitch checks this URL:

  1. killStatusCode is returned from my domain. This results in the app being killed (user brought to the killed_page.dart screen). I will make my URL return this if the client refuses to pay me after I've already handed-over the app to them and am shut-out from editing it myself.
  2. whitelistStatusCode is returned from my domain. This results in the app saving the fact it has been whitelisted and it'll never check if it should be killed again.
  3. doNothingStatusCode is returned from my domain. This means "sit and wait; don't do anything now". I would likely return this while the app is in development.

The integer values: killStatusCode, whitelistStatusCode, and doNothingStatusCode are all configurable in the Killswitch widget's code.

As a failsafe, upon the app trying to reach the specified URL n-times (ignoring cases where the client doesn't have connection), the app will automatically assume something went wrong with the domain configuration and be whitelisted, as to not accidentally kill an app.

Publishing to the Dart/Flutter package managerπŸ”—

First, I ensured my code was clean. This meant removing debugging messages, print statements, etc. I also was sure to use an my killswitch.dart file (as showed in my file layout above) to only expose (export from /src) what I wanted from my package's API.

Secondly, I commented my code using doc comments to achieve a high pub.dev comment score:

/// child widget of the killswitch (this is an example doc comment)
final  Widget  child;

comment score

Next, I added a LICENSE file to my package. I elected to use the MIT license for my safety. It's also extremely popular for open-source software.

Then, I updated my CHANGELOG.md to show the work I've done for each release. Also, I updated my README.md to show an overview of what the package does.

Next, I ran flutter create example inside my package's root directory to create an example of the package's usage. Flutter/Dart's package manager uses this; thus, it must be named exactly this. I proceeded to use the local version of my package inside this example app by adding this to my example app's pubspec.yaml:

...
dependencies:
	flutter:
		sdk: flutter
	killswitch: # <-- added this
		path: ../ # <-- added this
...

Following this, at the top of my package's pubspec.yaml, I added this:

name: killswitch
description: Remotely kill your application.
version: 0.0.1 # <-- this should match the version in your CHANGELOG.md
homepage: https://matthewtrent.me/
repository: https://github.com/mattrltrent/killswitch
issue_tracker: https://github.com/mattrltrent/killswitch/issues

The last bit of pre-publishing I had to do was run dart fix --dry-run to see if the compiler could automatically detect if I needed to fix anything (usually formatting/style issues). I had no problems, though! However, if I had, I could always have ran dart fix to fix them.

Finally, I ran dart pub publish --dry-run to test publish my app and see if it looks good to the package manager. Considering no errors or fix-recommendations were shown, I ran dart pub publish to publish it.

Then, a few minutes later, the package was up for public use here. Awesome!

DisclaimerπŸ”—

This is not meant to be used nefariously. It is only meant to be used by two consenting parties and/or be for informative/testing purposes. DO NOT DO ANYTHING ILLEGAL. IT IS YOUR RESPONSBILITY TO ENSURE YOU'RE USING THIS CORRECTLY. Use at your own risk. This is not guaranteed to work perfectly, or as expected.