Chuyển đến nội dung chính

Android Architecture starring Kotlin Coroutines, Jetpack (MVVM, Room, Paging), Retrofit and Dagger 2


Qua nhiều năm, kiến trúc Android đã phát triển để hỗ trợ các ứng dụng phức tạp, mạnh mẽ,

chất lượng sản xuất trên mọi quy mô. Thật tuyệt khi thấy các đề xuất của Google phù hợp

với nhu cầu và lựa chọn của cộng đồng Android như thế nào. Trong những năm gần

đây, Google đã thăng hạng Kotlin là công dân hạng nhất, giới thiệu

Guide to app architecture với sự tham gia của khái niệm uncle’s Bob Clean Architecture

với mẫu trình bày MVVM. Chúng tôi đã được đưa ra các câu đố kiến trúc ngoài hộp

được đóng gói trong Architecture Components và Android Jetpack. Retrofit, RxJava và

Dagger đã được đưa vào các hướng dẫn chính thức.


Cuối cùng, nhưng không kém phần quan trọng Kotlin Coroutines đã được cập bến Google IO 2019. Nguồn cảm hứng đến từ Reactive Programming (các khuôn khổ như RxJava).

Kotlin Coroutines quản lý các chuỗi nền bằng mã đơn giản hóa và giảm nhu cầu gọi lại.


Tuy nhiên, khi kết hợp tất cả các câu đố kiến trúc lại với nhau thành một ứng dụng khách đơn giản, rất khó để tìm thấy mẫu ứng dụng có nguồn mở để làm theo. Một trong những Infinity Stone luôn bị mất tích.



Chúng tôi sẽ triển khai ứng dụng mẫu với sự tham gia của Google Guide to app architecture (dựa trên các mẫu MVVM và Kho lưu trữ), sử dụng Android Jetpack (ViewModel, LiveData,

Room, Paging, Navigation), Retrofit và Dagger 2. Ứng dụng được viết hoàn toàn bằng Kotlin.

Khách mời đặc biệt là Kotlin Coroutines mà chúng tôi sẽ sử dụng thay vì RxJava2 cho trường

hợp tải dữ liệu cơ bản.


Chúng tôi sẽ triển khai ứng dụng từng bước thành thạo các khái niệm kiến trúc.

Lưu ý: Mục tiêu của bài viết là cung cấp tổng quan về trạng thái hiện tại của

Android Architecture kết hợp tất cả các thành phần lại với nhau thành ứng dụng

sản xuất chất lượng đơn giản. Bài báo giả định rằng người đọc đã quen với các khái niệm

và thư viện được đề cập.


Hãy đi sâu vào nó!


Mã nguồn đầy đủ của dự án này cùng với danh sách đầy đủ các thư viện được sử dụng

có thể được tìm thấy trên GitHub: https://github.com/Eli-Fox/LEGO-Catalog


1. The project we will develop


Mọi kỹ sư Android đều có hoặc sẽ có nhu cầu viết ứng dụng cơ bản từ đầu. Cho dự án mới,

tạo mẫu hoặc phỏng vấn. Ứng dụng cổ điển bao gồm 2 màn hình: cuộn vô cực danh sách

các mục và xem chi tiết bằng cách sử dụng mạng và tính bền bỉ.


Tôi là một fan hâm mộ lớn của LEGO® mini figure và LEGO® nói chung, vì vậy tôi đã chọn

viết một danh mục đơn giản để duyệt qua các bộ. Ứng dụng bao gồm 3 màn hình: danh

sách chủ đề, danh sách vô cực các bộ và chi tiết bộ.



2. Architecture overview


Clean architecture giả định tách các mối quan tâm với các lớp UI (Activity, Fragment, View),

Presentation (ViewModel) và Data (Repository). ViewModel giúp xử lý tương tác của người

dùng, lưu trạng thái hiện tại của ứng dụng và tự động quản lý vòng đời của các thành phần

giao diện người dùng Android thông qua LiveData.


Kho lưu trữ đóng vai trò là điểm dữ liệu, nơi ViewModel không biết gì về nguồn dữ liệu.

Tùy thuộc vào kho lưu trữ để quyết định xem dữ liệu cục bộ hay từ xa nên được cung

