Contents

Asynchronous programming: futures, async, await

本代码实验室教您如何使用Futures和asyncawait关键字编写异步代码. 使用嵌入式DartPad编辑器,您可以通过运行示例代码并完成练习来测试您的知识.

为了充分利用该代码实验室,您应该具备以下条件:

本代码实验室涵盖以下材料:

  • 如何以及何时使用asyncawait关键字.
  • 使用asyncawait如何影响执行顺序.
  • 如何使用async函数中的try-catch表达式处理来自异步调用的错误.

完成此代码实验室的估计时间:40-60分钟.

Why asynchronous code matters

异步操作使您的程序可以在等待另一个操作完成的同时完成工作. 以下是一些常见的异步操作:

  • 通过网络获取数据.
  • 写入数据库.
  • 从文件读取数据.

要在Dart中执行异步操作,可以使用Future类以及asyncawait关键字.

Example: Incorrectly using an asynchronous function

以下示例显示了使用异步函数( fetchUserOrder() )的错误方法. 稍后,您将使用asyncawait修复示例. 在运行此示例之前,请尝试找出问题所在–您认为输出将是什么?

// This example shows how *not* to write asynchronous Dart code.

String createOrderMessage() {
  var order = fetchUserOrder();
  return 'Your order is: $order';
}

Future<String> fetchUserOrder() =>
    // Imagine that this function is more complex and slow.
    Future.delayed(
      Duration(seconds: 2),
      () => 'Large Latte',
    );

void main() {
  print(createOrderMessage());
}

这就是示例无法打印fetchUserOrder()最终产生的值的原因:

  • fetchUserOrder()是一个异步函数,在延迟后,它提供了一个描述用户顺序的字符串:"大拿铁".
  • 要获取用户的订单, createOrderMessage()应调用fetchUserOrder()并等待其完成. 由于createOrderMessage() 不会等待fetchUserOrder()来完成, createOrderMessage()未能得到字符串值fetchUserOrder()最终提供.
  • 相反, createOrderMessage()获取待完成的工作的表示:未完成的未来. 您将在下一部分中了解有关期货的更多信息.
  • 由于createOrderMessage()无法获取描述用户订单的值,因此该示例无法在控制台上打印" Large Latte",而是打印"您的订单是:'_ Future的实例 '".

在接下来的部分中,您将学习期货以及使用期货(使用asyncawait ),以便您能够编写必要的代码,以使fetchUserOrder()将所需的值("大拿铁")打印到安慰.

What is a future?

未来(小写的" f")是未来 (大写的" F")类的实例. 未来表示异步操作的结果,并且可以具有两种状态:未完成或已完成.

Uncompleted

当您调用异步函数时,它将返回未完成的将来. 那个未来正在等待函数的异步操作完成或引发错误.

Completed

如果异步操作成功,则将来将以一个值完成. 否则,它会以错误完成.

Completing with a value

类型Future<T>的类型值为T 例如,类型为Future<String>会产生一个字符串值. 如果期货未产生可用值,则该期货的类型为Future<void> .

Completing with an error

如果该函数执行的异步操作由于任何原因而失败,则将来会出现错误.

Example: Introducing futures

在以下示例中, fetchUserOrder()返回在打印到控制台后完成的fetchUserOrder() . 由于fetchUserOrder()不返回可用值,因此其类型为Future<void> . 在运行示例之前,请尝试预测将首先打印的内容:"大拿铁"或"获取用户订单…".

Future<void> fetchUserOrder() {
  // Imagine that this function is fetching user info from another service or database.
  return Future.delayed(Duration(seconds: 2), () => print('Large Latte'));
}

void main() {
  fetchUserOrder();
  print('Fetching user order...');
}

在前面的示例中,即使fetchUserOrder()在第8行的print()调用之前执行,控制台fetchUserOrder()fetchUserOrder()的输出("大拿铁")之前显示第8行的输出("正在获取用户订单…"). . 这是因为fetchUserOrder()在打印"大拿铁"之前会延迟.

Example: Completing with an error

运行以下示例以查看将来如何完成并出现错误. 稍后,您将学习如何处理该错误.

Future<void> fetchUserOrder() {
// Imagine that this function is fetching user info but encounters a bug
  return Future.delayed(Duration(seconds: 2),
      () => throw Exception('Logout failed: user ID is invalid'));
}

void main() {
  fetchUserOrder();
  print('Fetching user order...');
}

