Contents
Chào bạn đến với Fx Studio. Chúng ta sẽ tiếp tục series RxSwift vs. UIKit. Chủ đề bài viết này là Fetching Data from API.
Về bài viết, bạn chỉ cần biết về cách sử dụng cơ bản RxSwift trong UIKit là đã ổn rồi. Nếu bạn chưa biết hoặc quên, thì có thể bắt đầu đọc từ link này:
Còn mọi thứ đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
-
- Xcode 11
- Swift 5.x
Chúng ta vẫn sử dụng project ở bài trước. Ngoài ra, dành cho các bạn chưa biết checkout ở đâu, thì hãy tham khảo link sau:
-
- Link: checkout
- Thư mục:
/Examples/BasicRxSwift
1. Fetching API
Chúng ta đã đi qua lần lượt các vấn đề trong UIKit khi sử dụng RxSwift như sau:
-
- Hiển thị dữ liệu
- Sự kiện tương tác trên một màn hình
- Tương tác giữa nhiều màn hình
- Viết model sử dụng với RxSwift
Tiếp theo, bạn cần phải tương tác với nguồn dữ liệu không ở trong ứng dụng. Và trong giới lập trình, tương tác dữ liệu với API hay với service, thì là một trong những điều bắt buộc. Có như vậy, ứng dụng của bạn mới sinh động được. Quá trình này thì bao gồm các bước lần lượt như sau:
-
- Tạo request tới endpoint của API/Server
- Tiến hành kết nối
- Nhận response
- Phần tích data nhận được
- Tiến hành cập nhật lại giao diện ứng dụng
Và công việc này thì người ta hay gọi là:
Fetching API
2. Setup
Trước khi bắt đầu tạo request, chúng ta cần chuẩn bị thêm một số thứ cần thiết thêm nữa.
Về giao diện, màn hình của chúng ta thì khá là đơn giản. Nó có 1 TableView với các cell để hiển thị item lấy được từ API. Tuỳ thuộc bạn sử dụng API nào, còn mình sẽ sử dụng API của iTunes để lấy các bài hát mới nhất.
Về thư viện sẽ sử dụng là:
-
- URLSession
- RxSwift & RxCocoa
Và kiến thức, cũng không cần đỏi hỏi quá nhiều kiến thức với RxSwift. Chủ yếu bạn cần nắm là sự biến đổi qua lại giữa các dữ liệu nhận được (các operators). Cũng không đưa ra một giải pháp hoàn hảo cho bạn với việc tương tác API.
Bạn tiến hành mở file MusicListViewController
. Trong này, mình có cấu hình cơ bản cho 1 UITableView. Bạn hãy chú ý tới function loadAPI
. Tại đó, ta sẽ thực hiện công việc chính của mình. Còn về code của ViewController bắt đầu thì sẽ như thế này.
import UIKit import RxSwift import RxCocoa class MusicListViewController: UIViewController { // MARK: - Outlets @IBOutlet weak var tableView: UITableView! // MARK: - Properties private let urlMusic = "https://rss.itunes.apple.com/api/v1/us/itunes-music/new-music/all/100/explicit.json" // MARK: - Life cycle override func viewDidLoad() { super.viewDidLoad() configUI() loadAPI() } // MARK: - Private Methods private func configUI() { title = "New Music" let nib = UINib(nibName: "MusicCell", bundle: .main) tableView.register(nib, forCellReuseIdentifier: "cell") tableView.delegate = self tableView.dataSource = self } private func loadAPI() { } } extension MusicListViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 20 } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 80 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) } }
Chúng ta tạm thời hard code cho TableView như vậy. Giúp bạn khi build & run thì cũng thấy được giao diện ban đầu của màn hình ra sao.
3. Request
Bây giờ, chính thức tới bước đầu tiên bạn cần làm. Đó là tạo request. Để lấy được dữ liệu, bạn phải tạo được request tới server. Nhưng hiện tại, bạn đang có là 1 link url
để truy cập tới. Và bạn cũng biết được là chúng ta cần tạo ra được đối tượng URLRequest
. Vì vậy, công việc chúng ta sẽ như sau:
URL String > URL > URLRequest
Okay, bắt đầu thôi. Tại hàm loadAPI()
chúng ta thêm dòng code đầu tiên như sau:
let observable = Observable<String>.of(urlMusic)
Bạn chỉ cần tạo ra 1 Observable với kiểu là String
, với toán tử of
thì dữ liệu cung cấp chính là urlMusic
ở trên. Tiếp tục nào!
let observable = Observable<String>.of(urlMusic) .map { urlString -> URL in return URL(string: urlString)! }
Đây là bước biến đổi thứ nhất , biết String thành URL với việc dùng toán tử map
. Cái này được handle bằng 1 closure và chỉ cần return
đúng về kiểu dữ liệu mà mình mong muốn, là URL. Tiếp tục tiếp nào!
let observable = Observable<String>.of(urlMusic) .map { urlString -> URL in return URL(string: urlString)! } .map { url -> URLRequest in return URLRequest(url: url) }
Áp dụng tiếp toán tử map
lần nữa, thì bạn có lần biến đổi thứ hai. Lúc này, sẽ thành là URLRequest
. Quá EZ phải không nào, từ 2 dòng lệnh chúng ta đã có được đối tượng cần thiết rồi.
4. Response
Tiếp sau việc tạo được request, là việc nhận response. Bạn tiếp tục với đoạn code trên và thêm toán tử flatMap
này vào.
.flatMap { request -> Observable<(response: HTTPURLResponse, data: Data)> in return URLSession.shared.rx.response(request: request) }
Giải thích như sau:
Với flatMap
:
- Không những giúp cho việc biến đổi dữ liệu mà còn biến đổi luôn của Observable này thành Observable khác
- Nó phù hợp với cách tương tác bất đồng bộ (vì 2 bước biến đổi trên vẫn toàn là đồng bộ)
- Nó sẽ chờ phản hồi đầy đủ từ server trả về. Sau đó sẽ thực thi các đoạn code tiếp theo
Với URLSession
:
- Bạn đang dùng các thuộc tính mới được thêm vào từ
RxCocoa
share.rx
sẽ gọi toán tửresponse
với tham số làrequest
từ trên- Kết quả trả về là 1 Observable, với kiểu giá trị của phần tử bao gồm
HTTPURLResponse
vàData
cho body
Cuối cùng bạn thêm dòng code này vào:
.share(replay: 1)
Để cho phép có nhiều subscriptions tới Observable đó và kết quả sẽ được lưu lại ở bộ đệm. Khi đó đảm bảo sẽ có được dữ liệu cho các Subscriber.
5. share() vs. share(replay: 1)
Update thêm kiến thức mới nha. À, nó cũng có ích trong phạm vi bài này đó.
Đầu tiên, khi bạn sử dụng URLSession.rx.response(request:)
, tức là bạn gởi yêu cầu tới máy chủ. Khi bạn nhận được phản hồi trở lại. Thì Observable sẽ emit
ra duy nhất một phần tử và kết thúc.
Mọi thứ sẽ không có vấn đề gì. Và nếu bạn tiếp tục subscribe
lần thứ 2, lần 2 … lần n.
Do đó, mọi công việc sẽ bắt đầu chạy lại từ đầu.
Để tránh việc làm tốn tài nguyên và công sức như thế này. Thì sử dụng toán tử share(replay:scope)
. Toán tử sẽ giữ lại phần tử cuối cùng trong bộ đệm. Cứ như vậy, các Subscriber tiếp theo khi đăng kí tới, thì sẽ nhận được dữ liệu ngay lập tức và không cần phải thực hiện lại đám lệnh kết nối tới API ở trên.
Về scopes
thì bạn có 2 lựa chọn
.forever
bộ đệm sẽ lưu lại mãi mãi. Chờ người đăng ký mới..whileConnected
bộ đệm sẽ giữ lại cho đến khi không còn người nào đăng kí tới và loại bỏ bộ đệm ngay sau đó. Các đăng ký tiếp theo thì sẽ load lại từ đầu.
Tuỳ thuộc vào ý đồ bạn muốn sử dụng việc load API đó ra sao. Mà có cách sử dụng share
phù hợp.
OKE, hết thời gian phụ đạo nha!
6. Parse Data
6.1. Object Model
Bắt đầu cho công việc Parse Data thì bạn cần có một Model cho dữ liệu cần phân tích. Ta tạo một file và đặt tên là Music.swift
và dựa vào dữ liệu json của link trên ta khai báo các thuộc tính như sau:
final class Music: Codable { var artistName: String var id: String var releaseDate: String var name: String var copyright: String var artworkUrl100: String }
Class này là dữ liệu của các item để hiện thị lên Tableview cell.
6.2. Handle Response
Giờ tới phần, phân tích dữ liệu nhận được từ server. Bạn cũng biết việc nhận response không phải lúc nào cũng thành công. Do đó, trước tiên chúng ta phải lọc đi các trường hợp không thành công khi tương tác với API.
observable .filter { response, _ -> Bool in return 200..<300 ~= response.statusCode }
Bạn hãy thêm một dòng code mới và bắt đầu bằng toán tử filter
. Các statusCode
từ 200~299 là thành công. Về các trường hợp lỗi thì chúng ta hãy phân tích tại một bài khác.
Tiếp tục, là phần parse data chính. Ta có đoạn code tiếp như sau với toán tử map
.
.map { _, data -> [Music] in }
Trong đó, map
là toán tử huyền thoại dùng để biến đổi kiểu dữ liệu. Trong bài toán này, chúng ta biến đổi (HTTPURLResponse, Data)
thành Array Music
.
6.3. Decode JSON
Để giúp cho khi biến đổi một cách nhanh chóng. Thì bạn hãy xem lại cấu trúc JSON của API là như thế nào. Rồi từ đó, chúng ta sẽ đưa ra cấu trúc dữ liệu phù hợp. Bạn mở file Music.swift
và thêm khai báo này vào.
final class Music: Codable { var artistName: String var id: String var releaseDate: String var name: String var copyright: String var artworkUrl100: String } struct MusicResults: Codable { var results: [Music] } struct FeedResults: Codable { var feed: MusicResults
Ta có:
FeedResults
là đại diện cho cấu trúc lớn nhất, nó có 1 key làfeed
MusicResults
là kiểu dữ liệu cho keyresults
, nó nằm trongfeed
. Nếu bạn muốn parse gì thêm trong cấu trúc này thì hãy thêm vàoMusic
là đại diện kiểu dữ liệu cho từng item của mãngresults
Tất cả đều kế thừa protocol Codable
, nếu bạn chưa biết chúng là gì. Thì nó đơn giản giúp cho bạn chuyển đổi kiểu dữ liệu một cách dễ dàng, thông qua các đối tượng Encoder hay Decoder.
Codable = Encoder + Decoder
-
- Tham khảo : Encoding và Decoding trong Swift
Quay lại file MusicListViewController
, chúng ta hoàn thành công việc phân tích dữ liệu từ server trả về.
.map { _, data -> [Music] in let decoder = JSONDecoder() let results = try? decoder.decode(FeedResults.self, from: data) return results?.feed.results ?? [] }
Ta đã biết dữ liệu nhận được từ server là JSON, nên sẽ dùng JSONDecoder để biến đổi data
thành FeedResults
. Từ đó, chúng ta sẽ return
về Array Music là dữ liệu cho Tableview.
Cuối cùng, nếu trường hợp phân tích JSON mà bị lỗi thì chúng ta sẽ lọc tiếp lần nữa. Để đảm bảo tính toàn vẹn của chương trình.
.filter { objects in return !objects.isEmpty }
7. Subscribe & Update UI
7.1. Subscribe
Công việc cuối cùng, chính là subscribe
tới Observable. Vì khi có kết nối từ Subscriber tới Observable kia, việc kết nối với API mới hoạt động và bạn sẽ nhận được dữ liệu từ server. Ta tiếp tục với subscribe
nào, bạn thêm đoạn code này vào tiếp đoạn code trên.
.subscribe(onNext: { musics in DispatchQueue.main.async { self.musics = musics self.tableView.reloadData() } }) .disposed(by: bag)
Trong closure onNext
là phần code bạn sẽ handle. Tại đó, bạn sẽ xử lý các công việc khi nhận được kết quả của quá trình phân tích JSON. Bao gồm:
- Lưu trữ lại dữ liệu
- Tiến hành update UI
Nếu bạn muốn làm thêm công việc khác, như: lưu vào bộ đệm, gọi thêm api khác …. thì cũng sẽ tiến hành gọi tại đó luôn.
7.2. Update UI
Với dữ liệu nhận được là một mãng Music và reload
Tableview, nên cần phải thực hiện chúng ở Main Thread. Vì, công việc gọi API luôn chạy ở thread khác. Nếu không update UI tại Main Thread thì sẽ crash chương trình.
Nhớ khai báo thêm túi rác quốc dân và array dữ liệu cho Tableview tại ViewController nha.
private let bag = DisposeBag() private var musics: [Music] = []
Cập nhật lại dữ liệu cho các protocol của TableView nào.
extension MusicListViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { musics.count } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 80 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MusicCell let item = musics[indexPath.row] cell.nameLabel.text = item.name cell.artistNameLabel.text = item.artistName cell.thumbnailImageView.kf.setImage(with: URL(string: item.artworkUrl100)!) return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) } }
Bạn hãy build lại ứng dụng và tận hưởng kết quả nào! Và kết quả của mình sẽ như thế này.
OKAY! Tới đây là kết thúc bài viết này. Tuỳ thuộc vào yêu cầu mà bạn hãy sử dụng chúng nó một cách hợp lý nha! 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.
Tạm kết
- Với RxSwift thì chúng ta có thể tương tác với API một cách đơn giản.
- Rất nhanh và hiệu quả.
Bạn có thể xem lại toàn bộ code như sau:
private func loadAPI() { // create Observable let response = Observable<String>.of(urlMusic) .map { urlString -> URL in return URL(string: urlString)! } .map { url -> URLRequest in return URLRequest(url: url) } .flatMap { request -> Observable<(response: HTTPURLResponse, data: Data)> in return URLSession.shared.rx.response(request: request) } .share(replay: 1) // parse data response .filter { response, _ -> Bool in return 200..<300 ~= response.statusCode } .map { _, data -> [Music] in let decoder = JSONDecoder() let results = try? decoder.decode(FeedResults.self, from: data) return results?.feed.results ?? [] } .filter { objects in return !objects.isEmpty } // update UI .subscribe(onNext: { musics in DispatchQueue.main.async { self.musics = musics self.tableView.reloadData() } }) .disposed(by: bag) }
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!
3 comments
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Prompt Engineering trong 10 phút
- Một số ví dụ sử dụng Prompt cơ bản khi làm việc với AI
- Prompt trong 10 phút
- 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
You may also like:
Archives
- December 2024 (3)
- 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)
Mình có 2 API để lấy dữ liệu, 1 để lấy thông tin, 1 để lấy ảnh. Thì làm sao để lấy được cả 2 và truyền cho 1 model để hiển thị ạ?
Bạn xem và tham khảo ở đây này. Về bản chất thì cũng giống như bài toán bạn đưa ra. https://fxstudio.dev/rxswift-vs-uikit-networking/#7_Connect_multi_APIs
Mình có 2 API, một để gọi data, một để gọi ảnh thì làm thế nào ạ?