cấp lại cho người dùng. Nó phục vụ tốt cho việc xử lý xung đột đồng bộ dữ liệu.



Lets show list of LEGO® themes to user


Cho phép phát triển một màn hình ứng dụng với danh sách các chủ đề. Chúng tôi sẽ tải một

danh sách các chủ đề và hiển thị chúng cho người dùng. Chúng tôi sẽ hỗ trợ chế độ ngoại

tuyến và tìm nạp dữ liệu từ API REST.


Model


data class LegoTheme(
val id: Int,
val name: String,
val parentId: Int? = null
)

3. User Interface and Experience


Hãy bắt đầu từ việc viết bố cục cho đoạn trong fragment_themes.xml.Liên kết dữ liệu được sử dụng để liên kết dữ liệu để xem theo kiểu khai báo. Xem Ràng buộc như một phần của Liên kết dữ liệu được sử dụng để có các tham chiếu của các chế độ xem trong một phân đoạn.


<?xml version="1.0" encoding="utf-8"?>
<layout 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">

<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:context="com.elifox.legocatalog.MainActivity"
tools:listitem="@layout/list_item_theme"/>

<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:visibility="visible"/>

</FrameLayout>

</layout>

Sau đó, hãy thêm list_item_theme.xml với biểu diễn giao diện người dùng mục.

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<data>

<variable
name="clickListener"
type="android.view.View.OnClickListener"/>

<variable
name="legoTheme"
type="com.elifox.legocatalog.legotheme.data.LegoTheme"/>
</data>

<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_normal"
android:layout_marginRight="@dimen/margin_normal"
android:onClick="@{clickListener}">

<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_normal"
android:text="@{legoTheme.name}"
android:gravity="center"
android:textAppearance="?attr/textAppearanceHeadline6"
tools:text="Super Heroes"/>

</androidx.cardview.widget.CardView>


</layout>

Để hiển thị các mục trong danh sách và sử dụng lại tài nguyên chế độ xem, hãy viết LegoThemeAdapter.

/**
* Adapter for the [RecyclerView] in [LegoThemeFragment].
*/
class LegoThemeAdapter : ListAdapter<LegoTheme, LegoThemeAdapter.ViewHolder>(DiffCallback()) {

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val legoTheme = getItem(position)
holder.apply {
bind(createOnClickListener(legoTheme.id, legoTheme.name), legoTheme)
itemView.tag = legoTheme
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ListItemThemeBinding.inflate(
LayoutInflater.from(parent.context), parent, false))
}

private fun createOnClickListener(id: Int, name: String): View.OnClickListener {
return View.OnClickListener {
val direction = LegoThemeFragmentDirections.actionThemeFragmentToSetsFragment(id, name)
it.findNavController().navigate(direction)
}
}

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

fun bind(listener: View.OnClickListener, item: LegoTheme) {
binding.apply {
clickListener = listener
legoTheme = item
executePendingBindings()
}
}
}
}

private class DiffCallback : DiffUtil.ItemCallback<LegoTheme>() {

override fun areItemsTheSame(oldItem: LegoTheme, newItem: LegoTheme): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: LegoTheme, newItem: LegoTheme): Boolean {
return oldItem == newItem
}
}

Cho phép viết một LegoThemeFragment có chèn viewModel.

class LegoThemeFragment : Fragment(), Injectable {

@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var viewModel: LegoThemeViewModel

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = injectViewModel(viewModelFactory)

val binding = FragmentThemesBinding.inflate(inflater, container, false)
context ?: return binding.root

val adapter = LegoThemeAdapter()
binding.recyclerView.addItemDecoration(
VerticalItemDecoration(resources.getDimension(R.dimen.margin_normal).toInt(), true) )
binding.recyclerView.adapter = adapter

subscribeUi(binding, adapter)

setHasOptionsMenu(true)
return binding.root
}

