Lập trình IOS: Triển khai MVVM cho project swift(phần 2)

Bài viết được sự cho phép của tác giả Lê Xuân Quỳnh

Hello guys! Trong bài trước chúng ta đã hiển thị 1 danh sách động vật theo mô hình MVVM rất đơn giản.

Trong bài này, chúng ta sẽ nâng độ khó lên bằng cách request data từ server thật, cụ thể là server của github. Và bài này tôi sẽ viết 1 lớp layer cho network mà bạn có thể bưng vào project của bạn luôn.

  5 bài học quí giá về việc phát triển ứng dụng iOS
  Cách làm một ứng dụng Chat cho Android & iOS bằng Contus Fly như thế nào?

Lần này chúng ta sẽ hiển thị danh sách các Repositories – các source code của Github và hiển thị lên app của mình. Hình ảnh như sau:

Danh sách repositories từ github server

Nhấp 1 ngụm cà phê và vào việc nào 

Đầu tiên source code vẫn là link bài 1:

https://github.com/codetoanbug/MVVMSample.git

Tuy nhiên tôi khuyên bạn chơi với terminal của MacBook cho nó pro nhé. Vì nếu bạn tải chay về bằng trình duyệt thì không thấy source code ở đâu đâu :v

Chơi với terminal đơn giản như sau:

  1. Bạn gõ lệnh cd vào source code hôm trước bạn tải về:
cd TableviewSample

2. Tiếp theo là gõ fetch để lấy source code mới nhất của tôi về:

git fetch

3. Tiếp theo là bạn show toàn bộ branch trên repo của tôi bằng lệnh:

git branch -a

Ở đây bạn sẽ thấy các branch sau:

  bai1
* bai2
  master

Ví dụ bài này, tôi để hết source vào branch tên là bai2. Bạn chuyển sang source code bài 2 như sau:

git checkout bai2

Sau khi branch bai2 được bôi đậm chuyển màu nghĩa là bạn đã thành công rồi đó. Còn nếu nó báo không thấy thì chứng tỏ bạn làm sai, hãy làm lại!

Vẫn chưa chạy được source nhá. Muốn chạy thì phải cài pod cho nó. Nếu máy bạn chưa cài pod thì gõ lệnh sau:

sudo gem install cocoapods

Nhập mật khẩu và bấm enter nhé. Còn nếu bạn cài rồi thì bỏ qua lệnh trên và gõ tiếp lệnh:

pod install

Nếu bạn vào thư mục và thấy như sau nghĩa là đã thành công:

pod sẽ tạo file mới bôi xanh

Và bây giờ hãy mở file TableviewSample.xcworkspace lên và tiến hành ngâm cứu nào 

Hãy chạy project lên và bạn sẽ thấy nó sẽ khác bài 1 với giao diện như sau:

Màn hình mới của app

Tôi đã tạo thêm 1 màn hình, có 2 nút bấm. Nút Local Animals List sẽ load lại ví dụ bài 1 hôm trước. Nút Remote repositories lists sẽ load ví dụ của bài hôm nay. Khi bạn bấm vào nút này nó sẽ show ra màn hình list như tôi đã trình bày ở đầu bài viết. OK, về cơ bản màn hình này tôi đã làm như sau:

  1. Tôi tạo table view như bài 1
  2. Tôi tạo 1 lớp network để call API
  3. Tôi tạo 1 service để viết API lấy data về show
  4. Tôi viết view model sử dụng service trên để lấy data về đổ vào view model
  5. Tôi bắn hết data sang view để hiển thị lên

Khi bạn tách các công việc riêng biệt như này, thì view sẽ chả quan tâm bạn lấy data từ local hay từ server. Nó chỉ quan tâm là view model sẽ trả về cho nó mà thôi. Và nó cũng không có xử lý dữ liệu gì cả.

Đúng phong cách của MVVM rồi đó nha. Còn bây giờ chúng ta hãy tìm hiểu qua về network moya trước nha.

