Async/Await in fetch API requests (Part 1)

iOS Jul 16, 2021

Trong WWDC 2021, Apple đã giới thiệu cho cộng đồng các lập trình viên Swift 5.5, trong đó giới thiệu khái niệm về bất đồng bộ với 2 keyword async/await. Trong bài viết trước, ta có thể dễ dàng nhận ngay thấy async/await giúp giải quyết vấn đề về callback trong swift, tránh việc các callback lồng nhau, làm bố cục của code trở nên phức tạp

Ngoài ra, với sự ra mắt của async/await, Apple cũng đã thiết kế lại bộ API của thư viện URLSession dùng để thực hiện các cuộc gọi API:

Trước Swift 5.5, chúng ta quen với việc gọi API thông qua hàm sau

Từ swift 5.5, Apple đã thiết kế lại Api trên, áp dụng tư tưởng của async/await

Ta có thể dễ dàng nhận thấy, Api mới  đã không còn closure bên trong nữa, đồng thời là hàm bất đồng bộ (được đánh dấu với từ khoá async), có kiểu trả về là 2 giá trị trị Data và Response

Ta xét ví dụ về việc gọi API load ảnh từ url:

(ApiService.swift)

Đoạn code trên hoạt động bình thường, không lỗi. Tuy nhiên chúng ta sẽ chú ý đến 1 vài điều sau:

  1. Đầu tiên, chúng ta hãy phân tích flow của đoạn code trên, Đầu tiên, chúng ta sẽ khởi tạo task bằng dataTask và resume it. Khi task hoàn thành sẽ nhảy vào completion Handler để check response, tạo Image hoặc trả về lỗi (nếu có); kết thúc flow. Chà, luồng hoạt động có vẻ khá khó hiểu với người mới bắt đầu khi code không chạy theo tuần tự từ trên xuống dưới.
(ApiService.swift)

2. Chú ý dòng 9: Mọi chuyện sẽ chẳng có gì nếu UIImage của ta được khởi tạo và không có lỗi, tuy nhiên với lý do nào đó data bị lỗi thì closure sẽ trả về completion(nil. nil), điều này ắt hẳn là ta không mong muốn xảy ra một chút nào

3. Ở ViewController.swift khi gọi hàm này, nếu muốn giao diện cập nhật, ta phải chuyển về main thread thông qua DispatchQueue.main.async, nếu không Xcode cảnh báo:

ViewController.swift

Sau đây chúng ta sẽ thử viết lại hàm getImage() ở trên sử dụng async/await xem sao:

(ApiService.swift)

Phân tích đoạn code trên:

  1. Dòng thứ 2, chúng ta sử dụng Api async mới của Apple mà mình giới thiệu ở trên và trả về data, response
  2. Sử dụng keyword throw khi kết quả của Api trả về lỗi hoặc create Image lỗi, điều nay cho phép hàm này khi được gọi ở nơi khác có thể catch được lỗi. Một lưu ý thêm là  trong thân hàm này có throw error nên ở dòng 1 khi tạo hàm ngoài async ra phải có key word throws đằng sau nữa

Và khi sử dụng hàm này trong ViewController:

ViewController.swift

Nhận xét:

  1. Để gọi được hàm bất đồng bộ trong thân hàm đồng bộ, ta phải wrap hàm đó trong ngữ cảnh async { }.
  2. Dòng 4: Ta sử dụng try await vì hàm bất đồng bộ ở trên có throw error, nên ngoài từ khoá await ta phải thêm try đứng đằng trước để có thể catch được lỗi
  3. Không có sự xuất hiện của DispatchQueue.main.async{ } nữa. Lý do là vì từ swift 5.5, các UI Component của swift được gán thêm @MainActor để đảm bảo rằng UI Control này luôn được sử dụng trên main thread


Ngoài việc tạo các async function, Apple còn cho phép chúng ta tạo một Async properties nữa. Ví dụ dưới đây sẽ một tạo Async properties mới từ String extension:

Extension.swift

Một vài điều cần lưu ý về sử dụng Async properties:

  • Phải khai báo thuộc tính get cho property đó. Đây là bắt buộc để đánh dấu properties đó có kiểu async. Do đó, property getters này cũng có thể throw lỗi, giống như các function async
  • Property này không có setter, chỉ các thuộc tính read only properties là có kiểu async mà thôi
  • Async properties hoạt động giống như async function

Tuy nhiên, trong 1 số trường hợp, chúng ta không muốn viết lại một hàm mới thay thế cho phiên bản chứa closure mà vẫn tận dụng hàm có sẵn này, đặc biệt nếu function chứa closure này được viết ở trong một thư viện, việc viết lại có vẻ không khả thi. Apple cung cấp cho ta một giải pháp để chuyển sang async/await function mà không phải viết lại toàn bộ hàm sẵn có:

Apple cung cấp cho ta 2 method ở bên trên. Hai method này chỉ khác nhau ở chỗ là một hàm thì có throw error và hàm còn lại thì không. Nhìn qua ta có thể nhận thấy ngay 2 method này đều có argument là một closure, lại còn được đánh dấu là các async method. Chà, đúng là hàm này sinh ra để kết nối giữa 2 thế giới rồi 😎

Sử dụng:

ApiService.swift

Giải thích

  1. Tạo hàm mới giống tên với hàm cũ và đánh dấu nó là hàm bất đồng bộ với async.
  2. Dòng 2: Wrap hàm chứa closure sẵn có bên trong hàm async withCheckedThrowingContinuation; vì đây là hàm có throw errors nên phải thêm try await đằng trước
  3. Dòng 3-8: Gọi hàm cũ chứa closure ra. Giá trị result được truyền vào closure sẽ biến thành giá trị trả về của hàm mới này thông qua method .resume(with:), ở đây result có kiểu dữ liệu ResultType nên sẽ tuỳ theo các case success or error của result mà truyền vào argument returning hoặc throwing tương ứng.

Tài liệu tham khảo:

  1. https://developer.apple.com/videos/play/wwdc2021/10095
  2. https://www.andyibanez.com/posts/converting-closure-based-code-into-async-await-in-swift/

Tags

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.