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
Đăng nhận xét