Moya là gì?

Đầu tiên, chúng ta cần hiểu qua 1 số kiến thức cơ bản về mạng internet và cách gọi API từ server. Các ứng dụng ta dùng hằng ngày như mạng xã hội thì đa số sẽ dùng cơ chế gọi tới các API và lấy kết quả trả về. Cơ chế đó có thể là Restful, GraphQL. Ở đây chúng ta sẽ nghiên cứu về Restful, còn thằng GraphQL thì tạm thời học bài khác ha, facebook nó cũng chơi với mấy kiểu GraphQL đó, viết API 1 lần, online realtime luôn, nhưng mà bây giờ chưa phải lúc ta học mấy cái này, để giành lần sau tôi viết bài khác về topic này.

Vậy Restful là gì? Nói nôm na nó là cơ chế gửi request lên, nhận 1 cái json trả về. Sau đó ứng dụng của chúng ta phải đọc được cái json này và chuyển thành model để sử dụng trong app của chúng ta. Có thể dùng get, post, put… Để hiểu nó hơn, bạn hãy vọc qua 1 tí về cái API mà chúng ta sắp sửa request nhé.

Đầu tiên bạn tải cho tôi cái ứng dụng Postman, trước khi code gì bên IOS, thì bạn sẽ có 1 cái API từ bọn backend gửi cho bạn(hoặc bạn làm nó), và chạy nó xem kết quả thế nào. Postman tải ở đây:

https://www.postman.com/downloads/

Khi tải xong bạn cài đặt lên máy, tạo 1 cái get request và chép đường link vào như hình:

https://api.github.com/search/repositories?q=language:swift&sort=stars&order=desc

OK nếu bạn thấy như này nghĩa là thành công:

Hình ảnh Postman gửi get request

Bạn để ý thì mình dùng get để lấy dữ liệu. Ở đây là 1 API của github, dùng để request repositories với ngôn ngữ lập trình là swift. Trong nhiều trường hợp sau này với ứng dụng bạn làm, thì phương thức gửi lên là post. Post hay get thì tùy cách backend định nghĩa bạn nha. OK, vậy là chúng ta sẽ viết code để xử lý API này. Bạn vào đây để xem source code của Moya:

https://github.com/Moya/Moya

Trong phần readme của Moya, nó viết rất chi tiết bằng tiếng Anh cách mà nó hoạt động nha. Moya viết theo kiểu là tao cho mày các tham số đây để gọi 1 API, mày chỉ cần điền vào chỗ trống cho tao, việc còn lại tao làm. Mày điền phương thức là gì(get hay post..), đường dẫn, path, params mày gửi lên là gì, file để test ra sao.. Rồi việc còn lại tao lo cho mày hết. Đó là moya. Rất sexy phải không nào  OK còn cụ thể nó như nào thì đọc tiếp nhé.

Và bây giờ là bắt đầu viết lớp layer network cực kỳ quan trọng sau đây.

  1. Tạo lớp base để request network

Các bạn xem thư mục code base như sau:

Và cùng xem từng thành phần nhé. Nào bây giờ hãy ngắm mấy em mông to ngực bự hoặc làm 1 ít cà phê cho thư giãn đầu óc rồi tiếp tục nhé 

Đầu tiên là file Configs như hình:

Code file config

Ở đây tôi dùng kỹ thuật tự động chuyển server test hay production nhờ flag đơn giản của Xcode. Các bạn để ý struct Network nằm trong struct Configs, nghĩa là nếu sau này bạn cần cố định các giá trị config cho nó thì có thể tạo thêm các struct khác như Color, Dimention… Ở đây bạn chú ý cho tôi đoạn macro #if DEBUG #else. Câu lệnh này nghĩa là nếu như các bạn đang chạy chế độ Debug(chế độ gỡ lỗi) trên Xcode, thì đường dẫn server sẽ là dòng nằm trên, còn nếu các bạn chạy chế độ Release(khi upload lên Appstore) thì dòng dưới. Nghĩa là trình biên dịch Xcode sẽ tự động thay đổi cái baseUrl cho bạn tùy thuộc vào bạn đang chạy debug hay đẩy app lên store nhé.

