Modern Android Architectures – MVC/MVP/MVVM – Phần 1: Giới Thiệu Các Mô Hình Kiến Trúc – Yellow Code Books

Rate this item:

Rating: 4.7/5. From 17 votes.

Please wait…

Được chỉnh sửa ngày 18/02/2022.

Chào mừng các bạn đã đến với loạt bài viết về chủ đề Modern Android Architectures.

Thực sự thì rất lâu rồi mình không viết bài nào mới cả. Điều này làm mình rất áy náy và ngứa ngáy. Mình sợ để lâu nữa sẽ không ai thèm ngó ngàng gì đến blog của mình nữa. Đùa thôi chứ mình biết nhiều bạn vẫn thường vào blog của mình để xem có bài viết mới chưa í mà.

Do đó ngay khi quyết định quay lại và duy trì lượng bài viết mà mình đã giữ nhịp trong suốt mấy năm qua, mình muốn nói ngay đến chủ đề mà nhiều bạn cũng đã đặt câu hỏi từ lâu, đó là chủ đề về Modern Android Architectures. Chính là nói về các kiến trúc theo các khuôn mẫu (pattern) MVC, MVP hay MVVM.

Với Phần 1 hôm nay mình sẽ nói sơ qua về tổng quan các khuôn mẫu kiến trúc này, chúng là gì và tại sao chúng ta lại cần một khuôn mẫu như thế này. Và chúng ta sẽ cùng xây dựng một ứng dụng nhỏ không phải là bất kỳ khuôn mẫu nào trong số cái mình đã kể ra trên đây, để có thể so sánh với khuôn mẫu ở các bài viết sau đó mà.

Modern Android Architectures Là Gì?

Modern Android Architectures hay nhiều tài liệu cũng chỉ gọi là Architecture, hay Kiến trúc, gì gì đó. Nói chung ở mức tổng quan nhất thì các tên gọi về lĩnh vực này mình thấy nó không có sự thống nhất với nhau. Tuy nhiên chúng đều đang nói đến một vấn đề, đó là cách bạn tạo ra và tổ chức các lớp bên trong một ứng dụng Android theo một phân cấp thư mục (hay package) như thế nào. Nói cho dễ hiểu hơn, là khi bạn đã hiểu và theo sát một Kiến trúc nào đó, thì bạn sẽ biết với một màn hình chức năng của ứng dụng, bạn sẽ phải tạo ra các lớp nào (ngoài lớp giao diện Activity hay Fragment), và để chúng vào các thư mục nào nữa. Mục đích tại sao phải theo các Kiến trúc này thì mình nói sau.

Chung quy lại cộng đồng lập trình viên tổng hợp thành 3 Kiến trúc được xem là phổ biến và thích hợp nhất cho việc xây dựng ứng dụng Android, được nhắc đến với 3 cái tên MVC, MVPMVVM.

Chà, nói vậy thì sẽ có khá nhiều Kiến trúc, biết vận dụng Kiến trúc nào bây giờ? Đó cũng là câu hỏi mà rất rất nhiều các lập trình viên đã đặt câu hỏi trên các diễn đàn hỏi đáp nổi tiếng. Cũng có rất nhiều phản hồi nhưng rốt cuộc thì sử dụng Kiến trúc nào là do bạn cả thôi. Mỗi loại Kiến trúc sẽ có cái hay, cái dở riêng. Mỗi loại sẽ phù hợp với từng quy mô dự án riêng. Và tùy theo sự thích thú của bạn vào Kiến trúc nào mà bạn sẽ quyết định dùng nó cho dự án mà bạn hoặc công ty bạn đang phát triển nữa.

Tại Sao Phải Tìm Hiểu Về Modern Android Architectures

Chắc hẳn câu hỏi này hiện lên trong đầu bạn ngay khi bạn đọc tiêu đề của bài viết này. Tất nhiên rồi, các Kiến trúc này là cái quái gì mà bắt bạn phải bận tâm?

Đúng vậy, có thể nói những năm đầu tiếp cận vào lập trình, mình cũng chẳng để ý lắm đến việc tổ chức các lớp bên trong ứng dụng theo kiểu Kiến trúc gì đó đâu, mặc dù khi đó mình biết có những người bạn của mình đã và đang xây dựng ứng dụng của họ theo một Kiến trúc nhất định nào đó rồi. Và khi đó mình đã nghe nhiều lời khen từ những người “đi trước” đó. Tuy nhiên mình vẫn không thèm áp ụng Kiến trúc nào cả, một phần vì… lười tìm hiểu, phần còn lại là vì cho dù chẳng cần biết một Kiến trúc nào, thì mình cũng xây dựng thành công nhiều ứng dụng đó thôi. Chắc nhiều bạn cũng đồng ý với suy nghĩ này.