private fun subscribeUi(binding: FragmentThemesBinding, adapter: LegoThemeAdapter) {
viewModel.legoThemes.observe(viewLifecycleOwner, Observer { result ->
when (result.status) {
Result.Status.SUCCESS -> {
binding.progressBar.hide()
result.data?.let { adapter.submitList(it) }
}
Result.Status.LOADING -> binding.progressBar.show()
Result.Status.ERROR -> {
binding.progressBar.hide()
Snackbar.make(binding.root, result.message!!, Snackbar.LENGTH_LONG).show()
}
}
})
}
}

Một Fragment bắt đầu quan sát các chủ đề LiveData<Result<List<LegoTheme>>> từ viewModel. Đó là nơi phép thuật bắt đầu. LiveData nhận biết vòng đời và quan sát dữ liệu cho bất kỳ thay đổi mới nào.


4. Error and Loading states handling


Lưu ý rằng Fragment quan sát một LiveData của một đối tượng Result. Nó dự định cung cấp phản hồi cho giao diện người dùng, cho dù nó sẽ hiển thị trạng thái Success, Loading hay Error. Trong trường hợp Success, chúng tôi hiển thị dữ liệu đã tải, Loading sẽ kích hoạt xuất hiện progressBar và Error sẽ cung cấp phản hồi cho người dùng thông qua Snackbar.


/**
* A generic class that holds a value with its loading status.
*
* Result is usually created by the Repository classes where they return
* `LiveData<Result<T>>` to pass back the latest data to the UI with its fetch status.
*/

data class Result<out T>(val status: Status, val data: T?, val message: String?) {

enum class Status {
SUCCESS,
ERROR,
LOADING
}

companion object {
fun <T> success(data: T): Result<T> {
return Result(Status.SUCCESS, data, null)
}

fun <T> error(message: String, data: T? = null): Result<T> {
return Result(Status.ERROR, data, message)
}

fun <T> loading(data: T? = null): Result<T> {
return Result(Status.LOADING, data, null)
}
}
}

5. Dependency injection with Dagger 2


Dependency Injection và Dagger 2 là chủ đề toàn diện để thảo luận trong một bài báo với tổng quan về kiến trúc. Nhưng một trường hợp đáng được chú ý: tiêm viewModel.


Vì Fragment được khởi tạo bởi Android SDK nên chúng tôi không thể đưa viewModel vào giai đoạn xây dựng. Do đó, chúng tôi sẽ đưa nó qua ViewModelFactory.


Bạn có thể tìm thấy toàn bộ việc triển khai biểu đồ mức độ phụ thuộc của ứng dụng trên Github.


@Singleton
class ViewModelFactory @Inject constructor(
private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = creators[modelClass] ?: creators.entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
try {
@Suppress("UNCHECKED_CAST")
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}

}
}

6. Trình bày với MVVM, LiveData và Lifecycles


Rất may với nhận thức về vòng đời của ViewModel và LiveData, LegoThemeViewModel của chúng tôi rất đơn giản. Nó chỉ có LegoThemeRepository làm phụ thuộc để cung cấp dữ liệu.


class LegoThemeViewModel @Inject constructor(repository: LegoThemeRepository) : ViewModel() {
val legoThemes: LiveData<Result<List<LegoTheme>>> = repository.themes
}

7. Kho lưu trữ với Kotlin Coroutines dựa trên một nguồn duy nhất của chiến lược sự thật


Để tránh sai lệch đồng bộ hóa, cần phải xác định một nguồn chân lý duy nhất. Kho lưu trữ đóng vai trò là nguồn dữ liệu trừu tượng cho Presentation Layer (viewModel). Mặc dù Repository nên quyết định nguồn dữ liệu nào là sự thật. Cho phép xác định một cơ sở dữ liệu cục bộ như một nguồn chân lý. Bất cứ khi nào dữ liệu được yêu cầu, chúng tôi sẽ trả lại bản sao cục bộ, sau đó chúng tôi tìm nạp dữ liệu từ xa, lưu nó vào cơ sở dữ liệu, điều này sẽ tự động thông báo cho Kho lưu trữ về dữ liệu mới có sẵn.


Hãy xem luồng dữ liệu:




Ở đây LiveData và Coroutines đến để giải cứu và đơn giản hóa mã. Hãy xem LegoThemeRepository.