Tôi dùng 2 dòng giống nhau vì tôi lười, nhưng thực tế nếu làm các dự án thật, bạn sẽ được cung cấp 2 server dev và release, khi đó bạn phải biết cách config như tôi chỉ nhé. Việc nhẹ lương cao cho người lười. Nói thêm, chạy chế độ Debug thì mặc định Xcode nó chọn. Bạn có thể chỉnh về release như sau:

Chỉnh edit Scheme
Chỉnh lại thành Release

Vậy tại sao lại có 2 chế độ này? Các bạn mới sẽ thắc mắc, mình nói đơn giản là nếu chạy debug thì Xcode thêm nhiều đoạn code ẩn vào chương trình nhằm mục đích gỡ lỗi, nên chương trình nặng hơn. Còn khi các bạn fix hết bug rồi, thì khi upload lên không cần phải có mấy đoạn code ẩn đó, cho nên mới sinh ra chế độ release. Mặc định các IDE khác đều xử lý như vậy cả nhé. Như android studio, visual studio..

Đơn giản đúng không nào? OK tiếp theo chúng ta vào file BaseError như hình:

File BaseError

File này 1 enum để tôi xử lý lỗi trả về từ server. Server chúng ta sẽ gặp lỗi cơ bản:

  • Một là lỗi request sai. Ví dụ thằng backend bảo mày gửi get lên nhé, nhưng bạn cố tình post. Hoặc nó bảo bạn truyền 2 param lên để lấy(như username, password để đăng nhập) thì bạn cố tình truyền 1 cái lên, thì cũng được gọi là request sai. Hoặc như mất kết nối và bạn request thì cũng gọi là sai. Đại loại sai thì nhiều loại lắm, bạn cứ hiểu thế cho nó đơn giản.
  • Tiếp theo là đã request lên, server có trả về kết quả. Tuy nhiên khi dịch cái file json của server thì bằng 1 cách nào đó nó sai. Có thể là trả về rỗng, trả về null, trả về không có mà chúng ta lại cố tình decode nó.

Ở đây có bạn hỏi mình vậy json anh nói là cái gì vậy? Cái này bạn hiểu nôm na nó là 1 cái object gồm key và value tương ứng. Còn nếu muốn biết thêm chi tiết xin vui lòng đợi mình viết bài khác hoặc google nhé.

Trong cái postman bạn làm ở trên cái json cũng trả về và bạn có thể nhìn sâu vào nó và đọc từng key và value tương ứng nha.

Rồi, vì mình vừa mô tả ở trên nên mình đã tạo enum như mình nói. Nó gồm 2 case tương ứng. Mỗi case mình lại có 1 title để hiển thị lỗi, và 1 cái description mô tả chi tiết lỗi.

OK, đến đây nếu bạn mông lung không hiểu tôi đang nói gì thì có lẽ bạn lại phải nghỉ ngơi thư giãn 1 tí và xem lại, vì phần sau đây là quan trọng nhất. Phần NetworkProvider là trong tâm của bài này. Các bạn xem file NetworkProvider như sau:

Ở dòng 9 tôi bắt đầu sử dụng thư viện Moya để viết lại lớp provider này. Provider nghĩa là cung cấp nha, nghĩa là tôi muốn refactor moya 1 tí cho dễ dùng hơn(mặc dù nó cũng dễ dùng rồi). Dòng 11 tôi đổi tên 1 tí cho nó ngắn.

