Các lập trình viên cá nhân phát triển ứng dụng di động theo tầm nhìn của họ, bao gồm ý tưởng và quan điểm về cách thực hiện các tác vụ. Đôi khi họ có thể bỏ qua các nguyên tắc chính của lập trình hướng đối tượng hoặc hàm, dẫn đến sự mất phương hướng cho các nhà phát triển.
Điều này xấu - họ sẽ không thể xử lý code của mình. Và nhà phát triển tiếp theo cần maintain dự án hoặc sửa đổi nó có thể phát điên. Tốt hơn là xây dựng lại dự án từ đầu, vì maintain trở thành quá trình phức tạp.
Cho đến khi Google phát hành kiến trúc được hỗ trợ đầu tiên, hầu như mọi công ty phát triển phần mềm đều dùng kiến trúc riêng. Điều này giúp code rõ ràng hơn và có thể chuyển đổi giữa các dự án. Nhưng nếu developer đổi công ty, họ sẽ mất thời gian học kiến trúc mới cùng với dự án mới.
Lúc này, có 16 kiến trúc khác nhau cho Android developer, nhờ Google:
- 6 sample ổn định (Java);
- 2 sample ổn định (Kotlin):
- 4 sample bên ngoài;
- 3 sample deprecated;
- 1 sample đang tiến hành.
Kiến trúc nào bạn dùng phụ thuộc vào mục đích cụ thể, cách tiếp cận, và việc áp dụng các toolkit khác nhau cho việc triển khai các chức năng. Và nó phụ thuộc vào ngôn ngữ lập trình.
Tuy nhiên, tất cả các kiến trúc này có một nền tảng kiến trúc chung, gần như chia đều logic cho việc làm việc với mạng, cơ sở dữ liệu, dependency, và xử lý callback.
Công cụ sử dụng trong quá trình
Sau khi nghiên cứu tất cả các kiến trúc này, tôi xây dựng một cách tiếp cận đơn giản hơn và đưa ra một kiến trúc với ít layer hơn. Tôi sẽ chỉ cho bạn cách triển khai một ứng dụng Android đơn giản tải danh sách tin tức, cho phép lưu bài viết vào Favorites, và xóa nếu cần dùng cách tiếp cận của tôi.