Tuy nhiên, mình viết bài này một phần cũng mong các bạn nhanh chóng tiếp cận và tập làm việc theo các nguyên tắc nhất định, nhất là lập trình, và cụ thể trong bài này là các Kiến trúc của ứng dụng.

Thứ nhất, các Kiến trúc mà chúng ta đang tìm hiểu nó đều là các Kiến trúc theo Khuôn mẫu (Pattern). Mà bạn biết đã gọi là Pattern thì đó là các nguyên tắc, các ràng buộc, các hướng dẫn với mục đích chính là làm rõ nghĩa hơn những gì bạn xây dựng. Và giảm thời gian bảo trì, tức giảm thời gian và công sức khi bạn chỉnh sửa hay thêm mới các tính năng vào các ứng dụng mà bạn đã xây dựng từ lâu. Thực ra nếu bạn từng xây dựng một ứng dụng rất lớn, đến cả trăm, thậm chí cả ngàn lớp, hoặc có quá nhiều dòng code trong các lớp của project của bạn, thì bạn mới thấy rõ nhu cầu của việc tổ chức Kiến trúc ngay từ đầu (mà vậy thì đã quá muộn để thay đổi).

Thứ hai, vì nó là các Khuôn mẫu, nên nó giúp những người làm cùng với bạn hiểu rõ bạn đã làm gì, nếu như họ đọc code của bạn đã viết. Hoặc bạn cũng sẽ dễ dàng nắm bắt ý tưởng và bắt tay vào sửa lỗi hoặc xây dựng thêm các tính năng từ code của người khác (và của chính bạn nữa).

Thứ ba, việc tổ chức lớp ra từng thành phần theo từng loại Kiến trúc như vậy sẽ giúp bạn dễ dàng xây dựng các Phương thức Test cho riêng các lớp chức năng chuyên biệt. Đây là một vấn đề khác mà mình hi vọng sẽ có chuỗi bài viết riêng về nó sau này.

Bắt Tay Xây Dựng Ứng Dụng

Chúng ta sẽ không nói lý thuyết suông. Với bài số 1 này chúng ta cùng nhau xây dựng một ứng dụng “thuần túy” của Android, tức không hề có bất cứ Kiến trúc nào cả. Nói như vậy không có nghĩa là chúng ta xây dựng “đại” một project với các lớp được để chung hết vào một package. Chúng ta vẫn phải tổ chức các lớp theo các package riêng biệt, có điều sự tổ chức này là “tùy tiện” theo sở thích của chúng ta mà thôi. Bạn sẽ thấy với cùng một ứng dụng hôm nay, chúng ta sẽ thay đổi chúng theo các Kiến trúc ở các bài sau như thế nào nhé.

Chúng ta sẽ xây dựng một ứng dụng có kết nối với Web Service. Ồ sẽ không cần bạn phải tạo một Web Service để mobile kết nối vào đâu, mình thấy trên mạng có khá nhiều các Web Service được xây dựng sẵn để chúng ta thực tập, một trong số đó có thể kể đến là trang restcountries.com này. Web Service này cho chúng ta các RESTful API, khi kết nối đến sẽ trả về các dữ liệu liên quan đến các quốc gia trên thế giới.

Để kết nối với một Web Service thông qua một RESTful API thì mình sẽ dùng đến Retrofit kết hợp với RxJava. Nếu các bạn muốn biết cách sử dụng RetrofitRxJava thế nào thì có thể xem ở link mình để sẵn. Bài viết này mình sẽ không nói đến Retrofit hay RxJava, bạn có thể code theo hướng dẫn của bài học để ứng dụng chạy được, để hiểu các Kiến trúc trong lập trình là chính, mình sẽ nói đến các kiến thức liên quan này ở các bài cụ thể khác sau nhé.

Tổng Quan Project

Project của tất cả bài viết trong chủ đề Modern Android Architectures này đều có chung một kết quả màn hình như sau.

Màn hình ứng dụng mẫuMàn hình ứng dụng mẫuMàn hình ứng dụng mẫu