Dòng 14 đến 18, tôi lại dùng kỹ thuật debug như trên, tôi chỉ show các log của moya khi chạy debug. Nếu bạn thấy khó chịu khi nhiều dòng chữ in ra ở màn hình console của Xcode, thì bạn có thể bỏ false hết ở 1 chế độ.

Dòng 20 đến 25, tôi tạo 1 protocol nhằm mục đích:

  • Nếu như tôi chạy chế độ defaultNetworking(), nghĩa là tôi gọi API trực tiếp lên server và nhận kết quả trả về.
  • Nếu tôi chạy chế độ stubbingNetworking(), nghĩa là tôi gọi API từ app luôn, và trả kết quả thông qua 1 file json ở dưới app. Do vậy bạn sẽ không cần server khi chạy chế độ này. Nó hay dùng để test API hoặc là khi thằng server đang bận bế vợ hay bồ nó chưa kịp viết thì bạn vẫn ok viết API chạy dưới app mà không phụ thuộc nó, miễn là nó cho bạn 1 file json kết quả trả về.

Đoạn code trên là hàm tạo cho cái provider, rất nhiều tham số của Moya và tôi cũng không muốn giải thích chi tiết nó làm gì. Nhưng mà bạn cứ viết y hệt vậy sau có thời gian vào thư viện Moya xem nó giải thích. Có 1 biến online nhằm mục đích kiểm tra xem mạng mẽo có hoạt động tốt không. Nhưng thực tế trong project này tôi không có xử lý, bạn có thể tự code xem nhé.

Hàm request API:

Hàm request trên tôi chọn chế độ mutil target, vì theo kinh nghiệm code của tôi thì khi chọn Mutil target, bạn có nhiều API, mỗi API 1 cụm nào đó ví dụ như app bạn có các cụm: login gồm (login, logout), Home gồm feed, hot feature… Thì khi xử lý mutil target ở trong Moya, bạn sẽ tách được nhiều file API riêng biệt cho dễ viết code và unit test. Target ở đây bạn hiểu nó là cái server bạn sẽ gọi đến, nó sẽ gồm các thành phần sau:

Nếu bạn đọc được tiếng Anh thì nó dễ hiểu đúng không? còn không thì tôi dịch nôm na thế này. Muốn call API bạn cần:

  • Biết base URL nó là gì
  • Path là gì? ví dụ của chúng ta https://api.github.com/search/repositories thì base URL là https://api.github.com/ còn path là search/repositories
  • Method là get, post, put, download…
  • Sample data là cái mà tôi nói bạn không cần server, chỉ cần 1 file json ở dưới app là có thể viết unit test cho API của bạn. Moya làm sẵn cho bạn để bạn viết test hoặc chạy app dưới local nha
  • Task thì có thể là request dưới dạng param là json gửi lên, hoặc parameter, hoặc plain. Đây là quy tắc call server nên bạn cũng chỉ cần hiểu nôm na nếu như thằng server nó bảo bạn tao bảo mày gọi lên bằng post thì mày chọn kiểu jsonEncoding cho tao. Còn nếu mày gọi là get thì mày chọn parameterEncoding cho tao. Tôi tạo sẵn cho bạn ở đây:

Trong nhiều trường hợp nếu bạn chọn sai thì code của bạn sẽ trả về lỗi không gọi được(gửi get thay vì postjsonEncoding thay vì parameterEncoding)… Rồi sau đó mất cả buổi chả hiểu mình sai chỗ nào. Chú ý cấu hình cho đúng nếu không đừng trách nước biển lại mặn 

Quay lại với cái provider ở trên, Moya trả về 2 case, 1 case success và 1 case là failed. khi call api success, thì nghĩa là response.statusCode == 200, tôi tiến hành dịch cái json đó thành object codeable của tôi.

 if response.statusCode == 200 {
                    guard let results = try? JSONDecoder().decode(T.self, from: response.data) else {
                        // Decode error
                        completion(.failure(BaseError.parseResponseDataFalse(title: target.path)))
                        return
                    }
                    DispatchQueue.main.async {
                        completion(.success(results))
                    }
                }

