Computer >> Hướng Dẫn Máy Tính >  >> Hệ Thống >> Android

Thành thạo kiến trúc App: Chọn pattern cho phát triển tin cậy, mở rộng

Series ứng dụng Kriptofolio - Phần 3

Điều quan trọng nhất khi bắt đầu xây dựng app mới là kiến trúc. Sai lầm lớn nhất là không có kiến trúc nào cả.

Việc chọn kiến trúc gây tranh cãi nhiều trong cộng đồng Android gần đây. Google cũng tham gia. Năm 2017 họ đề xuất kiến trúc chuẩn bằng Android Architecture Components. Mục đích làm cho cuộc sống developer dễ dàng hơn.

Bài này thảo luận tại sao cần kiến trúc app, các lựa chọn có, và cách thực hiện. Thay vì phát minh lại bánh xe, dùng hướng dẫn của Android team.

Bài này khó viết nhất do kiến thức hạn chế của tôi. Trước hết phải nghiên cứu kiến trúc thật kỹ để thấy bức tranh toàn cảnh. Giờ sẵn sàng chia sẻ.

Nội dung Series

  • Giới thiệu: Lộ trình xây dựng app Android hiện đại 2018–2019
  • Phần 1: Giới thiệu nguyên lý SOLID
  • Phần 2: Bắt đầu xây dựng app Android: Mockup, UI, XML layouts
  • Phần 3: Tất cả về kiến trúc: khám phá pattern và cách dùng (bạn đang ở đây)
  • Phần 4: Triển khai Dependency Injection với Dagger 2
  • Phần 5: Xử lý RESTful Web Services với Retrofit, OkHttp, Gson, Glide và Coroutines

Tại sao nên quan tâm kiến trúc app?

Thường khi bắt đầu Android, bạn viết hầu hết business logic trong Activity/Fragment. Mọi developer mới đều thế, kể cả tôi. Tất cả tutorial ngắn và sample đều làm vậy. Với app nhỏ để giải thích, cách đó ổn.

Tuy nhiên, thử làm trên app thực tế luôn thay đổi theo nhu cầu user và mở rộng tính năng. Sớm muộn bạn thấy trải nghiệm code ngày càng đau đớn. Mọi thứ do "God Classes" như Activity/Fragment quản lý. Chúng có quá nhiều dòng code, bạn dễ bị lạc.

Code bắt đầu giống spaghetti — mọi thứ lẫn lộn. Tất cả phần phụ thuộc lẫn nhau. Khi business yêu cầu thay đổi, bạn buộc phải rebuild toàn bộ project. Đó là lúc câu hỏi kiến trúc xuất hiện.

Có cách tốt hơn cấu trúc code?

Chắc chắn có! Chìa khóa code chất lượng là tuân thủ nguyên lý SOLID. Đã nói ở bài trước (ko ngẫu nhiên). Cũng áp dụng pattern kiến trúc cho separation of concerns (tách biệt mối quan tâm). Thực tế, separation of concerns phải là mục tiêu tối thượng. Điểm quan trọng nhất chỉ ra chất lượng code. Có nhiều pattern kiến trúc app. Ba pattern tier cổ điển nổi tiếng:

  • MVC: Model-View-Controller
  • MVP: Model-View-Presenter
  • MVVM: Model-View-ViewModel

Tất cả pattern đại diện ý tưởng chính tương tự: cấu trúc code project cách tách biệt bằng các layer generic. Mỗi layer có trách nhiệm riêng. Project trở nên modular: phần code tách biệt dễ test hơn, app đủ linh hoạt cho thay đổi liên tục.

Nói riêng từng pattern, chủ đề quá rộng. Chỉ giới thiệu để hiểu khác biệt chính.

Pattern Model-View-Controller (MVC)

Pattern đầu tiên Android áp dụng xưa. Đề xuất tách code thành 3 layer:

Model — data layer. Chịu trách nhiệm business logic và giao tiếp network, database.

View — UI layer. Trực quan hóa data từ Model.

Controller — logic layer, nhận thông báo hành vi user và update Model khi cần.

Thành thạo kiến trúc App: Chọn pattern cho phát triển tin cậy, mở rộng

Sơ đồ MVC. Cả Controller và View phụ thuộc Model. Controller update data. View lấy data. Model tách biệt, test độc lập UI.

Có vài cách áp dụng MVC. Hơi rối.

