Alamofire – Design Pattern in Swift 3
Bài đăng này đã không được cập nhật trong 8 năm
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
Bình luận