Hướng dẫn chi tiết về phát triển ứng dụng Android sử dụng mô hình Clean Architecture
Kể từ khi bắt đầu phát triển ứng dụng Android tôi đã có cảm giác rằng nó có thể được thực hiện một cách tốt hơn. Tôi đã nhìn thấy rất nhiều quyết định thiết kế phần mềm "xấu" trong suốt sự nghiệp của tôi, một số trong số đó là của riêng tôi - sự phức tạp của Android được pha trộn với mô hình thiết ...
Kể từ khi bắt đầu phát triển ứng dụng Android tôi đã có cảm giác rằng nó có thể được thực hiện một cách tốt hơn. Tôi đã nhìn thấy rất nhiều quyết định thiết kế phần mềm "xấu" trong suốt sự nghiệp của tôi, một số trong số đó là của riêng tôi - sự phức tạp của Android được pha trộn với mô hình thiết kế phần mềm "xấu" mang lại một kết quả thảm hoạ. Nhưng điều quan trọng là học được từ những sai lầm của bản thân và liên tục cải thiện. Sau rất nhiều lần tìm kiếm cách tốt nhất để phát triển ứng dụng, và tôi đã tìm được mô hình Clean Architecture. Sau khi áp dụng nó vào Android, cùng một sàng lọc từ những dự án tương tự, tôi tháy rằng phương pháp này rất khả thi và có giá trị chia sẻ.
Mục tiêu của bài viết là hướng dẫn từng bước để phát triển ứng dụng Android một cách Clean.
Ở đây, tôi sẽ không đi vào chi tiết quá nhiều, các bài viết khác có thể giải thích tốt hơn điều mà tôi nói. Nhưng đoạn tiếp theo của bài viết sẽ cung cấp các điểm then chốt bạn cần để hiểu nhưng thế nào là Clean.
Nói chung trong Clean, mã code được tách thành các layer với một quy tắc phụ thuộc. Các layer bên trong không nên biết bất kỳ điều gì về các layer bên ngoài. Điều này có nghĩa rằng đó là quan hệ phụ thuộc nên “hướng” vào bên trong. Ta có thể hình dung mô hình Clean Architecture như sau:
Clean Architecture làm cho mã code của bạn:
- Độc lập với Frameworks
- Testable
- Độc lập với UI
- Độc lập với Database
- Độc lập với bất kỳ external agency
Tôi hy vọng sẽ làm cho bạn điểu được làm thế nào để đạt được các điểm then chốt với ví dụ dưới đây. Đối với một giải thích chi tiết về Clean, tôi khuyến khích bạn đọc bài viết này và video này.
Điều này có ý nghĩa gì với Android
Thông thường, một ứng dụng của bạn có thể có một số lượng tùy ý các layer, trừ khi bạn có các logic kinh doanh của doanh nghiệp để áp dụng vào từng ứng dụng Android, vậy một ứng dụng thông thường sẽ có 3 layers:
- Outer: Implementation layer
- Middle: Interface adapter layer
- Inner: Business logic layer
Implementation layer là nơi mà tất cả mọi thứ của framework xảy ra. Framework specific code bao gồm tất cả dòng code mà chúng không giải quyết các vấn đề mà bạn thiết lập để giải quyết, điều này bao gồm tất cả các công cụ Android như là tạo các activity, các fragment, gửi intent, networking và databases.
Mục đích của interface adapter layer là hoạt động như một kết nối giữa business logic và framework specific code.
Layer quan trọng nhất là business logic layer. Đây là nơi bạn thực hiện giải quyết các vấn đề - mục đích khi xây dựng app. Layer này không chứa biết kỳ framework specific code nào và bạn có thể chạy nó mà không cần emulator. Bằng cách này bạn có thể dễ dàng test, develop và maintain business logic code. Đó chính là lợi ích chính của Clean Architecture.
Đối với mỗi layer ở trên core layer đều có trách nhiệm chuyển đổi các model thành các model layer thấp hơn trước khi các layer thấp hơn có thể sử dụng đến chúng. Một inner layer không thể có một tham chiếu đến model class thuộc về outer layer. Mặc dù vậy, một outer layer lại có thể sử dụng và tham chiếu đến model class của inner layer. Một lần nữa, điều này được tạo ra bởi quy tắc phụ thuộc. Nó tạo ra “chi phí” nhưng nó thực sự cần thiết để chắc chắn rằng code được tách rời giữa các layer.
Tại sao việc chuyển đổi model là cần thiết? Ví dụ, các business logic model của bạn có thể không thích hợp cho việc hiển thị chúng đối với người dùng cuối, bạn có thể sẽ phải kết hợp nhiều nhiều business logic model cùng một lúc. Vì vậy, bạn nên tạo một lớp ViewModel để có thể dễ dàng hiển thị UI. Sau đó, hãy sử dụng một lớp converter ở outer layer để chuyển đổi các business model của bạn sao cho thích hợp với ViewModel. Hay một ví dụ khác: bạn có được một Cursor object từ một ContentProvider trong outer database layer. Sau đó đầu tiên outer layer sẽ chuyển đổi nó thành inner business model, rồi gửi nó hàng đã được convert cho business logic layer sau khi được xử lý.
Bây giờ chúng đã đã biết về những nguyên tắc cơ bản của Clean Architecture. Tôi sẽ cho bạn thấy làm thế nào để xây dựng một chức năng sử dụng Clean trong phần tiếp theo.
Tôi đã thực hiện một boilerplate project , nó có tất cả plumbing được viết cho bạn. Nó hoạt động như là một Clean starter pack và được thiết kế, xây dựng dựa trên các công cụ phổ biến nhất. Bạn có thể download free, và chỉnh sửa tùy ý.
Bạn có thể tìm thấy project đó ở đây: Android Clean Boilerplate.
Phần này sẽ giải thích tất cả code bạn cần viết để tạo một use case sử dụng phương pháp Clean, hãy tham khảo Android Clean Boilerplate đã nêu ở trên để nắm rõ hơn. Một use case chỉ là một chức năng riêng biệt của ứng dụng, một use case có thể hoặc không thể được bắt bắt đầu bởi người dùng.
Đầu tiên, cùng tìm hiểu về cấu trúc và thuật ngữ của phương pháp này. Đây là cách tôi xây dựng apps nhưng nó không được định sẵn và bạn có thể tự tổ chức lại nếu bạn muốn.
Cấu trúc
Cấu trúc phổ biến cho một Android app sẽ như sau:
- Outer layer packages: UI, Storage, Network, etc.
- Middle layer packages: Presenters, Converters
- Inner layer packages: Interactors, Models, Repositories, Executor
Outer layer
Như đã đề cập, đây là nơi mà framework hoạt động.
UI - nơi bạn sẽ đặt tất cả các Activity, Fragment, Apdapter và tất cả các đoạn code liên quan đến user interface.
Storage - Database specific code được implements các Interactors interface để thực hiện truy cập dữ liệu và lưu trữ dữ liệu. Ví dụ như ContentProviders hay DBFlow.
Network - những thứ như là Retrofit.
Middle layer
Đây là layer kết nối thực hiện business logic của bạn.
Presenters - xử lý các event từ UI (như là click, touch) và phục vụ các callback từ inner layer.
Converters - Các Converters object có trách nhiệm chuyển đổi inner models thành outer models và ngược lại.
Inner layer
Core layer chứa code ở mức level cao nhất. Tất cả các classlà các POJO. Các Class và Object tại layer này không có “knowledge” mà chúng đang chạy trong một Android app và có thể dễ dàng thực hiện trên bất kỳ máy chạy JVM nào.
Interactors - Đây là những class thực sự chứa business logic của bạn. Chúng được chạy ở background và giao tiếp event với layer trên sử dụng callbacks. Và chúng cũng được gọi là UseCase trong một số project. Bình thường sẽ có rất nhiều Interactor class nhỏ trong project của bạn để giải quyết các vấn đề cụ thể. Điều này tuân theo Single Responsibility và quan điểm của tôi.
Models - là mô hình business của bạn, nơi bạn tao thao tác các business logic.
Repositories - gói này chỉ chứa các interface mà database hay một số outer layer thực hiện. Các interface này được sử dụng bởi Interactors để truy cập và lưu trữ data. Cái này cũng được gọi là một repository pattern.
Executor - gói này chứa code để tạo Interactors hoạt động ở background bằng cách sử dụng một worker thread executor. Gói này thông thường không cần thay đổi gì cả.
Trong ví dụ này, use case của chúng ta sẽ là: Chào mừng người sử dụng bằng một message được lưu trữ trong database khi app được khởi động.
Ví dụ này sẽ giới thiệu các viết 3 package cần để thực use case:
- presentation package
- storage package
- domain package
Đầu tiên sẽ là outer layer và cuối cùng sẽ là inner/core layer.
Presentation package có trách nhiệm cho tất cả mọi thứ liên quan đến việc hiển thị cái gì trên màn hình - nó bao gồm toàn bộ MVP stack (có nghĩa là nó bao gồm cả UI và Presenter package mặc dù chúng thuộc các layer khác nhau).
Writing a new Interactor (inner/core layer)
Thực tế, chúng ta có thể bắt đầu viết bất cứ layer nào của kiến trúc, nhưng tôi khuyên bạn nên thực hiện triển khai core business logic đầu tiên. Bạn viết nó, kiểm tra nó và chắc chắn rằng nó hoạt động mà không cần tạo ra một activity.
Vì vậy, chúng ta hãy bắt đầu tạo một Interactor. Interactor là nơi logic chính của use case được thể hiện. Tất cả Interactor được chạy tại background nên nó sẽ không có bất kỳ ảnh hưởng gì đến hiệu suất UI. Hãy tạo một Interactor tên là : WelcomingInteractor.
public interface WelcomingInteractor extends Interactor { interface Callback { void onMessageRetrieved(String message); void onRetrievalFailed(String error); } }
Callback có trách nhiệm giao tiếp với UI tại main thread, chúng ta đặt nó bên trong Interactor interface. Bây giờ chúng ta sẽ thực hiển logic lấy một message. Hãy nói rằng chúng ta có một MessageRepository có thể cung cấp message welcome.
public interface MessageRepository { String getWelcomeMessage(); }
Bây giờ chúng ta sẽ thực hiện Interactor interface cùng với business logic. Điều quan trọng là extends AbstractInteractor cái sẽ quản lý background thread.
public class WelcomingInteractorImpl extends AbstractInteractor implements WelcomingInteractor { ... private void notifyError() { mMainThread.post(new Runnable() { @Override public void run() { mCallback.onRetrievalFailed("Nothing to welcome you with :("); } }); } private void postMessage(final String msg) { mMainThread.post(new Runnable() { @Override public void run() { mCallback.onMessageRetrieved(msg); } }); } @Override public void run() { // retrieve the message final String message = mMessageRepository.getWelcomeMessage(); // check if we have failed to retrieve our message if (message == null || message.length() == 0) { // notify the failure on the main thread notifyError(); return; } // we have retrieved our message, notify the UI on the main thread postMessage(message); }
Class này có nhiệm vụ lấy message và thực hiện gửi message hoặc message lỗi đến UI để hiển thị. Chúng ta sẽ thông báo cho UI sử dụng Callback, đó là mấu chốt của business logic. Tất cả mọi thứ mà chúng ta cần đó là framework phụ thuộc.
import com.kodelabs.boilerplate.domain.executor.Executor; import com.kodelabs.boilerplate.domain.executor.MainThread; import com.kodelabs.boilerplate.domain.interactors.WelcomingInteractor; import com.kodelabs.boilerplate.domain.interactors.base.AbstractInteractor; import com.kodelabs.boilerplate.domain.repository.MessageRepository;
Như bạn có thể thấy, nó không hề đề cập đến bất gì mã Android nào. Đó chính là lợi ích chính của phương pháp này. Bạn có thể nhìn thấy sự độc lập của framework. Ngoài ra, chúng ra không cần quan tâm đến chi tiết cụ thể nào của UI hay database, chúng ta chỉ cần gọi các interface method mà ở đâu đó trong outer layer sẽ thực hiện. Do đó, nó độc lập với UI và độc lập với Database.
Testing our Interactor
Bây giờ chúng ta có thể chạy Interactor mà không cần emulator. Vì vậy, chúng ta hãy viết một JUnit đơn giản để đảm bảo nó hoạt động:
@Test public void testWelcomeMessageFound() throws Exception { String msg = "Welcome, friend!"; when(mMessageRepository.getWelcomeMessage()) .thenReturn(msg); WelcomingInteractorImpl interactor = new WelcomingInteractorImpl( mExecutor, mMainThread, mMockedCallback, mMessageRepository ); interactor.run(); Mockito.verify(mMessageRepository).getWelcomeMessage(); Mockito.verifyNoMoreInteractions(mMessageRepository); Mockito.verify(mMockedCallback).onMessageRetrieved(msg); }
Một lần nữa, mã Interactor này không có ý tưởng rằng nó sẽ “sống” trong một ứng dụng Android. Điều này chứng tỏ rằng business logic là Testable, đây là điểm thức 2 tôi muốn nói.
Writing the presentation layer
Presentation thuộc outer layer trong Clean. Nó bao gồm các mã code phụ thuộc vào framework để hiển thị UI cho người dùng. Chúng ta sẽ sử dụng MainActivity class để hiển thị message khi app resumes.
Hãy bắt đầu viết interface choPresenter vàView. Điều duy nhất chúng ta cần phải làm là hiển thị message
public interface MainPresenter extends BasePresenter { interface View extends BaseView { void displayWelcomeMessage(String msg); } }
Vậy làm như thế nào và ở chỗ nào chúng ta sẽ thực hiện Interactor khi app resumes?
@Override protected void onResume() { super.onResume(); // let's start welcome message retrieval when the app resumes mPresenter.resume(); }
Tất cả Presenter object thực hiện resume() method khi chúng extend BasePresenter.
Lưu ý: Bạn có thể sẽ thấy rằng tôi đẽ thêm các methods củaAndroid lifecycle vào BasePresenter interface như một phương thức trợ giúp, mặc dùPresenter là một layer thấp hơn.Presenter không nên biết về bất kỳ điều gì trong UI layer. Tuy nhiên, tôi không xác định cụ thể một event cụ nào nào ở đây giống như bất kỳ UI nào được hiển thị. Hãy tưởng tượng tôi gọi nó làonUIShow() thay vì onResume() - It’s all good now, right?