[Swift] เรามา Request API แบบ Async Await ด้วย Alamofire กันเถอะ

Wisanu P.
Q-CHANG

--

[Swift] Using Async await with Alamofire

หลาย ๆ คนที่พัฒนา iOS Application คงคุ้นชินกับ HTTP networking library ที่มีชื่อว่า “Alamofire” กันอยู่แล้ว เพราะเจ้าตัวนี้เค้าช่วยให้เราสามารถ request api ได้อย่างง่ายดาย ตัวอย่างเช่น

(1) ต้องการเรียกดูข้อมูลเพื่อแสดงสายพันธ์ของสุนัขและราคา

เพื่อที่จะรับข้อมูลจาก api มาแสดงเราจะต้องเขียน code ประมาณนี้

struct Animal: Decodable {
var id: String?
var breed: String?
var price: String?

}

class Api {
class func getDogs() {

let urlRequest = "https://63e0a44e59bb472a7425bb20.mockapi.io/my-async-await-with-alamofire/dogs"

AF.request(urlRequest).responseDecodable(of: [Animal].self) { response in
switch response.result {
case .success(let animals):
animals.forEach{ animal in
debugPrint("id \(animal.id!) \(animal.breed!) with price \(animal.price!)")
}
case .failure(let error):
debugPrint(error.localizedDescription)
}
}
}
}

และนี่ผลลัพธ์ที่ออกมาจาก code ด้านบน

“ก็ไม่เห็นยากอะไรเลยนี่นา Very easy”

ถูกครับมันง่ายมาก ๆ เลยถ้าเราต้อง request api เพียงเส้นเดียว

แต่…เดี๋ยวก่อนอย่าเพิ่งไป!!

ถ้าเกิดว่าเราได้รับ requirement เพิ่มหละ เช่น

(2) ต้องการแสดงข้อมูลของทั้งสุนัขและแมวโดยที่ข้อมูลจะมาจาก api คนละเส้น

“ยากและเอาท่าไหนดีเนี่ย?”

“เรียกพร้อมกันดีมั้ย getDogs(), getCats() แล้วถ้ากลับมาครบก็ค่อยแสดง แต่ด้วยท่าที่เราเขียน call bak ผ่าน closure ต้องมาเช็คอีกทีว่าจะกลับมาครบทุกเส้นเมื่อไร แล้วถ้าไม่กลับมาหละจะทำยังไงดี?”

“หรือจะ chain api ดีมั้ย getDogs() กลับมาค่อยไป getCats() แล้วถ้าเกิดอนาคตมี getBirds(), getFishs() อีกหละ มันไม่นานไปหรอ?”

อย่าไปคิดให้ปวดหัวเลยเรามาลอง Request API แบบ Async Await ด้วย Alamofire กันเถอะ

ไม่พูดพร่ำทำเพลงเรามาดู code กันก่อนเลยแล้วกัน

    class func getAnimals() {
Task {
do {
async let getDogs = getDogs()
async let getCats = getCats()
let (dogs, cats) = try await(getDogs, getCats)
let animals = dogs + cats
animals.forEach{ animal in
debugPrint("id \(animal.id!) \(animal.breed!) with price \(animal.price!)")
}
}catch {
debugPrint(error.localizedDescription)
}
}
}

class func getDogs() async throws -> [Animal] {
let urlRequest = "https://63e0a44e59bb472a7425bb20.mockapi.io/my-async-await-with-alamofire/dogs"
return try await AF.request(urlRequest).serializingDecodable([Animal].self).value
}

class func getCats() async throws -> [Animal] {
let urlRequest = "https://63e0a44e59bb472a7425bb20.mockapi.io/my-async-await-with-alamofire/cats"
return try await AF.request(urlRequest).serializingDecodable([Animal].self).value
}

และนี่คือผลลัพธ์ที่เกิดขึ้น

“แล้วการที่เราเขียน async await แบบนี้มันมีข้อดียังไงนะ?”