Dưới đây là tóm tắt công nghệ tôi dùng:
- Kotlin để phát triển app cùng với thư viện AndroidX
- Room SQLite làm database
- Stetho để duyệt dữ liệu trong database
- Retrofit2 cùng RxJava2 để log request server và nhận response.
- Glide để xử lý hình ảnh
- Android Architecture Components (LiveData, ViewModel, Room) và ReactiveX (RxJava2, RxKotlin và RxAndroid) để xây dựng dependency, thay đổi dữ liệu động, và xử lý bất đồng bộ.
Đây là technology stack ứng dụng di động tôi dùng cho dự án.
Hãy bắt đầu
Các bước đầu tiên
Kết nối AndroidX. Trong gradle.properties ở cấp app, viết:
android.enableJetifier=true android.useAndroidX=trueBây giờ cần thay thế dependencies trong build.gradle cấp app module từ Android sang AndroidX. Bạn nên trích xuất tất cả dependencies vào ext, như ví dụ versioning Kotlin out-of-the-box trong build.gradle cấp app. Sau đó thêm versioning Gradle:
buildscript { ext.kotlin_version = '1.3.0' ext.gradle_version = '3.2.1' repositories { google() jcenter() maven { url 'https://jitpack.io' } mavenCentral() } dependencies { classpath "com.android.tools.build:gradle:$gradle_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } }Đối với các dependencies khác, tôi sẽ xây file ext của nó, nơi tôi thêm tất cả dependencies bao gồm SDK versions, chia versioning và tạo dependencies arrays sẽ được implement tiếp trong build.gradle cấp app. Nó sẽ trông như sau:
ext { compileSdkVersion = 28 minSdkVersion = 22 buildToolsVersion = '28.0.3' targetSdkVersion = 28 appcompatVersion = '1.0.2' supportVersion = '1.0.0' supportLifecycleExtensionsVersion = '2.0.0' constraintlayoutVersion = '1.1.3' multiDexVersion = "2.0.0" testJunitVersion = '4.12' testRunnerVersion = '1.1.1' testEspressoCoreVersion = '3.1.1' testDependencies = [ junit : "junit:junit:$testJunitVersion", runner : "androidx.test:runner:$testRunnerVersion", espressoCore: "androidx.test.espresso:espresso-core:$testEspressoCoreVersion" ] supportDependencies = [ kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version", appCompat : "androidx.appcompat:appcompat:$appcompatVersion", recyclerView : "androidx.recyclerview:recyclerview:$supportVersion", design : "com.google.android.material:material:$supportVersion", lifecycleExtension: "androidx.lifecycle:lifecycle-extensions:$supportLifecycleExtensionsVersion", constraintlayout : "androidx.constraintlayout:constraintlayout:$constraintlayoutVersion", multiDex : "androidx.multidex:multidex:$multiDexVersion" ] }Tên version và massif được implement ngẫu nhiên. Sau đó, implement dependencies trong build.gradle cấp app như sau:
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { compileSdkVersion rootProject.ext.compileSdkVersion as Integer buildToolsVersion rootProject.ext.buildToolsVersion as Stringdependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) //Test testImplementation testDependencies.junit androidTestImplementation testDependencies.runner androidTestImplementation testDependencies.espressoCore //Support implementation supportDependencies.kotlin implementation supportDependencies.appCompat implementation supportDependencies.recyclerView implementation supportDependencies.design implementation supportDependencies.lifecycleExtension implementation supportDependencies.constraintlayout implementation supportDependencies.multiDexĐừng quên chỉ định multiDexEnabled true trong default configs. Trong hầu hết trường hợp, bạn sẽ nhanh chóng đạt giới hạn số phương thức dùng.
Cùng cách đó, bạn cần khai báo tất cả dependencies của app. Thêm permissions để kết nối app với Internet:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" />Nếu không có tên trong manifest, bạn nên thêm vì Stetho sẽ không thấy app vô danh và bạn không thể nhìn vào database.
Xây dựng các thành phần cơ bản
Đáng chú ý, pattern MVVM (Model-View-ViewModel) được dùng làm cơ sở cho kiến trúc này.
Hãy bắt đầu phát triển. Điều đầu tiên là tạo class kế thừa Application(). Trong class này, chúng ta sẽ cấp quyền truy cập app context để dùng sau.
@SuppressWarnings("all") class App : Application() { companion object { lateinit var instance: App private set } override fun onCreate() { super.onCreate() instance = this Stetho.initializeWithDefaults(this) DatabaseCreator.createDatabase(this) } }abstract class BaseViewModel constructor(app: Application) : AndroidViewModel(app) { override fun onCleared() { super.onCleared() } }App này không có chức năng phức tạp. Nhưng trong ViewModel cơ bản, chúng ta sẽ đặt 3 LiveData chính:
- xử lý lỗi
- xử lý loading với progress bar hiển thị
- và, vì tôi có app với list, xử lý nhận và dữ liệu có sẵn trong adapter như placeholder hiển thị khi vắng mặt.
val errorLiveData = MediatorLiveData<String>() val isLoadingLiveData = MediatorLiveData<Boolean>() val isEmptyDataPlaceholderLiveData = MediatorLiveData<Boolean>()Để chuyển kết quả thực thi function sang LiveData, tôi sẽ dùng Consumer.
Để xử lý lỗi ở bất kỳ đâu trong app, bạn cần tạo Consumer chuyển giá trị Throwable.message sang errorLiveData.
Cũng trong ViewModel cơ bản, bạn cần tạo method nhận LiveData list để hiển thị progress bar trong quá trình thực thi.
ViewModel cơ bản của chúng ta sẽ trông như sau:
abstract class BaseViewModel constructor(app: Application) : AndroidViewModel(app) { val errorLiveData = MediatorLiveData<String>() val isLoadingLiveData = MediatorLiveData<Boolean>() val isEmptyDataPlaceholderLiveData = MediatorLiveData<Boolean>() private var compositeDisposable: CompositeDisposable? = null protected open val onErrorConsumer = Consumer<Throwable> { errorLiveData.value = it.message } fun setLoadingLiveData(vararg mutableLiveData: MutableLiveData<*>) { mutableLiveData.forEach { liveData -> isLoadingLiveData.apply { this.removeSource(liveData) this.addSource(liveData) { this.value = false } } } } override fun onCleared() { isLoadingLiveData.value = false isEmptyDataPlaceholderLiveData.value = false clearSubscription() super.onCleared() } private fun clearSubscription() { compositeDisposable?.apply { if (!isDisposed) dispose() compositeDisposable = null } } }
Trong app này, không cần tạo nhiều Activity cho hai màn hình (màn hình danh sách tin tức và màn hình favorites). Nhưng vì sample này minh họa triển khai kiến trúc tối ưu và dễ mở rộng, tôi sẽ tạo app cơ bản.
App sẽ xây trên 1 Activity và 2 Fragment inflate trong Container activity. File XML Activity sẽ như sau:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="https://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <FrameLayout android:id="@+id/flContainer" android:layout_width="match_parent" android:layout_height="match_parent" /> <include layout="@layout/include_placeholder"/> <include layout="@layout/include_progress_bar" /> </FrameLayout>trong đó include_placeholder và include_progressbar trông như sau:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="https://schemas.android.com/apk/res/android" android:id="@+id/flProgress" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/bg_black_40"> <ProgressBar android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:background="@color/transparent" /> </FrameLayout><?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="https://schemas.android.com/apk/res/android" android:id="@+id/flPlaceholder" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/bg_transparent"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:background="@color/transparent" android:src="@drawable/ic_business_light_blue_800_24dp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="40dp" android:text="@string/empty_data" android:textColor="@color/colorPrimary" android:textStyle="bold" /> </FrameLayout>BaseActivity của chúng ta trông như sau:
abstract class BaseActivity<T : BaseViewModel> : AppCompatActivity(), BackPressedCallback, ProgressViewCallback, EmptyDataPlaceholderCallback { protected abstract val viewModelClass: Class<T> protected abstract val layoutId: Int protected abstract val containerId: Int protected open val viewModel: T by lazy(LazyThreadSafetyMode.NONE) { ViewModelProviders.of(this).get(viewModelClass) } protected abstract fun observeLiveData(viewModel: T) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(layoutId) startObserveLiveData() } private fun startObserveLiveData() { observeLiveData(viewModel) } }Hãy triển khai cách hiển thị lỗi có thể xảy ra trong quá trình của tất cả Activity tương lai. Tôi sẽ làm dưới dạng Toast thông thường cho đơn giản.
protected open fun processError(error: String) = Toast.makeText(this, error, Toast.LENGTH_SHORT).show()và gửi text lỗi này đến method hiển thị:
protected open val errorObserver = Observer<String> { it?.let { processError(it) } }Trong BaseActivity, tôi sẽ bắt đầu theo dõi thay đổi giá trị errorLiveData nằm trong Base ViewModel. Method startObserveLiveData() sẽ biến đổi như sau:
private fun startObserveLiveData() { observeLiveData(viewModel) with(viewModel) { errorLiveData.observe(this@BaseActivity, errorObserver) } }Bây giờ dùng onErrorConsumer của BaseViewModel làm processor onError , bạn sẽ thấy thông báo lỗi của method được triển khai.
Tạo method cho phép thay thế Fragment trong Activity với khả năng add vào Back Stack.
protected open fun replaceFragment(fragment: Fragment, needToAddToBackStack: Boolean = true) { val name = fragment.javaClass.simpleName with(supportFragmentManager.beginTransaction()) { replace(containerId, fragment, name) if (needToAddToBackStack) { addToBackStack(name) } commit() } }Hãy tạo interface để hiển thị progress và placeholder ở các vị trí cần thiết của app.
interface EmptyDataPlaceholderCallback { fun onShowPlaceholder() fun onHidePlaceholder() }interface ProgressViewCallback { fun onShowProgress() fun onHideProgress() }Triển khai chúng trong BaseActivity. Tôi tạo hàm set ID cho progress bar và placeholder, cũng khởi tạo các View này.
protected open fun hasProgressBar(): Boolean = false protected abstract fun progressBarId(): Int protected abstract fun placeholderId(): Int private var vProgress: View? = null private var vPlaceholder: View? = nulloverride fun onShowProgress() { vProgress?.visibility = View.VISIBLE } override fun onHideProgress() { vProgress?.visibility = View.GONE } override fun onShowPlaceholder() { vPlaceholder?.visibility = View.VISIBLE } override fun onHidePlaceholder() { vPlaceholder?.visibility = View.INVISIBLE } public override fun onStop() { super.onStop() onHideProgress() }Và cuối cùng trong method onCreate tôi set ID cho View:
if (hasProgressBar()) { vProgress = findViewById(progressBarId()) vProgress?.setOnClickListener(null) } vPlaceholder = findViewById(placeholderId()) startObserveLiveData()Tôi đã viết rõ việc tạo BaseViewModel và BaseActivity. BaseFragment sẽ được tạo theo cùng nguyên tắc.
Khi tạo từng màn hình riêng biệt, nếu bạn cân nhắc mở rộng và thay đổi trong tương lai, bạn cần tạo Fragment riêng với ViewModel riêng.
Lưu ý: trong trường hợp Fragment có thể gom cụm, và business logic không phức tạp lớn, một vài Fragment có thể dùng chung một ViewModel.
Chuyển đổi giữa Fragment xảy ra nhờ interface được implement trong Activity. Để làm vậy, mỗi Fragment nên có companion object{ } với method xây dựng object Fragment với khả năng truyền argument vào Bundle:
companion object { fun newInstance() = FavoriteFragment().apply { arguments = Bundle() } }Giải pháp kiến trúc
Khi các thành phần cơ bản được tạo, đến lúc tập trung vào kiến trúc. Lược đồ nó sẽ trông như Clean Architecture của Robert C. Martin nổi tiếng (Uncle Bob). Nhưng vì tôi dùng RxJava2, tôi loại bỏ interface Boundaries (là cách đảm bảo thực thi Dependency Rule ) thay bằng Observable và Subscriber chuẩn.
Ngoài ra, dùng công cụ RxJava2 , tôi tích hợp chuyển đổi dữ liệu để làm việc linh hoạt hơn. Nó liên quan đến cả việc xử lý response từ server và database.
Ngoài model chính, tôi sẽ tạo model response server và model table riêng cho Room. Chuyển đổi dữ liệu giữa hai model này, bạn có thể làm bất kỳ thay đổi nào trong quá trình chuyển đổi, chuyển đổi server response, và lưu dữ liệu cần thiết vào base trước khi hiển thị trên UI, v.v.
Fragment chịu trách nhiệm cho UI, và ViewModel Fragment chịu trách nhiệm thực thi business logic. Nếu business logic liên quan đến toàn bộ activity, thì ViewModel Activity.
ViewModels lấy dữ liệu từ provider bằng việc khởi tạo qua val … by lazy{}, nếu cần object bất biến, hoặc lateinit var, nếu ngược lại. Sau khi thực thi business logic, nếu cần chuyển dữ liệu để thay đổi UI, bạn tạo MutableLiveData mới trong ViewModel để dùng trong method observeLiveData() của Fragment.
Nghe khá dễ. Triển khai cũng đơn giản.
Một thành phần thiết yếu của kiến trúc là data converter dựa trên chuyển đổi đơn giản từ kiểu dữ liệu này sang kiểu khác. Để chuyển đổi stream dữ liệu RxJava, SingleTransformer hoặc FlowableTransformer được dùng tùy theo kiểu. Trong trường hợp app này, interface và abstract class của converter trông như sau:
interface BaseDataConverter<IN, OUT> { fun convertInToOut(inObject: IN): OUT fun convertOutToIn(outObject: OUT): IN fun convertListInToOut(inObjects: List<IN>?): List<OUT>? fun convertListOutToIn(outObjects: List<OUT>?): List<IN>? fun convertOUTtoINSingleTransformer(): SingleTransformer<IN?, OUT> fun convertListINtoOUTSingleTransformer(): SingleTransformer<List<OUT>, List<IN>> } abstract class BaseDataConverterImpl<IN, OUT> : BaseDataConverter<IN, OUT> { override fun convertInToOut(inObject: IN): OUT = processConvertInToOut(inObject) override fun convertOutToIn(outObject: OUT): IN = processConvertOutToIn(outObject) override fun convertListInToOut(inObjects: List<IN>?): List<OUT> = inObjects?.map { convertInToOut(it) } ?: listOf() override fun convertListOutToIn(outObjects: List<OUT>?): List<IN> = outObjects?.map { convertOutToIn(it) } ?: listOf() override fun convertOUTtoINSingleTransformer() = SingleTransformer<IN?, OUT> { it.map { convertInToOut(it) } } override fun convertListINtoOUTSingleTransformer() = SingleTransformer<List<OUT>, List<IN>> { it.map { convertListOutToIn(it) } } protected abstract fun processConvertInToOut(inObject: IN): OUT protected abstract fun processConvertOutToIn(outObject: OUT): IN }Trong ví dụ này, tôi dùng chuyển đổi cơ bản như model-model, list model - list model, và các tổ hợp tương tự nhưng chỉ dùng SingleTransformer cho xử lý server response và request trong database.
Hãy bắt đầu với network - với RestClient. retrofitBuilder method như sau:
fun retrofitBuilder(): Retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .addConverterFactory(NullOrEmptyConverterFactory().converterFactory()) .addConverterFactory(GsonConverterFactory.create(createGsonBuilder())) .client(createHttpClient()) .build()//base url const val BASE_URL = "https://newsapi.org"Dùng API bên thứ ba, luôn có cơ hội nhận response null tuyệt đối từ server, có nhiều lý do. Đó là lý do một NullOrEmptyConverterFactory bổ sung sẽ giúp xử lý tình huống. Nó trông như sau:
class NullOrEmptyConverterFactory : Converter.Factory() { fun converterFactory() = this override fun responseBodyConverter(type: Type?, annotations: Array<Annotation>, retrofit: Retrofit): Converter<ResponseBody, Any>? { return Converter { responseBody -> if (responseBody.contentLength() == 0L) { null } else { type?.let { retrofit.nextResponseBodyConverter<Any>(this, it, annotations)?.convert(responseBody) } } } } }Để tạo model, cần xây dựng dựa trên API. Ví dụ, tôi dùng API miễn phí cho phi thương mại từ newsapi.org. Nó có danh sách chức năng yêu cầu khá rộng, nhưng tôi chỉ dùng một phần nhỏ cho ví dụ. Sau đăng ký nhanh, bạn nhận được quyền truy cập API và api key bắt buộc cho mỗi request.
Là endpoint, tôi dùng https://newsapi.org/v2/everything. Từ các query đề xuất, tôi chọn: q - truy vấn tìm kiếm, from - sắp xếp từ ngày, to - sắp xếp đến ngày, sortBy - sắp xếp theo tiêu chí chọn, và bắt buộc apiKey.
Sau RestClient tạo xong, tôi tạo interface API với Query chọn cho app:
interface NewsApi { @GET(ENDPOINT_EVERYTHING) fun getNews(@Query("q") searchFor: String?, @Query("from") fromDate: String?, @Query("to") toDate: String?, @Query("sortBy") sortBy: String?, @Query("apiKey") apiKey: String?): Single<NewsNetworkModel> }//endpoints const val ENDPOINT_EVERYTHING = "/v2/everything"Chúng ta nhận response này trong NewsNetworkModel:
data class NewsNetworkModel(@SerializedName("articles") var articles: List<ArticlesNetworkModel>? = listOf())data class ArticlesNetworkModel(@SerializedName("title") var title: String? = null, @SerializedName("description") var description: String? = null, @SerializedName("urlToImage") var urlToImage: String? = null)Dữ liệu này từ toàn bộ response đủ để hiển thị list với hình, tiêu đề và mô tả tin tức.
Cho việc triển khai cách tiếp cận kiến trúc, hãy tạo model chung:
interface News { var articles: List<Article>? } class NewsModel(override var articles: List<Article>? = null) : Newsinterface Article { var id: Long? var title: String? var description: String? var urlToImage: String? var isAddedToFavorite: Boolean? var fragmentName: FragmentsNames? } class ArticleModel(override var id: Long? = null, override var title: String? = null, override var description: String? = null, override var urlToImage: String? = null, override var isAddedToFavorite: Boolean? = null, override var fragmentName: FragmentsNames? = null) : ArticleVì Article model sẽ dùng cho kết nối database và hiển thị dữ liệu trong adapter, cần thêm 2 trường tôi sẽ dùng để thay đổi UI element trong list.
Khi mọi thứ sẵn sàng cho request, tôi tạo converter cho network model sẽ dùng trong query nhận tin tức qua NetworkModule.
Converter được tạo theo thứ tự ngược từ nesting, và chuyển đổi theo thứ tự trực tiếp. Vậy converter đầu tiên tôi tạo trên Article, thứ hai trên News:
interface ArticlesBeanConverter class ArticlesBeanDataConverterImpl : BaseDataConverterImpl<ArticlesNetworkModel, Article>(), ArticlesBeanConverter { override fun processConvertInToOut(inObject: ArticlesNetworkModel): Article = inObject.run { ArticleModel(null, title, description, urlToImage, false, FragmentsNames.NEWS) } override fun processConvertOutToIn(outObject: Article): ArticlesNetworkModel = outObject.run { ArticlesNetworkModel(title, description, urlToImage) } }interface NewsBeanConverter class NewsBeanDataConverterImpl : BaseDataConverterImpl<NewsNetworkModel, News>(), NewsBeanConverter { private val articlesConverter by lazy { ArticlesBeanDataConverterImpl() } override fun processConvertInToOut(inObject: NewsNetworkModel): News = inObject.run { NewsModel(articles?.let { articlesConverter.convertListInToOut(it) }) } override fun processConvertOutToIn(outObject: News): NewsNetworkModel = outObject.run { NewsNetworkModel(articles?.let { articlesConverter.convertListOutToIn(it) }) } }Như bạn thấy, trong quá trình chuyển đổi News object, việc chuyển đổi list Article object cũng được thực thi.
Khi converter cho network model xong, hãy tạo module (repository network). Vì thường có nhiều hơn 1 hoặc 2 interface API, cần tạo BaseModule, typed API, Network Module, và ConversionModel.
Nó trông như sau:
abstract class BaseNetworkModule<A, NM, M>(val api: A, val dataConverter: BaseDataConverter<NM, M>)Theo đó, trên NewsModule:
interface NewsModule { fun getNews(fromDate: String? = null, toDate: String? = null, sortBy: String? = null): Single<News> } class NewsModuleImpl(api: NewsApi) : BaseNetworkModule<NewsApi, NewsNetworkModel, News>(api, NewsBeanDataConverterImpl()), NewsModule { override fun getNews(fromDate: String?, toDate: String?, sortBy: String?): Single<News> = api.getNews(searchFor = SEARCH_FOR, fromDate = fromDate, toDate = toDate, sortBy = sortBy, apiKey = API_KEY) .compose(dataConverter.convertOUTtoINSingleTransformer()) .onErrorResumeNext(NetworkErrorUtils.rxParseError()) }Đối với API này, api key là tham số quan trọng cho request bằng bất kỳ endpoint đề xuất. Vì vậy cần đảm bảo optional parameter không được chỉ định trước, và nullify chúng mặc định.
Như bạn thấy, tôi áp dụng chuyển đổi dữ liệu trong xử lý response.
Hãy làm việc với database. Tôi tạo app database, gọi là AppDatabase và kế thừa từ RoomDatabase().
Để khởi tạo database, cần tạo DatabaseCreator, khởi tạo trong class App.
object DatabaseCreator { lateinit var database: AppDatabase private val isDatabaseCreated = MutableLiveData<Boolean>() private val mInitializing = AtomicBoolean(true) @SuppressWarnings("CheckResult") fun createDatabase(context: Context) { if (mInitializing.compareAndSet(true, false).not()) return isDatabaseCreated.value = false Completable.fromAction { database = Room.databaseBuilder(context, AppDatabase::class.java, DB_NAME).build() } .compose { completableToMain(it) } .subscribe({ isDatabaseCreated.value = true }, { it.printStackTrace() }) } }Bây giờ trong method onCreate() của class App tôi khởi tạo Stetho và database:
override fun onCreate() { super.onCreate() instance = this Stetho.initializeWithDefaults(this) DatabaseCreator.createDatabase(this) }Khi database được tạo, tôi tạo BaseDao với method insert() đơn lẻ:
@Dao interface BaseDao<in I> { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(obj: I) }Dựa trên ý tưởng app, tôi sẽ lưu tin tức thích, lấy list bài viết đã lưu, xóa tin tức đã lưu theo ID, hoặc xóa tất cả tin tức từ table. NewsDao sẽ như sau:
@Dao interface NewsDao : BaseDao<NewsDatabase> { @Query("SELECT * FROM $NEWS_TABLE") fun getNews(): Single<List<NewsDatabase>> @Query("DELETE FROM $NEWS_TABLE WHERE id = :id") fun deleteNewsById(id: Long) @Query("DELETE FROM $NEWS_TABLE") fun deleteFavoriteNews() }Và news table:
@Entity(tableName = NEWS_TABLE) data class NewsDatabase(@PrimaryKey var id: Long?, var title: String?, var description: String?, var urlToImage: String?)Khi table tạo xong, liên kết với database:
@Database(entities = [NewsDatabase::class], version = DB_VERSION) abstract class AppDatabase : RoomDatabase() { abstract fun newsDao(): NewsDao }Bây giờ có thể làm việc với database, lưu và trích xuất dữ liệu.
Về module (repository network), tôi tạo model converter - database table model:
interface NewsDatabaseConverter class NewsDatabaseDataConverterImpl : BaseDataConverterImpl<Article, NewsDatabase>(), NewsDatabaseConverter { override fun processConvertInToOut(inObject: Article): NewsDatabase = inObject.run { NewsDatabase(id, title, description, urlToImage) } override fun processConvertOutToIn(outObject: NewsDatabase): Article = outObject.run { ArticleModel(id, title, description, urlToImage, true, FragmentsNames.FAVORITES) } }BaseRepository có sẵn cho làm việc với nhiều table. Hãy viết nó. Phiên bản đơn giản nhất đủ cho app:
abstract class BaseRepository<M, DBModel> { protected abstract val dataConverter: BaseDataConverter<M, DBModel> protected abstract val dao: BaseDao<DBModel> }Sau tạo BaseRepository, tôi tạo NewsRepository:
interface NewsRepository { fun saveNew(article: Article): Single<Article> fun getSavedNews(): Single<List<Article>> fun deleteNewsById(id: Long): Single<Unit> fun deleteAll(): Single<Unit> } object NewsRepositoryImpl : BaseRepository<Article, NewsDatabase>(), NewsRepository { override val dataConverter by lazy { NewsDatabaseDataConverterImpl() } override val dao by lazy { DatabaseCreator.database.newsDao() } override fun saveNew(article: Article): Single<Article> = Single.just(article) .map { dao.insert(dataConverter.convertInToOut(it)) } .map { article } override fun getSavedNews(): Single<List<Article>> = dao.getNews().compose(dataConverter.convertListINtoOUTSingleTransformer()) override fun deleteNewsById(id: Long): Single<Unit> = Single.just(dao.deleteNewsById(id)) override fun deleteAll(): Single<Unit> = Single.just(dao.deleteFavoriteNews()) }Khi repository và module bền vững tạo xong, dữ liệu nên chảy từ app provider request dữ liệu từ network hoặc database tùy yêu cầu. Provider nên kết hợp cả hai repository. Cân nhắc khả năng các model và repository, tôi tạo BaseProvider:
abstract class BaseProvider<NM, DBR> { val repository: DBR = this.initRepository() val networkModule: NM = this.initNetworkModule() protected abstract fun initRepository(): DBR protected abstract fun initNetworkModule(): NM }
Sau đó NewsProvider trông như sau:
interface NewsProvider { fun loadNewsFromServer(fromDate: String? = null, toDate: String? = null, sortBy: String? = null): Single<News> fun saveNewToDB(article: Article): Single<Article> fun getSavedNewsFromDB(): Single<List<Article>> fun deleteNewsByIdFromDB(id: Long): Single<Unit> fun deleteNewsFromDB(): Single<Unit> } object NewsProviderImpl : BaseProvider<NewsModule, NewsRepositoryImpl>(), NewsProvider { override fun initRepository() = NewsRepositoryImpl override fun initNetworkModule() = NewsModuleImpl(RestClient.retrofitBuilder().create(NewsApi::class.java)) override fun loadNewsFromServer(fromDate: String?, toDate: String?, sortBy: String?) = networkModule.getNews(fromDate, toDate, sortBy) override fun saveNewToDB(article: Article) = repository.saveNew(article) override fun getSavedNewsFromDB() = repository.getSavedNews() override fun deleteNewsByIdFromDB(id: Long) = repository.deleteNewsById(id) override fun deleteNewsFromDB() = repository.deleteAll() }Bây giờ chúng ta lấy list tin tức dễ dàng. Trong NewsViewModel khai báo tất cả method của provider để dùng tiếp:
val loadNewsSuccessLiveData = MutableLiveData<News>() val loadLikedNewsSuccessLiveData = MutableLiveData<List<Article>>() val deleteLikedNewsSuccessLiveData = MutableLiveData<Boolean>() private val loadNewsSuccessConsumer = Consumer<News> { loadNewsSuccessLiveData.value = it } private val loadLikedNewsSuccessConsumer = Consumer<List<Article>> { loadLikedNewsSuccessLiveData.value = it } private val deleteLikedNewsSuccessConsumer = Consumer<Unit> { deleteLikedNewsSuccessLiveData.value = true } private val dataProvider by lazy { NewsProviderImpl } init { isLoadingLiveData.apply { addSource(loadNewsSuccessLiveData) { value = false } }@SuppressLint("CheckResult") fun loadNews(fromDate: String? = null, toDate: String? = null, sortBy: String? = null) { isLoadingLiveData.value = true isEmptyDataPlaceholderLiveData.value = false dataProvider.loadNewsFromServer(fromDate, toDate, sortBy) .compose(RxUtils.ioToMainTransformer()) .subscribe(loadNewsSuccessConsumer, onErrorConsumer) } @SuppressLint("CheckResult") fun saveLikedNew(article: Article) { Single.fromCallable { Unit } .flatMap { dataProvider.saveNewToDB(article) } .compose(RxUtils.ioToMainTransformerSingle()) .subscribe({}, { onErrorConsumer }) } @SuppressLint("CheckResult") fun removeLikedNew(id: Long) { Single.fromCallable { Unit } .flatMap { dataProvider.deleteNewsByIdFromDB(id) } .compose(RxUtils.ioToMainTransformerSingle()) .subscribe({}, { onErrorConsumer }) } @SuppressLint("CheckResult") fun loadLikedNews() { Single.fromCallable { Unit } .flatMap { dataProvider.getSavedNewsFromDB() } .compose(RxUtils.ioToMainTransformerSingle()) .subscribe(loadLikedNewsSuccessConsumer, onErrorConsumer) } @SuppressLint("CheckResult") fun removeLikedNews() { Single.fromCallable { Unit } .flatMap { dataProvider.deleteNewsFromDB() } .compose(RxUtils.ioToMainTransformerSingle()) .subscribe(deleteLikedNewsSuccessConsumer, onErrorConsumer) }Sau khai báo tất cả method thực thi business logic trong ViewModel, chúng ta gọi lại từ Fragment nơi trong observeLiveData() kết quả của từng LiveData được xử lý.
Để triển khai dễ dàng, trong tham số SEARCH_FOR tôi chọn ngẫu nhiên Apple, và sắp xếp thêm bằng tag popularity . Nếu cần, bạn có thể thêm chức năng tối thiểu để thay đổi tham số này.
Vì newsapi.org không cung cấp news ID, tôi chấp nhận element index làm ID. Sắp xếp bằng popularity tag cũng triển khai qua API. Nhưng để tránh ghi đè dữ liệu với cùng ID trong base khi sắp xếp popularity, tôi kiểm tra dữ liệu trong base trước khi load list tin tức. Nếu base rỗng - load list mới, nếu không - hiển thị thông báo.
Hãy gọi trong method onViewCreated() của NewsFragment method sau:
private fun loadLikedNews() { viewModel.loadLikedNews() }Vì base rỗng, method loadNews() sẽ chạy. Trong method observeLiveData tôi dùng loading LiveData - viewModel.loadNewsSuccessLiveData.observe(..){news →}, nhận list bài viết nếu request thành công, truyền cho adapter:
isEmptyDataPlaceholderLiveData.value = news.articles?.isEmpty() with(newsAdapter) { news.articles?.toMutableList()?.let { clear() addAll(it) } notifyDataSetChanged() } loadNewsSuccessLiveData.value = nullChạy app, bạn sẽ thấy kết quả:

Trong menu toolbar bên phải, thấy 2 tùy chọn - sắp xếp và favorites. Hãy sắp xếp list theo popularity:

Nếu vào Favorites, chỉ thấy Placeholder vì chưa có dữ liệu. Màn hình Favorites:

UI fragment Favorites có màn hình hiển thị list tin tức thích và một tùy chọn toolbar để dọn database. Khi lưu dữ liệu bấm "Like", màn hình:

Như viết, trong model chuẩn 2 trường thêm được thêm vào model chung, dùng cho hiển thị dữ liệu trong adapter. Bây giờ bạn thấy element list tin tức đã lưu không có tùy chọn thêm Favorites.
var isAddedToFavorite: Boolean? var fragmentName: FragmentsNames?Nếu bấm "Like" lại, element đã lưu sẽ bị xóa khỏi base.
Tóm tắt
Vậy tôi đã chỉ cho bạn cách tiếp cận đơn giản, rõ ràng cho phát triển app Android. Chúng ta tuân thủ nguyên tắc Clean Architecture nhưng đơn giản hóa tối đa.
Khác biệt giữa kiến trúc tôi cung cấp và Clean Architecture của anh Martin? Đầu tiên, tôi đã lưu ý kiến trúc tôi tương tự CA vì nó là cơ sở. Dưới đây là sơ đồ CA:

Sự kiện đi đến Presenter, rồi đến Use Case. Use Case yêu cầu Repository. Repository nhận dữ liệu, tạo Entity, chuyển đến UseCase. Do đó, Use Case nhận tất cả Entity cần thiết. Sau thực thi business logic, kết quả quay về Presenter, nó chuyển kết quả đến UI.
Trong sơ đồ dưới, Controller gọi method từ InputPort triển khai UseCase, và interface OutputPort nhận response, Presenter triển khai. Thay vì UseCase phụ thuộc trực tiếp Presenter, nó phụ thuộc interface trong layer, không vi phạm Dependency Rule, Presenter nên implement interface.

Vậy, process ở layer external không ảnh hưởng process ở internal layer. Entity trong Clean Architecture là gì? Thực tế, nó là mọi thứ không phụ thuộc app cụ thể, và là khái niệm chung cho nhiều app. Nhưng trong phát triển mobile, Entity là business object của app, chứa quy tắc chung và cấp cao (business logic app).
Gateways? Theo tôi, Gateways là repository làm việc với database và module làm việc với network. Chúng ta loại bỏ controller vì Clean Architecture ban đầu cho app business phức tạp, và data converter thực hiện chức năng của nó trong app. ViewModel chuyển dữ liệu cho Fragment xử lý UI thay Presenter.
Trong cách tiếp cận của tôi, cũng tuân thủ Dependency Rule nghiêm ngặt, logic của repository, module, model, provider được đóng gói, truy cập qua interface. Do đó, thay đổi external layer không ảnh hưởng internal layer. Việc triển khai dùng RxJava2, KotlinRx, và Kotlin LiveData làm nhiệm vụ developer dễ dàng, rõ ràng, code dễ đọc và mở rộng.