Dart: throttling method invocation
Let’s consider the following situation, a method is called by a background service at a certain rate, this method has to handle these calls at another rate. My real situation is in a Flutter app, where location service updates a user’s position too often and the app has to resend the updates every n-th minute to a backend. In this article, I’ll describe how to achieve this by converting method calls into a stream of events.
Model situation
Let’s simulate the situation with the following code:
import 'dart:async';
import 'dart:math';
void probe(int id, int delay) {
print("Submitted(ID: $id; delay: $delay)");
}
void invokeWithDelays(List<List<int>> delays) async {
for (List<int> delay in delays) {
await Future.delayed(
Duration(milliseconds: delay[1]), () => probe(delay[0], delay[1]));
}
}
void main(List<String> args) async {
Random random = Random(0);
final List<List<int>> delays = [];
List<int>.generate(10, (i) => i + 1).forEach((id) {
delays.add([id, 50 + random.nextInt(200)]);
});
await invokeWithDelays(delays);
}
The method probe
is called with random delays producing the following output:
Submitted(ID: 1; delay: 105)
Submitted(ID: 2; delay: 59)
Submitted(ID: 3; delay: 214)
Submitted(ID: 4; delay: 149)
Submitted(ID: 5; delay: 56)
Submitted(ID: 6; delay: 241)
Submitted(ID: 7; delay: 144)
Submitted(ID: 8; delay: 131)
Submitted(ID: 9; delay: 91)
Submitted(ID: 10; delay: 219)
Now, we want to process these calls not more often then 100ms. In order to achieve that, we’ll introduce a stream, each method invocation will add value to the stream, then we’ll use StreamTransformer
to throttle it. The code will look as follows:
import 'dart:async';
import 'dart:math';
import 'package:stream_transform/stream_transform.dart';
StreamController<int> controller = StreamController<int>();
void probe(int id, int delay) {
print("Submitted(ID: $id; delay: $delay)");
controller.add(id);
}
void invokeWithDelays(List<List<int>> delays) async {
for (List<int> delay in delays) {
await Future.delayed(
Duration(milliseconds: delay[1]), () => probe(delay[0], delay[1]));
}
}
void main(List<String> args) async {
Random random = Random(0);
final List<List<int>> delays = [];
List<int>.generate(10, (i) => i + 1).forEach((id) {
delays.add([id, 50 + random.nextInt(200)]);
});final StreamTransformer<int, dynamic> throttle = StreamTransformer.fromBind(
(s) => s.throttle(const Duration(milliseconds: 100))); controller.stream
.transform(throttle)
.listen((event) => print("throttle: $event"));
await invokeWithDelays(delays);
await controller.close();
}
And the output:
Submitted(ID: 1; delay: 105)
throttle: 1
Submitted(ID: 2; delay: 59)
Submitted(ID: 3; delay: 214)
throttle: 3
Submitted(ID: 4; delay: 149)
throttle: 4
Submitted(ID: 5; delay: 56)
Submitted(ID: 6; delay: 241)
throttle: 6
Submitted(ID: 7; delay: 144)
throttle: 7
Submitted(ID: 8; delay: 131)
throttle: 8
Submitted(ID: 9; delay: 91)
Submitted(ID: 10; delay: 219)
throttle: 10
Why throttle but not debounce?
Throttling ensures the method is called at a certain rate, in this case, not more than once per 100ms. Debounce will wait until the timeout passes since the last method invocation, in this case, it would mean that the listener would be never called.