Đoạn code trên tôi try decode, mục đích là giả sử như bạn viết model để dịch cái response ra object của nó bị sai thì nó sẽ bắn ra lỗi nha(chỗ decode error), và return luôn.

Chú ý là tôi trả về completion hết vào main thread để đảm bảo rằng, UI của view khi có kết quả từ server sẽ xử lý trên main thread luôn nhé. Mặc định Apple chỉ cho phép xử lý UI trên main thread. Còn trường hợp case failed như sau:

Ở đây nghĩa là gọi API bị sai như tôi đã nói ở trên(mất mạng, gọi sai get, post, hay encode sai…) thì nó bắn vào đây. Tôi cũng completion thất bại như code. Tiếng anh completion nghĩa là hoàn thành, ở đây là hoàn thành việc gọi api nhé 

Đoạn code từ dòng 86 bạn thấy cũng na ná đoạn code trên, chỉ khác nhau cái trả về thôi. Ở API request trên là trả về 1 object codeable. Còn request dưới này là trả về mảng. Tôi viết sẵn cho bạn nếu như bạn muốn xử lý mảng nha.

Đoạn code xử lý chạy server real hay là server fake nà 

Như tôi nhắc 2 lần ở trên, đoạn này sẽ giúp bạn xử lý gọi API thật hay là gọi local nha. Cách dùng thì từ từ đọc tiếp nhá. Về cơ bản provider sẽ có những thứ quan trọng như vậy.

2. Tạo GithubAPI cho api của github

Rồi tiếp theo chúng ta sẽ viết GithubAPI cho github này. Sau này nếu có các cụm API khác như authen(bao gồm api Login và logout,…) thì bạn sẽ phải viết tách biệt ra 1 file khác nha.

Dòng 11 đến 13, các bạn xem lại Postman thì thấy rằng API gồm 3 tham số truyền lên:

Cho nên tôi lười tôi cũng đặt tên y hệt luôn như postman. Tên và param bạn nên đặt sát với API nhá. kiểu nó là string hết nha.

Tiếp theo từ dòng 15 trở đi, tôi bắt đầu mô tả cho API này.

  • Đầu tiên là base URL bằng việc lấy từ struct config đã trình bày.
  • – path thì là search/repositories(cũng đã trình bày ở trên).
  • Method là .get
  • sampleData là phần đọc file SearchRepositoriesResponse.json ở local như tôi đã trình bày. Bạn gọi dưới local thì nó sẽ lấy file này fake server trả về kết quả nha. File SearchRepositoriesResponse.json có nội dung y hệt cái response của backend nha bạn. Bạn có thể vào file đó mà xem.
  • Tiếp theo là headers, thường sẽ như dòng 59 nhé. Dòng 62 đến 70 sẽ là phần mô tả params truyền lên cho server:

Vậy là bạn đã hoàn thành viết API cho github nha.

3. Tạo service để gọi API github

Tiếp theo chúng ta phải viết 1 file service để gọi API. Các bạn vào file GithubSearchService:

File này đơn giản kế thừa từ BaseService. nó sẽ dùng provider của BaseService để gọi api của github. Bạn dùng MutilTarget để gọi, thì bạn sẽ tách được các cụm API riêng rẽ. Ví dụ trên là cụm GithubAPI, sau này có thể là AuthenAPI… Khi gọi request bạn cần truyền vào:

  • Target mà bạn muốn gọi. Ở đây là GithubAPI.searchRepositories
  • Kiểu model mà bạn muốn decode để trả về. Ở đây là GithubSearchResponse. Model này tôi tạo sau nhé.

Trong BaseService code như sau:

Ở đây tôi mặc định chế độ test là false. Nếu như bạn tạo service mà set isTest là true thì nó sẽ không bao giờ gọi lên server mà hoàn toàn lấy file json của bạn ở dưới local trả về kết quả. Nó phục vụ cho mục đích test API hay bạn không muốn đợi thằng backend viết xong API.