在此示例中, fetchUserOrder()完成并显示一条错误,指示用户ID无效.

您已经了解了期货及其完成方式,但是如何使用异步函数的结果呢? 在下一节中,您将学习如何使用asyncawait关键字获得结果.

Working with futures: async and await

asyncawait关键字提供了一种声明性的方式来定义异步函数并使用其结果. 使用asyncawait时,请记住以下两个基本准则:

  • 要定义异步函数,请在函数主体之前添加async
  • await关键字仅在async函数中起作用.

这是一个将main()从同步功能转换为异步功能的示例.

首先,在函数体之前添加async关键字:

void main() async { ··· }

如果函数具有声明的返回类型,则将类型更新为Future<T> ,其中T是函数返回的值的类型. 如果函数没有显式返回值,则返回类型为Future<void>

Future<void> main() async { ··· }

现在您有了async功能,可以使用await关键字等待将来完成:

print(await createOrderMessage());

如以下两个示例所示, asyncawait关键字生成的异步代码看起来与同步代码非常相似. 异步示例中突出显示了唯一的区别,如果您的窗口足够宽,则在同步示例的右侧.

Example: synchronous functions

String createOrderMessage() {
  var order = fetchUserOrder();
  return 'Your order is: $order';
}

Future<String> fetchUserOrder() =>
    // Imagine that this function is
    // more complex and slow.
    Future.delayed(
      Duration(seconds: 2),
      () => 'Large Latte',
    );

void main() {
  print('Fetching user order...');
  print(createOrderMessage());
}
Fetching user order...
Your order is: Instance of _Future<String>

Example: asynchronous functions

Future<String> createOrderMessage() async {
  var order = await fetchUserOrder();
  return 'Your order is: $order';
}

Future<String> fetchUserOrder() =>
    // Imagine that this function is
    // more complex and slow.
    Future.delayed(
      Duration(seconds: 2),
      () => 'Large Latte',
    );

Future<void> main() async {
  print('Fetching user order...');
  print(await createOrderMessage());
}
Fetching user order...
Your order is: Large Latte

异步示例在三种方面有所不同:

  • createOrderMessage()的返回类型从String更改为Future<String> .
  • async关键字出现在createOrderMessage()main()的函数体之前.
  • 在调用异步函数fetchUserOrder()createOrderMessage()之前,将出现await关键字.

Execution flow with async and await

async函数将同步运行,直到第一个await关键字. 这意味着在async函数体内,第一个await关键字之前的所有同步代码将立即执行.

Example: Execution within async functions

运行以下示例,以查看执行如何在async函数体内进行. 您认为输出将是什么?

Future<void> printOrderMessage() async {
  print('Awaiting user order...');
  var order = await fetchUserOrder();
  print('Your order is: $order');
}

Future<String> fetchUserOrder() {
  // Imagine that this function is more complex and slow.
  return Future.delayed(Duration(seconds: 4), () => 'Large Latte');
}

Future<void> main() async {
  countSeconds(4);
  await printOrderMessage();
}

// You can ignore this function - it's here to visualize delay time in this example.
void countSeconds(int s) {
  for (var i = 1; i <= s; i++) {
    Future.delayed(Duration(seconds: i), () => print(i));
  }
}

在前面的示例中运行代码后,请尝试反转第2行和第3行:

var order = await fetchUserOrder();
print('Awaiting user order...');

请注意,输出的时间发生了变化,现在print('Awaiting user order')出现在printOrderMessage()的第一个await关键字之后.

Exercise: Practice using async and await

以下练习是一个失败的单元测试,其中包含部分完成的代码段. 您的任务是通过编写代码使测试通过来完成练习. 您不需要实现main() .

要模拟异步操作,请调用为您提供的以下函数:

Function 类型签名 Description
fetchRole() Future<String> fetchRole() 获取用户角色的简短描述.
fetchLoginAmount() Future<int> fetchLoginAmount() 获取用户登录的次数.

Part 1: reportUserRole()

Add code to the reportUserRole() function so that it does the following:

  • 返回以以下字符串结尾的Future: "User role: <user role>"
    • 注意:您必须使用fetchRole()返回的实际值; 复制并粘贴示例返回值不会使测试通过.
    • 示例返回值: "User role: tester"
  • 通过调用提供的函数fetchRole()获得用户角色.

Part 2: reportLogins()

