Contents
Chào mừng bạn đến với Fx Studio. Chúng ta đã tìm hiểu về async/await trong bài viết trước rồi. Lần này, chúng ta sẽ thử sử dụng nó vào việc lấy dữ liệu từ một Rest API. Bên cạnh đó, ta cũng sẽ phân tích xem cách dùng mới và cũ có gì khác nhau. Hy vọng bạn sẽ bắt đầu hứng thú với async/await mới này.
Nếu bạn chưa biết gì về async/await, bạn hãy truy cập link dưới đây để bổ tục kiến thức nha.
Còn nếu mọi thứ đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Chúng ta cần những gì mới nhất, vì đây là Swift 5.5 & nó rất mới. Do đó, bạn cần chuẩn bị như sau:
-
- Xcode 13 (beta)
- Swift 5.5
Về mặt kiến thức, bạn cần phải hiểu được phần cơ bản & với một số điểm lý thuyết như sau:
Cũng kha khá kiến thức cần có, để hiểu được được phần mới này nha!
Về mặt demo, bạn chỉ cần chuẩn bị một project đơn giản. Mình sử dụng một ViewController với một UITableView.
Về Rest API, mình sử dụng The Cocktail Database. Bạn có thể xem và tham khảo một API cho riêng mình.
1. Completion Handler
Để tương tác với API, bạn chắc chắn sẽ liên tưởng tới hình ảnh về Completion Handler. Đó chính là call back
mà bạn sẽ trả result
về ViewModel hay ViewController.
Hiện tại, đây là cách dùng tốt nhất và phổ biến nhất. Nên bạn hoàn toàn yên tâm.
1.1. Define
Bạn tham khảo một function cơ bản để fetch data từ API như sau:
func fetchAPI<T: Decodable>(url: URL, completion: @escaping (Result<T, Error>) -> ()) { URLSession.shared.dataTask(with: url) { data, response, error in if let error = error { completion(.failure(error)) return } guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data else { completion(.failure(APIError.error("Bad HTTP Response"))) return } do { let decodedData = try JSONDecoder().decode(T.self, from: data) completion(.success(decodedData)) } catch { completion(.failure(error)) } } .resume() }
Nhìn qua như vậy, chứ trong function trên chứa cả một bầu trời kiến thức. Bạn sẽ ôn lại như sau:
- Sử dụng URLSession để dùng là hạt nhân tương tác.
- Cách dùng với
dataTask
dùng để kết nối với API (trước đây bạn sẽ dùng, delegate nhưng nó đã xưa lắm rồi) - Triệu hồi
resume()
để thực thi toàn bộ quá trình kết nối với API.
Quá trình tương tác đó có một số điểm chú ý như sau:
- Bạn cần có một URL hay một URLRequest để tạo và gởi request của bạn đi
- Vì chúng nó hoạt động bất đồng bộ, nên bạn phải capture lại closure cho việc handler response nhận được. Bằng từ khoá
@escaping
. - Trong lòng closure đó bạn sẽ thấy:
- Xử lý
error
nếu có - Phân tích response & status code
- Parse dữ liệu (cũng may mắn vì chúng ta có các đối tượng Decoder làm hết việc rồi)
- Xử lý
Bạn có thấy kí ức xưa ùa về không nào. Ahihi!
1.2. Result Type
Phần không thể thiếu cuối cùng đó là Result Type
. Đây là thứ mà đại đa số dev không hiểu chi. Chỉ biết cắm đầu thả vào đó một kiểu dữ liệu & kiểu Error. (đây cũng là một phần đau khổ trong bể khổ của dev iOS đó)
Tuy nhiên, đây là Fx Studio và mình lại phải chịu trách nhiệm thông não bạn tiếp. Bạn hãy xem 2 thứ
-
- Cấu trúc JSON của API trả về
- Cấu trúc của class/struct mà bạn Decoder ra từ data của response
Tham khảo, cấu trúc cho Result với API trên nha.
struct Category: Codable, Hashable { var strCategory: String } struct CategoryResult: Codable { var drinks: [Category] }
Nhiệm vụ của bạn là cần phải xếp hình cho okay, với:
- Cấu trúc JSON
- Key
- Tương thích với Decoder
- Nếu cầu trúc phức tạp, hãy chia ra như mình ở trên.
Phần cuối cùng trong Result Type là Error. Bạn cần phải tạo ra một Error riêng. Đừng có lười mà casting về NSError (nhục lắm). Bạn tham khảo các khai báo một Error nhoé (chuẩn nhất luôn rồi).
enum APIError: Error { case error(String) var localizedDescription: String { switch self { case .error(let string): return string } } }
Muốn thêm case
gì nữa thì thêm nha. Ahihi!
1.3. Load API
Khi bạn đã có được một function thực hiện kết nối & xử lý dữ liệu từ API rồi. Bây giờ, bạn sẽ tìm cách triệu hồi chúng từ ViewController hay ViewModel hay bất cứ đâu mà bạn có thể làm.
Mình sẽ chọn ViewController cho nó đơn giản và chúng ta sẽ tương tác thêm với Main Thread (cái thứ đang quản lý UI).
Tránh cho việc bạn bất ngờ, mình sẽ lấy code ví dụ cho một ViewController cực kì cơ bản như sau nha.
import UIKit class ViewController: UIViewController { // MARK: - Properties @IBOutlet weak var tableView: UITableView! @IBOutlet weak var indicatorView: UIActivityIndicatorView! let urlString = "https://www.thecocktaildb.com/api/json/v1/1/list.php?c=list" var drinks: [Category] = [Category(strCategory: "Hello, I'm Fx Studio")] // MARK: - Life cycle override func viewDidLoad() { super.viewDidLoad() configureLayout() } // MARK: - Private functions private func configureLayout() { tableView.delegate = self tableView.dataSource = self tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") } // MARK: - Fetch data // MARK: Completion handle // MARK: Async with Continuation // MARK: Async Await } extension ViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { drinks.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.textLabel?.text = drinks[indexPath.row].strCategory return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) print("Ahihi!") } }
Công việc của bạn bây giờ là thêm một function để gọi function fetchAPI
ở trên. Tham khảo code như sau:
func loadAPI1() { let url = URL(string: urlString)! fetchAPI(url: url) { (result: Result<CategoryResult, Error>) in DispatchQueue.main.async { switch result { case .success(let result): self.drinks.append(contentsOf: result.drinks) for item in result.drinks { print(item.strCategory) } // Update UIs self.tableView.reloadData() self.indicatorView.isHidden = true case .failure(let error): print(error) } } } }
Build thử và xem kết quả nha.
1.4. Vấn đề
Tất nhiên, phải có vấn đề gì đó. Thì mới tạo cho bạn động lực tiếp tục cải tiến code của mình. Và ta có thể liệt kê ra một số vấn đề cơ bản như sau.
Update UI
Đây được xem là vấn đề kinh điển nhất khi bạn tương tác với API. Vì bạn làm gì đó thì làm, nhưng bạn phải cập nhất lại giao diện. Người dùng mới biết được trạng thái ứng dụng sẽ như thế nào.
Nó sẽ dễ bị crash vì nhiều lúc bạn quên đi là cần phải về Main Thread để cập nhật UI. Đó là lý do mà DispatchQueue
& main
đeo bám bạn lâu nay.
Error
Bạn để ý rằng, không có bất cứ một Error nào được phát ra ở đây. Vì mọi thứ return
là Void. Và đó là lý do mà bạn sẽ phải dùng thêm Result Type
như là một giải pháp thay thế.
Để dùng bóc tách được giá trị của Result Type, bạn lại cần phải switch ... case
chúng nó. Rồi lúc đó, bạn phải nhớ và làm thêm việc update giao diện khi có Error nữa.
Quan trọng là bạn khó phân biệt nguyên nhân nào sinh ra lỗi nào. Tất cả, đều bị ép kiểu về 1 error duy nhất.
Primary of doom
Khi bạn thêm nhiều API và một lần gọi. Hoặc tách ra nhiều thành phần (như Core, API, ViewModel) để gọi lần lượt qua lại. Thì đây là một cú pháp khá tồi và rối mắt. Vì một nguyên nhân duy nhất, đó là call back
quá nhiều mà thôi.
Pyramid of Doom aka callback hell!
2. Async/Await
Bây giờ, chúng ta sẽ sang thời đại mới với async/await và tiến gần hơn với ánh sáng của nhân loại.
Swift mặc dù sinh sau đẻ muộn, nhưng nó tới bây giờ mới hỗ trợ hay đưa ra khái niệm về
async/await
. Còn hầu hết các ngôn ngữ khác để hỗ trợ từ lâu rồi.
2.1. Define
Vào thẳng luôn, việc viết một function để sử dụng async/await
với API, thì ta cần 2 làm việc như sau:
Thứ nhất, cần lấy được data
của response khi bạn tương tác với API. Bạn tham khảo code sau.
extension URLSession { func data(url: URL) async throws -> Data { try await withCheckedThrowingContinuation({ c in dataTask(with: url) { data, _, error in if let error = error { c.resume(throwing: APIError.error(error.localizedDescription)) } else { if let data = data { c.resume(returning: data) } else { c.resume(throwing: APIError.error("Bad response")) } } }.resume() }) } }
Trong đó:
- Khai báo function với
async
để đánh dấu nó là bất đồng bộ - Ta có thêm
throws
, vì công việc hiện tại có khả năng sinh ra lỗi. - Dùng
throws
để đúng với logic. Chúng ta không xử lý lỗi mà là nén chúng cho thèn khác xử lý. - Với
withCheckedThrowingContinuation
, để thực thi tiếp code bất đồng bộ sau khi thực hiện code đồng bộ ở một Thread khác. c.resume
cho từng trường hợp thành công hay có lỗi
Thứ hai, bạn sẽ viết một function để fetchAPI
. Tham khảo code như sau:
func fetchAPI<T: Decodable>(url: URL) async throws -> T { let data = try await URLSession.shared.data(url: url) let decodeData = try JSONDecoder().decode(T.self, from: data) return decodeData }
Ở trên, chúng ta khai báo với async throws
, thì với fetchAPI
ta lại dùng try await
. Mọi thứ lúc này đơn giản lại với 3 dòng code như trên mà thôi. Bạn có thể tuỳ ý truyền kiểu dữ liệu bất kỳ nào, với Generic <T>
. Dùng decoder ra kiểu dữ liệu mà bạn mong muốn. Và nếu có lỗi phát sinh thì tự động Decoder sẽ throws
lỗi đi tiếp.
2.2. Load API
Chúng ta sẽ quay về lại ViewController và thực hiện công việc loadAPI
với function fetch mới (có async
). Bạn sẽ thêm một function mới cho ViewController như sau:
func loadAPI3() { async { do { let url = URL(string: urlString)! let result: CategoryResult = try await fetchAPI(url: url) // Sự lợi hại ở đây, không crash chương trình vì mọi thứ đã chờ đợi rồi. self.drinks.append(contentsOf: result.drinks) for item in result.drinks { print(item.strCategory) } // Update UIs self.tableView.reloadData() self.indicatorView.isHidden = true } catch { print(error.localizedDescription) } } }
Bạn sẽ thấy mọi thứ bây giời chỉ là những dòng code lần lượt mà thôi. Hãy build lại project với việc thực thi function loadAPI
mới này nha. Cảm nhận kết quả.
2.3. Lợi điểm
Chúng ta sẽ bóc tách tiếp cái function loadAPI
mới có gì trong đó.
- Vì
loadAPI
đang chạy ở Main Thread nên sẽ hoạt động đồng bộ với Main Thread. Do đó, bạn muốn thực thi một function bất đồng bộ trong nó, thì phải triệu hồiasync { .... }
- Tại dòng code gọi
fetchAPI
chúng ta cótry await
. Lúc này, mọi dòng code ở dưới đó sẽ chờ dòng code đó thực thi xong. Kết quả của nó, thì chúng ta sẽ lưu vào một biến. Hoặc nếu có lỗi phát sinh, thì ta sẽ bắt chúng ở phầncatch
- Bạn cứ thoải mái
update UIs
mà không sợ bịcrash
. - Bạn không cần dùng tới
DispatchQueue
&.main
. Chắc sau này cũng không cần dùng nó luôn.
Điểm quan trọng mà
async
đem lại cho bạn đó là sự đơn giản và liền mạch logic trong code.
3. Async with Completion Handler
Chắc sẽ có bạn thắc mắc là chúng ta bỏ hết đi đống code cũ đã viết đi hay sao. Đó là cả một bầu trời kiến thức đó. Và nếu như trong dự án đã phát triển từ lâu thì khó mà đạp đổ đi xây lại.
3.1. Define
Chúng ta sẽ có một cách dung hoà giữa 2 cách cũ và mới. Ta sẽ dùng tới Checked Continuation. Bạn tham khảo đoạn code mới cho function loadAPI
con lai giữa 2 cách như sau:
func loadAPI2() async throws -> [Category] { try await withCheckedThrowingContinuation({ c in let url = URL(string: urlString)! fetchAPI(url: url) { (result: Result<CategoryResult, Error>) in switch result { case .success(let result): for item in result.drinks { print(item.strCategory) } c.resume(returning: result.drinks) case .failure(let error): c.resume(throwing: error) } } }) }
Trong đó:
- Chú ý về khai báo
async throws
- Kiểu giá trị trả về đã khác Void. Bạn cần nhận được gì, thì hãy định nghĩa kiểu trả về nha.
try await
vớiwithCheckedThrowingContinuation
cho việc gọi lại functionfetchAPI
(sử dụng Completion Handler).- Sử dụng
c.resume
cho từng vị trí thích hợp - Không cần sử dụng tới
DispatchQueue
&.main
3.2. Load API
Và đây là cách dùng function loadAPI
kết hợp. Mình vẫn sử dụng ở ViewController, nhưng lần này ta sẽ gọi trực tiếp ở functionviewDidLoad
. Bạn tham khảo code sau nha:
override func viewDidLoad() { super.viewDidLoad() configureLayout() //loadAPI1() //loadAPI3() async { do { drinks = try await loadAPI2() indicatorView.isHidden = true tableView.reloadData() } catch { print("lỗi") indicatorView.isHidden = true } } }
Hãy tập trung vào đoạn code async { .... }
, code nó cực kì đơn giản. Hiểu nôm na,
- Ta sẽ chờ lấy được dữ liệu cho array
drinks
, bằng việc gọiloadAPI
- Sau khi, bạn đã có được kết quả thì tiến hành cập nhật lại giao diện.
- Nếu có lỗi phát sinh thì xử lý ở
catch
Mọi thứ càng lúc càng EZ rồi! Bạn hãy build lại project và cảm nhật kết quả tiếp nha.
4. Fetch group APIs
Việc gọi nhiều API một lúc hoặc bạn gọi nhiều API cùng trả về một kiểu Result Type một lúc, là một công việc phức tạp. Vì,
- Ta không biết lúc nào API nào về trước hay về sau, vì chúng nó hoạt động bất đồng bộ với nhau.
- Cập nhật giao diện như thế nào cho ổn với toàn bộ các APIs khi đang thực thi.
- Đồng bộ lại dữ liệu của các APIs về cùng một mối.
Giải pháp trước đây của chúng ta sẽ dùng tới DispatchGroup
. Và giờ đây, ta sẽ lại tiếp cận với ánh sáng của nhân loại bằng async/await
nha.
4.1. Define
Bạn sẽ thêm một function fetchAPIs
, lần này chúng ta sẽ fetch
nhiều API một lúc. Bạn tham khảo function sau nha:
func fetchAPIs<T: Decodable>(urls: [URL]) async throws -> [T] { try await withThrowingTaskGroup(of: T.self, body: { group in for url in urls { group.async { try await fetchAPI(url: url) } } var results = [T]() for try await result in group { results.append(result) } return results }) }
Trong đó:
- Các API này sẽ trả về cùng một kiểu dữ liệu
<T>
- Sử dụng
async group
, nó sẽ nhóm các tác vụasync
lại với nhau. Bằng việctry await
emwithThrowingTaskGroup
- Dữ liệu của mỗi API trả về sẽ được tự đồng thêm vào
group
. Bạn không cần lo lắng vềreturn
gì và cho ai. - Cuối cùng, bạn tách dữ liệu từ
group
và nén về một array đểreturn
trở lại.
4.2. Load APIs
Để sử dụng function mới cho hiệu quả, thì bạn cần thêm lại một property mới cho ViewController như sau:
var drinks2: [[Drink]] = []
Mình sử dụng link API khác, các API mới này trả về một cấu trúc khác. Và sau đây là định nghĩa struct cho cấu trúc bạn sẽ dùng.
struct Drink: Codable { var strDrink: String var strDrinkThumb: String var idDrink: String } struct DrinkResult: Codable { var drinks: [Drink] }
Vẫn như ở trên, ta sẽ thực hiện công việc gọi một lúc 3 API ở trong function viewDidLoad
tại ViewController. Bạn tham khảo code như sau:
async { do { print(" --- API WITH ASYNC GROUP ---") let urls = [URL(string: "https://www.thecocktaildb.com/api/json/v1/1/filter.php?c=Ordinary_Drink")!, URL(string: "https://www.thecocktaildb.com/api/json/v1/1/filter.php?c=Cocktail")!, URL(string: "https://www.thecocktaildb.com/api/json/v1/1/filter.php?c=Cocoa")!] let results: [DrinkResult] = try await fetchAPIs(urls: urls) for result in results { let items = result.drinks drinks2.append(items) } indicatorView.isHidden = true tableView.reloadData() } catch { print((error as! APIError).localizedDescription) } }
Cách dùng này cũng lại khá là đơn giản về mặt logic. Bạn sẽ không cần nhớ DispatchGroup
với mỗi lần enter
& leave
cực khổ nữa. Chỉ đơn giản, chờ await
nó thực thi xong và trả một array results
về cho bạn. Sau đó, bạn tiến hành cập nhật giao diện trở lại.
Bạn tiến hành cập nhật lại các function Delegate & DataSource của TableView. Sau đó, build lại project và cảm nhận kết quả nha.
4.3. Group Tasks
Sẽ có bạn thắc mắc là: chúng ta gọi những API với những kiểu dữ liệu trả về khác nhau thì như thế nào? Lúc đó, bạn hãy xem mỗi function thực hiện 1 tác vụ (load 1 loại API) và function đó trả về kiểu Void.
Thực sự, mình sẽ không khuyến khích bạn làm theo cách này. Tiềm ẩn rủi ro khá là cao.
Để giúp bạn dễ hình dung hơn, ta sẽ có ví dụ nhỏ như sau.
- Giả sử có các function với các kiểu dữ liệu trả về khác nhau
func hamA() async -> String { await withCheckedContinuation({ c in Thread.sleep(forTimeInterval: 2.0) c.resume(returning: "AAAA") }) } func hamB() async -> Float { Float.random(in: 0...100) } func hamC() async -> Int { Int.random(in: 0...100) }
- Bạn sẽ nhóm chúng vào cùng một
group
như sau:- Đừng quan tâm tới kiểu trả về là như thế nào
- Mối lần
group.async { .... }
thì bạn sẽ giải quyết tác vụ của bạn trong đó.
func groupHam() async { await withTaskGroup(of: Void.self, body: { group in group.async { let a = await hamA() print(a) } group.async { let b = await hamB() print(b) } group.async { let c = await hamC() print(c) } }) }
- Thực thi cả
group
cũng khá đơn giản
async { await groupHam() }
Như vậy, bạn đã xong việc nhóm các tác vụ khác nhau (về kiểu dữ liệu trả về lại) với nhau rồi. Và tuỳ thuộc vào bài toán cụ thể mà bạn muốn giải quyết, bạn sẽ chọn cách giải quyết phù hợp với bạn.
Tạm kết
- Các vấn đề đang tồn tại với việc tương tác API bằng Completion Handler
- Áp dụng async/await trong việc tương tác với API
- Các cách sử dụng khác nhau trong việc viết
fetch
function với async/await - Nhóm các tác vụ lại với nhau bằng
group.async
- Tạm thời quyên đi DispatchQueue & DispatchGroup khi sử dụng async/await
Okay! Tới đây, mình xin kết thúc bài viết tại đây. Như đã trình bày ở trên, nếu có gì thay đổi hay cập nhật thêm, mình sẽ tiếp tục update cho bài viết. Với Swift 5.5 còn rất nhiều những thứ hay nữa, nên bạn hãy tiếp tục theo dõi và chờ đón các bài viết sau nha.
Và nếu có gì thắc mắc hay góp ý cho mình thì bạn có thể để lại bình luận hoặc gởi email theo trang Contact.
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
You may also like:
Archives
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)