Cách 1: Activity/Fragment đóng vai Controller. Chịu trách nhiệm xử lý data và update view. Vấn đề: Activity/Fragment trở nên lớn, khó test.

Cách 2 (logic và đúng hơn): Activity/Fragment là View trong MVC. Controller là class riêng không extend/use class Android. Model cũng vậy.

Dù 조사 sâu MVC, khi áp dụng Android, ngay cả cách đúng code layer vẫn phụ thuộc lẫn nhau. Tôi không khuyến nghị dùng MVC cho app Android tiếp theo.

Pattern Model-View-Presenter (MVP)

Sau cách đầu không ổn, developer Android chuyển sang pattern phổ biến — MVP. Iteration thứ 2 của kiến trúc. Pattern này dùng rộng rãi và vẫn được khuyến nghị. Dễ học cho người mới Android. Xem 3 layer:

Model — data layer, giống MVC. Chịu trách nhiệm business logic, giao tiếp network, database.

View — UI layer. Hiển thị data và báo cho Presenter về user action.

Presenter — lấy data từ Model, áp dụng UI logic, quản lý state View, quyết định hiển thị gì, phản ứng thông báo user input từ View. Bản chất là Controller từ MVC nhưng không gắn kết với View, chỉ interface.

Thành thạo kiến trúc App: Chọn pattern cho phát triển tin cậy, mở rộng

Sơ đồ MVP: View và Presenter liên quan chặt. Cần reference lẫn nhau. Mối quan hệ định nghĩa trong interface Contract.

Pattern này có nhược điểm đáng kể nhưng kiểm soát được. Presenter dễ phình to thành class all-knowing nếu không cẩn thận chia code theo single responsibility principle. Tuy nhiên, nói chung MVP tách biệt mối quan tâm tốt. Có thể là lựa chọn chính cho project.

Pattern Model-View-ViewModel (MVVM)

MVVM là iteration thứ 3. Trở thành pattern khuyến nghị bởi Android team với Android Architecture Components. Tập trung học pattern này. Cũng dùng cho app "My Crypto Coins". Xem các layer:

Model — trừu tượng hóa data source. ViewModel làm việc với Model để lấy/lưu data.

View — báo cho ViewModel về user action.

ViewModel — expose stream data liên quan đến View.

Khác biệt so với MVP: trong MVVM, ViewModel không hold reference đến View như Presenter. ViewModel expose stream events mà các View có thể bind. MVP thì Presenter trực tiếp bảo View hiển thị gì. Xem sơ đồ MVVM:

Thành thạo kiến trúc App: Chọn pattern cho phát triển tin cậy, mở rộng

MVVM: View có reference đến ViewModel. ViewModel không biết View. Mối quan hệ many-to-one giữa View và ViewModel.

So sánh MVC vs MVP vs MVVM

Bảng tóm tắt các pattern:

Thành thạo kiến trúc App: Chọn pattern cho phát triển tin cậy, mở rộng

Như thấy, MVC không tốt bằng MVP và MVVM cho app modular, testable, hiện đại. Mỗi pattern có ưu/nhược điểm. Lựa chọn tốt nếu khớp nhu cầu. Khuyến nghị nghiên cứu thêm tất cả pattern — đáng giá.

Tạm thời, tôi tiếp tục project với pattern xu hướng 2018, do Google đẩy — MVVM.

Android Architecture Components

Nếu quen lifecycle Android app, bạn biết đau đầu xây app tránh vấn đề data flow, persistence, stability khi configuration change.

2017, Android team quyết định chúng ta đã khổ đủ. Chịu trách nhiệm và giới thiệu Android Architecture Components. Cuối cùng giải quyết mọi issue không làm phức tạp code hoặc hack.

Android Architecture Components là bộ thư viện giúp thiết kế app robust, testable, maintainable. Hiện tại gồm:

  • Data Binding — bind declarative observable data đến UI element
  • Lifecycles — quản lý activity/fragment lifecycle
  • LiveData — thông báo view khi database thay đổi
  • Navigation — xử lý mọi thứ cần cho navigation in-app
  • Paging — load dần thông tin on-demand từ data source
  • Room — truy cập SQLite database fluent
  • ViewModel — quản lý UI-related data lifecycle-aware
  • WorkManager — quản lý background job Android

Với Android Architecture Components, triển khai MVVM trong My Crypto Coins theo sơ đồ:

Thành thạo kiến trúc App: Chọn pattern cho phát triển tin cậy, mở rộng

Kiến trúc khuyến nghị bởi Google. Chỉ cover các component cụ thể dùng trong project.