@Singleton
class LegoThemeRepository @Inject constructor(private val dao: LegoThemeDao,
private val remoteSource: LegoThemeRemoteDataSource) {

val themes = resultLiveData(
databaseQuery = { dao.getLegoThemes() },
networkCall = { remoteSource.fetchData() },
saveCallResult = { dao.insertAll(it.results) })

}

Chúng tôi có 2 phụ thuộc: dao: LegoThemeDao và remoteSource: LegoThemeRemoteSource. Chủ đề được nhận thông qua các chức năng tạm ngưng của Coroutines để thực hiện cuộc gọi này không đồng bộ và bỏ chặn luồng giao diện người dùng chính khỏi công việc. Hãy đi sâu vào triển khai SingleSourceOfTruth.kt của chúng tôi để hiểu rõ cơ chế đa luồng.

/**
* The database serves as the single source of truth.
* Therefore UI can receive data updates from database only.
* Function notify UI about:
* [Result.Status.SUCCESS] - with data from database
* [Result.Status.ERROR] - if error has occurred from any source
* [Result.Status.LOADING]
*/
fun <T, A> resultLiveData(databaseQuery: () -> LiveData<T>,
networkCall: suspend () -> Result<A>,
saveCallResult: suspend (A) -> Unit): LiveData<Result<T>> =
liveData(Dispatchers.IO) {
emit(Result.loading<T>())
val source = databaseQuery.invoke().map { Result.success(it) }
emitSource(source)

val responseStatus = networkCall.invoke()
if (responseStatus.status == SUCCESS) {
saveCallResult(responseStatus.data!!)
} else if (responseStatus.status == ERROR) {
emit(Result.error<T>(responseStatus.message!!))
emitSource(source)
}
}

Chúng tôi đang sử dụng hàm mở rộng liveData() từ lớp CoroutineLiveData.kt của thư viện LiveData. Hàm liveData() chấp nhận Coroutine Dispatcher để chọn đường chạy.


Có 4 loại Dispatchers:

  • Default (Được thiết kế cho công việc tính toán nặng)
  • Main (Chuỗi chính hoạt động với các đối tượng UI)
  • IO (Được thiết kế để giảm tải các tác vụ IO chặn như mạng, hoạt động cơ sở dữ liệu)
  • Unconfined (Không giới hạn trong bất kỳ chuỗi cụ thể nào)


​​Trong trường hợp của chúng tôi, Dispatchers.IO là chính xác những gì chúng tôi cần. Chúng tôi sử dụng các chức năng tạm ngưng của Coroutines để tìm nạp, tải và lưu dữ liệu. Để giao tiếp với giao diện người dùng với trạng thái Success, Loading hoặc Error, chúng tôi sử dụng hàm release(), hàm này tự động thông báo cho người quan sát trên chuỗi giao diện người dùng chính.


8. Persistency with Room and Kotlin Coroutines



Room database đã được xây dựng hỗ trợ LiveData và Kotlin Coroutines, giúp thực hiện nhiệm vụ đa luồng dễ dàng. Chúng ta hãy xem xét kỹ hơn LegoTheme Entity và LegoThemeDao.

@Entity(tableName = "themes")
data class LegoTheme(
@PrimaryKey
@field:SerializedName("id")
val id: Int,
@field:SerializedName("name")
val name: String,
@field:SerializedName("parent_id")
val parentId: Int? = null
) {
override fun toString() = name
}


@Dao
interface LegoThemeDao {

@Query("SELECT * FROM themes ORDER BY id DESC")
fun getLegoThemes(): LiveData<List<LegoTheme>>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(plants: List<LegoTheme>)
}

9. Network with Retrofit and Kotlin Coroutines


Retrofit cũng được tích hợp hỗ trợ cho các chức năng của Kotlin Coroutines. Đối với điều này, chỉ cần xác định các phương thức điểm cuối API của bạn với từ khóa tạm ngưng. Dữ liệu đã tìm nạp sẽ được lưu vào cơ sở dữ liệu, nó sẽ kích hoạt cơ sở dữ liệu để thông báo cho viewModel với liveData về thông tin cập nhật. Do đó, chúng tôi không cần phải trả lại liveData từ cuộc gọi Retrofit trực tiếp.