Như đã nói, màn hình chính sẽ kết nối với Web Service. Mục đích của project chỉ để lấy về danh sách các quốc gia kèm tên thủ đô của mỗi quốc gia đó rồi hiển thị lên màn hình chính này. Chúng ta cũng xây dựng một vùng tìm kiếm ở phía trên để hỗ trợ người dùng tìm kiếm nhanh một quốc gia thay vì phải cuộn và tìm trên danh sách. Khi click vào một quốc gia nào đó sẽ hiển thị một message dạng Toast, lát nữa khi xây dựng ứng dụng bạn sẽ biết chúng ta sẽ hiển thị gì cho Toast sau nhé.

Còn bây giờ thì hãy tạo mới project nào.

Tạo Mới Project

Bạn hãy tạo một project, rồi đặt tên ModernAndroidArchitectures, hoặc bất cứ tên nào mà bạn thích nhé. Project này sẽ được viết bằng ngôn ngữ Kotlin.

Tạo mới ProjectTạo mới ProjectTạo mới Project

Trong trường hợp bạn muốn biết nhiều hơn về cách tạo một project Android thì có thể xem thêm ở bài viết này.

Khai Báo Thư Viện

Bước đầu tiên nhất, chúng ta cần phải cấu hình cho build.gradle sao cho có thể sử dụng được thư viện Retrofit, RxJavaRecyclerView (để hiển thị danh sách). Bạn hãy mở file build.gradle ở cấp độ module lên và thêm vào các dòng tô sáng sau vào khối dependencies.

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'

    // Retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.8.1'
    implementation 'com.squareup.retrofit2:converter-scalars:2.1.0'

    // RxJava
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
    implementation 'io.reactivex.rxjava2:rxjava:2.2.21'

    // RecyclerView
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
    
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

Cũng trong file này, bạn hãy xem trong khối android có khai báo đoạn tô sáng dưới đây không, nếu không có thì hãy thêm vào. Với khai báo này chúng ta sẽ dễ dàng sử dụng View Binding để tương tác với các view ở XML sau này.

android {
    ...
    buildFeatures {
        viewBinding = true
    }
}

Giờ thì bạn có thể tìm và nhấn nút sync lại file build.gradle này.

Tiếp theo, bạn hãy mở file Manifest lên để chúng ta đăng ký quyền kết nối Internet cho ứng dụng. Không có quyền này thì ứng dụng chúng ta sẽ không thể kết nối đến Web Service để lấy kết quả JSON về đâu nên bạn đừng quên bước này nhé. Việc xin quyền được khai báo bằng dòng tô sáng như sau.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.yellowcode.modernandroidarchitectures">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".activity.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Tổ Chức Kiến Trúc Theo Kinh Nghiệm Cá Nhân

Vâng chúng ta sẽ dành sự tập trung cho các Kiến trúc MVC, MVP, MVVM ở các phần sau. Phần này mình xin phép được đưa ra “kiến trúc” theo kinh nghiệm của mình ở nhiều project trước khi chính thức tìm hiểu và áp dụng một Kiến trúc chuẩn. Mình gọi tắt Kiến trúc-theo-kinh-nghiệm-cá-nhân này là Kiến trúc custom cho ngắn gọn nhé.

Tuy nói là theo kinh nghiệm cá nhân, nhưng việc tổ chức theo dạng này đã được mình tham khảo khá nhiều, và có đưa vô một vài kinh nghiệm bản thân. Và mình cũng có hướng dẫn các bạn tổ chức theo kiểu Kiến trúc này ở nhiều nơi khác. Bạn cũng sẽ thấy quen thôi với cách tổ chức này. Hãy xem nào.

Bạn hãy vào tab bên trái Android Studio và chuyển view về dạng Project trước cho việc cấu trúc được dễ dàng hơn. Sau đó hãy mở thư mục chứa source code (nơi chứa file MainActivity). Click chuột phải lên thư mục này và chọn New > Package.

Tạo package con chứa source codeTạo package con chứa source codeTạo package con chứa source code

Cửa sổ New Package xuất hiện sau đó bạn hãy điền vào tên cho thư mục (chính là package) này là activity.

Tạo package có tên activityTạo package có tên activityTạo package có tên activity

Sau khi tạo thư mục activity ở bước trên. Bạn hãy lặp lại các bước này cho đến khi tạo được tất cả các thư mục cần thiết cho Kiến trúc custom, chúng bao gồm: activity, adapter, model, networking. Như hình sau.

Kiến trúc CustomKiến trúc CustomKiến trúc Custom

