Compute With Flutter


Dart is a single threaded language, but it comes with a handy compute function to spawn isolates. In a nutshell, the compute function is useful for doing extra work on a different “thread”–it’s actually an isolate–so your flutter app does not experience “jank”. Jank occurs when the UI doesn’t render smoothly. For example, every so often, a frame takes 10 times longer to render, so it gets dropped, and the animation visibly jerks.

To use compute, you need a function and data. compute will return a future of whatever your function returns.

final Future<R> result = compute(someFunction, someData)

In my app, I had to parse about 200 tweets that were scored with sentiment analysis. Each tweet has a message and created property, but the sentiment property had some intense parsing. So when I created a new TweetDto, I also had to create a new SentimentDto.

class TweetDto {
  final int id;
  final String message;
  final String created;
  final SentimentDto sentiment;

  TweetDto({
    @required this.id,
    @required this.message,
    @required this.created,
    @required this.sentiment,
  });

  factory TweetDto.fromJson(Map<String, dynamic> json) {
    return TweetDto(
      id: json['id'],
      message: json['message'],
      created: json['created'],
      sentiment: SentimentDto.fromJson(json['sentiment']),
    );
  }
}

class SentimentDto {
  final List<_SentimentSentence> sentences;
  final _SentimentSummary documentSentiment;
  final String language;

  SentimentDto({
    @required this.sentences,
    @required this.documentSentiment,
    @required this.language,
  });

  factory SentimentDto.fromJson(Map<String, dynamic> json) {
    final List<dynamic> sentencesRef = json['sentences'];
    final List<_SentimentSentence> sentences = List<_SentimentSentence>.from(
      sentencesRef.map(
        (s) => _SentimentSentence(
          text: _SentimentText(
            content: s['text']['content'],
            beginOffset: (s['text']['beginOffset']).toDouble(),
          ),
          sentiment: _SentimentSummary(
            score: (s['sentiment']['score']).toDouble(),
            magnitude: (s['sentiment']['magnitude']).toDouble(),
          ),
        ),
      ),
    );
    final documentSentiment = _SentimentSummary(
      score: (json['documentSentiment']['score']).toDouble(),
      magnitude: (json['documentSentiment']['magnitude']).toDouble(),
    );
    final language = json['language'];

    return SentimentDto(
      sentences: sentences,
      documentSentiment: documentSentiment,
      language: language,
    );
  }
}

class _SentimentSentence {
  final _SentimentText text;
  final _SentimentSummary sentiment;

  _SentimentSentence({
    @required this.text,
    @required this.sentiment,
  })  : assert(text != null),
        assert(sentiment != null);
}

class _SentimentText {
  final String content;
  final double beginOffset;

  _SentimentText({
    @required this.content,
    @required this.beginOffset,
  });
}

class _SentimentSummary {
  final double magnitude;
  final double score;

  _SentimentSummary({
    @required this.magnitude,
    @required this.score,
  })  : assert(magnitude != null),
        assert(score != null);
}

I experienced some performance when I was parsing about 200 tweets each with their own sentiment score, so I looked to compute all of this json parsing with Flutter’s compute. In my TweetBloc, I had a function called loadTweets which was responsible for fetching 200 scored tweets. You’ll see that I switch to new Observable with Observable.fromFuture(...) in order to keep this operation reactive.

...
 void loadTweets() {
    _authService
        .get(ApiConsts.getTweets(200))
        .switchMap((signal) => Observable.fromFuture(
            compute(TweetBloc._decodeAndParseTweets, signal.body)))
        .take(1)
        .listen(
      (List<TweetDto> signal) {
        _tweets.add(signal);
      },
    );
  }

..

Because compute takes a function that performs the computation, I created a static function decodeAndParseTweets which parses all of my json.

 void loadTweets() {
    _authService
        .get(ApiConsts.getTweets(200))
        .switchMap((signal) => Observable.fromFuture(
            compute(TweetBloc._decodeAndParseTweets, signal.body)))
        .take(1)
        .listen(
      (List<TweetDto> signal) {
        _tweets.add(signal);
      },
    );
  }

  static List<TweetDto> _decodeAndParseTweets(String jsonBody) {
    final List<dynamic> decodedJson = jsonDecode(jsonBody);
    final List<TweetDto> tweets =
        decodedJson.map((t) => TweetDto.fromJson(t)).toList();
    return tweets;
  }

Flutter’s compute helped prevent jank in my flutter app by offloading intense parsing of data onto a new isolate. Consider using it in the future when you have any intense data processing.