实现一个async函数reportLogins()以便执行以下操作:

  • 返回字符串"Total number of logins: <# of logins>" .
    • 注意:您必须使用fetchLoginAmount()返回的实际值; 复制并粘贴示例返回值不会使测试通过.
    • 示例来自reportLogins()返回值: "Total number of logins: 57"
  • 通过调用提供的函数fetchLoginAmount()获得登录次数.
{$ begin main.dart $}
// Part 1
// You can call the provided async function fetchRole()
// to return the user role.
Future<String> reportUserRole() async {
  // TO DO: Your implementation goes here.
}

// Part 2
// Implement reportLogins here
// You can call the provided async function fetchLoginAmount()
// to return the number of times that the user has logged in.
reportLogins(){}
{$ end main.dart $}
{$ begin solution.dart $}
Future<String> reportUserRole() async {
  var username = await fetchRole();
  return 'User role: $username';
}

Future<String> reportLogins() async {
  var logins = await fetchLoginAmount();
  return 'Total number of logins: $logins';
}
{$ end solution.dart $}
{$ begin test.dart $}
const role = 'administrator';
const logins = 42;
const passed = 'PASSED';
const testFailedMessage = 'Test failed for the function:';
const typoMessage = 'Test failed! Check for typos in your return value';
const didNotImplement = 'Test failed! Did you forget to implement or return from ';
const oneSecond = Duration(seconds: 1);
List<String> messages = [];
Future<String> fetchRole() => Future.delayed(oneSecond, () => role);
Future<int> fetchLoginAmount() => Future.delayed(oneSecond, () => logins);

main() async {
  try {
    messages
      ..add(makeReadable(

        testLabel: 'Part 1',
        testResult: await asyncEquals(
          expected: 'User role: administrator',
          actual: await reportUserRole(),
          typoKeyword: role
        ),
        readableErrors: {
          typoMessage: typoMessage,
          'null': '$didNotImplement reportUserRole?',
          'User role: Instance of \'Future<String>\'': '$testFailedMessage reportUserRole. Did you use the await keyword?',
          'User role: Instance of \'_Future<String>\'': '$testFailedMessage reportUserRole. Did you use the await keyword?',
          'User role:' : '$testFailedMessage reportUserRole. Did you return a user role?',
          'User role: ' : '$testFailedMessage reportUserRole. Did you return a user role?',
          'User role: tester' : '$testFailedMessage reportUserRole. Did you invoke fetchRole to fetch the user\'s role?',
        }))

      ..add(makeReadable(

        testLabel: 'Part 2',
        testResult: await asyncEquals(
          expected: 'Total number of logins: 42',
          actual: await reportLogins(),
          typoKeyword: logins.toString()
        ),
        readableErrors: {
          typoMessage: typoMessage,
          'null': '$didNotImplement reportLogins?',
          'Total number of logins: Instance of \'Future<int>\'': '$testFailedMessage reportLogins. Did you use the await keyword?',
          'Total number of logins: Instance of \'_Future<int>\'': '$testFailedMessage reportLogins. Did you use the await keyword?',
          'Total number of logins: ': '$testFailedMessage reportLogins. Did you return the number of logins?',
          'Total number of logins:': '$testFailedMessage reportLogins. Did you return the number of logins?',
          'Total number of logins: 57': '$testFailedMessage reportLogins. Did you invoke fetchLoginAmount to fetch the number of user logins?',
        }))
      ..removeWhere((m) => m.contains(passed))
      ..toList();

    if (messages.isEmpty) {
      _result(true);
    } else {
      _result(false, messages);
    }
  } catch (e) {
    _result(false, ['Tried to run solution, but received an exception: $e']);
  }
}

////////////////////////////////////////
///////////// Test Helpers /////////////
////////////////////////////////////////
String makeReadable({ String testResult, Map readableErrors, String testLabel }) {
  if (readableErrors.containsKey(testResult)) {
    var readable = readableErrors[testResult];
    return '$testLabel $readable';
  } else {
    return '$testLabel $testResult';
  }
}

///////////////////////////////////////
//////////// Assertions ///////////////
///////////////////////////////////////
Future<String> asyncEquals({expected, actual, String typoKeyword}) async {
  var strActual = actual is String ? actual : actual.toString();
  try {
    if (expected == actual) {
      return passed;
    } else if (strActual.contains(typoKeyword)) {
      return typoMessage;
    } else {
      return strActual;
    }
  } catch(e) {
    return e;
  }
}
{$ end test.dart $}
{$ begin hint.txt $}
Did you remember to add the async keyword to the reportUserRole() function?