Nhìn vào các thư mục vừa tạo, chắc hẳn bạn cũng hiểu ý đồ tổ chức các file source code trong ứng dụng về thành các Kiến trúc như thế nào rồi đúng không nào. Mình sẽ nói tóm tắt cách tổ chức này như sau.

  • activty: đơn giản chứa đựng tất cả Activity trong project này. Một lát nữa chúng ta sẽ dời MainActivity vào thư mục này.
  • adapter: chứa đựng tất cả adapter của project vào trong thư mục này.
  • model: chứa các lớp data, các lớp này chứa đựng dữ liệu trả về khi chúng ta gọi API.
  • networking: chứa các lớp liên quan đến kết nối Internet.

Ngoài ra thì ví dụ này còn thiếu một thư mục quan trọng, đó là fragment. Vì chỉ có 1 màn hình duy nhất nên mình không tạo thư mục này. Tuy nhiên với dự án thực tế của các bạn thì dĩ nhiên sẽ phải có fragment để mà chứa tất cả các Fragment trong project vào một chỗ rồi.

Bước cuối cùng trong việc tổ chức Kiến trúc custom này là phải mang MainActivity vào trong thư mục activity. Việc này rất đơn giản, vẫn trong cửa sổ Project, bạn hãy kéo file MainActivity vào thư mục activity như hình minh họa sau.

Kéo file MainActivity thả vào thư mục activityKéo file MainActivity thả vào thư mục activityKéo file MainActivity thả vào thư mục activity

Việc kéo thả này sẽ xuất hiện một popup xác nhận. Bạn hãy cẩn thận kiểm tra mục To package đã hiển thị đúng đường dẫn đến tận activity là được. Để tránh trường hợp bạn kéo nhầm vào thư mục khác í mà. Cơ mà nếu có kéo nhầm thì kéo lại vẫn không sao nhé.

Cửa sổ xác nhận di chuyển MainActivityCửa sổ xác nhận di chuyển MainActivityCửa sổ xác nhận di chuyển MainActivity

Sau khi nhấn Refactor ở cửa sổ trên thì MainActivity của chúng ta khi này đã nằm đúng thư mục rồi đó.

Bạn hãy nhớ các bước cấu trúc cho Kiến trúc của project ở bài hôm nay nhé, các phần sau chúng ta sẽ cấu trúc chúng theo các Kiến trúc MVC, MVP hay MVVM cụ thể đấy.

Xây Dựng Các Lớp Kết Nối Web Service

Trước khi bắt tay vào viết các dòng code kết nối. Nguyên tắc đầu tiên khi xây dựng chức năng kết nối này là phải xây dựng trước các lớp chứa data trả về.

Các Lớp Chứa Data

Mà muốn viết các lớp chứa data này, bạn phải biết nội dữ liệu của kết quả trả về. Với ví dụ của bài hôm nay bạn sẽ nhận về một JSON.

Như đã nói, chúng ta sẽ lấy dữ liệu từ trang https://restcountries.com/, trang này chứa đựng rất nhiều API cho chúng ta thử nghiệm. Nhưng chúng ta chỉ cần duy nhất API này thôi: https://restcountries.com/v3.1/all.

Bạn hãy thử click vào đường link này xem sẽ thấy kết quả trả về rất nhiều đúng không nào. Bạn có thể dùng một vài công cụ để format kết quả đã xem về dạng JSON để dễ xem hơn nhé.

Xem kết quả JSON được format dễ xem hơnXem kết quả JSON được format dễ xem hơnXem kết quả JSON được format dễ xem hơn

Nào, dựa trên kết quả này, chúng ta biết phải xây dựng lớp data như thế nào. JSON như bạn thấy, trả về là mảng các object, mỗi một object đó có đầy đủ các thông tin của một quốc gia. Có một điều rằng chúng ta không nhất thiết phải xây dựng một lớp sao cho chứa tất cả thông tin của một quốc gia mà JSON trả về, mà chỉ cần những thông tin cần dùng thôi. Như hình minh họa màn hình ứng dụng trên kia, chúng ta chỉ cần thông tin tên quốc gia (chứa ở object name rồi đến field common trong JSON), và tên thủ đô (chứa ở list capital trong JSON).

Để bắt đầu xây dựng các lớp chứa data, chúng ta dùng tới kiểu lớp data (lớp có kiểu data trong Kotlin) như sau. Các lớp chứa data này sẽ nằm trong thư mục model. Chúng ta cần một lớp Name sẽ dùng để chứa object name trong Json. Và cần một lớp Country để chứa một object trong mảng các object ở cấp độ ngoài cùng của JSON.

Nếu bạn nào còn chưa rành lắm về việc tạo mới một lớp Kotlin trong Android Stutio, thì hãy click chuột phải vào thư mục cần tạo lớp, trong trường hợp này là model, rồi chọn New > Kotlin File/Class.

