Room là một Persistence Library được google giới thiệu trong sự kiện google I/O mới đây, nó là một abstract layer cung cấp cách thức truy câp thao tác với dữ liệu trong cơ sở dữ liệu SQLite. Các thành phần: Database, DAO (Data Access Object) và Entity.
Các ứng dụng sử dụng một lượng lớn dữ liệu có cấu trúc có thể hưởng lợi lớn từ việc lưu lại dữ liệu trên local thông qua Room Database. Trường hợp thường gặp nhất là chỉ cache những dữ liệu có liên quan. Nếu làm vậy thì khi thiết bị không có kết nối internet thì user vẫn có thể truy cập data đấy khi đang offline. Mọi dữ liệu được phát sinh hay thay đổi do user sau đó sẽ được đồng bộ với server khi họ online trở lại.
Room Database đơn giản hóa việc mã hóa và giảm thiểu các hoạt động liên quan đến cơ sở dữ liệu. Nếu các bạn đã chán ngán với việc phải khai báo các câu lệnh rất dài mới có thể xây dựng được database thì hãy sử dụng Room ngay nhé!
2. Đặc điểm của Room database
Trong trường hợp SQLite, Không có xác minh thời gian biên dịch của các truy vấn SQLite thô. Nhưng trong Room có xác thực SQL tại thời điểm biên dịch.
Khi lược đồ của bạn thay đổi, bạn cần cập nhật các truy vấn SQL bị ảnh hưởng theo cách thủ công. Room giải quyết vấn đề này.
Bạn cần sử dụng nhiều mã soạn sẵn để chuyển đổi giữa các truy vấn SQL và các Data objects. Tuy nhiên, Room ánh xạ các đối tượng cơ sở dữ liệu của chúng tôi tới Data objects mà không cần mã soạn sẵn.
Room được xây dựng để hoạt động với LiveData để quan sát dữ liệu, trong khi SQLite thì không.
3. Cách import Room
Mở build.gradle (app) và thêm dòng lệnh sau:
dependencies {
def room_version = "2.4.0"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// optional - RxJava2 support for Room
implementation "androidx.room:room-rxjava2:$room_version"
// optional - RxJava3 support for Room
implementation "androidx.room:room-rxjava3:$room_version"
// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
// optional - Paging 3 Integration
implementation "androidx.room:room-paging:2.4.0"
}
4. Các thành phần chính trong Room
Có 3 thành phần chính trong Room:
Database
Có thể dùng componenet này để tạo database holder. Annotation sẽ cung cấp danh sách các thực thể và nội dung class sẽ định nghĩa danh sách các DAO (đối tượng truy cập CSDL) của CSDL. Nó cũng là điểm truy cập chính cho các kết nối phía dưới.
@Database(
entities = [
School::class,
Student::class
],
version = 2,
exportSchema = false
)
abstract class SchoolDataBase : RoomDatabase() {
abstract fun schoolDAO(): ISchoolDAO
abstract fun studentDAO(): IStudentDAO
companion object {
@Volatile
private var INSTANCE: SchoolDataBase? = null
fun getInstance(context: Context): SchoolDataBase = INSTANCE ?: synchronized(this) {
return Room.databaseBuilder(
context.applicationContext,
SchoolDataBase::class.java,
Constant.SCHOOL_DATABASE
).build().also {
INSTANCE = it
}
}
}
}
Entity
Component này đại diện cho một class chứa một row của database. Với mỗi một entity thì một database table sẽ được tạo để giữ các items tương ứng. Nên tham chiếu lớp enity thông qua mảng entities trong class Database.
Mỗi Object phải xác định ít nhất 1 trường làm khóa chính. Ngay cả khi chỉ có 1 trường, bạn vẫn cần chú thích trường này bằng anotation @PrimaryKey. Ngoài ra, nếu bạn muốn Room gán ID tự động cho các thực thể, bạn có thể đặt thuộc tính autoGenerate của @PrimaryKey.(Trường hợp thuộc tính là int, long).
@Entity(tableName = SCHOOL_TABLE)
data class School(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = SCHOOL_ID)
val schoolId: Int = 0,
@ColumnInfo(name = SCHOOL_NAME)
val schoolName: String,
@ColumnInfo(name = SCHOOL_ADDRESS)
val schoolAddress: String
)
Trường hợp các bạn muốn đánh index cho một số trường trong database để tăng tốc độ truy vấn các bạn có thể sử dụng như sau:
Định nghĩa foreignKeys. Ví dụ bạn có đối tượng khác là Student và bạn có thể định nghĩa relationship tới đối tượng School thông qua @ForeignKey annotation như sau:
@Entity(
tableName = STUDENT_TABLE,
foreignKeys = [ForeignKey(
entity = School::class,
parentColumns = [SCHOOL_ID],
childColumns = [SCHOOL_ID],
onDelete = ForeignKey.CASCADE
)]
)
data class Student(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = STUDENT_ID)
val studentId: Int = 0,
@ColumnInfo(name = SCHOOL_ID)
val schoolId: Int,
@ColumnInfo(name = STUDENT_NAME)
val studentName: String,
@ColumnInfo(name = STUDENT_GRADE)
val studentGrade: Float
)
DAO (Data Access Object)
Đây là component đại diện cho lớp hoặc interface như một đối tượng truy cập dữ liệu (DAO). DAO là thành phần chính của Room là chịu trách nhiệm trong việc định nghĩa các phương thức truy cập CSDL.
@Dao
interface ISchoolDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSchool(school: School)
@Delete
suspend fun deleteSchool(school: School)
@Update
suspend fun updateSchool(school: School)
@Transaction
@Query("SELECT * FROM $SCHOOL_TABLE")
fun getAllSchool(): Flow<List<SchoolAndStudent>>
}
5. Kết luận
Trên đây là phần giới thiệu cơ bản về Room database của mình.
AIDL hay Android Interface Definition Language là một cách cho phép bạn có thể định nghĩa một cách mà cả client và server (1 ứng dụng đóng vai trò là server cho các ứng dụng khác đóng vai trò là client có thể truy cập tới) có thể giao tiếng với nhau thông qua truyền thông liên tiến trình Interprocess communication (IPC). Thông thường, trong Android một process (tiến trình) không thể trực tiếp truy cập vào bộ nhớ của một tiến trình khác. Vì vậy để có thể các tiến trình có thể giao tiếp với nhau, chúng cần phân tách các đối tượng thành dạng đối tượng nguyên thủy (primitive object) mà hệ thống có thể hiểu được.
Lưu ý Chỉ nên sử AIDL nếu bạn muốn các ứng dụng khác có thể kết nối tới service của ứng dụng bạn thông qua truyền thông liên tiến trinh. Còn nếu bạn chỉ đơn thuần muốn sử dụng service trong ứng dụng của mình bạn nên sử dụng Binder.
2. Tạo .aidl file
AIDL sử dụng cú pháp rất đơn giản cho phép bạn khai báo một interface với một hoặc nhiều phuơng thước có thể có một hoặc nhiều tham số và trả về một giá trị. Các tham số và giá trị trả về có thể thuộc bất kì loại nào thậm chí các interface hoặc object được tạo ra bởi các AIDL khác.
Bạn cần xây dựng một .aidl file sử dụng ngôn ngữ Java, mỗi một .aidl file cần định nghĩa duy nhất một interface.
Mặc định AIDL hộ trỡ các kiểu dữ liệu sau:
8 kiểu dữ liệu nguyên thủy trong java (int, byte, short, long, float, double, boolean, char).
String.
CharSequence.
List, Map Tất cả các phần tử trong List, Map phải là một trong những loại dữ liệu được hỗ trợ trong danh sách này hoặc một trong các đối tượng được định nghĩa thông qua một aidl interface khác.
Khi định nghĩa aidl interface bạn cần lưu ý:
Phuơng thưsc có thể nhận vào 0 hoặc nhiều parameter và trả về void hoặc một giá trị cụ thể.
aidl chỉ hỗ trợ các phương thức không hỗ trợ các static field.
Để tạo file aidl các bạn sử dụng AS làm như sau:
Đây là một tệp .aidl ví dụ:
// IRemoteService.aidl
package com.example.android;
// Declare any non-default types here with import statements
/** Example service interface */
interface IRemoteService {
/** Request the process ID of this service, to do evil things with it. */
int getPid();
/** Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
}
Lưu ý sau khi viết xong file aidl các bạn cần rebuild lại project để SDK tool gen ra file java để có thể sử dụng.
3. Demo với AIDL
Client
Tạo tệp .aidl:
// IIPCExample.aidl
package com.pmirkelam.ipcserver;
// Declare any non-default types here with import statements
interface IIPCExample {
/** Request the process ID of this service */
int getPid();
/** Count of received connection requests from clients */
int getConnectionCount();
/** Set displayed value of screen */
void setDisplayedValue(String packageName, int pid, String data);
}
data class Client(
var clientPackageName: String?,
var clientProcessId: String?,
var clientData: String?,
var ipcMethod: String
)
object RecentClient {
var client: Client? = null
}
Tạo lớp dữ liệu khách hàng.
Tạo một lớp RecentClient singleton nơi chúng tôi sẽ giữ máy khách được kết nối cuối cùng.
Constant.kt:
Đây là dữ liệu dùng chung.
// Bundle keys
const val PID = "pid"
const val CONNECTION_COUNT = "connection_count"
const val PACKAGE_NAME = "package_name"
const val DATA = "data"
AidlFragment.kt:
class IPCServerService : Service() {
companion object {
// How many connection requests have been received since the service started
var connectionCount: Int = 0
// Client might have sent an empty data
const val NOT_SENT = "Not sent!"
}
// AIDL IPC - Binder object to pass to the client
private val aidlBinder = object : IIPCExample.Stub() {
override fun getPid(): Int = Process.myPid()
override fun getConnectionCount(): Int = IPCServerService.connectionCount
override fun setDisplayedValue(packageName: String?, pid: Int, data: String?) {
val clientData =
if (data == null || TextUtils.isEmpty(data)) NOT_SENT
else data
// Get message from client. Save recent connected client info.
RecentClient.client = Client(
packageName ?: NOT_SENT,
pid.toString(),
clientData,
"AIDL"
)
}
}
// Pass the binder object to clients so they can communicate with this service
override fun onBind(intent: Intent?): IBinder? {
connectionCount++
// Choose which binder we need to return based on the type of IPC the client makes
return when (intent?.action) {
"aidlexample" -> aidlBinder
else -> null
}
}
// A client has unbound from the service
override fun onUnbind(intent: Intent?): Boolean {
RecentClient.client = null
return super.onUnbind(intent)
}
}
Tạo đối tượng chất kết dính của chúng tôi bằng cách mở rộng từ lớp Stub. Ở đây chúng tôi điền các phương thức.
Trả lại đối tượng kết dính của chúng tôi cho khách hàng với ràng buộc.
Cập nhật đối tượng RecentClient của chúng tôi khi máy khách liên kết và hủy liên kết để giao diện người dùng được cập nhật.
Bây giờ chúng tôi có thể cập nhật Hoạt động theo thông tin khách hàng.
Máy khách được kết nối cuối cùng sẽ được hiển thị. Bố cục sẽ bị ẩn nếu không có ứng dụng khách được kết nối.
Chú ý: Hầu hết các ứng dụng không nên sử dụng AIDL để tạo một bound service, bởi vì nó có thể yêu cầu khả năng đa luồng và có thể dẫn đến việc triển khai phức tạp hơn. Như vậy, AIDL không phù hợp với hầu hết các ứng dụng nên ta sẽ không thảo luận về cách sử dụng nó.
4. Kết Luận
Trên Đây là cách đơn giản nhất để thực hiện IPC bằng AIDL.
Inter-process communication (IPC) là một cơ chế cho phép các quá trình giao tiếp với nhau và đồng bộ hóa các hành động của chúng.
Thông thường, các ứng dụng có thể sử dụng IPC, được phân loại là máy khách và máy chủ, trong đó máy khách (client) yêu cầu dữ liệu và máy chủ (server) đáp ứng yêu cầu của máy khách.
2. Tại sao cần giao tiếp?
Chia sẻ dữ liệu giữa 2 người dùng.
Mô-đun riêng biệt.
Thực thi nhiều quy trình dễ dàng hơn.
3. Các mô hình IPC
Đây là 2 mô hình IPC mình biết:
Share Memory: Giao tiếp giữa các tiến trình sử dụng bộ nhớ dùng chung yêu cầu các tiến trình phải chia sẻ một số biến và nó hoàn toàn phụ thuộc vào cách lập trình viên sẽ thực hiện nó.
Message passing: Các quá trình giao tiếp với nhau mà không cần sử dụng bất kỳ loại bộ nhớ dùng chung nào. Hai tiến trình p1 và p2 muốn giao tiếp với nhau, chúng tiến hành như sau:
Thiết lập giao tiếp.
Bắt đầu trao đổi tin nhắn bằng cách sử dụng cơ bản primitives.
send(message, destinaion) or send(message)
receive(message, host) or receive(message)
4. IPC trong Android
Thường có hai cách để triển khai IPC trong Android:
Using a Messenger: Thực hiện giao tiếp giữa các quá trình (IPC) với các ứng dụng khác bằng đối tượng Messenger và cho phép dịch vụ xử lý một cuộc gọi tại một thời điểm.
Using AIDL: Tạo tệp .aidl xác định IPC để cho phép máy khách từ các ứng dụng khác nhau truy cập dịch vụ và xử lý đa luồng đến dịch vụ.
5. Sử dụng Messenger
Messenger là một Trình xử lý được gửi đến quy trình từ xa.
Các bước triển khai IPC bằng Messenger:
Service implements mmột Handler nhận một cuộc gọi lại cho mỗi cuộc gọi từ một máy khách.
Service sử dụng Handler để tạo một đối tượng Messenger (là một tham chiếu đến Handler).
Messenger tạo IBinder mà dịch vụ trả về cho khách hàng từ onBind().
Clients sử dụng IBinder để khởi tạo Messenger (tham chiếu đến Handler của Service), mà client sử dụng để gửi các đối tượng Message tới service.
Service từng Message trong Handler của nó — cụ thể là trong phương thức handleMessage().
// Messenger IPC - Messenger object contains binder to send to client
private val mMessenger = Messenger(IncomingHandler())
// Messenger IPC - Message Handler
internal inner class IncomingHandler : Handler() {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
// Get message from client. Save recent connected client info.
val receivedBundle = msg.data
RecentClient.client = Client(
receivedBundle.getString(PACKAGE_NAME),
receivedBundle.getInt(PID).toString(),
receivedBundle.getString(DATA),
"Messenger"
)
// Send message to the client. The message contains server info
val message = Message.obtain(this@IncomingHandler, 0)
val bundle = Bundle()
bundle.putInt(CONNECTION_COUNT, connectionCount)
bundle.putInt(PID, Process.myPid())
message.data = bundle
// The service can save the msg.replyTo object as a local variable
// so that it can send a message to the client at any time
msg.replyTo.send(message)
}
}
Tạo một đối tượng Handler trong dịch vụ để xử lý các thông báo từ client.
Cập nhật đối tượng RecentClient để cập nhật GUI theo thông báo đến.
Hãy tạo Messenger với đối tượng Handler. Giao tiếp sẽ diễn ra thông qua đối tượng này.
Các msg.replyTo đối tượng được lấy từ thông điệp của khách hàng.
Các hằng số chúng tôi sử dụng làm khóa gói trong thư mục:
package com.pmirkelam.ipcserver
// Bundle keys
const val PID = "pid"
const val CONNECTION_COUNT = "connection_count"
const val PACKAGE_NAME = "package_name"
const val DATA = "data"
Trả lại cho client:
// Pass the binder object to clients so they can communicate with this service
override fun onBind(intent: Intent?): IBinder? {
connectionCount++
// Choose which binder we need to return based on the type of IPC the client makes
return when (intent?.action) {
"aidlexample" -> aidlBinder
"messengerexample" -> mMessenger.binder
else -> null
}
}
Client
Hãy thêm các hằng số mà tôi sử dụng làm khóa gói ở đây:
package com.pmirkelam.ipcclient
// Bundle keys
const val PID = "pid"
const val CONNECTION_COUNT = "connection_count"
const val PACKAGE_NAME = "package_name"
const val DATA = "data"
class MessengerFragment : Fragment(), ServiceConnection, View.OnClickListener {
private var _binding: FragmentMessengerBinding? = null
private val viewBinding get() = _binding!!
// Is bound to the service of remote process
private var isBound: Boolean = false
// Messenger on the server
private var serverMessenger: Messenger? = null
// Messenger on the client
private var clientMessenger: Messenger? = null
// Handle messages from the remote service
var handler: Handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
// Update UI with remote process info
val bundle = msg.data
viewBinding.linearLayoutClientInfo.visibility = View.VISIBLE
viewBinding.btnConnect.text = getString(R.string.disconnect)
viewBinding.txtServerPid.text = bundle.getInt(PID).toString()
viewBinding.txtServerConnectionCount.text =
bundle.getInt(CONNECTION_COUNT).toString()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentMessengerBinding.inflate(inflater, container, false)
return viewBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewBinding.btnConnect.setOnClickListener(this)
}
override fun onClick(v: View?) {
if(isBound){
doUnbindService()
} else {
doBindService()
}
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
serverMessenger = Messenger(service)
// Ready to send messages to remote service
sendMessageToServer()
}
override fun onServiceDisconnected(className: ComponentName) {
clearUI()
serverMessenger = null
}
private fun clearUI(){
viewBinding.txtServerPid.text = ""
viewBinding.txtServerConnectionCount.text = ""
viewBinding.btnConnect.text = getString(R.string.connect)
viewBinding.linearLayoutClientInfo.visibility = View.INVISIBLE
}
override fun onDestroy() {
doUnbindService()
super.onDestroy()
}
private fun doBindService() {
clientMessenger = Messenger(handler)
Intent("messengerexample").also { intent ->
intent.`package` = "com.pmirkelam.ipcserver"
activity?.applicationContext?.bindService(intent, this, Context.BIND_AUTO_CREATE)
}
isBound = true
}
private fun doUnbindService() {
if (isBound) {
activity?.applicationContext?.unbindService(this)
isBound = false
}
}
private fun sendMessageToServer() {
if (!isBound) return
val message = Message.obtain(handler)
val bundle = Bundle()
bundle.putString(DATA, viewBinding.edtClientData.text.toString())
bundle.putString(PACKAGE_NAME, context?.packageName)
bundle.putInt(PID, Process.myPid())
message.data = bundle
message.replyTo = clientMessenger // we offer our Messenger object for communication to be two-way
try {
serverMessenger?.send(message)
} catch (e: RemoteException) {
e.printStackTrace()
} finally {
message.recycle()
}
}
}
7. Kết Luận
Trên Đây là cách đơn giản nhất để thực hiện IPC, bởi vì Messenger xếp hàng tất cả các requests vào một luồng đơn để bạn không phải thiết kế service của mình là an toàn luồng.
Ở phần tiếp theo mình sẽ nói tiếp cách sử dụng ADIL trong IPC.
Ở bài viết trước mình có nó qua của hạn chế chủa MVP so với MVVM. Nên ở bài viết này mình sẽ tìm hiểu kĩ về mô hình MVVM để biết lý do chọn MVVM hơn là MVP.
Content
Liva Data là gì?
DataBinding là gì?
MVVM là gì?
Demo MVVM
Kết Luận
Live Data là gì?
LiveData là observable data holder class. Không giống như các observables khác, nó nhận thức được vòng đời, tức là nhận nhận thức được vòng đời của các components như là activity, fragment hay services. Điều này có nghĩa là nó chỉ cập nhật các component observers khi chúng ở trạng thái vòng đời hoạt động.
DataBinding là gì?
Là library hỗ trợ cho phép chúng ta liên kết với các thành phần UI của mình từ layout với data source trong ứng dụng của chúng ta bằng cách khai báo thay vì lập trình.
Set up:
android {
...
dataBinding {
enabled = true
}
}
MVVM là gì?
Mô hình MVVM
MVVM gồm 3 phần:
Model: Cũng tương tự như trong mô hình MVC. Model là các đối tượng giúp truy xuất và thao tác trên dữ liệu thực sự.
View: là thành phần UI đại diện cho trạng thái hiện tại cảu thông tin mà người dùng có thể nhìn thấy.
ViewModel:
ViewModel nằm giữa Model và View, có tác dụng là cầu nối để giao tiếp giữa Model và View.
ViewModel còn có tác dụng xử lí các logic convert, format data trước khi View hiển thị data đó cho người dùng.
Đây là phần khác biệt của MVVM so với MVP.
ViewModel và View được kết thông thông qua Databiding và observable Livedata,
Demo MVVM
Model layer:
Ví dụ về cách triển khai 1 Model:
data class User(
var username: String,
var password: String
)
ViewModel layer:
Ví dụ về cách triển khai 1 ViewModel:
class LoginViewModel(
private val userRepository: IUserRepository,
private val dispatcher: CoroutineDispatcher = Dispatchers.Main
) : ViewModel() {
private val _loginResult = MutableLiveData<Resource<Status>>()
val loginResult: LiveData<Resource<Status>>
get() = _loginResult
private val _stateLoading = MutableLiveData<Resource<Boolean>>()
val stateLoading: LiveData<Resource<Boolean>>
get() = _stateLoading
private var listDataUser = listOf<User>()
fun clickLogin(inputUser: User) {
viewModelScope.launch(dispatcher) {
listDataUser = userRepository.getUserList()
_stateLoading.value = Resource.loading(true)
val invalid = checkInvalidInput(inputUser)
if (invalid) {
_loginResult.postValue(Resource.invalid(Status.INVALID, Status.INVALID.value))
_stateLoading.value = Resource.loading(false)
} else {
checkLogin(inputUser)
}
}
}
private suspend fun checkInvalidInput(inputUser: User) =
userRepository.checkInvalidInput(inputUser)
private suspend fun checkLogin(inputUser: User) {
val isSuccess = userRepository.loginResult(inputUser, listDataUser)
if (isSuccess) {
_loginResult.postValue(Resource.success(Status.SUCCESS, Status.SUCCESS.value))
} else {
_loginResult.postValue(Resource.error(Status.ERROR, Status.ERROR.value))
}
_stateLoading.value = Resource.loading(false)
}
class Factory : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return LoginViewModel(Injector.userRepository) as T
}
}
}
Thuộc tính userRepository để lấy dữ liệu từ data.
Hàm checkInvalidInput dùng để check dữ liệu nhập vào null hay không.
Hàm checkLogin check dữ liệu nhập vào có trùng với data không.
Thuộc tính _loginResult trả về Success nếu đúng dữ liệu, Error là sai còn Invalid là dữ liệu null.
class LoginFragment : BaseFragment<FragmentLoginBinding>(), View.OnClickListener {
private val loginViewModel: LoginViewModel by activityViewModels { LoginViewModel.Factory() }
override fun createBinding(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): FragmentLoginBinding {
return FragmentLoginBinding.inflate(inflater, container, false)
}
override fun observeLiveData() {
loginViewModel.loginResult.observeNotNull(this) { resultMode ->
when (resultMode.status) {
Status.INVALID -> resultMode.message?.let {
MessageDialog(it).show(
requireActivity().supportFragmentManager,
MessageDialog::class.java.name
)
}
Status.SUCCESS -> {
val action = LoginFragmentDirections.actionLoginFragmentToHomeFragment()
findNavController().navigate(action)
}
Status.ERROR -> resultMode.message?.let {
MessageDialog(it).show(
requireActivity().supportFragmentManager,
MessageDialog::class.java.name
)
}
else -> {
// do nothing
}
}
}
loginViewModel.stateLoading.observeNotNull(this) { stateLoading ->
binding.prbLoading.visibility = if (stateLoading.data == true) {
View.VISIBLE
} else {
View.GONE
}
}
}
override fun initAction() {
binding.btnLogin.setOnClickListener(this)
binding.btnClear.setOnClickListener(this)
}
override fun onClick(v: View?) {
when (v) {
binding.btnLogin -> {
handleClickLogin()
}
binding.btnClear -> {
clearInputData()
}
}
}
private fun handleClickLogin() {
val username = binding.edtUserName.text.toString()
val password = binding.edtPassword.text.toString()
val inputUser = User(username = username, password = password)
loginViewModel.clickLogin(inputUser)
}
Kết Luận
Cũng giống như MVP, MVVM thực hiện abtract trạng thái và thể hiện của View, cho phép chúng ta phân tách rõ ràng việc phát triển giao diện với xử lý business logic. MVVM đã kế thừa những ưu điểm vốn có của MVP, kết hợp với những lợi thế của data binding đem đến một pattern có khả năng phân chia các thành phần với từng chức năng riêng biệt, dễ dàng trong việc maintain, redesign. MVVM cũng đem lại khả năng test rất dễ dàng.
Trên đây là 1 số chia sẻ về MVVM. Nếu có gì chưa chính xác hoặc còn thiếu thì rất mong nhận được góp ý từ mọi người. Cảm ơn các bạn đã theo dõi bài viết!
Bài viết này, mình sẽ trình bày tại sao chọn MVVM hơn là MVP.
I. Vấn đề
Một số vấn đề để một lập trình viên quyết định chọn mô hình xây dựng ứng dụng như là làm sao để tái sử dụng code, dễ maintenance, dễ viết unit test hay dễ đọc hiểu với người mới vào trong dự án. Một số vấn đề trên dẫn đến việc chọn lựa mô hình khi bắt đầu một dự án mới là một điều hết sức quan trọng đối với mỗi lập trình viên. Hiện nay, có thể thấy 2 mô hình phổ biến nhất là MVVM và MVP. Trong bài viết này, chúng ta sẽ cùng tìm hiểu về chúng và xem cái nào ưu việt hơn.
II. Giải pháp
Bản thân Android được viết dưới dạng MVC trong đó Activity chịu trách nhiệm cho rất nhiều thứ trong đó bao gồm tất cả các logic. Với những ứng dụng đơn giản thì có thể mọi thứ vẫn còn dễ dàng, nhưng khi ứng dụng đủ lớn, số lượng logic tăng lên và mức độ vấn đề cũng tăng theo. Có nhiều mô hình tiếp cận khác nhau như MVP, MVVM,… được chứng minh là có thể giải quyết các vấn đề trên. Người ta có thể sử dụng bất kỳ cách tiếp cận nào, chúng thích ứng với các cách thay đổi một cách nhanh chóng,…
III. Mục tiêu
Xây dựng mọi thứ một cách phân tán như vậy để tách biệt dữ liệu – logic – view ra để đối với những project lớn khi số lượng logic và dữ liệu đủ lớn sẽ hữu ích trong việc mở rộng, bảo trì, test,…
IV. Tại sao là MVVM?
Có khá nhiều bài viết về MVP về sự sử dụng rộng rãi của mô hình này: Model — View — Presenter. Đó là một mô hình trưởng thành và ở mức độ nhất định, có thể giải quyết vấn đề nhưng vẫn có khá nhiều hạn chế và nó cần phải cải thiện một số thứ.
Một mô hình MVP đơn giản như sau:
Mô hình MVP
Và một mô hình MVVM đơn giản như sau:
Mô hình MVVM
Hãy bắt đầu vào những hạn chế của MVP và cách chúng ta có thể khắc phục chúng bằng cách sử dụng MVVM.
Đối với mỗi View thì đều yêu cầu 1 Presenter, đây là quy tắc ràng buộc cứng nhắc. Presenter giữ tham chiếu đến View và View cũng giữ tham chiếu đến Presenter. Mối quan hệ 1:1 và đó là vấn đề lớn nhất.
Khi sự phức tạp hay độ lớn của ứng dụng tăng lên dẫn đến việc duy trì và xử lý mối quan hệ này cũng vậy.
Chính vì những hạn chế trên của Presenter, MVVM được giới thiệ
ViewModel là các class mô phỏng tương tác với logic/model layer và chỉ hiện trạng thái/ dữ liệu mà không quan tâm ai hoặc dữ liệu sẽ được tiêu thụ thế nào. Chỉ View giữ tham chiếu đến ViewModel và không có trường hợp ngược lại, điều này giải quyết vấn đề của Presenter và View. Một View có thể giữ tham chiếu nhiều ViewModel. Ngay cả một View cũng có thể giữ tham chiếu đến nhiều ViewModel.
V. Khả năng test
Bởi vì Presenter bị ràng buộc chặt chẽ với View, dẫn đến việc unit test trở nên hơi khó khăn. ViewModel thâm chí còn thân thiện hơn với Unit test dù chúng chỉ hiển thị trạng thái và do đó có thể được kiểm tra độc lập mà không yêu cầu kiểm tra dữ liệu được tiêu thụ như thế nào. Đây là 2 lý do chính làm cho sự phân biệt, lựa chọn rõ ràng ảnh hưởng đến khả năng unit test của 2 mô hình.
VI. Tổng kết
Các mô hình này đang tiếp tục phát triển và MVVM có thể nói tiềm năng để trở nên mạnh mẽ, hữu ích nhưng tuyệt vời để thực hiện. MVP cũng rất hữu dụng và phổ biến nhưng chưa có thể hoàn hảo. Không có thể chắc chắn về tương lai, phù hợp tốt cho tất cả các giải pháp. Người ta có thể thích hoặc không thích MVVM nhưng đó cũng không quá quan trọng, miễn là chúng ta đạt được mục tiêu đang đáp ứng tốt cho project phát triển. Đây cũng là một số cảm nhận khi mình sử dụng qua 2 mô hình này. Do chưa có kinh nghiệm nên bài viết này không thể tránh khỏi sai sót, rất mong nhận được góp ý từ mọi người.
Trên đây là 1 số chia sẻ về hạn chế của MVP và nó có thể khắc phục bằng MVVM. Cảm ơn các bạn đã theo dõi bài viết!