Did you remember to use the await keyword before invoking fetchRole()?

Remember: reportUserRole() needs to return a future!
{$ end hint.txt $}

Handling errors

要处理async函数中的错误,请使用try-catch:

try {
  var order = await fetchUserOrder();
  print('Awaiting user order...');
} catch (err) {
  print('Caught error: $err');
}

async函数中,您可以像在同步代码中一样编写try-catch子句 .

Example: async and await with try-catch

运行以下示例以查看如何处理异步函数中的错误. 您认为输出将是什么?

Future<void> printOrderMessage() async {
  try {
    var order = await fetchUserOrder();
    print('Awaiting user order...');
    print(order);
  } catch (err) {
    print('Caught error: $err');
  }
}

Future<String> fetchUserOrder() {
  // Imagine that this function is more complex.
  var str = Future.delayed(
      Duration(seconds: 4),
      () => throw 'Cannot locate user order');
  return str;
}

Future<void> main() async {
  await printOrderMessage();
}

Exercise: Practice handling errors

以下练习提供了使用上一节中介绍的方法来处理异步代码错误的实践. 为了模拟异步操作,您的代码将调用为您提供的以下函数:

Function 类型签名 Description
fetchNewUsername() Future<String> fetchNewUsername() 返回可用于替换旧用户名的新用户名.

使用asyncawait以实现异步changeUsername()函数,该函数执行以下操作:

  • 调用提供的异步函数fetchNewUsername()并返回其结果.
    • changeUsername()返回值的示例: "jane_smith_92"
  • 捕获发生的任何错误并返回错误的字符串值.
{$ begin main.dart $}
// Implement changeUsername here
{$ end main.dart $}
{$ begin solution.dart $}
Future<String> changeUsername () async {
  try {
    return await fetchNewUsername();
  } catch (err) {
    return err.toString();
  }
}
{$ end solution.dart $}
{$ begin test.dart $}
List<String> messages = [];
bool logoutSucceeds = false;
const passed = 'PASSED';
const noCatch = 'NO_CATCH';
const typoMessage = 'Test failed! Check for typos in your return value';
const oneSecond = Duration(seconds: 1);

class UserError implements Exception {
  String errMsg() => 'New username is invalid';
}

Future fetchNewUsername() {
  var str = Future.delayed(oneSecond, () => throw UserError());
  return str;
}

main() async {
  try {
    // ignore: cascade_invocations
    messages
      ..add(makeReadable(
          testLabel: '',
          testResult: await asyncDidCatchException(changeUsername),
          readableErrors: {
            typoMessage: typoMessage,
            noCatch: 'Did you remember to call fetchNewUsername within a try/catch block?',
          }
      ))
      ..add(makeReadable(
          testLabel: '',
          testResult: await asyncErrorEquals(changeUsername),
          readableErrors: {
            typoMessage: typoMessage,
            noCatch: 'Did you remember to call fetchNewUsername within a try/catch block?',
          }
      ))
      ..removeWhere((m) => m.contains(passed))
      ..toList();

    if (messages.isEmpty) {
      _result(true);
    } else {
      _result(false, messages);
    }
  } catch (e) {
    _result(false, ['Tried to run solution, but received an exception: $e']);
  }
}

////////////////////////////////////////
///////////// Test Helpers /////////////
////////////////////////////////////////
String makeReadable({ String testResult, Map readableErrors, String testLabel }) {
  if (readableErrors.containsKey(testResult)) {
    var readable = readableErrors[testResult];
    return '$testLabel $readable';
  } else {
    return '$testLabel $testResult';
  }
}

void passIfNoMessages(List<String> messages, Map<String, String> readable){
  if (messages.isEmpty) {
    _result(true);
  } else {

    // ignore: omit_local_variable_types
    List<String> userMessages = messages
        .where((message) => readable.containsKey(message))
        .map((message) => readable[message])
        .toList();
    print(messages);

    _result(false, userMessages);
  }
}
///////////////////////////////////////
//////////// Assertions ///////////////
///////////////////////////////////////
Future<String> asyncErrorEquals(Function fn) async {
  var result = await fn();
  if (result == UserError().toString()) {
    return passed;
  } else {
    return 'Test failed! Did you stringify and return the caught error?';
  }
}