Tạo lớp mớiTạo lớp mớiTạo lớp mới

Hộp thoại sau xuất hiện, bạn hãy đảm bảo tùy chọn Data Class bên dưới được chọn. Sau đó điền tên lớp là Name ở phần trên của hộp thoại này.

Tạo một lớp mang tên NameTạo một lớp mang tên NameTạo một lớp mang tên Name

Sau đó trong lớp Name vừa tạo, hãy khai báo nội dung như sau.

data class Name(val common: String)

Bạn thấy bên trong lớp Name này có chứa một thuộc tính common, nó cùng tên với field common bên trong object name của JSON đúng không nào. Trong ví dụ của bài hôm nay bạn nhớ đặt tên các thuộc tính trong các lớp chứa data sao cho giống với tên của field trong JSON nhé. Việc đặt tên giống nhau này nhằm báo cho hệ thống có thể giúp chúng ta gán dữ liệu từ JSON vào lớp data một cách tự động theo biến được khai báo.

Tương tự bạn hãy tạo ra một lớp data nữa có tên Country, nội dung lớp này như sau.

data class Country(val name: Name, val capital: List<String>?)

Tương tự như Name, Country có hai thuộc tính là namecapital đều được đặt tên giống với các field này trong JSON. Ngoài ra bạn có chú ý đến capital có kiểu là List<String>??. Dấu ? cuối cùng của thuộc tính này cho phép capital có thể null. Ồ một quốc gia có thể không có thủ đô ư? Bạn cứ thử lát nữa xem sao nhé.

Vậy thôi, bạn thấy có đơn giản không nào. Cấu trúc project chúng ta đến giai đoạn này như sau.

Kiến trúc Custom đến bước nàyKiến trúc Custom đến bước nàyKiến trúc Custom đến bước này

Các Lớp Kết Nối Đến Web Service

Mình xin nhắc lại rằng là chúng ta sẽ dùng thư viện Retrofit để kết nối Web Service. Nên nếu bạn tìm hiểu cách thức sử dụng thư viện này, bạn sẽ thấy có nhiều cách tổ chức các lớp để thực hiện việc kết nối. Ở đây mình sẽ dùng một cách trong số đó, nên nó có thể khác với bạn đôi chút, nhưng chung quy lại vẫn là dựa trên quy tắc của Retrofit mà thôi.

Ngoài Retrofit ra, mình còn dùng RxJava để thông báo kết quả trả về khi kết nối được hoàn thành.

Kịch bản là vậy thôi, mình không nói sâu vào 2 thằng này lắm. Nếu bạn có thắc mắc, hãy tự tìm hiểu 2 thư viện này nhé. Việc của bạn là hãy xây dựng 2 lớp CountriesApiCountriesService như sau.

Lớp CountriesApi được tạo trong thư mục networking nhé, lớp này chứa định nghĩa các API, đây là một interface xây dựng theo hướng dẫn của Retrofit.

Để tạo một interface thì bạn cứ click chuột phải vào thư mục cần tạo file như bạn đã tạo lớp Country trên kia, chọn New > Kotlin File/Class, đến hộp thoại điền tên lớp bạn nhớ để vệt sáng chọn ở Interface. Hoặc có lỡ thao tác sai chọn lựa thì bạn vẫn có thể thay đổi trong code sau cũng được, yên tâm.

Tạo Interface CountriesApiTạo Interface CountriesApiTạo Interface CountriesApi

Vì chúng ta chỉ cần có 1 API nên bạn thấy chúng ta xây dựng 1 phương thức trong interface này. Về sau nếu các bạn muốn xây dựng thêm các API khác để lấy các dữ liệu khác từ Web Service thì cứ add thêm vào nhé.

interface CountriesApi {
 
    @GET("all")
    fun getCountries(): Single<List<Country>>
}

Bạn hãy tạo tiếp một lớp CountriesService trong thư mục networking này luôn nhé. Đây là lớp chứa đựng các dòng khởi tạo một Retrofit, sẽ được dùng để kết nối đến Web Service sau này.

Có một lưu ý là CountriesService khi này không phải là một lớp bình thường nhé. Lớp này không được khai báo là class như các lớp khác mà là object. Trong Kotlin một lớp được khai báo là object chính là một Singleton. Trong lúc tạo mới lớp bạn hãy chọn sẵn vệt sáng ở Object hoặc có thể chỉnh sửa sau khi tạo lớp bình thường vẫn được.