interface LegoService {

@GET("lego/themes/")
suspend fun getThemes(@Query("page") page: Int? = null,
@Query("page_size") pageSize: Int? = null,
@Query("ordering") order: String? = null): Response<ResultsResponse<LegoTheme>>

@GET("lego/sets/")
suspend fun getSets(@Query("page") page: Int? = null,
@Query("page_size") pageSize: Int? = null,
@Query("theme_id") themeId: Int? = null,
@Query("ordering") order: String? = null): Response<ResultsResponse<LegoSet>>

@GET("lego/sets/{id}/")
suspend fun getSet(@Path("id") id: String): Response<LegoSet>

}


/**
* Works with the Lego API to get data.
*/
class LegoThemeRemoteDataSource @Inject constructor(private val service: LegoService) : BaseDataSource() {

suspend fun fetchData() = getResult { service.getThemes(1, 1000, "-id") }

}

Để xử lý lỗi, hãy viết lớp BaseDataSource, lớp này nhận đối tượng Retrofit Response và biến đổi nó thành Success hoặc Error. Trạng thái này cuối cùng sẽ được truyền tới UI thông qua Repository -> ViewModel -> Fragment.

/**
* Abstract Base Data source class with error handling
*/
abstract class BaseDataSource {

protected suspend fun <T> getResult(call: suspend () -> Response<T>): Result<T> {
try {
val response = call()
if (response.isSuccessful) {
val body = response.body()
if (body != null) return Result.success(body)
}
return error(" ${response.code()} ${response.message()}")
} catch (e: Exception) {
return error(e.message ?: e.toString())
}
}

private fun <T> error(message: String): Result<T> {
Timber.e(message)
return Result.error("Network call has failed for a following reason: $message")
}

}

10. Paging library


Vì chúng tôi đã hoàn thành màn hình với số lượng Themes hạn chế, chúng tôi có thể xử lý màn hình bằng Sets, nơi chúng tôi cần hiển thị số lượng mặt hàng vô hạn. Chúng tôi muốn tải và hiển thị các phần nhỏ dữ liệu tại một thời điểm. Vì mục đích này, hãy sử dụng thư viện Paging.


Tin tốt là Room database hỗ trợ phân trang ngoại tuyến. Nó hoạt động giống như một sự quyến rũ khi kết hợp với Paging. Số lượng mã tối thiểu cần phải viết. Tất cả những gì bạn cần làm là thay đổi phương thức lớp Dao của bạn từ LiveData <List <T>> thành DataSource.Factory <Int, T>.


Sau đó, sử dụng LivePagedListBuilder, nó có thể được chuyển đổi thành LiveData đã biết sẽ được quan sát trong viewModel.


@Dao
interface LegoSetDao {

@Query("SELECT * FROM sets WHERE themeId = :themeId ORDER BY year DESC")
fun getLegoSets(themeId: Int): LiveData<List<LegoSet>>

@Query("SELECT * FROM sets WHERE themeId = :themeId ORDER BY year DESC")
fun getPagedLegoSetsByTheme(themeId: Int): DataSource.Factory<Int, LegoSet>

// ...
}

Đối với Paging PageKeyedDataSource trực tuyến có thể được tạo, nơi các điểm cuối API sẽ được sử dụng để cuộn các trang tới và lui.

/**
* Data source for lego sets pagination via paging library
*/
class LegoSetPageDataSource @Inject constructor(
private val themeId: Int? = null,
private val dataSource: LegoSetRemoteDataSource,
private val dao: LegoSetDao,
private val scope: CoroutineScope) : PageKeyedDataSource<Int, LegoSet>() {

override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, LegoSet>) {
fetchData(1, params.requestedLoadSize) {
callback.onResult(it, null, 2)
}
}

override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, LegoSet>) {
val page = params.key
fetchData(page, params.requestedLoadSize) {
callback.onResult(it, page + 1)
}
}

override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, LegoSet>) {
val page = params.key
fetchData(page, params.requestedLoadSize) {
callback.onResult(it, page - 1)
}
}