Future<String> asyncDidCatchException(Function fn) async {
  var caught = true;
  try {
    await fn();
  } on UserError catch(_) {
    caught = false;
  }

  if (caught == false) {
    return noCatch;
  } else {
    return passed;
  }
}
{$ end test.dart $}
{$ begin hint.txt $}
Implement changeUsername() to return the string from fetchNewUsername() or
(if that fails) the string value of any error that occurs.
You'll need a try-catch statement to catch and handle errors.
{$ end hint.txt $}

Someone's looking at you!
您发现了一些特别的东西!

Exercise: Putting it all together

现在是时候练习最后一次练习中学到的知识了. 为了模拟异步操作,此练习提供了异步函数fetchUsername()logoutUser()

Function Type signature Description
fetchUsername() Future<String> fetchUsername() 返回与当前用户关联的名称.
logoutUser() Future<String> logoutUser() 执行当前用户的注销并返回已注销的用户名.

编写以下内容:

Part 1: addHello()

  • 编写一个带有单个String参数的函数addHello() .
  • addHello()返回其字符串参数, addHello() " Hello".
    示例: addHello('Jon')返回'Hello Jon' .

Part 2: greetUser()

  • 编写一个不带参数的greetUser()函数.
  • 要获取用户名, greetUser()调用提供的异步函数fetchUsername() .
  • greetUser()通过调用addHello() ,为用户传递用户名并返回结果来为用户创建问候语.
    示例:如果fetchUsername()返回'Jenny' ,则greetUser()返回'Hello Jenny' .

Part 3: sayGoodbye()

  • 编写一个函数sayGoodbye() ,该函数执行以下操作:
    • 不带参数.
    • 捕获任何错误.
    • 调用提供的异步函数logoutUser() .
  • 如果logoutUser()失败, sayGoodbye()将返回您喜欢的任何字符串.
  • 如果logoutUser()成功,则sayGoodbye()返回字符串'<result> Thanks, see you next time' ,其中<result>是调用logoutUser()返回的字符串值.
{$ begin main.dart $}
// Part 1
addHello(){}

// Part 2
// You can call the provided async function fetchUsername()
// to return the username.
greetUser(){}

// Part 3
// You can call the provided async function logoutUser()
// to log out the user.
sayGoodbye(){}
{$ end main.dart $}
{$ begin solution.dart $}
String addHello(user) => 'Hello $user';

Future<String> greetUser() async {
  var username = await fetchUsername();
  return addHello(username);
}

Future<String> sayGoodbye() async {
  try {
    var result = await logoutUser();
    return '$result Thanks, see you next time';
  } catch (e) {
    return 'Failed to logout user: $e';
  }
}
{$ end solution.dart $}
{$ begin test.dart $}
List<String> messages = [];
bool logoutSucceeds = false;
const passed = 'PASSED';
const noCatch = 'NO_CATCH';
const typoMessage = 'Test failed! Check for typos in your return value';
const didNotImplement = 'Test failed! Did you forget to implement or return from ';
const oneSecond = Duration(seconds: 1);

Future<String> fetchUsername() => Future.delayed(oneSecond, () => 'Jean');
String failOnce () {
  if (logoutSucceeds) {
    return 'Success!';
  } else {
    logoutSucceeds = true;
    throw Exception('Logout failed');
  }
}

logoutUser() => Future.delayed(oneSecond, failOnce);