Tạo Object CountriesServiceTạo Object CountriesServiceTạo Object CountriesService

object CountriesService {
 
    private val BASE_URL = "https://restcountries.com/v3.1/"
 
    fun create(): CountriesApi {
        val retrofit = Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build()
 
        return retrofit.create(CountriesApi::class.java)
    }
}

Đến bước này thì cấu trúc của project chúng ta như sau.

Kiến trúc Custom đến bước nàyKiến trúc Custom đến bước nàyKiến trúc Custom đến bước này

Xây Dựng RecyclerView

Về cụ thể RecyclerView như thế nào thì mình sẽ nói trong loạt bài Android cơ bản. Ở đây mình chỉ nêu ra các file được xây dựng cho mục đích của RecyclerView.

Mà ReclyclerView là gì? À nó đơn giản là một dạng UI hiển thị theo kiểu Danh sách lên màn hình í mà. Chúng ta cần UI kiểu này để hiển thị danh sách các quốc gia lên màn hình chính của ứng dụng, thế thôi.

File item_country.xml

Chúng ta tạo một file giao diện cho một phần tử Danh sách. Phần tử này có tên item_country và được tạo trong thư mục res/layout/.

Do mỗi phần tử này sẽ bao gồm tên quốc gia và thủ đô kèm theo, nên giao diện được tạo như sau.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp">
 
    <TextView
        android:id="@+id/tvCountry"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="Country"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="18sp" />
 
    <TextView
        android:id="@+id/tvCapital"
        app:layout_constraintStart_toStartOf="@+id/tvCountry"
        app:layout_constraintTop_toBottomOf="@+id/tvCountry"
        tools:text="Capital"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp" />
 
</androidx.constraintlayout.widget.ConstraintLayout>

Trên đây là code XML, còn giao diện thiết kế của Phần tử danh sách như sau.

Giao diện thiết kế phần tử danh sáchGiao diện thiết kế phần tử danh sáchGiao diện thiết kế phần tử danh sách

Lớp CountriesAdapter.kt

Kế tiếp bạn hãy tạp mới lớp Adapter của RecyclerView. Lớp này nằm trong thư mục adapter và được đặt tên là CountriesAdapter.

Lớp này nhận truyền vào danh sách các Country, sau đó hiển thị tên quốc gia và thủ đô lên từng phần tử danh sách tương ứng. Adapter này còn kiêm nhiệm việc nhận sự kiện click lên mỗi phần tử danh sách và trả về nội dung của phần tử được click ra ngoài thông qua một interface tự định nghĩa nữa. Một điểm nữa ở Adapter này, đó là nó đã sử dụng View Binding để kết nối tới các view id bên XML.

class CountriesAdapter(val countries: ArrayList<Country>) :
    RecyclerView.Adapter<CountriesAdapter.CountryViewHolder>() {

    var listener: OnItemClickListener? = null

    fun updateCountries(newCountries: List<Country>) {
        countries.clear()
        countries.addAll(newCountries)
        notifyDataSetChanged()
    }

    fun setOnItemClickListener(listener: OnItemClickListener) {
        this.listener = listener
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = CountryViewHolder(
        ItemCountryBinding.inflate(LayoutInflater.from(parent.context))
    )

    override fun getItemCount() = countries.size

    override fun onBindViewHolder(holder: CountryViewHolder, position: Int) {
        holder.bind(countries[position], listener)
    }

    class CountryViewHolder(private val binding: ItemCountryBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(country: Country, listener: OnItemClickListener?) {
            binding.apply {
                tvCountry.text = country.name.common
                tvCapital.text = country.capital?.joinToString(", ")
                root.setOnClickListener { listener?.onItemClick(country) }
            }
        }
    }

    interface OnItemClickListener {
        fun onItemClick(country: Country)
    }
}

Và cấu trúc của project chúng ta đến giai đoạn này như sau.

Kiến trúc Custom đến bước nàyKiến trúc Custom đến bước nàyKiến trúc Custom đến bước này

Hoàn Thiện MainActivity

Chà chà xây dựng một project trong Android quả là khá dài. Dù sao thì chúng ta cũng đã đến những bước cuối cùng rồi.

Chỉnh sửa activity_main.xml

Trước hết chúng ta cần chỉnh sửa lại activity_main sao cho hiển thị giao diện tìm kiếm phía trên, rồi danh sách các quốc gia (chính là RecyclerView) ở phần không gian còn lại. Ngoài ra cũng cần có một loading bar xuất hiện khi danh sách đang được tải về.

Code giao diện sẽ như sau.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#dfdfdf"
    tools:context=".activity.MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/listView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="16dp"
        android:background="#ffffff"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/searchField"
        tools:visibility="visible">

    </androidx.recyclerview.widget.RecyclerView>

    <ProgressBar
        android:id="@+id/progress"
        style="?android:attr/progressBarStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/searchField"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:ems="10"
        android:hint="Search"
        android:inputType="textPersonName"
        android:minHeight="48dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Còn đây là màn hình thiết kế giao diện main_activity.

Giao diện thiết kế màn hình chínhGiao diện thiết kế màn hình chínhGiao diện thiết kế màn hình chính

Chỉnh Sửa MainActivity.kt

Nào, với Kiến trúc custom, mình sẽ để MainActivity chịu trách nhiệm gọi thực hiện các kết nối đến Web Service, MainActivity cũng chịu trách nhiệm lắng nghe kết quả trả về, và điều khiển UI để hiển thị kết quả hoặc thông báo lỗi.

Chúng ta đi từng bước để bạn dễ nắm. Đây là code nguyên thủy của MainActivity khi bạn mới tạo project.

class MainActivity : AppCompatActivity() {
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

Đầu tiên chúng ta phải khai báo View Binding cho Activity này. Việc chuyển sang sử dụng View Binding sẽ giúp các lớp tương tác được đến các view id dễ dàng hơn (như ở bước thiết kế Adapter trên kia bạn đã làm quen), mà không cần phải gọi thông qua findViewById() dài dòng hay Kotlin Synthetic đã chính thức bị khai tử. Với việc sử dụng View Binding thì MainActivity sẽ trông như sau.

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
    }
}