Tổ chức source file

Trước khi dev, cân nhắc tổ chức source file project. Không thể bỏ qua, sau này cấu trúc rối, khó hiểu/sửa.

Có vài cách. Cách 1: tổ chức theo category component. Ví dụ: activity riêng folder, adapter riêng folder...

Cách 2: tổ chức theo feature app. Ví dụ: search và add crypto trong feature list crypto vào folder addsearchlist. Ý tưởng chính: làm theo cách cụ thể thay vì đặt lung tung. Tôi dùng mix cả hai.

Thành thạo kiến trúc App: Chọn pattern cho phát triển tin cậy, mở rộng

Cấu trúc folder My Crypto Coins app

Bên cạnh cấu trúc folder, cân nhắc quy tắc đặt tên file. Ví dụ: đặt tên class Android nên thể hiện rõ mục đích class.

ViewModel

Bắt đầu kiến trúc app, trước tiên tạo ViewModel. ViewModel là object cung cấp data cho UI component và sống sót qua configuration change.

Dùng ViewModel retain data xuyên suốt lifecycle activity/fragment. Activity/Fragment ngắn đời, tạo/hủy thường xuyên khi user tương tác. ViewModel phù hợp hơn quản lý task network, thao tác data, persistence.

Ví dụ tạo ViewModel cho MainListFragment tách UI data:

class MainViewModel : ViewModel() { ... }

Lấy ViewModel với một dòng code:

