2016-11-28

Android evolution ... the right architecture?

Gần đây mình được hỏi sắp xếp, gợi ý giùm một cái simple architecture để phát triển Android app. Thật sự việc này rất là khó nhất là khi chỉ nói chung chung, và không nắm 1 team cụ thể nào.

Hiện tại việc phát triển ứng dụng Android hình như là thay đổi quá nhanh. Mỗi ngày đều có những libraries mới, tools mới (mình hay theo dõi trên Medium). Những lần dev Android apps từ những năm 2010 đến sau này gần giữa 2014 mình đều cố gắng bắt nhịp với sự phát triển của Android. Đợt này bỏ quá lâu, có quá nhiều thứ để tìm hiểu :(.

Khoảng trước năm 2011 việc dev có vẻ đơn giản, gọi Restful service, dùng SQLite, + disk cache, và có lẽ vậy là đủ, mọi thứ tổ chức sao cho clean nhất có thể. 2010, mình cũng có tham khảo Google I/O 2010 - Android REST client applications và tìm hiểu thêm. Thật ra presentation tại Google I/O chỉ trình bày hướng phát triển, cũng không chỉ rõ cách implementation. Một số code nói là demo lại thì chỉ được 1 phần, không take care hết các trường hợp như configuration changes, lifecycle, reference Activity ...

Từ RoboSpice đến Volley + EventBus

Khoảng 2010, trước RoboSpice, mình có thử dùng Droid-Fu (sau này chuyển thành Ignition), 1 dạng hack trực tiếp Activity lifecycle với việc sử dụng WeakReferences. Nói chung là cũng không thích lắm. Khoảng 2012 thì xuất hiện RoboSpice implement theo hướng sử dụng Service và cache request result, 1 hướng mà Google I/O đã đề cập. Nhưng code theo RoboSpice cũng khá là rườm rà, mỗi lần tạo request phải chỉ định cache key và các thứ liên quan, lúc đó RoboSpice cũng còn thay đổi khá nhiều mãi sau này mới hoàn chỉnh với addListenerIfPending() hay getDataFromCache(). Code cũng không sáng sủa là mấy khi Activity hay Fragment có quá nhiều việc để làm. Rất khó xác định cache bao lâu là vừa (với cách thực hiện hiển thị UI là lấy cache trước, request network sau "get cache first then request" ... sẽ đề cập sau). Việc xử lý rotation hay back với activity nhiều khi cũng không quá quan trọng. Lý do app nhiều khi chỉ để portrait mode và thật sự thì việc user nhấn back có vẻ bị "bi kịch hóa" hơi quá. RoboSpice có một cái theo mình rất dở là thích ôm quá nhiều thứ từ caching, OR mapping, parser, HTTP request... thành ra 1 đống khó kiểm soát.

Sau đó, 2013 thì Volley ra đời và cả EventBus cũng xuất hiện vào thời gian này. Mọi người chuyển sang xài Volley với suy nghĩ rằng nó là của Google :D, ko gọi là xài 3rd party library. Bên cạnh đó EventBus được xài để thực hiện loosely coupling. Lúc này mình cũng hay develop apps ko hỗ trợ rotation. Việc code thì đơn giản theo dạng Activity, Fragment nói chuyện với một dạng Repository thông qua EventBus, việc gọi Restful service được giao cho Volley. Tất nhiên dùng Volley và EventBus vẫn phải care vụ Acvitity leak về mặt kỹ thuật vẫn có thể xảy ra, nhưng về tổ chức code thì có vẻ clean hơn.

Tham khảo thêm bài viết Android Application Architecture của Iván Carballo hay
Robust and readable architecture for an Android app part 2 (part 1) của Joan Zapata.

https://github.com/JoanZapata/android-asyncservice/wiki






Một số tổng hợp về chủ đề này trên github của ziem

MVP, MVVM và Clean architect

Sau đó là đến thời gian mình tìm hiểu Clean Architecture của Robert C. Martin a.k.a Uncle Bob. Như Uncle Bob đã nói architecture là mục tiêu, ý định (intent) không phải là framework: "Architecture is about intent, not frameworks".

Một architecture tốt là một architecture cho phép một quyết định đột ngột có thể thực hiện một cách dễ dàng, bao gồm một số đặc điểm:
+ Dễ maintain: kể cả thêm/bớt tính năng, thay đổi yêu cầu (cả những tính năng chưa biết trước)
+ Dễ test
+ Decoupled (tạo từ những thành phần độc lập tương đối và liên kết yếu - independent and loosely-coupled components)

Khoảng thời điểm này có rất nhiều "architecture" từ những platform khác được áp dụng cho phát triển Android từ MVC, MVP tới MVVM. Thật ra nếu gọi architecture chung chung thì không đầy đủ, chỉ là cho phần tổ chức UI vì đây thực ra là những UI design patterns. Mình biết MVP, MVVM từ khi làm việc với .NET/C# (ASP.NET, WPF và Prism). Mình ko đi sâu chi tiết về các pattern này. Có thể tham khảo theo các bài viết của Martin Fowler (phân tích rất kỹ) về MVC, Passive View, Presentation ModelMVP.

MVVM (theo MSDN)


MVVM (được phát triển từ Microsoft) thông thường đi kèm với data bindings, commands không được ưa chuộng bằng MVP trên Android (có lẽ đa phần không thích binding Android Data Binding Library). 

Các project mẫu thực hiện MVP trên GitHub
https://github.com/googlesamples/android-architecture

https://github.com/googlesamples/android-architecture/tree/todo-mvp


Xem thêm series giới thiệu MVP của Tin Megali, Model View Presenter (MVP) in Android, bài viết MVP for Android: how to organize the presentation layer của Antonio Leiva.

Có thể kể đến các libraries thực hiện MVP architecture như Mosby, Nucleus... Theo ý kiến của mình thì ít nhất cũng phải xem qua các libraries này thực hiện MVP như thế nào. Nếu không thích tự mình viết MVP thì hoàn toàn có thể dùng luôn các library này. Một trong những tính năng được ưa thích của các libraries này là Presenter có thể survive qua configuration/orientation changes (sẽ đề cập kỹ phần sau).

Mosby là library đặt theo tên architect Ted Mosby trong series "How I met your mother", mình cũng thích series này :D. Hiện tại Mosby đã có version 3.0 SNAPSHOT. Mosby bao gồm 2 phần chính MVP và ViewState, một component nhỏ liên kết giữa Presenter và View để giải quyết orientation changes.

Tương tự Mosby Nucleus cũng có tính năng tự động re-attach background task vào view mới khi orientation changes.

Mục đích đầu tiên MVP là cho phép thực hiện test dễ dàng hơn (nhất là unit tests với mocking). Nhưng theo cá nhân mình suy nghĩ thì dùng MVP cũng nhiều overhead. Đầu tiên là phải khai báo và quản lý quá nhiều interface. Những tính năng thông thường như login đôi khi phải khai báo vài interface trong khi dev nào cũng có thể hình dung login với username/password còn những thứ khác cũng chỉ là thứ yếu? Việc cố gắng tách Presenter khỏi Android có thể cần rất nhiều mô tả về UI logic từ những thứ rất nhỏ như show/hide loading progress.

Đôi khi việc phát triển app trên Android cũng ko quá phức tạp mà đòi hỏi về thời gian phát triển nhiều hơn, vòng đời của app cũng khá ngắn → thông thường người dev Presenter cũng là người dev View, việc mô tả lại UI logic và implement cho Presenter là quá tốn công (lại khá chán). Nhiều khi mình thiên về sử dụng một dạng simple như mô tả của Joan Zapata ở trên.

Tất nhiên nếu sp có vòng đời lâu và testing thật sự quan trọng thì code theo MVP không có vấn đề gì. Còn nếu không quá cần thiết thì có thể bỏ qua trong một vài trường hợp. Với một developer cứng tay dù app đã viết theo cách nào (tất nhiên có tổ chức) thì khi join team, developer này cũng có thể nhanh chóng bắt kịp. Và rồi thì những công nghệ chúng ta đang dùng cũng sẽ trở thành dead technologies ngày nào đó giống như "good code today, legacy code tomorrow".

Theo như Robert C. Martin a.k.a Uncle Bob, Clean Code: A Handbook of Agile Software Craftsmanship

"Who can justify the expense of a sixlane highway through the middle of a small town that anticipates growth? Who would want such a road through their town?"

"Ai có thể biện minh/thuyết phục người khác về chi phí để làm một con đường cao tốc 6 làn xe qua một thị trấn nhỏ rằng sẽ dành cho việc tăng trưởng được tiên đoán trước. Ai muốn có một con đường như vậy đi qua thị trấn của họ".

Kết hợp với RxJava
Nếu sử dụng MVP thì có lẽ sẽ thiếu sót nếu bỏ qua RxJava. Có thể tham khảo việc kết hợp RxJava và MVP như architecture của Iván Carballo, gọi là "MVP-based architecture powered by RxJava".

MVP-based architecture
https://labs.ribot.co.uk/android-application-architecture-8b6e34acda65#.tgds3woxp

Lưu ý trong architecture này event bus chỉ dùng rất "hạn chế" không dùng cho những event chỉ liên quan đến một màn hình mà liên quan đến brocasting toàn bộ như user logged out event.

MVP presenter và Activity/Fragment lifecycle, view state

Việc thực hiện MVP có cả ngàn cách. Và thật sự thì MVP cũng chỉ là chỉ dẫn về architecture còn việc thực hiện ntn thì vẫn tùy thuộc vào implementation. Một câu hỏi quan trọng đặt ra là mối quan hệ giữa Presenter và Activity/Fragment lifecycle và view state ntn?

Theo định nghĩa của Presenter thì không có chức năng nào liên quan đến lifecycle và việc persistence view state. Đầu tiên hãy tham khảo bài viết Presenters don't need lifecycle events của Hannes Dorfmann. Tóm tắt như sau: tác giả thấy ko có lý do gì để Presenters có những callbacks liên quan đến Activity/Fragment lifecycle như onCreate(), onPause() và onResume()... 

Việc bắt Presenter quản lý luôn cả lifecycle của Android là không hợp lý. Tại sao lại focus vào những view phức tạp như Fragment? Và nếu đem "phức tạp" đó vào Presenter thì nó cũng sẽ thành 1 đám hỗn độn. Team SoundCloud đã tạo một library là LightCycle để "tách vấn đề này ra" nhưng nói chung "liên quan" Presenter chỉ là đứng riêng ra. Team của Trello thì tạo RxLifecycle tiếp cận lifecycle bằng RxJava. Theo Hannes Dorfmann walk-around là tách khỏi View những logic liên quan đến lifecycle bằng cách thêm 1 layer.

Có thể xem thêm một số thông tin ở các bài viết:
  1. Android MVP - Part 2: Presenters and view state của Nathan Zylbersztejn  
  2. Android code that scales, with MVP của Nathan Barraille.
  3. MVP - Presenters that survive configuration changes của Brad Campbell
Tóm lại vấn đề Presenter và lifecycle có thể liệt kê một số cách giải quyết như sau:
  1. Để Presenter die khi Activity/Fragment die và restore state thông qua onSaveInstanceState (liên quan đến vụ callbacks). Việc này có thể thực hiện với các data đơn giản (có thể serialization được, Parcelable với Bundle). Nhưng sẽ phức tạp khi Presenter reference tới các object khó thực hiện serialization hoặc reference tới một background operation.
  2. Dùng Presenter pool, một dạng static caching đâu đó. Nếu dùng cách này thì phải chú ý đến trường hợp có nhiều Fragment cùng loại, cần tổ chức key của Presenter. Static caching cũng ko được đẹp đẽ cho lắm nhất là khi số lượng Presenter khá nhiều.
  3. Một cách khác là dùng headless Fragment (nghe giống kị sĩ không đầu :D), tên do community gọi Fragment không có UI và gọi setRetainInstance(true). Cách này có thể dùng với Dagger để Inject Presenter với fragment scope.
  4. Sử dụng Loaders và cache.
  5. Sử dụng các libraries hỗ trợ tính năng này như MosbyNucleus.

Unidirectional UI pattern/data flow

OK, mình tiếp tục liệt kê thêm những lựa chọn khác.

Flux, "Flux is the application architecture that Facebook uses for building client-side web applications.", là kiến trúc được Facebook dùng để build ứng dụng web đi chung với React.
--- và ---
Redux, "Redux is an application architecture inspired by Facebook Flux and and functional programming language Elm.", một application architecture "lấy cảm hứng" từ Flux và Elm.

Cả hai architecture đều có mấu chốt là dòng dữ liệu 1 chiều unidirectional data flow và đều là architecture cho web application (rất hot với front-end development).

Để thực hiện theo Redux thì khó khăn đầu tiên là phải suy nghĩ "theo Redux". Redux muốn developer nghĩ rằng ứng dụng bắt đầu với initial state và sẽ tương tác với một dòng actions (đến đây thì sẽ thấy bóng dáng reactive và functional programming). Nó đóng vai trò một container chứa state của ứng dụng.

Tất nhiên khi nói tới Flux và Redux thì không thể không nhắc React vì các kiến trúc này rất phù hợp với cách hoạt động của React (cho phần View).

Giới thiệu qua đã đủ, bây giờ tập trung cho Anroid. Nếu đồng ý với nhận định Android app không quá phức tạp như back-end và có vẻ giống client-side web app hơn thì đầu tiên hãy đọc một bài rất hay của Luis G. Valle Flux Architecture on Android.
"Well you have to deal with platform issues: memory, storage, pause, resume, network, location, etc. But that is not your app business logic. You have all of that in every app."



Với kiến trúc này thì mọi thứ có vẻ gọn hơn với 4 phần như diagram, nhưng để ý lại thì nó tập trung phức tạp rất nhiều lên Store. Library hỗ trợ Flux trên Android khá ít, ngay cả Facebook khi mới đề xuất về Flux cũng không hướng dẫn cụ thể thực hiện ntn.

Tuy nhiên ý tưởng của Facebook được mọi người ủng hộ và cải tiến thành Redux. Trikita có implement một library gọi là Jedux xem bài viết Writing a Todo app with Redux on Android.

Dù với UI pattern nào thì các vấn đề cần giải quyết với Android cũng vẫn như cũ và cần phải tìm hiểu thêm. Chi tiết thực hiện app với Redux có lẽ sẽ bàn kỹ ở một bài khác.


No comments:

Post a Comment