Tiếp theo hãy xây dựng việc gọi đến Web Service bằng các khai báo được tô sáng sau.

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private var apiService: CountriesApi? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        apiService = CountriesService.create()

        onFetchCountries()
    }

    fun onFetchCountries() {
        apiService?.let {
            it.getCountries()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ result ->
                    // Lấy về thành công danh sách quốc gia
                    // Làm gì đó sau
                }, { error ->
                    onError()
                })
        }
    }

    fun onError() {
        Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show()
    }
}

Code không quá khó hiểu đúng không bạn. Cơ bản ở onCreate() sẽ lo việc khởi tạo CountriesService. Rồi gọi onFetchCountries() để kết nối tới Web Service lấy về danh sách các quốc gia. Nếu kết quả trả về thành công thì chúng ta code sau. Còn trả về thất bại thì onError() sẽ thụ lý hiển thị một thông báo dạng Toast.

Tiếp theo, chúng ta xây dựng đến logic của loading. Bạn chú ý tới những dòng code mới thêm vào được tô sáng thôi nhé.

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private var apiService: CountriesApi? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        apiService = CountriesService.create()

        onFetchCountries()
    }

    fun onFetchCountries() {
        binding.progress.visibility = View.VISIBLE

        apiService?.let {
            it.getCountries()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ result ->
                    binding.progress.visibility = View.GONE
                    // Lấy về thành công danh sách quốc gia
                    // Làm gì đó sau
                }, { error ->
                    onError()
                })
        }
    }

    fun onError() {
        binding.progress.visibility = View.GONE
        
        Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show()
    }
}

Về logic của kết quả trả về thành công và hiển thị lên RecyclerView, có các dòng code được tô sáng sau. À chúng ta cũng xây dựng sự kiện click trên phần tử của danh sách này luôn nhé.

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private var apiService: CountriesApi? = null
    private val countriesAdapter = CountriesAdapter(arrayListOf())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        apiService = CountriesService.create()

        binding.listView?.apply {
            layoutManager = LinearLayoutManager(context)
            adapter = countriesAdapter
        }
        countriesAdapter.setOnItemClickListener(object : CountriesAdapter.OnItemClickListener {
            override fun onItemClick(country: Country) {
                Toast.makeText(this@MainActivity, "Country ${country.name}, capital is ${country.capital} clicked", Toast.LENGTH_SHORT).show()
            }
        })

        onFetchCountries()
    }

    fun onFetchCountries() {
        binding.listView.visibility = View.GONE
        binding.progress.visibility = View.VISIBLE

        apiService?.let {
            it.getCountries()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ result ->
                    binding.listView.visibility = View.VISIBLE
                    binding.progress.visibility = View.GONE

                    countriesAdapter.updateCountries(result)
                }, { error ->
                    onError()
                })
        }
    }

    fun onError() {
        binding.listView.visibility = View.GONE
        binding.progress.visibility = View.GONE

        Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show()
    }
}

