+1

Alamofire – Design Pattern in Swift 3

Alamofire là một thư viện thực hiện các phương thức kết nối Client - Server được viết bằng swift. Sau khi chuyển sang code swift thì có khá nhiều thay đổi, một trong số đó là các thư viện thường dùng trong objective-C Thì trên swift không còn nữa hoặc được xây dựng lại với nhiều thay đổi về format. Trong bài này mình sẽ cố gắng xây dựng một pattern cho Alamofire để việc kết nối client server một cách đơn giản nhất có thể.

Cài đặt Alamofire

Để cài đặt Alamofire, mở terminal di chuyển đến thư mục chứa project của bạn. Nếu bạn đã sử dụng pod trước đó thì chỉ cần thêm Alamofire vào Podfile và install lại là được, còn không bạn thực hiện các lệnh sau.

tienpm$ pod init
tienpm$ nano Podfile

Sau khi chạy lệnh trên một màn hình GNU nano hiện lên cho phép bạn chỉnh sửa Podfile. Sau khi thêm pod 'Alamofire' vào bạn save lại và chạy pod install để cài đặt Alamofire vào project của mình.

Xây dưng một lớp APIEngine

open class APIEngine {
    static let sharedInstance = APIEngine()
    func getDefaultParams() -> [String:Any] {}
    
    open func requestURL(withURL url: String, withHeader _headers:[String:String]!, withParams params:[String:Any]!, withHash hash:Bool, usingCache cache:Bool) -> MTResult {
        var headers = [String:String]()
        headers = ["Content-Type": "application/json"]
        if _headers != nil {
            for key in _headers.keys {
                headers[key] = _headers[key]
            }
        }
    
        var _params = self.getDefaultParams()
        if params != nil {
            for key in params.keys {
                _params[key] = params[key]
            }
        }
        if hash {   ....  }
        if(cache){ 
                // Get data from cache 
                
        }
        // Load new data
        let result = MTResult()
        result._usingCache = cache
        let strURL:String = self.getFullLinkWithSubPath(url)
        result.request = Alamofire.request(strURL, method: .get, parameters: _params, encoding: URLEncoding())
            .downloadProgress(queue: DispatchQueue.global(qos: .utility)) { progress in
                print("Progress: \(progress.fractionCompleted)")
            }.validate { request, response, data in
                return .success
            }
        debugPrint(result.request)
        return result;
    }
}

Mình xây dựng một lớp khá đơn giản trong đó chứa một function requestURL có truyền các giá trị headers : Có config thêm gì không nếu có thì append thêm vào headers mặc định trong function mà đã config. url : Có thể là API URL hoặc subpath. (Thông thường root url chúng ta config riêng mỗi khi thay đổi domain chúng ta chỉ cần thay đổi root url mà thôi). params : params của API. Trong lớp APIEngine mình cũng đã xây dụng một function getDefaultParams để get cách giá trị default của params, thông thường sẽ có nhiều trường mặc định trong params sẽ được sử dụng cho tất cả các API (appversion, build, timestamp, ...) hash : Một phương thức authentication phụ thuộc vào dự án của bạn dùng loại nào. Ở đây mình hay dùng md5 hoặc sha1. cache: Có sử dụng cache không. Nếu có thì trả về cache response sau đó request lên server lấy dữ liệu mới nhật trả về.

Xong phần này khá simple. Tiếp theo mình xấy dụng một class MTResult để handling response trả về từ Alamofire.

Handling the response from the Alamofire

Để xử lý response trả về cũng như xử lý các trường hợp lỗi khi request client-server mình xây dụng thêm một lớp nữa đặt tên là MTResult.

open class MTResult {
    var rq: DataRequest?
    var _onComplete:CompletionBlock?
    var _onError:ErrorBlock?
    var _usingCache:Bool = false
 
    func completeBlock(onComplete:@escaping CompletionBlock)->Self{ ...}
    func errorBlock(onError:@escaping ErrorBlock){...}
   
