Dart: throttling method invocation

Sergey Royz
2 min readMar 2, 2020

--

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.

--

--

Sergey Royz
Sergey Royz

Written by Sergey Royz

Co-founder and CTO of a crypto startup. A full-stack software engineer with a passion for creating innovative tech solutions that make a difference.

No responses yet