Cuối cùng chúng ta sẽ xây dựng chức năng tìm kiếm trên danh sách quốc gia ở các dòng code được tô sáng.

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private var apiService: CountriesApi? = null
    private val countriesAdapter = CountriesAdapter(arrayListOf())
    private var countries: List<Country>? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        apiService = CountriesService.create()

        binding.listView?.apply {
            layoutManager = LinearLayoutManager(context)
            adapter = countriesAdapter
        }
        countriesAdapter.setOnItemClickListener(object : CountriesAdapter.OnItemClickListener {
            override fun onItemClick(country: Country) {
                Toast.makeText(this@MainActivity, "Country ${country.name}, capital is ${country.capital} clicked", Toast.LENGTH_SHORT).show()
            }
        })

        binding.searchField.addTextChangedListener(object : TextWatcher {

            override fun afterTextChanged(s: Editable) {
                if (s.isNotEmpty()) {
                    val filterCountries = countries?.filter { country ->
                        country.name.common.contains(s.toString(), true)
                    }
                    filterCountries?.let { countriesAdapter.updateCountries(it) }
                } else {
                    countries?.let { countriesAdapter.updateCountries(it) }
                }
            }

            override fun beforeTextChanged(
                s: CharSequence, start: Int,
                count: Int, after: Int
            ) {
            }

            override fun onTextChanged(
                s: CharSequence, start: Int,
                before: Int, count: Int
            ) {
            }
        })

        onFetchCountries()
    }

    fun onFetchCountries() {
        binding.listView.visibility = View.GONE
        binding.progress.visibility = View.VISIBLE
        binding.searchField.isEnabled = false

        apiService?.let {
            it.getCountries()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ result ->
                    binding.listView.visibility = View.VISIBLE
                    binding.progress.visibility = View.GONE
                    binding.searchField.isEnabled = true

                    countries = result
                    countriesAdapter.updateCountries(result)
                }, { error ->
                    onError()
                })
        }
    }

    fun onError() {
        binding.listView.visibility = View.GONE
        binding.progress.visibility = View.GONE
        binding.searchField.isEnabled = false

        Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show()
    }
}

Xong rồi, giờ bạn có thể thực thi chương trình để xem thành quả của bài hôm nay rồi đó.

Kết Luận

Qua bài viết hôm nay thì bạn có thể thấy tổng quan cách thức xây dựng một ứng dụng Android theo kinh nghiệm của bản thân mình (dĩ nhiên có kèm tìm hiểu và thống nhất với nhau của cả nhóm làm việc chung). Về cơ bản thì cách thức xây dựng project theo Kiến trúc custom này không hề dở, bạn vẫn có thể theo nó đến suốt cuộc đời của một project mà không cần phải quá khổ sở trong việc quản lý nó.

Nhưng bạn có thể dễ nhận ra, MainActivity hay các lớp liên quan đến giao diện sẽ ngày một phình ra theo độ lớn của project, đến nỗi một ngày nào đó bạn sẽ gần như mất quyền kiểm soát logic của các màn hình này. Mất kiểm soát ở đây ví dụ như tình huống ngày càng có nhiều code dư thừa mà bạn không hiểu hoặc không biết làm sao dọn dẹp, khiến chúng ảnh hưởng đến hiệu năng của sản phẩm. Hoặc sẽ rất khó khăn khi tách Activity thành các Fragment chẳng hạn. Chưa kể đến các lỗi crash ứng dụng nhức đầu liên quan đến leak memory nữa. Và một vấn đề nữa đó là chúng ta sẽ rất khó xây dựng các Unit Test cho project nếu tất cả đều đổ dồn vào các lớp giao diện như thế này. Chúng ta cần chia nhỏ nhiệm vụ ra, giao diện là giao diện, xử lý là xử lý, lưu trữ là lưu trữ. Chính vì vậy mà MVC, MVPMVVM ra đời ở các bài học tiếp theo.

Download Source Code Mẫu

Bạn có thể download source code mẫu của bài này ở đây.

Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy ủng hộ blog bằng cách:
– Đánh giá 5 sao ở mỗi bài viết nếu thấy thích.
– Comment bên dưới mỗi bài viết nếu có thắc mắc.
– Để lại địa chỉ email của bạn ở thanh bên phải để nhận được thông báo sớm nhất khi có bài viết mới.
– Chia sẻ các bài viết của Yellow Code Books đến nhiều người khác.
– Ủng hộ blog theo hướng dẫn ở thanh bên phải để blog ngày càng phát triển hơn.