main() async {
  try {
    // ignore: cascade_invocations
    messages
      ..add(makeReadable(

        testLabel: 'Part 1',
        testResult: await asyncEquals(
          expected: 'Hello Jerry',
          actual: addHello('Jerry'),
          typoKeyword: 'Jerry'
        ),
        readableErrors: {
          typoMessage: typoMessage,
          'null': '$didNotImplement addHello?',
          'Hello Instance of \'Future<String>\'': 'Looks like you forgot to use the \'await\' keyword!',
          'Hello Instance of \'_Future<String>\'': 'Looks like you forgot to use the \'await\' keyword!',
        }))
      ..add(makeReadable(

        testLabel: 'Part 2',
        testResult: await asyncEquals(
          expected: 'Hello Jean',
          actual: await greetUser(),
          typoKeyword: 'Jean'
        ),
        readableErrors: {
          typoMessage: typoMessage,
          'null': '$didNotImplement greetUser?',
          'HelloJean' : 'Looks like you forgot the space between \'Hello\' and \'Jean\'',
          'Hello Instance of \'Future<String>\'': 'Looks like you forgot to use the \'await\' keyword!',
          'Hello Instance of \'_Future<String>\'': 'Looks like you forgot to use the \'await\' keyword!',
          '{Closure: (String) => dynamic from Function \'addHello\': static.(await fetchUsername())}': 'Did you place the \'\$\' character correctly?',
          '{Closure \'addHello\'(await fetchUsername())}': 'Did you place the \'\$\' character correctly?', 
        }))
      ..add(makeReadable(
        testLabel: 'Part 3',
        testResult: await asyncDidCatchException(sayGoodbye),
        readableErrors: {
          typoMessage: '$typoMessage. Did you add the text \'Thanks, see you next time\'?',
          'null': '$didNotImplement sayGoodbye?',
          noCatch: 'Did you remember to call logoutUser within a try/catch block?',
          'Instance of \'Future<String>\' Thanks, see you next time':'Did you remember to use the \'await\' keyword in the sayGoodbye function?',
          'Instance of \'_Future<String>\' Thanks, see you next time':'Did you remember to use the \'await\' keyword in the sayGoodbye function?',
        }
      ))
      ..add(makeReadable(
        testLabel: 'Part 3',
        testResult: await asyncEquals(
          expected: 'Success! Thanks, see you next time',
          actual: await sayGoodbye(),
          typoKeyword: 'Success'
        ),
        readableErrors: {
          typoMessage: '$typoMessage. Did you add the text \'Thanks, see you next time\'?',
          'null': '$didNotImplement sayGoodbye?',
          noCatch: 'Did you remember to call logoutUser within a try/catch block?',
          'Instance of \'Future<String>\' Thanks, see you next time':'Did you remember to use the \'await\' keyword in the sayGoodbye function?',
          'Instance of \'_Future<String>\' Thanks, see you next time':'Did you remember to use the \'await\' keyword in the sayGoodbye function?',
          'Instance of \'_Exception\'': 'CAUGHT Did you remember to return a string?',
          }
      ))
    ..removeWhere((m) => m.contains(passed))
    ..toList();

    if (messages.isEmpty) {
      _result(true);
    } else {
      _result(false, messages);
    }
  } catch (e) {
    _result(false, ['Tried to run solution, but received an exception: $e']);
  }
}

////////////////////////////////////////
///////////// Test Helpers /////////////
////////////////////////////////////////
String makeReadable({ String testResult, Map readableErrors, String testLabel }) {
  String readable;
  if (readableErrors.containsKey(testResult)) {
    readable = readableErrors[testResult];
    return '$testLabel $readable';
  } else if ((testResult != passed) && (testResult.length < 18)) {
    readable = typoMessage;
    return '$testLabel $readable';
  } else {
    return '$testLabel $testResult';
  }
}

void passIfNoMessages(List<String> messages, Map<String, String> readable){
  if (messages.isEmpty) {
    _result(true);
  } else {

    // ignore: omit_local_variable_types
    List<String> userMessages = messages
        .where((message) => readable.containsKey(message))
        .map((message) => readable[message])
        .toList();
    print(messages);

    _result(false, userMessages);
  }
}
///////////////////////////////////////
//////////// Assertions ///////////////
///////////////////////////////////////
Future<String> asyncEquals({expected, actual, String typoKeyword}) async {
  var strActual = actual is String ? actual : actual.toString();
  try {
    if (expected == actual) {
      return passed;
    } else if (strActual.contains(typoKeyword)) {
      return typoMessage;
    } else {
      return strActual;
    }
  } catch(e) {
    return e;
  }
}

Future<String> asyncDidCatchException(Function fn) async {
  var caught = true;
  try {
    await fn();
  } on Exception catch(_) {
    caught = false;
  }

  if (caught == true) {
    return passed;
  } else {
    return noCatch;
  }
}
{$ end test.dart $}
{$ begin hint.txt $}
The greetUser() and sayGoodbye() functions are asynchronous;
addHello() isn't.
{$ end hint.txt $}

What’s next?

Congratulations, you’ve finished the codelab! If you’d like to learn more, here are some suggestions for where to go next:

如果您有兴趣像此代码实验室一样使用嵌入式DartPad,请在教程中查看使用DartPad的最佳实践 .

by  ICOPY.SITE