    var request:DataRequest!{...}
    func cacheAPI() {...}
    func loadAPIFromCache(){...}
}

Để sử lý action khi request success hoặc error mình sẽ khai báo hai block trong MTResult. var _onComplete:CompletionBlock?
var _onError:ErrorBlock? Đồng thời viết thêm 2 function để đăng kí các action

    func completeBlock(onComplete:@escaping CompletionBlock)->Self{
        _onComplete = onComplete
        return self 
    }
    
    func errorBlock(onError:@escaping ErrorBlock){
        _onError = onError
    }

Tiếp theo khái báo một attribute request có kiểu DataRequest và sử dụng syntax để detect response từ Alamofire

var request:DataRequest {
       get{ return rq! }
       set (newVal){
           newVal.responseJSON { (response) in
               switch response.result {
               case .success(let result):
                   // Nếu success thì callback về thông qua block onComplete
                   self._onComplete?(JSON(result), false)
               case .failure(let error):
                   print("Request failed with error: \(error)")
                   // Nếu error thì callback về thông qua block onError
                   self._onError?(error)
               }
           }
           rq = newVal
       }
   }

Về cơ bản là xong nhìn cũng khá clear không có chỗ nào quá khó cả 😄. Tiếp theo là làm thế nào để sử dụng nó. Minh lên khuyên các bạn chia nhỏ cái Engine ra để tiện quản lý. Với một dự án nhỏ thì không sao nhưng với dự án lớn việc viết tất cả các API vào một class là điều không lên. Khi mình xây dụng một lớp Engine mình sẽ không viết thêm function request api trên đó nữa nó chỉ có viết thêm các function support cho requestURL mà thôi (getDefaultParam(), getURL(withSubpath _subpath: String) ...). Giả xử để xử lý các api liên quan đến User, mình sẽ tạo ra một extension của APIEngine và tất nhiên là trên một file hoàn toàn mới mình hay đặt như trong Objective-C: APIEngine+user.swift hoặc APIEngine+newsfeed.swift để chứa các API liên quan đến news feed chẳng hạn.

Bây giờ mình muốn lấy một list các tin trên newsfeed mình sẽ viết thêm vào extension của APIEngine+newsfeed như sau.

import Foundation

extension APIEngine{
    func getNewsfeed(withStart _start: Int, withLimit _limit: Int, usingHash hash:Bool, usingCache cache:Bool) -> MTResult {
        let url:String = NEWSFEED_API_URL
        var params:[String:Any] = [String:Any]()
        params["start"] = _start
        params["limit"] = _limit
        return self.requestURL(withURL:url, withHeader: [:], withParams: params, withHash: hash, usingCache: cache)
    }    

   func getRelated(withNewsfeedID _newsfeedID: String, usingHash hash:Bool, usingCache cache:Bool) -> MTResult { ... }    
   ....
}

Request & Handling

Trong NewsfeedController bạn request lấy danh sách các news feed và xử lý khi gặp lỗi như sau

APIEngine.sharedInstance.getNewsfeed(usingHash: true, usingCache: false).completeBlock { (result, cache) in
            if let items = result["data"].dictionary {
                    let newsfeed = NewsfeedObject.createListObjectFromListDict(items)
                    self.updateWithListItem(listItems, listSection: listSection as [AnyObject])
             }else {
             // Data nill
             
             }
        }.errorBlock { (error) in
        // Error handling
            self.reloadCollectionView()
        }

Xong phần xử lý response từ Alamofire. Mỗi khi thêm một API bọn chỉ cần viết một function để khai báo params của nó, Endpoint, có sử dụng cache hay không có hash hay không mà thôi.

Kết Luận

Phần cache và xử lý load data từ cache hiện tại mình chưa kịp làm do thời gian có hạn trong bài này mình chỉ đưa ra một pattern mình đang dùng để xử lý response, error handling từ Alamofire mà thôi. Mong rằng nó giúp được mọi người :">


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí