Tìm hiểu về thư viện Moya
Bài đăng này đã không được cập nhật trong 5 năm
Moya là một thư viện trừu tượng . Nó cung cấp một cách trừu tượng để thực hiện call network mà không cần làm việc trực tiếp với Alamofire.
1. Tại sao lại Moya?
- Giúp quản lý API ngay lập tức.
- clean code và dễ thực hành.
- Tập trung vào các tính năng cốt lõi thay vì kết nối mạng.
- Dễ Dàng cho stub network response dễ dàng cho việc viết Unit test.
2. Chuẩn bị
Bây giờ sẽ cùng mình xây dựng base API kết hợp với Moya nhé ^_^:
Mình sẽ sử dụng Json Movie:
Đầu tiên mình sẽ tạo Movie Model adopt Decodable:
struct Movie {
let id: Int
let posterPath: String
let backdrop: String
let title: String
let releaseDate: String
let rating: Double
let overview: String
}
extension Movie: Decodable {
enum MovieCodingKeys: String, CodingKey {
case id
case posterPath = "poster_path"
case backdrop = "backdrop_path"
case title
case releaseDate = "release_date"
case rating = "vote_average"
case overview
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MovieCodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
posterPath = try container.decode(String.self, forKey: .posterPath)
backdrop = try container.decode(String.self, forKey: .backdrop)
title = try container.decode(String.self, forKey: .title)
releaseDate = try container.decode(String.self, forKey: .releaseDate)
rating = try container.decode(Double.self, forKey: .rating)
overview = try container.decode(String.self, forKey: .overview)
}
}
Note: Khi bạn muốn tên các property của model khác với tên do API cung cấp bạn cần implement CodingKey enum. Nó sẽ cho Swift biết chính xác cách ánh xạ Json của bạn.
Tạo MovieResults Model:
struct MovieResults {
let page: Int
let numberOfResults: Int
let numberOfPages: Int
let movies: [Movie]
}
extension MovieResults: Decodable {
private enum ResultsCodingKeys: String, CodingKey {
case page
case numberOfResults = "total_results"
case numberOfPages = "total_pages"
case movies = "results"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResultsCodingKeys.self)
page = try container.decode(Int.self, forKey: .page)
numberOfResults = try container.decode(Int.self, forKey: .numberOfResults)
numberOfPages = try container.decode(Int.self, forKey: .numberOfPages)
movies = try container.decode([Movie].self, forKey: .movies)
}
}
Tạo API Enum:
Tạo 1 enum chứa tất cả các endPoint (chứa các path url) của bạn. Mỗi case chứa các parameter mà bạn truyển đến endpoint cụ thể
enum MovieApi {
case recommended(id:Int)
case popular(page:Int)
case newMovies(page:Int)
case video(id:Int)
}
Note:
- Tạo mỗi case cho mỗi endPoint.
- Khi bạn có một endPoint yêu cầu nhiều tham số, thay vì bạn phải truyền nhiều tham số vào func nhìn code ko được đẹp ban chỉ cần truyền 1 dictionary vào làm tham số :
// First import Alamofire to make use of ‘Parameters’
case endPointWithLotsOfParams(parameters: Parameters)
3. Implement thư viện Moya:
Chúng ta tạo Target Type Protocol và xây dựng trình quản lý Network Manager, Dependency Injection and Plugins.
Target Type Protocol:
Bạn đã tạo enum API cùng với list các endpoint, chúng ta cần import Moya và adopt Target Type Protocol . Moya’s Target Type protocol gồm of 7 properties: baseUrl, path, method, sampleData, task, validate, header.
Các bạn thêm đoạn code sau vào :
extension MovieApi: TargetType {
var baseURL: URL {
guard let url = URL(string: "https://api.themoviedb.org/3/movie/") else { fatalError("baseURL could not be configured.")}
return url
}
var path: String {
switch self {
case .recommended(let id):
return "\(id)/recommendations"
case .popular:
return "popular"
case .newMovies:
return "now_playing"
case .video(let id):
return "\(id)/videos"
}
}
var method: Moya.Method {
return .get
}
var sampleData: Data {
return Data()
}
var task: Task {
switch self {
case .recommended, .video:
return .requestParameters(parameters: ["api_key": NetworkManager.MovieAPIKey], encoding: URLEncoding.queryString)
case .popular(let page), .newMovies(let page):
return .requestParameters(parameters: ["page":page, "api_key": NetworkManager.MovieAPIKey], encoding: URLEncoding.queryString)
}
}
var headers: [String : String]? {
return ["Content-type": "application/json"]
}
// stuff
}
4. Thực hiện
Create class NetworkManager and một protocol Networkable Procol này sẽ định nghĩa các property and method mà bạn yêu cầu người quản lý bạn có:
protocol Networkable {
associatedtype T: TargetType
var provider: MoyaProvider<T> { get }
}
struct NetworkManager: Networkable {
static let MovieAPIKey = "myKey"
let provider = MoyaProvider<MovieApi>(plugins: [NetworkLoggerPlugin(verbose: true)])
func getNewMovies(page: Int, completion: @escaping ([Movie])->()){
provider.request(.newMovies(page: page)) { result in
switch result {
case let .success(response):
do {
let results = try JSONDecoder().decode(MovieResults.self, from: response.data)
completion(results.movies)
} catch let err {
print(err)
}
case let .failure(error):
print(error)
}
}
}
}
Dependency Injection
Các dev hay có thói quen viết lớp network layer.Họ gọi ở lớp ViewController bằng cách sử dụng singleton mà họ có thể truy cập mọi lúc mọi nơi. Sử dụng Singleton rất khó cho việc test.Vậy làm thế nào để bạn sử dụng NetworkManager ở trong lớp ViewController? Bạn có thể sử dụng Dependency Injection.Đơn giản bạn chỉ cần truyền NetworkManager vào hàm init của Viewcontroller.
let provider = NetworkManager()
let appStartVC = MainViewController(networkProvider: provider)
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.rootViewController = appStartVC
self.window?.makeKeyAndVisible()
Bạn truyền object NetworkManager vào cho lớp MainViewController nghĩa là là bạn inject instance NetworkManager vào MainViewController.
class MainViewController: UIViewController {
var networkProvider: NetworkManager!
init(networkProvider: NetworkManager) {
super.init(nibName: nil, bundle: nil)
self.networkProvider = networkProvider
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Bạn làm như trên thì bạn dể kiểm soát object NetworkManager. Làm cho mã ít bị dính chặt và phụ thuộc vào nhau hơn -> Dễ viết Unit Test.
Plugins
Moya có sẵn một số plugin tiện lợi mà bạn dễ sử dụng.Nó có thể log ra tất cả các hoạt động mạng trên console nếu có gì đó xảy ra có thể là linh url request trên server, data trả về, error trả về ...
let provider = MoyaProvider<MovieApi>(plugins: [NetworkLoggerPlugin(verbose: true)])
5. Kết luận:
Bằng cách dùng Lib Moya giúp cho bạn có thể enum adopt protocol TargetType. Với việc dùng sử dụng nó sẽ giúp các dev mới dễ phát triển đọc và hiểu ngay lập tức. Cám ơn các bạn đã theo dõi bài viết của mình. Bạn có thể tham khảo source code: https://github.com/dunglh-1464/MovieMoyaDemo
Tài liệu tham khảo:
https://medium.com/flawless-app-stories/getting-started-with-moya-f559c406e990
All rights reserved