Skip to content
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
Fx Studio
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
Written by chuotfx on October 23, 2020

RxSwift vs. UIKit – Networking

RxSwift

Contents

  • Chuẩn bị
  • Mục đích việc tương tác Networking
  • 1. Entities models
  • 2. Networking model
    • 2.1. Error
    • 2.2. Result
    • 2.3. Networking
  • 3. Request
    • 3.1. URLComponents
    • 3.2. URLRequest
  • 4. Connection
    • 4.1. Connect
    • 4.2. Call API
  • 5. Update UI
    • 5.1. Setup UI
    • 5.2. Load API
  • 6. Using contentIdentifier
  • 7. Connect multi APIs
    • 7.1. Vấn đề
    • 7.2. Giải pháp
    • 7.3. Lặp
    • 7.4. Biến đổi
      • Giải quyết bằng toán tử như thế nào?
    • 7.5. Bind
  • Tạm kết

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:

    • RxSwift vs. UIKit – Fetching Data from API
    • RxSwift vs. UIKit – Working with Cache Data

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ủa Codable
  • items là một property mình thêm vào. Nó không có trong JSON, nên khi decode thì sẽ bị lỗi. Vì vậy, bạn cần có thêm CodingKeys
  • Với CodingKeys thì các case sẽ chỉ định việc map dữ liệu từ JSON sang CocktailCategory

Đâ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 qua JSONDecoder.

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ạn decode. Để 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ến url để 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ành T.
  • 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ãng CocktailCategory , chứ không phải từng Category riêng lẻ.
  • query và url theo đúng link của API
  • rq là một Observable có được do gọi hàm request 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
  • 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ến categories.
    • Khi nhận được dữ liệu là onNext thì gọi Tableview reloadData, 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.
  • 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ại if ... 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ành Observable<[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à 1 array
    • categories.map duyệt qua lần lượt các phần tử trong categories.
    • 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]>]
  • 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ào newCategories.
    • 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.

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ỗi emit
    • 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ử được emit . 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ượng CocktailCategory đầy đủ thông tin. Sau đó thêm vào mãng updated
    • 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.

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!

FacebookTweetPinYummlyLinkedInPrintEmailShares20
Tags: rxswift
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

  • Annomynous has written: May 11, 2021 at 4:42 am Reply

    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

    • chuotfx has written: May 11, 2021 at 8:01 am Reply

      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.

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Donate – Buy me a coffee!

Fan page

Fx Studio

Tags

Actor Advanced Swift AI api AppDistribution autolayout basic ios tutorial blog ci/cd closure collectionview combine concurrency crashlytics dart dart basic dart tour Declarative delegate deploy design pattern fabric fastlane firebase flavor flutter GCD gradients iOS MVVM optional Prompt engineering protocol Python rxswift safearea Swift Swift 5.5 SwiftData SwiftUI SwiftUI Notes tableview testing TravisCI unittest

Recent Posts

  • Role-playing vs. Persona-based Prompting
  • [Swift 6.2] Raw Identifiers – Đặt tên hàm có dấu cách, tại sao không?
  • Vibe Coding là gì?
  • Cách Đọc Sách Lập Trình Nhanh và Hiệu Quả Bằng GEN AI
  • Nỗ Lực – Hành Trình Kiến Tạo Ý Nghĩa Cuộc Sống
  • Ai Sẽ Là Người Fix Bug Khi AI Thống Trị Lập Trình?
  • Thời Đại Của “Dev Tay To” Đã Qua Chưa?
  • Prompt Engineering – Con Đường Để Trở Thành Một Nghề Nghiệp
  • Vấn đề Ảo Giác (hallucination) khi tương tác với Gen AI và cách khắc phục nó qua Prompt
  • Điều Gì Xảy Ra Nếu… Những Người Dệt Mã Trở Thành Những Người Bảo Vệ Cuối Cùng Của Sự Sáng Tạo?

Archives

  • May 2025 (2)
  • April 2025 (1)
  • March 2025 (8)
  • January 2025 (7)
  • December 2024 (4)
  • 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)

About me

Education, Mini Game, Digital Art & Life of coders
Contacts:
contacts@fxstudio.dev

Fx Studio

  • Home
  • About me
  • Contact us
  • Mail
  • Privacy Policy
  • Donate
  • Sitemap

Categories

  • Art (1)
  • Blog (44)
  • Code (11)
  • Combine (22)
  • Flutter & Dart (24)
  • iOS & Swift (102)
  • No Category (1)
  • RxSwift (37)
  • SwiftUI (80)
  • Tutorials (87)

Newsletter

Stay up to date with our latest news and posts.
Loading

    Copyright © 2025 Fx Studio - All rights reserved.