Rồi bây giờ sau khi xong file service thì bạn tiếp tục viết model. Nghe phức tạp nhỉ? viết code dài dòng vãi. Chắc các bạn mới sẽ phàn nàn với tôi như vậy. Nhưng khi mà bạn làm team lớn thì việc chia nhỏ sẽ như này sẽ hợp lý cho team work nè, và làm nhiều quen thì lại nghiện đó.

4. Tạo model để hứng dữ liệu từ server

Các bạn vào file SearchRepositoriesResponse.json, các bạn sẽ thấy rất nhiều key và value. Ở đây nếu bạn lười bạn có thể vào trang này để tạo model 1 cách nhanh chóng:

https://app.quicktype.io/

Các bạn chép cái json đó rồi parse vào nha. Đảm bảo nó ra 1 cái model luôn. Tuy nhiên vì tôi không cần tất cả data trong đó mà chỉ cần tên và đường dẫn để hiển thị cho nên tôi tạo đơn giản như sau(xem ở file GithubSearchResponse):

Model hứng dữ liệu từ server

Sau khi có model rồi thì các bạn tạo view model nhé!

5. Tạo view model để lấy dữ liệu từ service

Các bạn vào GithubViewModel để theo dõi code như sau:

  • Dòng 12 tôi dùng GithubSearchService. Thực tế dự án thật 1 view model có thể dùng nhiều services các bạn nhé.
  • Dòng 14 là biến closure để nhằm mục đích callback ra view, báo cho view reload lại table khi có kết quả từ server.
  • Dòng 15 tương tự nhưng báo lỗi và hiển thị lên màn hình.
  • Dòng 26 đến 42, tôi viết hàm gọi API từ view model, thông qua service. Có 2 trường hợp, nếu có kết quả tôi tiến hành lưu vào model nằm trong viewmodel và callback ra view để reload lại table. Nếu như thất bại tôi sẽ hiển thị lỗi. Rất clear đúng không nào 

Rồi bây giờ thì đơn giản rồi, việc cuối cùng từ view gọi view model.

6. Sử dụng viewmodel trong view

Các bạn vào file GithubViewController, nhưng tập trung đoạn code trên cho tôi.

  • Dòng 30 là tạo viewmodel nha.
  • Dòng 31 tới 22 là call back khi có kết quả từ server, thì tôi reload lại table để hiển thị kết quả.
  • Dòng 35 đến 37 là khi server báo lỗi, tôi hiển thị lỗi lên view controller.

Vậy là các bạn đã hoàn thành quá trình viết 1 chương trình theo kiến trúc MVVM để gọi api thật. Ở ứng dụng này tôi cũng hay sử dụng thực tế trong các dự án công ty hay cá nhân. Còn thiếu 1 mục quan trọng là unit test cho nó nhưng thôi, vì bài dài quá rồi nên ta để sau nha. Nếu như bạn thấy bài viết hay thì chia sẻ hộ mình. Còn nếu sai sót chỗ nào vui lòng comment ở dưới bài nhé.

OK, tôi tổng kết lại các mục chính cho bạn nắm vững khi viết MVVM thực tế:

  • Tạo provider để call API
  • Tạo file mô tả API của API đó
  • Tạo services để call API
  • Tạo model để hứng dữ liệu từ server
  • Tạo view model để sử dụng services
  • Cuối cùng là dùng view model trong view để hiển thị kết quả

Hi vọng bài viết sẽ giúp các bạn mới làm quen dễ dàng hơn và chuyên nghiệp hơn trong việc viết code MVVM trong dự án của bạn.

Bài viết gốc được đăng tải tại codetoanbug.com

Có thể bạn quan tâm:

Xem thêm Việc làm it swift hấp dẫn trên TopDev