private fun fetchData(page: Int, pageSize: Int, callback: (List<LegoSet>) -> Unit) {
scope.launch(getJobErrorHandler()) {
val response = dataSource.fetchSets(page, pageSize, themeId)
if (response.status == Result.Status.SUCCESS) {
val results = response.data!!.results
dao.insertAll(results)
callback(results)
} else if (response.status == Result.Status.ERROR) {
postError(response.message!!)
}
}
}

private fun getJobErrorHandler() = CoroutineExceptionHandler { _, e ->
postError(e.message ?: e.toString())
}

private fun postError(message: String) {
Timber.e("An error happened: $message")
}

}


class LegoSetPageDataSourceFactory @Inject constructor(
private val themeId: Int? = null,
private val dataSource: LegoSetRemoteDataSource,
private val dao: LegoSetDao,
private val scope: CoroutineScope) : DataSource.Factory<Int, LegoSet>() {

private val liveData = MutableLiveData<LegoSetPageDataSource>()

override fun create(): DataSource<Int, LegoSet> {
val source = LegoSetPageDataSource(themeId, dataSource, dao, scope)
liveData.postValue(source)
return source
}

companion object {
private const val PAGE_SIZE = 100

fun pagedListConfig() = PagedList.Config.Builder()
.setInitialLoadSizeHint(PAGE_SIZE)
.setPageSize(PAGE_SIZE)
.setEnablePlaceholders(true)
.build()
}

}

Cho phép xem LegoSetRepository với các chức năng quan sát danh sách dữ liệu được phân trang cục bộ và từ xa.

@Singleton
@OpenForTesting
class LegoSetRepository @Inject constructor(private val dao: LegoSetDao,
private val legoSetRemoteDataSource: LegoSetRemoteDataSource) {

private fun observeLocalPagedSets(themeId: Int? = null): LiveData<PagedList<LegoSet>> {
val dataSourceFactory = dao.getPagedLegoSetsByTheme(themeId)

return LivePagedListBuilder(dataSourceFactory,
LegoSetPageDataSourceFactory.pagedListConfig()).build()
}

private fun observeRemotePagedSets(themeId: Int? = null, ioCoroutineScope: CoroutineScope)
: LiveData<PagedList<LegoSet>> {
val dataSourceFactory = LegoSetPageDataSourceFactory(themeId, legoSetRemoteDataSource,
dao, ioCoroutineScope)
return LivePagedListBuilder(dataSourceFactory,
LegoSetPageDataSourceFactory.pagedListConfig()).build()
}
// ...
}

Ưu điểm: Paging dễ sử dụng cho các nguồn ngoại tuyến và trực tuyến riêng biệt.

Nhược điểm:

- Xử lý lỗi không đơn giản, nó yêu cầu thực hiện các lệnh gọi lại lỗi tùy chỉnh;

- Phân trang yêu cầu tùy chỉnh không nhỏ để thực hiện chiến lược Single Source of Truth khi tải các trang từ cơ sở dữ liệu và tải nhiều mục hơn một cách nhanh chóng từ máy chủ. Do đó, để đơn giản hóa nghiên cứu điển hình này, ứng dụng hiện tại sử dụng phân trang trực tuyến và ngoại tuyến riêng biệt. Kho lưu trữ chọn phân trang cục bộ hoặc từ xa tùy thuộc vào kết nối khả dụng của người dùng.


11. CoroutineWorker with Work Manager


Work Manager hỗ trợ Kotlin Coroutines thông qua lớp CoroutineWorker. Nó không cung cấp luồng không đồng bộ để chạy theo mặc định như IntentService. Vì vậy, chúng ta cần sử dụng hàm coroutineScope và withContext() chỉ định luồng hoạt động thông qua Dispatcher đã chọn. Hãy xem ví dụ SeedDatabaseWorker tải trước các mục cơ sở dữ liệu trên sự kiện tạo cơ sở dữ liệu.


