This example demonstrates how to integrate the SpeedOf.Me speed test API into a Flutter app using webview_flutter.
The SpeedOf.Me API is JavaScript-based, so Flutter apps use WebView to:
# pubspec.yaml
dependencies:
webview_flutter: ^4.4.2
Android (AndroidManifest.xml):
<uses-permission android:name="android.permission.INTERNET" />
iOS: no extra Info.plist entry is needed. (The old io.flutter.embedded_views_preview key has been a no-op since Flutter 1.22 and is not required by webview_flutter 4.x.)
Copy speed_test_screen.dart to your project.
SomApi.account = "YOUR_API_KEY";
SomApi.domainName = "your-domain.com";
Upload speedtest.html to your registered domain and load it over https:
..loadRequest(Uri.parse('https://your-domain.com/speedtest.html'));
This is the only loading method that works. The engine validates the page that embeds api.js against the domain registered on your account, and it reads that domain from the page's own URL. loadFlutterAsset and loadHtmlString give the page no host, so they can never match your registration and every test fails with error 1002 (Domain Mismatch).
Flutter App
└── SpeedTestScreen (StatefulWidget)
└── WebViewWidget (webview_flutter)
└── speedtest.html (on your domain)
└── api.js
▼
window.SpeedTest.postMessage(json)
▼
JavaScriptChannel.onMessageReceived()
The HTML page sends messages via:
window.SpeedTest.postMessage(JSON.stringify({
type: 'completed',
data: result
}));
Flutter receives them via JavaScriptChannel:
..addJavaScriptChannel(
'SpeedTest', // This becomes window.SpeedTest in JS
onMessageReceived: (message) {
final json = jsonDecode(message.message);
// Handle message
},
)
speedtest.html normalizes the numbers before forwarding them, because the engine sends an empty string for any value that is not available, which would break Dart's typed casts:
function num(v) { return typeof v === 'number' ? v : 0; }
class SpeedTestResult {
final double download;
final double upload;
final double latency;
final double jitter;
final String? testServer;
final String? ipAddress;
final String? hostname;
}
class SpeedTestProgress {
final String type; // "download", "upload" or "latency"
final double currentSpeed; // Mbps, 0 on latency events
final double percentDone; // 0-100
final int pass; // 0 during the latency phase
}
class SpeedTestError {
final int code;
final String message;
}
currentSpeed is only a real number when SomApi.config.progress.verbose = true, which speedtest.html sets. Without it the engine sends '' on every progress event, and it always sends '' on latency events, which num() turns into 0.
The engine's full result also carries maxDownload, maxUpload, userAgent and testDate. Add them to the forwarded object in speedtest.html and to SpeedTestResult if you need them. Every field is always present: a disabled sub-test sends an empty string rather than omitting the key.
1001 Invalid Account1002 Domain Mismatch2001 Test Error2002 Invalid server response2003 Request timeout2004 Test timeout2005 Speed test could not start (status N)2006 Speed test engine did not loadCode 2006, and code 2005 with status 0, mean the request never left the WebView, normally a content blocker or a frame-src Content-Security-Policy that does not allow https://speedof.me. A 2005 carrying any other status is a real HTTP response from the server, so read the status in the message.
For larger apps, consider:
StateNotifierProvider<SpeedTestNotifier, SpeedTestState>SpeedTestBloc extends Bloc<SpeedTestEvent, SpeedTestState>ChangeNotifierspeedtest.html is reachable over https on your registered domainloadFlutterAsset or loadHtmlString