Contents
Chào bạn đến với Fx Studio. Bài viết này sẽ là một bài viết dài và chủ đề là Tương tác Networking bằng RxSwift. Mình sẽ cung cấp cho bạn một giải pháp toàn diện nhất có thể. Để bạn có thể tung hoành ngang dọc với RxSwift.
Vì là bài viết này là bài viết tiếp nối các bài viết liên quan tới kết nối mạng. Nên bạn cần phải đọc qua các bài viết sau:
Và nếu mọi thứ đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Chuẩn bị cơ bản thì như sau:
-
- 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
Về API, sử dụng các API đơn giản của trang The Cocktail DB. Chúng ta sẽ sử dụng 2 API sau:
- Cocktail Categories: https://www.thecocktaildb.com/api/json/v1/1/list.php?c=list
- Drinks List: https://www.thecocktaildb.com/api/json/v1/1/filter.php?c=Cocktail
Về Project, bạn thêm 2 màn hình với 2 file ViewController cho 2 danh sách trên. Chúng ta chỉ cần sử dụng UITableView cơ bản là được. Không cần màu mè quá.
Mục đích việc tương tác Networking
Như đã nói ở trên, phần này sẽ là phần nâng cao của phần Fetching Data from API. Vì lý do đơn giản là tất cả mọi người đều không code chay code xử lý trong 1 file ViewController rồi.
Nên công việc của chúng ta khá là vất vả, bao gồm:
- Thiết kế cấu trúc dữ liệu cho các Model
- Thiết kế Model tương tác với API (tạm gọi là Networking)
- Xử lý việc
request
cơ bản - Update giao diện với dữ liệu từ API
- Gọi nhiều API cùng một lúc
- Xử lý dữ liệu của các API trả về
Và để cho bài viết dễ đi vào lòng người, cũng yêu cầu bạn phải đọc qua về Fetching Data from API trước. Cũng như có kiến thức cơ bản về RxSwift.
Tất nhiên, bài viết này vẫn ở phạm tru là siêu cơ bản mà thôi.
1. Entities models
Khi bạn làm việc với các API, thì công việc đầu tiên vẫn là phân tích cấu trúc dữ liệu nhận được từ các API đó. Theo cấu trúc JSON của 2 API được đề cập ở trên, chúng ta sẽ tạo các files để mô tả cấu trúc dữ liệu cho chúng.
Với Cocktail Categories thì tạo một file CocktailCategory.swift
, bạn tham khảo cấu trúc sau:
struct CocktailCategory: Codable { var strCategory: String var items = [Drink]() private enum CodingKeys: String, CodingKey { case strCategory } }
Giải thích:
Codable
để giúp bạn có thể chuyển đổi kiểu dữ liệu một cách nhanh chóng. Nhưng bắt buộc tất cả các properties trong đó cũng phải có khả năng củaCodable
items
là một property mình thêm vào. Nó không có trong JSON, nên khidecode
thì sẽ bị lỗi. Vì vậy, bạn cần có thêmCodingKeys
- Với
CodingKeys
thì cáccase
sẽ chỉ định việcmap
dữ liệu từ JSON sangCocktailCategory
Đây là cấu trúc dữ liệu đại diện cho 1 item trong JSON mà lấy được từ API. Nó không phải là toàn bộ cấu trúc JSON.
Để chuẩn bị cho tương lai. Và vì ngoài Cocktail, bạn có thể có rất nhiều loại đồ uống khác. Nên chúng ta sẽ chuẩn bị tiếp cấu trúc cho toàn bộ JSON của api này.
struct CategoryResult<T: Codable> : Codable { var drinks: [T] }
Sử dụng Generic
cho khoẻ nha. Sau này, chỉ cần ráp các kiểu dữ liệu khác vào thôi. Còn drinks
là key của mãng items cần phải decode
.
Với Drink List, ta cũng áp dụng tương tự. Bạn tạo một file là Drink.swift
và thêm đoạn code sau vào.
struct Drink: Codable { var strDrink: String var strDrinkThumb: String var idDrink: String } struct DrinkResult { var drinks: [Drink] }
Cũng không có gì mới. Nhưng mà với DrinkResult
thì mình sẽ không dùng trong demo lần này. Thêm vào cho có hoa lá cành …. cho đẹp mà thôi.
2. Networking model
Sang phần tiếp theo, chúng ta cần tạo thêm các file cho Networking Model này. Nó sẽ bao gồm 3 file chính hoặc có thể là 3 phần chính cũng được. Tuỳ thuộc bạn muốn dùng như thế nào là hợp lý với bạn mà thôi. Tham khảo như sau:
- Networking : chứa phần tương tác chính và xử lý tương tác API
- Error : định nghĩa các
error
trong cả quá trình tương tác API - Results : tạo ra một xử lý biến đổi dữ liệu là
Data
từ API, thành các đối tượng như ta mong muốn. Thông quaJSONDecoder
.
2.1. Error
Bắt đầu với Error nào. Vì nó dễ nhất à. Bạn tạo 1 file NetworkingError.swift
và thêm đoạn code sau vào.
enum NetworkingError: Error { case invalidURL(String) case invalidParameter(String, Any) case invalidJSON(String) case invalidDecoderConfiguration }
Đó là các case
mình định nghĩa ra, bạn muốn thêm các case riêng thì vẫn Okay. Ngoài ra, nếu muốn xử lý hiển thị các text cho từng lỗi thì vẫn được. EZ!
Chúng ta sẽ có một chương nói về Handle Error. Bạn hãy chờ đợi thêm nha!
2.2. Result
Tiếp tục, với Result. Bạn cũng cần tạo 1 file NetworkingResult.swift
và thêm đoạn code khai báo này vào.
extension CodingUserInfoKey { static let contentIdentifier = CodingUserInfoKey(rawValue: "contentIdentifier")! } struct NetworkingResult<Content: Decodable>: Decodable { let content: Content private struct CodingKeys: CodingKey { var stringValue: String var intValue: Int? = nil init?(stringValue: String) { self.stringValue = stringValue self.intValue = 0 } init?(intValue: Int) { return nil } } init(from decoder: Decoder) throws { guard let ci = decoder.userInfo[CodingUserInfoKey.contentIdentifier], let contentIdentifier = ci as? String, let key = CodingKeys(stringValue: contentIdentifier) else { throw NetworkingError.invalidDecoderConfiguration } do { let container = try decoder.container(keyedBy: CodingKeys.self) content = try container.decode(Content.self, forKey: key) print(content) } catch { print(error.localizedDescription) throw error } } }
Nó bao gồm 2 phần chính
- Extension của
CodingUserInfoKey
, nó sẽ dùng làuserInfor
khi bạndecode
. Để việc decode chính xác hơn. NetworkingResult
với một Generic làContent
. Chính là kiểu dữ liệu bạn muốn chuyển đổi về.
Struct này còn có việc kế thừa Decodable Protocol. Nên ta thêm hàm init
từ Decoder. Trong đó, vẫn quan trong nhất là việc lấy contentIdentifier
từ đối tượng decoder
. Nó sẽ dùng là nội dung cho CodingKeys
. Sau đó, việc map dữ liệu sẽ duyệt theo key đó và lấy đúng các kiểu dữ liệu mà mình mong muốn.
Phần này hơi khó hiểu. Nhưng nếu hiểu được thì lại rất dễ, khá hay và đơn giản. Nếu không hiểu thì bạn có thể skip hoặc tưởng tượng như thế này. Ta có JSON như sau:
{ "đây là key nè" : [ { // JSON cho item với kiểu là A }, { // JSON cho item với kiểu là A }, { // JSON cho item với kiểu là A }, { // JSON cho item với kiểu là A }, .... ] }
Bạn sẽ thấy là đây là key nè
chính là key cho 1 mãng items với kiểu là Array A
. Khi đó,
- Content là
[A]
- contentIdentifier là
đây là key nè
2.3. Networking
Cuối cùng trong phần này là cấu trúc cho Model quan trọng nhất. Bạn tạo một file là Networking.swift
, với cấu trúc như sau:
import Foundation import RxSwift final class Networking { // MARK: - Endpoint enum EndPoint { static let baseURL: URL? = URL(string: "https://www.thecocktaildb.com/api/json/v1/1/") case categories case drinks var url: URL? { switch self { case .categories: return EndPoint.baseURL?.appendingPathComponent("list.php") case .drinks: return EndPoint.baseURL?.appendingPathComponent("filter.php") } } } // MARK: - Singleton private static var sharedNetworking: Networking = { let networking = Networking() return networking }() class func shared() -> Networking { return sharedNetworking } private init() { } // MARK: - Properties // MARK: - Process methods static func jsonDecoder(contentIdentifier: String) -> JSONDecoder { let decoder = JSONDecoder() decoder.userInfo[.contentIdentifier] = contentIdentifier decoder.dateDecodingStrategy = .iso8601 return decoder } // MARK: - Request // MARK: - Business }
Nó chia ra các thành phần chính như sau:
Endpoint
dùng để define các link URL mà bạn sử dụng. Trong demo, chúng ta dùng 2 link nên sẽ có 2 case. Và một biếnurl
để tiện biến đổi String thành đối tượng URL nhanh hơn và tập trung hơn.Singleton
dùng để gọi API nhanh, giúp cho các thanh niên lười code trở nên code pro hơn.- Các phần
Properties
vàProcess methods
thì khai báo các đối tượng cần thiết và biến đổi dữ liệu cũng như xử lý logic. Request
là xử lý việc kết nối. Nó là chung nhất, không theo bất kì API nào.Business
là phần bọc các request lại. Tại đây, các tham số và endpoint sẽ phù hợp với từng API.
OKAY! phần chuẩn bị xem như là hoàn tất. Tiếp theo là phần xử lý request.
3. Request
Tiếp tục cuộc vui của chúng ta nào. Công việc của chúng ta sẽ là biến đổi URL để có được một URLRequest. Request này sẽ dùng cho việc connect tới server.
Tại phần MARK Request trong file Networking.swift. Bạn thêm function sau vào.
func request<T: Codable>(url: URL?, query: [String: Any] = [:], contentIdentifier: String = "") -> Observable<T> { // .... }
Các tham số cho hàm là rất cơ bản. Chú ý tới 2 điểm sau:
T
là kiểu Generic bất kì và yêu cầu là cần implement Codable Protocol . Nó dùng để parse data thànhT
.- Return về là 1 Observable với kiểu dữ liệu cho phần tử là
T
contentIdentifier
có thể dùng hoặc không, tuỳ ý bạn
3.1. URLComponents
Bạn tiếp tục vào trong function request
và thêm đoạn code sau vào:
do { // code ở đây nha } catch { print(error.localizedDescription) return Observable.empty() } }
Bắt đầu với đoạn do ... catch
. Nó dùng để bắt lỗi trong quán trình xử lý. Vì có thể có rất nhiều chỗ có thể sinh ra lỗi. Trong phạm vi bài viết, thì mình sẽ không handle error (để dành phần sau).
Nếu có lỗi thì return là
empty()
. Tức không có gì.
Giờ vào trong phần do
, bạn thêm đoạn code sau vào.
guard let URL = url, var components = URLComponents(url: URL, resolvingAgainstBaseURL: true) else { throw NetworkingError.invalidURL(url?.absoluteString ?? "n/a") }
Nó sẽ kiểm tra các tham số url
ổn không. Và tạo thêm biến components
để tiện xử lý thêm các param/path/value/queryString … Nếu thất bại thì sẽ throw
error. Bạn có thể tuỳ ý với việc handle error riêng theo ý bạn. Tiếp tục nào!
components.queryItems = try query.compactMap { (key, value) in guard let v = value as? CustomStringConvertible else { throw NetworkingError.invalidParameter(key, value) } return URLQueryItem(name: key, value: v.description) }
Sử dụng components
để thêm các tham số lấy từ query
vào link url. Thành kiểu ...?key1=value1&key2=value2
. Nó cũng giúp bạn hạn chế việc nhầm lẫn khi thêm hoặc nối bằng tay cho URL.
Cái này là cho xử lý API với method là
GET
nha. Nó liên quan nhiều tới query string của url link.
3.2. URLRequest
Tiếp tục, với việc lấy ra cái url cuối cùng sau khi thêm các param vào.
guard let finalURL = components.url else { throw NetworkingError.invalidURL(url?.absoluteString ?? "n/a") }
Việc cuối cùng trong phần này là bạn tạo URLRequest
với finalURL
trên.
let request = URLRequest(url: finalURL)
Muốn thêm header hay body request, thì bạn có thể tự thêm vào. Còn trong các link api demo thì không cần thiết.
Xong, chuyển qua phần khác nào!
4. Connection
4.1. Connect
Bây giờ là phần kết nối. Tất nhiên, bạn sẽ sử dụng RxSwift để connect và nhận response trả về. Bạn thêm đoạn code này vào tiếp nha.
return URLSession.shared.rx.response(request: request) .map { (result: (response: HTTPURLResponse, data: Data)) -> T in let decoder = JSONDecoder() return try! decoder.decode(T.self, from: result.data) }
Về URLSession.shared.rx.response
, mình đã giải thích ở bài trước rồi. Còn về parse dữ liệu lại khá đơn giản. Dùng JSONDecoder để biến đổi Data
về kiểu T
thôi.
contentIdentifier
chúng ta sẽ bàn luận ở dưới nữa.
Tới đây, cũng là kết thúc function request
và bạn đã có 1 file Networking Model xịn sò với RxSwift dùng để tương tác với API rồi.
4.2. Call API
Ta đã có phần xử lý biến đổi URL thành URLRequest và sử dụng nó rồi. Giờ với từng API thì chúng ta sẽ có các phần xử lý khác nhau. Bắt đầu với API lấy Cocktail Category nào. Tại file Networking phần Business bạn thêm 1 function sau.
func getCategories(kind: String) -> Observable<[CocktailCategory]> { let query: [String: Any] = [kind : "list"] let url = EndPoint.categories.url let rq: Observable<CategoryResult<CocktailCategory>> = request(url: url, query: query) return rq .map { $0.drinks } .catchErrorJustReturn([]) .share(replay: 1, scope: .forever) }
Giải thích chút nha!
Observable<[CocktailCategory]>
là kiểu giá trị trả về của hàm.- Bạn nên nhớ
element
của Observable này là 1 mãngCocktailCategory
, chứ không phải từng Category riêng lẻ.
- Bạn nên nhớ
query
vàurl
theo đúng link của APIrq
là một Observable có được do gọi hàmrequest
vừa viết ở trên.- Ta cần cung cấp kiểu Generic cho hàm
request
. - Trong ví dụ này là
CategoryResult<CocktailCategory>
. - Phần này, bạn cần phải cực kì cẩn thận. Chỉ nhầm kiểu dữ liệu một phát là sẽ không
decode
được
- Ta cần cung cấp kiểu Generic cho hàm
- Vì mãng Cocktail Category nó nằm trong thuộc tính
drink
của CategoryResult. Do đó, để lấy được chính xác thì bạn cần phải dùng toán tửmap
, biến đổi để được kiểu dữ liệu chính xác. .catchErrorJustReturn([])
tạm thời không quan tâm tới error.- Và nếu có error thì trả về mãng rỗng
.share(replay: 1, scope: .forever)
phần này mình đã đề cập trong phần Cache.- Nó giúp lưu trữ lại dữ liệu đã được xử lý vào bộ đệm.
- Các subscription tiếp theo chỉ cần trỏ tới và lấy, chứ không gọi request lại từ đầu
Bạn yên tâm là việc kết nối API sẽ thực hiện khi có subscription tới nó. Còn không có đăng ký tới, thì mọi thứ vẫn nằm im và đợi chờ bạn tới.
5. Update UI
5.1. Setup UI
Qua được tới đây thì chúng ta sẽ nhẹ não đi nhiều rồi. Các phần khó đã xong, giờ là phần quẩy. Bạn mở file CocktailViewController.swift
, nó có 1 UITableView đơn giản với các cell mặc định.
Hoặc bạn có thể tự tạo riêng cho bạn một file ViewController.
Thêm 2 thư viện RxSwift & RxCocoa vào file và khai báo thêm 2 properties sau.
private let bag = DisposeBag() private let categories = BehaviorRelay<[CocktailCategory]>(value: [])
Trong đó:
bag
là túi rác quốc dân, lưu trữ các subscription.- Nó sẽ tự giải phóng mình và các subscription khi ViewController bị giải phóng.
categories
là một Relay. Nó sẽ được khởi tạo bằng 1 giá trị là mãng rỗng.- Và khi có bất cứ dữ liệu nào, thì nó sẽ phát lại cho các subscriber tới nó.
- Đảm bảo các kết nối đều có dữ liệu.
- Quan trọng là nó dùng vừa như là 1 biến, vừa như là 1 Observable.
BehaviorRelay là thay thế cho Variable trong version trước của RxSwift
Sử dụng như thế nào trong ViewController, bạn tham khảo qua cách dùng Relay
đó cho các protocol của TableView sau:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { categories.value.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) let item = categories.value[indexPath.row] cell.textLabel?.text = "\(item.strCategory) - \(item.items.count) items" return cell }
Quan tâm tới .value
của Relay
là ổn hết thôi.
5.2. Load API
Muốn có dữ liệu thì mình phải gọi tới Networking Model và thực thi function request
vừa viết. Ta viết thêm một hàm cho ViewController với tên là loadAPI
như sau:
private func loadAPI() { let newCategories = Networking.shared().getCategories(kind: "c") newCategories .bind(to: categories) .disposed(by: bag) }
Bạn chưa cần hiểu về bind
là gì. Đơn giản là nó đưa dữ liệu trực tiếp lên biến Relay
ta vừa khai báo. Kết nối chúng với nhau.
Công việc chỉ hoàn thành khi bạn thực hiện subscribe
mà thôi. Lúc đó, việc kết nối tới API sẽ được thực thi. Bạn quay lại function viewDidLoad
của ViewController nào. Thêm đoạn code sau:
override func viewDidLoad() { super.viewDidLoad() configUI() categories .asObservable() .subscribe(onNext: { [weak self] _ in DispatchQueue.main.async { self?.tableView.reloadData() } }) .disposed(by: bag) loadAPI() }
Trong đó:
- Tiến hành
subscribe
tới biếncategories
.- Khi nhận được dữ liệu là
onNext
thì gọi TableviewreloadData
, nhằm hiển thị kết quả mới nhất. - Chú ý việc tiến hành xử lý chúng ở
Main Thread
. Vì gọi API được thực thi ở Thread khác, mà cố tình update UI sẽ bị crash app, nên phải về Main Thread để update.
- Khi nhận được dữ liệu là
loadAPI
để gọi việc kết nối và nhận dữ liệu từ API
Tới đây, là xong phần cơ bản rồi. Bạn tiến hành build và cảm nhận kết quả nào. Nếu không có gì lỗi lầm nữa thì bạn sẽ tiếp qua phần tiếp theo.
6. Using contentIdentifier
Giờ chúng ta sẽ lấy các Drinks của từng Category nha. Phần này sẽ dùng tới contentIdentifier
cho nó thêm thi vị cuộc sống. Còn mục đích chính của việc làm này là …
Vui thôi!
Đầu tiên, bạn chỉnh sửa lại đoạn gọi request của URLSession.rx
trong file Networking.
return URLSession.shared.rx.response(request: request) .map { (result: (response: HTTPURLResponse, data: Data)) -> T in if contentIdentifier != "" { let decoder = Networking.jsonDecoder(contentIdentifier: contentIdentifier) let envelope = try decoder.decode(NetworkingResult<T>.self, from: result.data) return envelope.content } else { let decoder = JSONDecoder() return try! decoder.decode(T.self, from: result.data) } }
Nếu contentIdentifier
khác rỗng, thì chúng ta sẽ parse dữ liệu bằng JSONDecoder với userInfor
được thêm vào. Nó sẽ được sử dụng trong toàn bộ quán trình decode
.
Bạn hãy xem chúng như là một bí thuật vậy.
Việc còn lại của parse là dùng tới NetworkingResult với Generic T
truyền nào (nó sẽ khác cách ở phần trên). Bạn tạo tiếp function để lấy danh sách Drink nào.
func getDrinks(kind: String, value: String) -> Observable<[Drink]> { let query: [String: Any] = [kind : value] let url = EndPoint.drinks.url let rq: Observable<[Drink]> = request(url: url, query: query, contentIdentifier: "drinks") return rq.catchErrorJustReturn([]) }
Bạn hãy chú ý tới Observable<[Drink]>
. Nó sẽ dễ hiểu hơn cách trên và nó tương minh hơn nhiều. Bạn không cần phải bọc lại nó bằng một class/struct nào nữa. Và vì, key root trong API get Drinks List là drinks
, nên nó cũng là giá trị cho contentIdentifier
.
Và cuối cùng, khi đã xác định và lấy đúng kiểu dữ liệu rồi, thì bạn chỉ cần return thôi. Không cần biến đổi chúng.
Quan tâm tới việc lấy chính xác cái cần lấy và không bọc qua nhiều lớp. Tránh việc khai báo lèn nhèn thì
contentIdentifier
sẽ phát huy hiệu quả tốt nhất cho bạn.
Update lên UI của DrinksViewController thì cũng tương tự như màn hình kia. Bạn tham khảo qua code sau.
// load api func loadAPI() { Networking.shared().getDrinks(kind: "c", value: categoryName) .bind(to: drinks) .disposed(by: bag) } // subscribe drinks .asObservable() .subscribe(onNext: { [weak self] drinks in DispatchQueue.main.async { self?.tableView.reloadData() self?.title = "\(self!.categoryName) (\(drinks.count))" } }) .disposed(by: bag)
Bạn thêm function cho TableViewDelegate ở CocktailViewController , để có thể push sang DrinksViewController.
- Với cách áp dụng tương tự như ở CocktailViewController, để bạn load dữ liệu cho danh sách các đồ uống của từng Category.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) let item = categories.value[indexPath.row] print("\(item.strCategory) - \(item.items.count) items") let vc = DrinksViewController() vc.categoryName = item.strCategory self.navigationController?.pushViewController(vc, animated: true) }
Build chúng và cảm nhận kết quả nào!
7. Connect multi APIs
Cuộc sống đôi lúc nó không giống cuộc đời.
7.1. Vấn đề
Bạn sẽ được nâng cấp bài toán Networking này lên ở tầm mới nữa. Và bài toán mới, cũng là khó hơn và cũng hay gặp nhiều trong thực tế cuộc sống.
- Bạn có 1 danh sách Categories được lấy từ API. Nhưng dữ liệu trong mỗi Category đó chưa đủ với yêu cầu của project
- Bạn sẽ phải request từng Categories một và ráp dữ liệu lại của từng item lại với nhau. Để có được 1 array đầy đủ thông tin như yêu cầu của dự án.
- Cuối cùng là kết thúc việc gọi một lúc nhiều API đó.
- Update dữ liệu lên UI
Giải quyết bài toán này thì gần như 100% dev sẽ nghĩ tới:
For
thần thánh hay cái gì đó màloop
tương tự.
Đó không phải là giải pháp tồi. Nhưng chúng ta lại phải quan tâm tới nhiều điều còn kinh khủng hơn việc lặp request đó.
- Bất đồng bộ
- Tài nguyên của máy
- Error
Nếu như việc lặp chỉ khoản vài lần (dưới 5), thì mọi thứ vẫn ổn hoặc iphone của bạn quá trâu bò. Và bạn sẽ không nhận ra điều bất thường. Nhưng khi số lượng quá nhiều, thì gây áp lực lên vi xử lý. Nó dễ gây treo máy và crash app.
Toang!
- Ngoài mong đợi là
error
. Nếu xử lý không gọn, chỉ cần 1 request error là cả đám còn lại sẽ toang theo. Còn nếu xử lý được, thì lạiif ... else
loạn thêm. - Cú chốt cuối là khi nào chúng sẽ hoàn thành hết tất cả. Đây mới là cái quan trong nhất.
OKAY, khó khăn là thế. Tất nhiên đã có nhiều cách sử dụng để giải quyết. Trong bài này, chúng ta vẫn sẽ giải quyết Networking phực tạp này với RxSwift.
7.2. Giải pháp
Tư tưởng chính là biến các request riêng lẻ đó thành các sequence observables.
Ta minh hoạ như sau:
List: - item 1 --> request --> Observavle 1 - item 2 --> request --> Observavle 1 - item 3 --> request --> Observavle 3 - item 4 --> request --> Observavle 4 - item 5 --> request --> Observavle 5 .... Observable final = Observavle 1 + Observavle 2 + Observavle 3 + ....
- Chúng ta sẽ có nhiều
stream
cho nhiều request api. - Chúng sẽ độc lập với nhau.
- Sau đó, chúng ta sẽ tiến hành gom chúng lại thành 1
steam
duy nhất. - Với mỗi element được
emit
ra chính là response của một request api sau khi thành công.
Công việc sẽ diễn ra một cách bất đồng bộ.
- Khi có 1 request xong ,thì sẽ xử lý dữ liệu từ chúng.
- Khi tất cả xong, thì kết thúc cả quá trình.
Và điều quan trọng nữa là mọi thứ sẽ thực hiện khi có subscription
tới Observable kia thôi.
Chắc đọc qua cũng muốn nổ não rồi. Thôi sang phần code cho nó nhẹ đầu nào.
7.3. Lặp
Lần này, ta không cần xử lý hay thêm gì cho file Networking nữa. Tất cả sẽ là sử dụng các kĩ thuật mà RxSwift cho bạn thông qua các Operator của nó.
Bạn mở file CocktailViewController.swift và tới function loadAPI
. Tại đây chúng ta sẽ làm các công việc sau:
- Gọi API để lấy danh sách Cocktail Categories
- Gọi các API để lấy danh sách Drinks của từng loại Category trên
- Ráp dữ liệu list Drinks của từng Category vào thuộc tính
items
của nó - Cập nhật lại dữ liệu lên UI với việc
count
số lượng của tất cả các Drinks trong mỗi loại Category
Công việc cũng không vất vả phải không nào. Bạn thêm 1 đoạn code sau đoạn code để get
các Cocktail Categories nào.
private func loadAPI() { let newCategories = Networking.shared().getCategories(kind: "c") let downloadItems = newCategories .flatMap { categories in return Observable.from(categories.map { category in Networking.shared().getDrinks(kind: "c", value: category.strCategory) }) } .merge(maxConcurrent: 2) // ..... }
Bóc trần nó một chút nha:
downloadItems
là một Observable với kiểu dữ liệu là[Drink]
- Sử dụng chính Observable lấy Cocktail Categories, là
newCategories
và biến đổi bằng toán tửflatmap
- Biến đổi từ kiểu
Observable<[CocktailCategory]>
thànhObservable<[Drink]>
- Quá trình biến đổi như sau:
- Sử dụng toán tử
Observable.from
. Bạn cũng biết tham số này cần đối tượng là 1array
categories.map
duyệt qua lần lượt các phần tử trongcategories
.- Tại nỗi lần lặp thì gọi
getDrinks
- 1 Observable sẽ được trả về với kiểu
Observable<[Drink]>
- Cứ như thế, lặp hết mãng
categories
thì ta có 1 mãng mới[Observable<[Drink]>]
- Sử dụng toán tử
- Như vậy, ta có 1 Observable mà các phần tử của nó có kiểu dữ liệu là 1 Observable.
- Để hợp nhất chúng lại, ta sử dụng toán tử
merge
. Như vậy, với các Observable con đó mà phát ra dữ liệu thì chúng sẽ được gom về 1 Observable duy nhất. - Đảm bảo tài nguyên của máy ổn định, thì cho ta tối đa là 2 stream chạy đồng thời
7.4. Biến đổi
Observable này sẽ emit
ra 1 mãng Drink
và nó phát ra nhiều lần. Số lần phát chính là số lượng item trong categories
. Như vậy, nó sẽ là mãng 2 chiều. Và cứ như vậy, bạn có 1 Observable bá đạo. Nó sẽ giúp bạn thực thi toàn bộ các request con mà yên tâm về mặt xử lý và hiệu năng.
Tiếp tục hack não với Networking nào!
Bạn thêm đoạn code sau vào dưới đoạn code vừa rồi.
let updateCategories = newCategories.flatMap { categories in downloadItems .enumerated() .scan([]) { (updated, element:(index: Int, drinks: [Drink])) -> [CocktailCategory] in var new: [CocktailCategory] = updated new.append(CocktailCategory(strCategory: categories[element.index].strCategory, items: element.drinks)) return new } }
Giải thích tiếp nào!
- Lại tao ra một Observable với tên là
updateCategories
, nó có kiểu dữ liệu cho các element của nó là[CocktailCategory]
.- Sau khi một hồi biến đổi các kiểu thì lại quay về kiểu ban đầu.
- Cần phải vậy để update lên UI của bạn (đã setup trước đó rồi)
- Công việc chính này là ráp dữ liệu của
downloadItems
vàonewCategories
.- Tách các phần tử từ mãng 2 chiều
[[Drink]]
- Gán cho thuộc tính
.items
của từng item trong mãng Category.
- Tách các phần tử từ mãng 2 chiều
Giải quyết bằng toán tử như thế nào?
- Đầu tiên là dùng chính Observable
newCategories
bằng toán tửflatMap
- Trong closure handle đó, bạn lại sử dụng Observable
downloadItems
và biến đổi lần lượt như sau:- Vì dữ liệu rất thô sơ, không có gì để phân biệt. Nên công việc chúng ta rất vất vả. Và sử dụng toán tử
.enumerated()
, để đánh index cho mỗiemit
- Kiểu dữ liệu mới của Observable
downloadItems
là 1 Tuple(index: Int, drinks: [Drink])
.scan
để làm giảm đi các phần tử đượcemit
. Ta có n phần tử thì sẽ còn 1 phần tử mà thôi. Quan trọng nhất là thay đổi lại kiểu dữ liệu return từ[Drink]
thành[CocktailCategory]
- Lợi dụng
element.index
để tạo 1 đối tượngCocktailCategory
đầy đủ thông tin. Sau đó thêm vào mãngupdated
- Làm cho tới hết thì chúng ta có đầy đủ dữ liệu, sau đó array mới đó được return về cho toán từ
flatmap
đầu tiên.
- Vì dữ liệu rất thô sơ, không có gì để phân biệt. Nên công việc chúng ta rất vất vả. Và sử dụng toán tử
Như vậy đã xong việc ráp dữ liệu.
7.5. Bind
Phần cuối trong Networking này, là bind
Observable updateCategories
tới thuộc tính categories
của ViewController.
updateCategories .bind(to: categories) .disposed(by: bag)
Như vậy là hoàn thành. Bạn tiến hành build lại project và cảm nhận kết quả nào. Nếu có lỗi thì cần chú ý 2 việc:
- Bình tĩnh hết sức
- Mở code demo của mình lên đối chiếu
Nếu vẫn không hết lỗi, thì tắt máy và đi ra ngoài hít thở cho thư giản đầu óc. Rồi tiếp tục lại. Đùa vậy thôi, phần này là phần nâng cao. Khó ở chỗ ráp dữ liệu và nó tuỳ thuộc vào đặc điểm JSON trả về của mỗi loại API. Chứ không phải 100% phải xử lý như trên. Và trường hợp thành công thì trông như thế này.
OKAY! Tới đây là kết thúc bài viết Networking rất dài này. 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
- Tạo Networking Model để dùng cho nhiều project
- Xử lý request & response
- Decoder dữ liệu nhận được một cách tự động
- Xử lý việc gọi API & gọi nhiều API
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!
2 comments
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- 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
- Lập trình hướng giao thức (POP) với Swift
You may also like:
Archives
- 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)
Những phần liên quan đến việc kết nối API viết khá khó hiểu, mình gần như k hiểu được mục đích của các dòng code, mong bạn có thể cập nhật lại bài viết
Cần phải có kiến thức từ 2 bài viết trước thì mới qua bài viết này được bạn. Mình có liệt kê ở 2 phần giới thiệu đầu tiên để link tới 2 bài đó:
– RxSwift vs. UIKit – Fetching Data from API
– RxSwift vs. UIKit – Working with Cache Data
Hi vong giúp ích được cho bạn.