class SeedDatabaseWorker(
context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {

override suspend fun doWork(): Result = coroutineScope {
withContext(Dispatchers.IO) {

try {
applicationContext.assets.open(DATA_FILENAME).use { inputStream ->
JsonReader(inputStream.reader()).use { jsonReader ->
val type = object : TypeToken<List<LegoSet>>() {}.type
val list: List<LegoSet> = Gson().fromJson(jsonReader, type)

AppDatabase.getInstance(applicationContext).legoSetDao().insertAll(list)

Result.success()
}
}
} catch (e: Exception) {
Timber.e(e, "Error seeding database")
Result.failure()
}
}
}
}

12. Navigation component



Navigation component cấu trúc điều hướng và hiển thị tất cả các chuyển đổi trong một sơ đồ thông qua xml. Thật tuyệt khi thấy tất cả quá trình chuyển đổi ở một nơi có quyền kiểm soát chúng. Thư viện loại bỏ mã soạn sẵn để quản lý trạng thái phân mảnh. Hai đặc quyền bổ sung là hoạt ảnh mặc định của các giao dịch trên màn hình, định nghĩa tham số tại chỗ với khai báo phân đoạn.


Navigation component phục vụ rất nhiều cho mục đích của ứng dụng cơ bản. Mặc dù từ kinh nghiệm với các ứng dụng phức tạp hơn, sử dụng BottomNavigationView và các trình duyệt điều hướng khác, mọi thứ trở nên không tầm thường và yêu cầu tùy chỉnh. Vì vậy, hãy ghi nhớ quy mô ứng dụng của bạn khi bạn quyết định sử dụng Navigation component.


Tôi nghĩ rằng trong thời gian và thành phần tác động cộng đồng của chúng tôi sẽ phát triển thành giải pháp toàn diện.


Chúng ta hãy xem xét kỹ hơn nav_main.xml.


<?xml version="1.0" encoding="utf-8"?>
<navigation 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"
app:startDestination="@+id/legothemes_fragment">

<fragment
android:id="@+id/legothemes_fragment"
android:name="com.elifox.legocatalog.legotheme.ui.LegoThemeFragment"
android:label="@string/legothemes_title"
tools:layout="@layout/fragment_themes">

<action
android:id="@+id/action_theme_fragment_to_sets_fragment"
app:destination="@id/legosets_fragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment>

<fragment
android:id="@+id/legosets_fragment"
android:name="com.elifox.legocatalog.legoset.ui.LegoSetsFragment"
android:label="@string/legosets_title"
tools:layout="@layout/fragment_legosets">

<argument
android:name="themeId"
app:argType="integer"
android:defaultValue="-1"/>

<argument
android:name="themeName"
app:argType="string"
app:nullable="true"
android:defaultValue="@null"/>

<action
android:id="@+id/action_to_legoset_detail_fragment"
app:destination="@id/legoset_detail_fragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment>

<fragment
android:id="@+id/legoset_detail_fragment"
android:name="com.elifox.legocatalog.legoset.ui.LegoSetFragment"
android:label="@string/legoset_details_title"
tools:layout="@layout/fragment_lego_set">

<argument
android:name="id"
app:argType="string" />
</fragment>

</navigation>

13. Summary


Đúng rồi! Chúng tôi vừa hoàn thành một ứng dụng sử dụng Kotlin Coroutines, Android Jetpack (ViewModel, LiveData, Room, Paging, Navigation), Retrofit và Dagger 2. Ứng dụng có thể mở rộng và mạnh mẽ, rất may là Architecture được thiết kế dựa trên Guide to app architecture.


Chúng ta có thể thấy rằng Kotlin Coroutines kết hợp với Kotlin được xây dựng trong các chức năng và hỗ trợ LiveData là một sự thay thế mạnh mẽ cho RxJava2. Nó loại bỏ các lệnh gọi lại và ít dài dòng hơn, làm cho mã ngắn gọn hơn. Ngoài ra, có ít phụ thuộc bên thứ 3 hơn cũng là một lợi ích lớn. Kotlin Coroutines phục vụ tuyệt vời cho các ứng dụng cơ bản và có tiềm năng cho các giải pháp quy mô lớn.


Tôi hy vọng Architecture Overview này kết hợp với tất cả các thành phần đã sử dụng sẽ nâng cao năng suất và kiến thức chuyên môn của bạn!


Tôi muốn xem ý kiến và kinh nghiệm của bạn với Architecture hiện tại và các thành phần bên dưới trong phần bình luận.


Trong bài viết tiếp theo, tôi muốn đề cập đến các quyết định về Android Architecture. Làm thế nào để không chạy theo các xu hướng mới nhất một cách mù quáng, nhưng thực tế suy nghĩ về ưu và nhược điểm của mọi quyết định kiến trúc mà chúng ta đưa ra. Giữ nguyên!



Nếu bạn thấy hướng dẫn này hữu ích thì Claps của bạn sẽ được đánh giá cao và tạo cảm hứng cho các bài viết tiếp theo! 


Mã nguồn đầy đủ của dự án này cùng với danh sách đầy đủ các thư viện được sử dụng có thể được tìm thấy trên GitHub: https://github.com/Eli-Fox/LEGO-Catalog



Nhận xét

Bài đăng phổ biến từ blog này

Jetpack Compose VS SwiftUI !VS Flutter

  Việc phát triển Android đã trở nên dễ dàng hơn khi các bản cập nhật liên tục đến. Sau bản cập nhật 2020.3.1, rất nhiều thứ đã thay đổi. Nhưng thay đổi chính mà tôi nghĩ hầu hết các nhà phát triển phải chờ đợi là Jetpack Compose cho ứng dụng sản xuất. Và Kotlin là lựa chọn duy nhất cho jetpack Compose, cũng là ngôn ngữ được ưu tiên. Để biết thêm chi tiết hoặc các thay đổi trên Jetpack Compose, bạn có thể truy cập vào https://developer.android.com/jetpack/compose Tương tự, IOS Development cũng cung cấp một tùy chọn để phát triển khai báo, SwiftUI. Trong IDE, không có thay đổi nào do điều này. Nhưng khái niệm gần giống với Jetpack Compose. Thay vì bảng phân cảnh, chúng tôi tạo giao diện người dùng bằng Swift. Để biết thêm chi tiết hoặc các thay đổi trên SwiftUI, hãy truy cập https://developer.apple.com/xcode/swiftui/ Hãy xem cách cả hai hoạt động bằng cách sử dụng một dự án demo. Tôi đã lấy một số ví dụ về số lần chạm tương tự của Flutter. 1. Android Jetpack Compose Chúng tôi có thể tạo

Thiết kế giao diện với DotNetBar (Phần 1)

Đây là phiên bản DotNetBar hỗ trợ C# và Visual Basic https://www.dropbox.com/s/wx80jpvgnlrmtux/DotNetBar.rar  , phiên bản này hỗ trợ giao diện Metro cực kỳ “dễ thương” Các bạn load về và cài đặt, khi cài đặt xong sẽ có source code mẫu của tất cả các control. Để sử dụng được các control của DotNetBar các bạn nhớ add item vào controls box. Thiết kế giao diện với DotNetBar, giao diện sẽ rất đẹp. Link các video hướng dẫn chi tiết cách sử dụng và coding: http://www.devcomponents.com/dotnetbar/movies.aspx Hiện tại DotNetBar có rất nhiều công cụ cực mạnh, trong đó có 3 công cụ dưới đây: DotNetBar for Windows Forms Requires with Visual Studio 2003, 2005, 2008, 2010 or 2012.   DotNetBar for WPF Requires with Visual Studio 2010 or 2012 and Windows Presentation Foundation.   DotNetBar for Silverlight Requires with Visual Studio 2010 or 2012 and Silverlight. Dưới đây là một số hình ảnh về các control trong DotnetBar.   Metro User Interface  controls with Metro Tiles, toolbars, slide panels, forms,

Một số bài tập Winform C#

Một số bài tập: 1. Mô phỏng game đoán số. Luật chơi:         o Đúng số và đúng vị trí   +         o Đúng số mà sai vị trí      ?         o Sai số và sai vị trí          -         . . .         - Kết quả được tạo ngẫu nhiên từ các số có 4 chữ số.         - Các chữ số có giá trị từ 0-6.         - Người chơi có 6 lần đoán. Chương trình tham khảo: 2. In số điện tử Yêu cầu: người dùng nhập vào 1 số ( hoặc 1 chuỗi số) yêu cầu in ra số đó dưới dạng số điện tử. Chương trình tham khảo: 3. Mô phỏng game CARO  (update) 4. Mô phỏng game DÒ MÌN (update)