class MainListFragment : Fragment() { ... private lateinit var viewModel: MainViewModel ... override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setupList() // Obtain ViewModel từ ViewModelProviders, dùng fragment này làm LifecycleOwner. viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) ... } ... }

Cơ bản thế, chúc mừng! 😊 Tiếp tục.

LiveData

LiveData là observable data holder class. Theo dõi observer pattern. LiveData lifecycle-aware. Chỉ update observer (activity, fragment...) ở active lifecycle state.

LiveData trả về giá trị mới nhất của data. Khi data thay đổi, trả về giá trị cập nhật. LiveData phù hợp nhất với ViewModel.

Dùng LiveData với ViewModel như sau:

... class MainViewModel : ViewModel() { private val liveData = MutableLiveData<ArrayList<Cryptocurrency>>() val data: LiveData<ArrayList<Cryptocurrency>> get() = liveData init { val tempData = ArrayList<Cryptocurrency>() val btc:Cryptocurrency = Cryptocurrency("Bitcoin", 1, 0.56822348, "BTC", 8328.77, 4732.60, 0.19, -10.60, 0.44, 20.82) val eth:Cryptocurrency = Cryptocurrency("Etherium", 2, 6.0, "ETH", 702.99, 4217.94, 0.13, -7.38, 0.79, 33.32) tempData.add(btc) tempData.add(eth) liveData.value = tempData } }

Observe data trên ViewModel, expose as LiveData:

... class MainListFragment : Fragment() { private lateinit var recyclerView: RecyclerView private lateinit var recyclerAdapter: MainRecyclerViewAdapter private lateinit var viewModel: MainViewModel ... override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setupList() // Obtain ViewModel từ ViewModelProviders, dùng fragment này làm LifecycleOwner. viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) // Observe data trên ViewModel, expose as LiveData viewModel.data.observe(this, Observer { data -> // Set data exposed by LiveData if (data != null) { recyclerAdapter.setData(data) } }) } ... }

Duyệt repository tại thời điểm này tại đây.

Data Binding

Data Binding Library tạo ra để loại bỏ boilerplate code connect XML layout.

Dùng Data Binding trong Kotlin project, bật annotation processor với kapt compiler plugin. Thêm block data binding vào gradle Android config:

... apply plugin: 'kotlin-kapt' android { ... dataBinding { enabled = true } } ...

Dùng data binding generated class, đặt toàn bộ view code trong thẻ <layout>. Khái niệm mạnh nhất: bind data class vào XML layout và item properties đến field trực tiếp.

<layout xmlns:app="https://schemas.android.com/apk/res-auto" xmlns:tools="https://schemas.android.com/tools"> <data> <variable name="cryptocurrency" type="com.baruckis.mycryptocoins.data.Cryptocurrency" /> </data> ... <android.support.v7.widget.AppCompatTextView android:id="@+id/item_name" style="@style/MainListItemPrimeText" android:layout_marginEnd="@dimen/main_cardview_list_item_text_between_margin" android:layout_marginStart="@dimen/main_cardview_list_item_inner_margin" android:text="@{cryptocurrency.name}" android:textAlignment="viewStart" app:layout_constraintBottom_toTopOf="@+id/item_amount_symbol" app:layout_constraintEnd_toStartOf="@+id/guideline1_percent" app:layout_constraintStart_toEndOf="@+id/item_image_icon" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="spread" tools:text="@string/sample_text_item_name" /> ... </layout>

RecyclerView adapter với data binding:

class MainRecyclerViewAdapter() : RecyclerView.Adapter<MainRecyclerViewAdapter.BindingViewHolder>() { private lateinit var dataList: ArrayList<Cryptocurrency> fun setData(newDataList: ArrayList<Cryptocurrency>) { dataList = newDataList } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder { val inflater = LayoutInflater.from(parent.context) val binding = FragmentMainListItemBinding.inflate(inflater, parent, false) return BindingViewHolder(binding) } override fun onBindViewHolder(holder: BindingViewHolder, position: Int) = holder.bind(dataList[position]) override fun getItemCount(): Int = dataList.size ... inner class BindingViewHolder(var binding: FragmentMainListItemBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(cryptocurrency: Cryptocurrency) { binding.cryptocurrency = cryptocurrency binding.itemRanking.text = String.format("${cryptocurrency.rank}") ... binding.executePendingBindings() } } }

Cuối cùng không còn viết findViewById 😊 Duyệt repository tại thời điểm này tại đây.

Room

App cần lưu persistent data các cryptocurrency user nắm giữ. Lưu trong local database private trên thiết bị Android.

Lưu structured data trong private database, dùng SQLite. Thường là lựa chọn tốt nhất.

Tạo SQLite database cho app, dùng Room. Room là persistence library của Android team, wrapper trên SQLite. Abstraction layer loại bỏ boilerplate code interact SQLite. Cũng thêm compile-time checking SQL query.

Cách hiểu tốt nhất: ORM (Object Relational Mapper) tự động sinh glue code map giữa object instance và row database.

3 component chính trong Room:

  1. Entity — class hold database row. Mỗi entity, một table tạo ra hold item.
  2. DAO (Data Access Object) — component chính định nghĩa method truy cập database.
  3. Database — holder class dùng annotation định nghĩa list entity, list DAO, version database, main access point cho underlying connection.

Thành thạo kiến trúc App: Chọn pattern cho phát triển tin cậy, mở rộng

Setup Room trong My Crypto Coins app theo bước:

  1. Tạo Entity.
@Entity data class Cryptocurrency(val name: String, val rank: Short, val amount: Double, @PrimaryKey val symbol: String, val price: Double, val amountFiat: Double, val pricePercentChange1h: Double, val pricePercentChange7d: Double, val pricePercentChange24h: Double, val amountFiatChange24h: Double)

Thêm info để Room hiểu cấu trúc database.

2. Tạo DAO.

@Dao interface MyCryptocurrencyDao { @Query("SELECT * FROM Cryptocurrency") fun getMyCryptocurrencyLiveDataList(): LiveData<List<Cryptocurrency>> @Insert fun insertDataToMyCryptocurrencyList(data: List<Cryptocurrency>) }

Đầu tiên, tạo DAO chỉ cho phép retrieve record từ table tạo bởi Entity và insert sample data.

3. Tạo và setup Database.

Quan trọng: Database instance nên build chỉ một lần per session. Cách đạt được: Singleton pattern.

@Database(entities = [Cryptocurrency::class], version = 1, exportSchema = false) abstract class AppDatabase : RoomDatabase() { abstract fun myCryptocurrencyDao(): MyCryptocurrencyDao // AppDatabase singleton để tránh multiple instance database mở cùng lúc. companion object { @Volatile private var instance: AppDatabase? = null fun getInstance(context: Context): AppDatabase { return instance ?: synchronized(this) { instance ?: buildDatabase(context).also { instance = it } } } private fun buildDatabase(context: Context): AppDatabase { return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) .addCallback(object : Callback() { override fun onCreate(db: SupportSQLiteDatabase) { super.onCreate(db) ioThread { getInstance(context).myCryptocurrencyDao().insertDataToMyCryptocurrencyList(PREPOPULATE_DATA) } } }) .build() } val btc: Cryptocurrency = Cryptocurrency("Bitcoin", 1, 0.56822348, "BTC", 8328.77, 4732.60, 0.19, -10.60, 0.44, 20.82) val eth: Cryptocurrency = Cryptocurrency("Etherium", 2, 6.0, "ETH", 702.99, 4217.94, 0.13, -7.38, 0.79, 33.32) val PREPOPULATE_DATA = listOf(btc, eth) } }
private val IO_EXECUTOR = Executors.newSingleThreadExecutor() fun ioThread(f : () -> Unit) { IO_EXECUTOR.execute(f) }

Lần chạy đầu, database prepopulate sample data để test.

4. BƯỚC EXTRA. Tạo Repository.

Repository không thuộc Architecture Components. Là best practice cho code separation và kiến trúc.

Thành thạo kiến trúc App: Chọn pattern cho phát triển tin cậy, mở rộng

Đứng là single source of truth cho tất cả app data nếu quản lý multiple data source.

class MyCryptocurrencyRepository private constructor( private val myCryptocurrencyDao: MyCryptocurrencyDao ) { fun getMyCryptocurrencyLiveDataList(): LiveData<List<Cryptocurrency>> { return myCryptocurrencyDao.getMyCryptocurrencyLiveDataList() } companion object { @Volatile private var instance: MyCryptocurrencyRepository? = null fun getInstance(myCryptocurrencyDao: MyCryptocurrencyDao) = instance ?: synchronized(this) { instance ?: MyCryptocurrencyRepository(myCryptocurrencyDao).also { instance = it } } } }

Dùng repository trong ViewModel.

class MainViewModel(myCryptocurrencyRepository: MyCryptocurrencyRepository) : ViewModel() { val liveData = myCryptocurrencyRepository.getMyCryptocurrencyLiveDataList() }

Fragment code cũng evolve.

class MainListFragment : Fragment() { ... private lateinit var viewModel: MainViewModel ... override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setupList() subscribeUi() } ... private fun subscribeUi() { val factory = InjectorUtils.provideMainViewModelFactory(requireContext()) viewModel = ViewModelProviders.of(this, factory).get(MainViewModel::class.java) viewModel.liveData.observe(this, Observer<List<Cryptocurrency>> { data -> if (data != null && data.isNotEmpty()) { emptyListView.visibility = View.GONE recyclerView.visibility = View.VISIBLE recyclerAdapter.setData(data) } else { recyclerView.visibility = View.GONE emptyListView.visibility = View.VISIBLE } }) } }

Vì ViewModel class giờ có constructor không rỗng, cần implement provider factory pattern. Truyền vào ViewModelProviders.of() làm param thứ 2.

object InjectorUtils { fun provideMainViewModelFactory( context: Context ): MainViewModelFactory { val repository = getMyCryptocurrencyRepository(context) return MainViewModelFactory(repository) } private fun getMyCryptocurrencyRepository(context: Context): MyCryptocurrencyRepository { return MyCryptocurrencyRepository.getInstance( AppDatabase.getInstance(context).myCryptocurrencyDao()) } }
class MainViewModelFactory(private val repository: MyCryptocurrencyRepository) : ViewModelProvider.NewInstanceFactory() { override fun <T : ViewModel?> create(modelClass: Class<T>): T { return MainViewModel(repository) as T } }

Duyệt repository tại thời điểm này tại đây.

Final thoughts

Kiến trúc thảo luận nên dùng làm hướng dẫn có thông tin, không phải quy tắc cứng. Không muốn đi sâu chi tiết mỗi topic. Với Android Architecture Components đã xem quy trình code. Nhớ rằng còn nhiều hơn để học từng component, khuyến nghị tự học.

Tóm tắt những gì đã làm:

  • My Crypto Coins app, mỗi màn hình riêng có ViewModel riêng. Sống sót configuration change, bảo vệ user khỏi data loss.
  • UI app kiểu reactive. Update ngay khi data thay đổi backend. Nhờ LiveData.
  • Project ít code hơn do bind trực tiếp variable trong code dùng Data Binding.
  • App lưu user data local trên device SQLite database. Database tạo tiện lợi với Room. Code cấu trúc theo feature, kiến trúc MVVM — pattern khuyến nghị Android team.

Repository

Giờ bạn thấy "Kriptofolio" (trước "My Crypto Coins") thực sự hình thành. Với commit mới nhất phần 3, app hiển thị đẹp data prepopulated database cho user với total portfolio value tính đúng.

View Source On GitHub

Ačiū! Cảm ơn đã đọc! Bài gốc đăng trên blog www.baruckis.com ngày 22/8/2018.