รู้สึกได้เลยว่า code ของเราดูสวยขึ้น ตรงไปตรงมา ถ้าเทียบกับการเขียน call back ผ่าน closure จะจัดการได้ลำบากมากแถม code ของเราก็จะ complex มาก ๆ และจาก code ตัวอย่างด้านบนก็ยังเป็นการ call api แบบ paralle อีกด้วยนะ

“แล้วเราจะรู้ได้อย่างไงว่าเค้า call api ให้เราแบบ paralle จริง ๆ ?”

ก็ลองเพิ่ม log เวลาดูกันเลย

    class func getDogs() async throws -> [Animal] {
debugPrint("request dogs: \(Date())")
let urlRequest = "https://63e0a44e59bb472a7425bb20.mockapi.io/my-async-await-with-alamofire/dogs"
let dogs = try await AF.request(urlRequest).serializingDecodable([Animal].self).value
debugPrint("response dogs: \(Date())")
return dogs
}

class func getCats() async throws -> [Animal] {
debugPrint("request cats: \(Date())")
let urlRequest = "https://63e0a44e59bb472a7425bb20.mockapi.io/my-async-await-with-alamofire/cats"
let cats = try await AF.request(urlRequest).serializingDecodable([Animal].self).value
debugPrint("response cats: \(Date())")
return cats
}

และนี่คือผลลัพธ์ที่เกิดขึ้น

จากผลลัพธ์จะเห็นได้ว่ามีการ “request dogs” และ “request cats” ในเวลาที่ไล่เลี่ยกันโดยที่ไม่รอให้เกิด “response dogs …” ก่อน จึงสรุปได้ว่า getDogs() และ getCats() นั้นทำงานแบบ parallel แต่เราจะใช้ข้อมูลได้ก็ต่อเมื่อ api ทั้ง 2 เส้นตอบกลับมาครบสมบูรณ์

“อยากใช้ Async Await เพราะ code มันดูสวยดีนะ แต่ไม่อยากให้ call api แบบ paralle เลย เพราะอยากจะได้ข้อมูลบางอย่างกลับมาใช้ก่อนได้มั้ยนะ?”

คำตอบคือได้! เรามาลองแก้ code กันดูเลย

    class func getAnimals() {
Task {
do {

let dogs = try await getDogs()
dogs.forEach{ animal in
debugPrint("id \(animal.id!) \(animal.breed!) with price \(animal.price!)")
}
let cats = try await getCats()
cats.forEach{ animal in
debugPrint("id \(animal.id!) \(animal.breed!) with price \(animal.price!)")
}
}catch {
debugPrint(error.localizedDescription)
}
}
}

เพียงแค่เราแยก try await ของ getDogs() และ getCats() ออกจากกันก็จะไม่เกิดการทำงานแบบ parallel แล้วนะ

และนี่คือผลลัพธ์ที่เกิดขึ้น

“แต่ Async Await ก็ใช่ว่าจะมีแต่ข้อดีนะ เค้าก็มีข้อที่ควรระวังเช่นกัน”

เช่น การจัดการเกี่ยวกับ exception หรือ failure ต่าง ๆ ที่เกิดขึ้นจะอยู่ภายใต้ catch block เพียงอย่างเดียวเราจำเป็นจะต้องกำหนดอะไรบางอย่างเพื่อบอกว่า catch ที่เกิดขึ้นมาจาก function หรือ api ตัวใดหรืออาจจะต้องทำ Api Manager เพื่อมาจัดการซึ่งถ้ามีโอกาสเราคงได้มาคุยกันอีกในเรื่องนี้

หวังเป็นอย่างยิ่งว่าบทความนี้จะเป็นประโยชน์กับใครหลาย ๆ คนนะครับ

แล้วพบกันใหม่ครับ

Ref:

https://github.com/Alamofire/Alamofire
https://www.kodeco.com/books/modern-concurrency-in-swift/v1.0/chapters/2-getting-started-with-async-await

https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md
https://www.youtube.com/watch?v=ZSHdZSwZy7w

--

--

Wisanu P.
Q-CHANG
Editor for

Senior Mobile Developer At Q-CHANG, there is music as the sound of the heart beats and loves to travel.