Author: Cao Ngọc Hiệp

  • Android – Room Persistence Library

    Android – Room Persistence Library

    1. Room database là gì?

    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:

    @Entity(indices = {@Index(value = {"first_name", "last_name"}})
    

    Đị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.

    Cám ơn các bạn đã đọc bài viết của mình.

    Tham khảo: https://developer.android.com/training/data-storage/room#groovy

  • Inter-Process Communication with android (phần 2)

    Inter-Process Communication with android (phần 2)

    Ở phần trước mình đã giới thiệu về IPC và sử dụng IPC thông qua Messenger.

    Phần 1: https://magz.techover.io/2021/12/19/inter-process-communication-with-android-phan-1/

    Còn bây giờ mình sẽ sự dung IPC thông qua AIDL.

    Content

    1. AIDL là gì?
    2. Tạo .aidl
    3. Demo với aidl
    4. Kết luận

    1. AIDL là gì?

    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);
    }

    fragment_aidl.xml:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        tools:context=".ui.MainActivity">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="@string/say_something_to_server"
            android:textSize="25sp" />
    
        <EditText
            android:id="@+id/edt_client_data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="@string/salut"
            android:inputType="text"
            android:textAlignment="center"
            android:textSize="35sp" />
    
        <Button
            android:id="@+id/btn_connect"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="@string/connect"
            android:textSize="18sp" />
    
        <LinearLayout
            android:id="@+id/linear_layout_client_info"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="vertical"
            android:visibility="invisible">
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="50dp"
                android:text="@string/process_id_of_server"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_server_pid"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="50sp" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="30dp"
                android:text="@string/connection_count_of_server"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_server_connection_count"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="50sp" />
        </LinearLayout>
    </LinearLayout>

    AIDLFragmet:

    class AidlFragment : Fragment(), ServiceConnection, View.OnClickListener {
    
        private var _binding: FragmentAidlBinding? = null
        private val binding get() = _binding!!
        var iRemoteService: IIPCExample? = null
        private var connected = false
    
        override fun onCreateView(
                inflater: LayoutInflater,
                container: ViewGroup?,
                savedInstanceState: Bundle?
        ): View? {
            _binding = FragmentAidlBinding.inflate(inflater, container, false)
            val view = binding.root
            return view
        }
    
        override fun onActivityCreated(savedInstanceState: Bundle?) {
            super.onActivityCreated(savedInstanceState)
            binding.btnConnect.setOnClickListener(this)
        }
    
        override fun onDestroyView() {
            super.onDestroyView()
            _binding = null
        }
    
        override fun onClick(v: View?) {
                connected = if (connected) {
                    disconnectToRemoteService()
                    binding.txtServerPid.text = ""
                    binding.txtServerConnectionCount.text = ""
                    binding.btnConnect.text = getString(R.string.connect)
                    binding.linearLayoutClientInfo.visibility = View.INVISIBLE
                    false
                } else {
                    connectToRemoteService()
                    binding.linearLayoutClientInfo.visibility = View.VISIBLE
                    binding.btnConnect.text = getString(R.string.disconnect)
                    true
                }
        }
        private fun connectToRemoteService() {
            val intent = Intent("aidlexample")
            val pack = IIPCExample::class.java.`package`
            pack?.let {
                intent.setPackage(pack.name)
                activity?.applicationContext?.bindService(
                    intent, this, Context.BIND_AUTO_CREATE
                )
            }
        }
    
        private fun disconnectToRemoteService() {
            if(connected){
                activity?.applicationContext?.unbindService(this)
            }
        }
    
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            // Gets an instance of the AIDL interface named IIPCExample,
            // which we can use to call on the service
            iRemoteService = IIPCExample.Stub.asInterface(service)
            binding.txtServerPid.text = iRemoteService?.pid.toString()
            binding.txtServerConnectionCount.text = iRemoteService?.connectionCount.toString()
            iRemoteService?.setDisplayedValue(
                context?.packageName,
                Process.myPid(),
                binding.edtClientData.text.toString())
            connected = true
        }
    
        override fun onServiceDisconnected(name: ComponentName?) {
            Toast.makeText(context, "IPC server has disconnected unexpectedly", Toast.LENGTH_LONG).show()
            iRemoteService = null
            connected = false
        }
    }

    main_activity.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingTop="?attr/actionBarSize">
    
        <fragment
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:layout_constraintBottom_toTopOf="@id/nav_view"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:navGraph="@navigation/mobile_navigation" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>

    Server

    Tạo 1 tệp .aidl giống Client:

    activity_main.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        tools:context=".MainActivity">
    
        <TextView
            android:id="@+id/connection_status"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/no_connected_client"
            android:textSize="30sp"
            android:textStyle="bold" />
    
        <LinearLayout
            android:id="@+id/linear_layout_client_state"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="vertical">
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="75dp"
                android:text="@string/package_name"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_package_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="30sp"
                android:textStyle="bold"
                tools:text="com.someone.something" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="50dp"
                android:text="@string/pid"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_server_pid"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="30sp"
                android:textStyle="bold"
                tools:text="21282" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="50dp"
                android:text="@string/data"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_data"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="30sp"
                android:textStyle="bold"
                tools:text="Salut!" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="50dp"
                android:text="@string/ipc_method"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_ipc_method"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="30sp"
                android:textStyle="bold"
                tools:text="Something" />
        </LinearLayout>
    
    </LinearLayout>

    Client.kt:

    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.

    Trong file Manifest:

    <service
                android:name=".IPCServerService">
                <intent-filter>
                    <action android:name="aidlexample" />
                </intent-filter>
            </service>

    MainActivity.kt:

    class MainActivity : AppCompatActivity() {
    
        private lateinit var binding: ActivityMainBinding
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            binding = ActivityMainBinding.inflate(layoutInflater)
            setContentView(binding.root)
        }
    
        override fun onStart() {
            super.onStart()
            val client = RecentClient.client
            binding.connectionStatus.text =
                if (client == null) {
                    binding.linearLayoutClientState.visibility = View.INVISIBLE
                    getString(R.string.no_connected_client)
                } else {
                    binding.linearLayoutClientState.visibility = View.VISIBLE
                    getString(R.string.last_connected_client_info)
                }
            binding.txtPackageName.text = client?.clientPackageName
            binding.txtServerPid.text = client?.clientProcessId
            binding.txtData.text = client?.clientData
            binding.txtIpcMethod.text = client?.ipcMethod
        }
    }
    • 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.

    Cảm ơn mọi người đã đọc bài viết.

  • Inter-Process Communication with android (phần 1)

    Inter-Process Communication with android (phần 1)

    Content

    1. Inter-Process Communication(ipc) là gì?
    2. Tại sao cần giao tiếp?
    3. Các mô hình IPC
    4. IPC trong Android
    5. Sử dụng Messenger
    6. Demo với Messenger
    7. Kết luận

    1. Inter-Process Communication(ipc) là gì?

    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:
      1. Thiết lập giao tiếp.
      2. 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:

    1. 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.
    2. Service sử dụng Handler để tạo một đối tượng Messenger (là một tham chiếu đến Handler).
    3. Messenger tạo IBinder mà dịch vụ trả về cho khách hàng từ onBind().
    4. 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.
    5. Service từng Message trong Handler của nó — cụ thể là trong phương thức handleMessage().

    6. Demo với Messenger

    Server

    Bắt đầu với ứng dụng Server.

    Khai báo trong Manifest:

    <service android:name=".IPCServerService">
                <intent-filter>
                    <action android:name="messengerexample" />
                </intent-filter>
            </service>
    // 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"

    Trong tệp xml:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        tools:context=".ui.MainActivity">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="@string/say_something_to_server"
            android:textSize="25sp" />
    
        <EditText
            android:id="@+id/edt_client_data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="@string/salut"
            android:inputType="text"
            android:textAlignment="center"
            android:textSize="35sp" />
    
        <Button
            android:id="@+id/btn_connect"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="Connect via Messenger"
            android:textSize="18sp" />
    
        <LinearLayout
            android:id="@+id/linear_layout_client_info"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="vertical"
            android:visibility="invisible">
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="50dp"
                android:text="@string/process_id_of_server"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_server_pid"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="50sp" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="30dp"
                android:text="@string/connection_count_of_server"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_server_connection_count"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="50sp" />
        </LinearLayout>
    </LinearLayout>

    Trong tệp code:

    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.

    Cảm ơn mọi người đã đọc bài viết.

  • MVVM with Android

    MVVM with Android

    Ở 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 View, có tác dụng là cầu nối để giao tiếp giữa Model 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
            }
        }
    }
    1. Thuộc tính userRepository để lấy dữ liệu từ data.
    2. Hàm checkInvalidInput dùng để check dữ liệu nhập vào null hay không.
    3. Hàm checkLogin check dữ liệu nhập vào có trùng với data không.
    4. 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.

    View layer:

    Ví dụ về cách triển khai 1 View:

    Code file .xml:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout 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"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:orientation="vertical">
    
            <TextView
                android:id="@+id/tvLogin"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginBottom="36dp"
                android:text="@string/login_text_view"
                android:textSize="@dimen/login_text_view_size" />
    
            <EditText
                android:id="@+id/edtUserName"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:drawableStart="@drawable/ic_baseline_person"
                android:drawablePadding="6sp"
                android:hint="@string/user_name_text_view"
                android:importantForAutofill="no"
                tools:ignore="TextFields" />
    
            <EditText
                android:id="@+id/edtPassword"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:drawableStart="@drawable/ic_pass"
                android:drawablePadding="6sp"
                android:hint="@string/pass_word_text_view"
                android:importantForAutofill="no"
                tools:ignore="TextFields" />
    
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="12dp"
                android:gravity="center">
    
                <Button
                    android:id="@+id/btnLogin"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginEnd="40dp"
                    android:text="@string/login_button"
                    tools:ignore="ButtonStyle" />
    
                <Button
                    android:id="@+id/btnClear"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/clear_button"
                    tools:ignore="ButtonStyle" />
    
            </LinearLayout>
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="12dp"
                android:text="@string/information_text_view" />
    
    
        </LinearLayout>
    
        <ProgressBar
            android:id="@+id/prbLoading"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:visibility="gone"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:visibility="visible" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>

    Code file .kt

    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!

  • Android Architecture – Tại sao chọn MVVM hơn là MVP

    Android Architecture – Tại sao chọn MVVM hơn là MVP

    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!

    Tham khảo: https://android.jlelse.eu/why-to-choose-mvvm-over-mvp-android-architecture-33c0f2de5516