Guide to Flutter for Android Developers: Asynchronous UI

1. What is the corresponding method of runOnUiThread() in Flutter?

Dart has a single-threaded execution model and also supports Isolate (a method of running Dart code in another thread), which is an event loop and asynchronous programming. Unless you create an Isolate, your Dart code runs on the main UI thread and is driven by an event loop. The event loop of a Flutter corresponds to the Android main Looper — that is, the Looper bound to the main thread.

Dart’s single-threaded model doesn’t mean you need to run all your code in a blocking operation that causes the UI to freeze. Unlike Android, which requires you to keep the main thread idle at all times, Flutter can use asynchronous tools provided by the Dart language, such as async/await, to perform asynchronous tasks. If you’ve used the async/await paradigm in C# or Javascript, or the coroutine in Kotlin, you should be familiar with it.

For example, you can run network code without causing the UI to hang by using async/await, while letting Dart handle the heavy details behind it:

Future<void> loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = jsonDecode(response.body);
  });
}
Copy the code

Once the network operation decorated with await is complete, call setState() to update the UI, which triggers a rebuild of the widget subtree and updates the data.

The following example shows asynchronously loading data and displaying it in a ListView:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); }}class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView.builder(
        itemCount: widgets.length,
        itemBuilder: (BuildContext context, int position) {
          returngetRow(position); },),); } Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}")); } Future<void> loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL); setState(() { widgets = jsonDecode(response.body); }); }}Copy the code

2. How do I transfer tasks to background threads?

In Android, when you want to access a network resource without blocking the main thread and avoiding ANR, you typically run the task in a background thread. For example, you can use an AsyncTask, a LiveData, an IntentService, a JobScheduler task, or switch the task to a background thread with a scheduler via RxJava’s pipeline.

Because Flutter is single-threaded and runs an event loop (similar to Node.js), you do not need to worry about thread management or background thread creation. If you are performing tasks bound to I/O, such as storage access or network requests, then you can safely use async/await without worry. For example, if you need to perform cpu-intensive computationally intensive work, you can move it to an Isolate to avoid blocking event loops, just as you would put any task out of the main thread in Android.

For tasks bound to I/O, declare the method async and await a long-running task within the method:

Future<void> loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = jsonDecode(response.body);
  });
}
Copy the code

This is how you should generally perform network and database operations, both of which are I/O operations.

In Android, when you inherit AsyncTask, you typically override three methods: onPreExecute(), doInBackground(), and onPostExecute(). There is no corresponding API in Flutter. You just await a time-consuming method call and Dart’s event loop will do the rest for you.

However, there may be times when you need to process large amounts of data and suspend your UI. Within Flutter, time-consuming or computationally intensive tasks can be performed using the Isolate to take advantage of multi-core processors.

The Isolate is a thread that executes independently and does not share memory with the main execution heap. This means you can’t access the main thread variables, or call setState() to update the UI. Unlike threads in Android, Isolate, as its name suggests, cannot share memory (for example, in the form of static variables).

The following example shows how a simple Isolate can share data with the main thread to update the UI.

Future<void> loadData() async {
  ReceivePort receivePort = ReceivePort();
  await Isolate.spawn(dataLoader, receivePort.sendPort);

  // The 'echo' isolate sends its SendPort as the first message.
  SendPort sendPort = await receivePort.first;

  List msg = await sendReceive(
    sendPort,
    "https://jsonplaceholder.typicode.com/posts",); setState(() { widgets = msg; }); }// The entry point for the isolate.
static Future<void> dataLoader(SendPort sendPort) async {
  // Open the ReceivePort for incoming messages.
  ReceivePort port = ReceivePort();

  // Notify any other isolates what port this isolate listens to.
  sendPort.send(port.sendPort);

  await for (var msg in port) {
    String data = msg[0];
    SendPort replyTo = msg[1];

    String dataURL = data;
    http.Response response = await http.get(dataURL);
    // Lots of JSON to parse
    replyTo.send(jsonDecode(response.body));
  }
}

Future sendReceive(SendPort port, msg) {
  ReceivePort response = ReceivePort();
  port.send([msg, response.sendPort]);
  return response.first;
}
Copy the code

The dataLoader() here is the Isolate running in its own independent thread of execution. With Isolate you can perform more CPU intensive operations (such as parsing a large JSON data), or perform computationally intensive mathematical operations such as encryption or signal processing.

You can run the full example below:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:isolate';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); }}class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  showLoadingDialog() {
    if (widgets.length == 0) {
      return true;
    }

    return false;
  }

  getBody() {
    if (showLoadingDialog()) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: getBody());
  }

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}")); } Future<void> loadData() async {
    ReceivePort receivePort = ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);

    // The 'echo' isolate sends its SendPort as the first message
    SendPort sendPort = await receivePort.first;

    List msg = await sendReceive(
      sendPort,
      "https://jsonplaceholder.typicode.com/posts",); setState(() { widgets = msg; }); }// the entry point for the isolate
  static Future<void> dataLoader(SendPort sendPort) async {
    // Open the ReceivePort for incoming messages.
    ReceivePort port = ReceivePort();

    // Notify any other isolates what port this isolate listens to.
    sendPort.send(port.sendPort);

    await for (var msg in port) {
      String data = msg[0];
      SendPort replyTo = msg[1];

      String dataURL = data;
      http.Response response = await http.get(dataURL);
      // Lots of JSON to parse
      replyTo.send(jsonDecode(response.body));
    }
  }

  Future sendReceive(SendPort port, msg) {
    ReceivePort response = ReceivePort();
    port.send([msg, response.sendPort]);
    returnresponse.first; }}Copy the code

3. What is the counterpart of OkHttp in Flutter?

It is easy to make web requests using popular HTTP packets in Flutter.

While the HTTP package doesn’t have all of the functionality found in OkHttp, it abstracts a lot of networking functionality that you would normally implement yourself, making it easy to use when performing network requests.

If you want to use HTTP packages, you need to add a dependency to the pubspec.yaml file:

dependencies: ... HTTP: ^ 0.11.3 + 16Copy the code

To make a network request, call await on the async method http.get() :

import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; [...].  Future<void> loadData() async { String dataURL = "https://jsonplaceholder.typicode.com/posts"; http.Response response = await http.get(dataURL); setState(() { widgets = jsonDecode(response.body); }); }}Copy the code

4. How do I display progress for time-consuming tasks?

On Android you usually display a ProgressBar in the background while performing a time-consuming task.

In Flutter, we use the ProgressIndicator widget. The rendering of the progress bar is controlled by code logic using a Boolean flag value.

In the following example, the build method is broken down into three different methods. Render ProgressIndicator if showLoadingDialog() returns true (when widgets.length == 0). Otherwise, render the data returned by the network request in the ListView.

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); }}class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  showLoadingDialog() {
    return widgets.length == 0;
  }

  getBody() {
    if (showLoadingDialog()) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: getBody());
  }

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}")); } Future<void> loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL); setState(() { widgets = jsonDecode(response.body); }); }}Copy the code