Category: Android

  • Dependency Injection trong Android

    Dependency Injection trong Android

    Giới thiệu

    Dependency Injection là một trong những pattern giúp code tuân thủ nguyên tắc Dependency Inversion. Làm cho một class độc lập với phụ thuộc của nó mang lại nhiều ưu điểm khi phát triển, test. Android có một số thư viện giúp việc DI trở nên dễ dàng như Dagger, Dagger Hilt. Để hiểu và sử dụng được các thư viện này trong bài này ta sẽ tìm hiểu một số khái niệm cơ bản về Dependency Injection.

    Các nguyên tắc cơ bản của DI

    Dependency injection là gì?

    Các class thường yêu cầu tham chiếu đến các class khác. Cho ví dụ, một class Car có thể cẩn một tham chiếu đến class Engine . Những class được Car tham chiếu đến như Engine được gọi là Dependencies (sự phụ thuộc), và trong ví dụ này Car là class phụ thuộc vào một instance của class Engine để chạy.

    Có ba cách để một class lấy một object nó cần:

    1. Class xây dựng phần phụ thuộc mà nó cần. Trong ví dụ ở trên, Car sẽ tạo và khởi tạo một instance Engine của riêng nó.
    2. Lấy nó từ một nơi khác. Một số Android API như là Context getters và getSystemService(), cách này hoạt động.
    3. Cung cấp nó như một parameter. Ứng dụng có thể cung cấp những sự phụ thuộc này khi class được khởi tạo hoặc pass chúng vào các function mà cần từng phụ thuộc. Trong ví dụ ở trên, contructor của Car sẽ nhận Engine như một tham số.

    Tùy chọn thứ ba là Dependence injection. Với cách tiếp cận này bạn nhận sự phụ thuộc của một class bằng cách cung cấp chúng thay vì việc để instance class tự lấy chúng.

    Đây là một ví dụ không dependency injection, Đại diện cho một Car mà tạo ra sự phự thuộc Engine của riêng nó trong code như này:

    class Car {
    
        private val engine = Engine()
    
        fun start() {
            engine.start()
        }
    }
    
    fun main(args: Array) {
        val car = Car()
        car.start()
    }
    

    Screenshot from 2021-09-30 20-12-42.png

    Đây không phải là một ví dụ về DI bởi vì class Car đang xây dựng Engine của riêng nó, điều này vi phạm nguyên tắc DI (Dependence inversion). Có vấn đề bởi vì:

    • CarEngine được liên kết chặc chẽ – một instance Car sử dụng một loại Engine, và không có subclass hay triển khai thay thế nào có thể dễ sử dụng. Nếu Car tạo ra Engine của riêng nó, bạn sẽ tạo hai loại Car thay vif tái sử dụng Car cho các động cơ loại Gas and Electric.
    • Việc phụ thuộc nhiều vào Engine làm cho công việc test gặp nhiều khó khăn hơn. Car sử dụng một instance của Engine, vì vậy ngăn bạn sử dụng các bản test trong các trường hợp khác nhau.

    Code sử dụng Dependence injection như dưới đây:

    class Car(private val engine: Engine) {
        fun start() {
            engine.start()
        }
    }
    
    fun main(args: Array) {
        val engine = Engine()
        val car = Car(engine)
        car.start()
    }
    

    Screenshot from 2021-09-30 20-12-42.png

    Function main sử dụng Car. bởi vì Car phụ thuộc vào Engine, ứng dung tạo một instance của Engine và sau đó sử dụng nó để xây dựng một instacne Car. Những lợi ích của phương pháp tiếp cận sự trên DI này là:

    • Khả năng tái sử dụng Car. Bạn có thẻ pass các implementation khác nhau của Engine tới Car. Cho ví dụ, bạn có thể định nghĩa một subclass mới của Engine được gọi là ElectricEngine mà bạn muốn Car sử dụng. Nếu bạn sử dụng DI, tất cả những gì bạn cần làm là pass một instance được cập nhật ElectricEngine subclass của Engine, và Car vẫn hoạt động không có bất kỳ thay đổi thêm.
    • Dễ dàng testing Car. Bạn có thể pas một test double đến test các kịch bản khác nhau của bạn. Cho ví dụ, bạn có thể tạo một test double của Engine được gọi là FakeEngine và cấu hình nó cho các bài tests khác nhau.

    Có hai cách chính để thực hiện Dependence Injection trong android:

    • Contructor injection. cách này được mô tả ở trên. Bạn pass sự phụ thuộc của class vào contructor của chính nó.
    • Field Injection( họăc Setter Injection). Một số lớp Android framework như là activity and fragment kà được khỏi tạo bởi hệ thống, vì vậy constructor injection là không thể. với field injection, Các sự phụ thuộc được khởi tạo sau khi class được tạo. Code sẽ giống như này:
    class Car {
        lateinit var engine: Engine
    
        fun start() {
            engine.start()
        }
    }
    
    fun main(args: Array) {
        val car = Car()
        car.engine = Engine()
        car.start()
    }
    

    Dependency injection tự động nhờ thư viện

    Trong ví dụ trước, ta đã tạo, cung cấp và quản lý các sự phụ thuộc của những class khác. Không cần dựa vào thư viện. Điều này được gọi là DI bằng tay, hoặc manual dependency injection. Trong ví dụ Car, chỉ có một sự phụ thuộc, nhưng nhiều sự phụ thuộc và các class có thể làm cho việc tiêm phụ thuộc thủ công trở nên tẻ nhạc hơn. Việc tiêm phụ thuộc thủ công cũng xảy ra một số vấn đề:

    • Cho các ứng dụng lớn, nhận tất cả sự phụ thuộc và kết nối chúng chính xác có thể yêu cầu một số lượng lớn mã mẫu ghi sẵn. Trong một kiến trúc nhiều lớp, để tạo một đối tượng cho một top layer, bạn phải cung cấp tất cả sự phụ thuộc của các layer dưới nó. Một ví dụ cụ thể, để build một oto thật bạn có thể cần một engine, một bộ phận truyền tải, một khung xe và các bộ phận khác; và một engine lần lượt cần xilanh và bugi.
    • Khi bạn không thể khởi tạo sự phụ thuộc trước khi pass chúng vào – cho ví dụ, khi sử dụng khởi tạo lazy hoặc xác định phạm vi đối tượng cho các luồng ứng dụng của bạn, bạn cần viết và maintain một vùng chứa tùy chỉnh( hoặc graph của Dependencies ) cái mà quản lý lifetimess của các sự phụ thuộc của bạn trong memory.

    Có các thư viện giải quyết vấn đề này bằng cách tự động tiến hành tạo và cung cấp các sư phụ thuộc. Chúng phù hợp với hai loại:

    • Reflection-based, giải pháp kết nối các sự phụ thuộc tại runtime.
    • Static, giải pháp tạo code để kết nối các sự phụ thuộc tại compile time.

    Dragger là một thư viện DI phổ biến cho java, kotlin và Android do Google maintain. Dragger tạo điều kiện thuận lợi cho việc sử dụng DI trong ứng dụng của bạn bằng cách tạo và quản lý graph dependencies cho bạn. Nó cung cấp cá phụ thuộc hoàn toàn static và compiletime giải quyết nhiều vấn đề về phát triển và hiêu xuấn của các giải pháp sự trên phản xạ.

    Hilt là một thư viện được đề xuất của Jetpack để chèn phụ thuộc trong Android.

    Kết luận

    Dependence injection cung cấp cho ứng dụng của bạn những ưu điểm sau:

    • Khả năng tái sử dụng của các class, việc hoán đổi các implementation dễ dàng hơn.
    • Dễ tái cấu trúc: Các phần phụ thuộc có thể kiểm tra tại thời điểm tạo đối tượng hoặc compile time thay vì bị ẩn dưới dạng chi tiết triển khai.
    • Dễ test: Một class không quản lý các phụ thuộc của nó, vì vậy ta có thể truyền các implementation khác nhau để kiểm tra các trường hợp khác nhau của mình.
  • 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

  • SSL Pinning & Signature checking(SecureCoding – P2)

    SSL Pinning & Signature checking(SecureCoding – P2)

    SecureCoding – P2

    Hi, lại là mình, rambler coder đây

    Trong phần trước chúng ta đã tìm hiểu về MITM attack và một số thủ thuật đi kèm

    Trước khi đi vào detail rule/coding tips ở những phần sau, phần thứ 2 này mình muốn chia sẻ thêm về 1 số rule config project, runtime application để hạn chế việc application bị tấn công, sửa đổi.

    Table of contents

    Ssl Pinning

    Hành động này gần như là bắt buộc với tất các ứng dụng sử dụng giao thức HTTPS. Tuy nhiên trên Google developer page chúng ta có thể thấy một đoạn warning như sau

    Caution: Certificate Pinning is not recommended for Android applications due to the high risk of future server configuration changes, such as changing to another Certificate Authority, rendering the application unable to connect to the server without receiving a client software update.
    

    Sự lo lắng này hoàn toàn hợp lý, khi certificate hết hạn, khi thay đổi certificate..etc – không có cách nào chắc chắn người dùng sẽ cập nhật phần mềm, đồng nghĩa với việc người dùng không thể kết nối tới máy chủ, và có thể chúng ta sẽ mất một lượng user rất lớn. Nên nếu sử dụng SSL pinning, chúng ta phải handle được các case liên quan đến certification và phải tính đến khả năng force update đối với client khi có exception liên quan đến ssl certificate

    Có rất nhiều cách để triển khai SSL pinning, tuỳ thuộc cách thức mà ứng dụng kết nối với server mà chúng ta lựa chọn item phù hợp

    1. NetworkSecurityConfig

    Step 1 Create networkSecurityconfig file

    res/xml/network_security_config.xml
    

    với nội dung sau

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <domain-config>
            <domain includeSubdomains="true">example.com</domain> // your domain
            <pin-set expiration="2018-01-01">
                <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin> // your hash key 
                <!-- backup pin -->
                <pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin> // your hash key 
            </pin-set>
        </domain-config>
    </network-security-config>
    

    Step2

    Khai báo sử dụng trong AndroidManifest

    <?xml version="1.0" encoding="utf-8"?>
    <manifest ... >
        <application android:networkSecurityConfig="@xml/network_security_config"
                        ... >
            ...
        </application>
    </manifest>
    

    Chi tiết vui lòng tham khảo https://developer.android.com/training/articles/security-config#CertificatePinning

    1. OKHttpClient

    Nếu đang triển khai kết nối bằng OKHttpClient thì có thể tham khảo link bên dưới https://github.com/baka3k/CleanArchitecture/blob/main/data/src/main/java/com/baka3k/architecture/data/service/base/Service.kt

    private fun buildCertificatePinner(): CertificatePinner {
            return if (pinning != null && pinning.isNotEmpty()) {
                val builder = CertificatePinner.Builder()
                for (item in pinning) {
                    builder.add(item.value, item.key)
                }
                builder.build()
            } else {
                CertificatePinner.Builder()
                    .add("api.themoviedb.org", "sha256/+vqZVAzTqUP8BGkfl88yU7SQ3C8J2uNEa55B7RZjEg0=") // your hash key (pinning SSL)
                    .build()
            }
        }
    }
    

    Sử dụng

    val pining = buildCertificatePinner()
    val okHttpClient = OkHttpClient.Builder()
                .certificatePinner(pining)
                .build()
    
    1. Hash Key? – sha256 lấy ở đâu?

    Trong 2 ví dụ ở trên chúng ta đều thấy sự xuất hiện của chuỗi ký tự sha256, có nhiều cách để lấy chuỗi này, ví dụ dùng open-ssl hoặc cách đơn giản nhất là truy cập https://www.ssllabs.com/ – Điền link muốn check vào và thông tin sẽ hiện ra như sau

    pinning

    Self signature

    Đây là một kỹ thuật mình thấy khá hiếm người sử dụng trong ứng dụng(mặc dù nó rất dễ, mất vài dòng code thôi) – dùng để ngăn chặn(phần nào đó) việc APK bị sửa đổi, thêm mã độc.

    Hầu hết lập trình viên Android đều từng ít nhất một lần tải APK từ nguồn ko chính thống, ví dụ apkpure, apkresult hoặc các game trên appstorevn

    Điều gì đảm bảo các apk này là apk sạch? ko có bị sửa đổi với mục đích thu thập thông tin người dùng, chèn quảng cáo hoặc tệ hơn là Phishing, hoặc tạo ra các attack vector để tấn công vào một hệ thống nào đó – ở đây có thể là hệ thống của chính chúng ta? – nếu đó là APK của chúng ta?

    Ví dụ, attacker có thể modify một file APK cho app ngân hàng/ví điện tử nào đó, ở giao diện đăng nhập thay vì việc gửi thông tin đăng nhập thì attacker sẽ log lại thông tin đăng nhập này, bao gồm mật khẩu, password và gửi về máy chủ của attacker chả hạn.

    Một file APK có thể được sửa đổi và sign đi sign lại nhiều lần, cách nhanh nhất để kiểm tra một file APK có bị sửa đổi rồi sign lại hay ko là check signature của file APK, signature là duy nhất, nó chỉ được gen ra bởi key dùng để sign apk và ko có cách nào từ signature trong apk sinh ngược được ra sign key cả.

    1. Dùng Keytool để lấy ra SHA của file APK release
    keytool -printcert -jarfile app-release.apk
    ... 
    MD5:  B3:4F:BE:07:AA:78:24:DC:CA:92:36:FF:AE:8C:17:DB
    SHA1: 16:59:E7:E3:0C:AA:7A:0D:F2:0D:05:20:12:A8:85:0B:32:C5:4F:68XXXX
    SHA256: 1XXXXXXXXXXXX1XXXXXXXXXXXX1XXXXXXXXXXXX1XXXXXXXXXXXX1XXXXXXXXXXXX
    
    1. Trên source code ứng dụng, khi vào một màn hình nào đó chúng ta có thể check SHA dựa vào đoạn code đơn giản bên dưới
    Signature[] sigs = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES).signatures;
    for (Signature sig : sigs)
    {
        Trace.i("MyApp", "Signature hashcode : " + sig.hashCode());
    }
    

    Compare các mã sha này => có thể phán đoán được ứng dụng có bị sửa đổi hay ko, tuỳ thuộc business, bạn có thể quyết định stop hẳn ứng dụng hoặc disable các tính năng tương ứng.

    API Key

    Đây là rule liên quan đến việc lưu trữ các API key/ User/Password cần thiết khi buid ứng dụng

    1. API Key Thông thường khi phát triển ứng dụng, developer hay có xu hướng hardcode API key trực tiếp vào source code, ví dụ thế này
    object MovieDBServer {
        const val MOVIE_DB_ACCESS_KEY = "xxxxxxxxxxxxx"
    }
    
     @GET("popular")
        suspend fun getPopular(
            @Query("api_key") clientId: String = MovieDBServer.MOVIE_DB_ACCESS_KEY
        ): MovieResult
    

    Giả sử chúng ta có 2 API key dùng cho test server và production server thì sao?

    object MovieDBServer {
        const val MOVIE_DB_ACCESS_KEY_DEBUG = "xxxxxxxxxxxxx"
        const val MOVIE_DB_ACCESS_KEY_PRODUCTION = "xxxxxxxxxxxxx"
    }
    

    Tất nhiên khi query thì phải check như thế này

    @GET("popular")
        suspend fun getPopular(
            @Query("api_key") clientId: String = if (BuildConfig.DEBUG)
                MovieDBServer.MOVIE_DB_ACCESS_KEY_DEBUG
                else MovieDBServer.MOVIE_DB_ACCESS_KEY_PRODUCTION
        ): MovieResult
    

    Có 2 issue ở đây

    • Có source code là có key, key không được quản lý tập trung
    • Nếu thêm nhiều loại key, vd stagging, production, develop..etc thì phải sửa source để build

    Để giải quyết vấn đề này, người ta sẽ đẩy việc define các key ra file – ngoài source code và sẽ inject vào trong quá trình build time, các job build khác nhau cho các môi trường khác nhau sẽ inject các API key khác nhau. Tức là thế này

    @GET("popular")
        suspend fun getPopular(
            @Query("api_key") clientId: String = BuildConfig.MOVIE_DB_ACCESS_KEY // chú ý dòng này BuildConfig.
        ): MovieResult
    

    MOVIE_DB_ACCESS_KEY được inject vào tại build time, nên có thể tuỳ chỉnh, thay đổi cho các job build khác nhau.

    Một ví dụ đơn giản, mình chọn lưu ra gradle.properties

    moviedb_access_key="PLACE YOUR KEY IN HERE"
    

    Trên file build.gradle

      defaultConfig {
            ...
            buildConfigField("String", "MOVIE_DB_ACCESS_KEY", "\"" + getMovieDBAccessKey() + "\"")
        }
    .....
    def getMovieDBAccessKey() {
        return project.findProperty("moviedb_access_key")
    }
    

    Cụ thể, nếu các bạn cần sample, thì có thể refer tại https://github.com/baka3k/CleanArchitecture/blob/main/data/build.gradle

    Tương tự đối với các sensitive information như user/password của private repository, pass của keystore..etc chúng ta cũng có thể inject trong build time như vậy

    Authors

    [email protected]

  • How to make a super app?

    How to make a super app?

    Ngày nay, các ứng dụng Super app ngày càng phổ biến. Đứng ở phía người dùng, super app là 1 app mà "thứ gì cũng có". Sự xuất hiện của Super app hướng đến sự tiện lợi dành cho người dùng khi mà họ có thể làm mọi thứ trên cùng 1 ứng dụng, từ việc thanh toán tiền điện, nước, đặt xe, đặt vé máy bay, đặt phòng, shopping, …

    Còn đứng ở trên góc nhìn của 1 developer thì sao? Làm thế nào để có thể đưa 1 app vào thành 1 ứng dụng nhỏ bên trong 1 app khác? Nếu đưa nhiều ứng dụng vào trong 1 super app như vậy, size của super app chắc chắn sẽ phải tăng lên rất nhiều. 1 app thông thường sẽ có size từ khoảng 50-150MB. Vậy tại sao 1 super app như shopee, grab, … có rất nhiều mini app lại chỉ có size 300MB?… Đó là 2 trong những câu hỏi mình đã từng băn khoăn về super app và chưa có câu trả lời. Sau 1 khoảng thời gian trực tiếp tham gia phát triển 1 super app, mình đã hiểu hơn về cách phát triển 1 super app.

    Nội dung

    • Super app app, mini app là gì?
    • Nguồn gốc của Super app?
    • Lợi ích của super app?
    • Tạo ra 1 super app?
    • Tổng kết
    • Reference

    Super app, mini app là gì?

    2 thuật ngữ super app và mini app đi song hành cùng với nhau.

    Super app là những app cung cấp nhiều services khác nhau, và những service này có thể được làm từ những team khác nhau.

    Mỗi service mà super app cung cấp được coi là 1 mini app (hay mini programs).

    Lợi ích của super app

    Đối với người dùng

    • 1 Ứng dụng cung cấp nhiều services cho người dùng => Người dùng có thể trải nghiệm nhiều loại dịch vụ, tiện ích ngay trên cùng 1 ứng dụng mà không cần phải chuyển app, hay download 1 app mới. Ví dụ: Zalo là 1 super app, gồm nhiều mini app như: Thanh toán tiền điện/nước, mua vé máy bay, nạp thẻ điện thoại, đặt xe, shoping, … => Người dùng không cần phải down thêm nhiều app mà chỉ cần 1 app là đủ.
    • Tiết kiệm dung lượng điện thoại vì không cần tải các app khác.

    Đối với công ty

    • Chia sẻ được lượng người dùng giữa các super app và mini app với nhau. Ví dụ: Zalo là 1 super app, chứa Lazada là 1 mini app nhỏ => Lazada có thể hưởng được lượng user hiện có của Zalo.
    • super app trở nên sinh động, nhiều tính năng hơn để cạnh tranh với các app khác.
    Công ty xây dựng 1 hệ sinh thái lớn mạnh, thu hút người dùng

    Super App và Mini App kết nối với nhau thế nào?

    Hãy thử suy nghĩ 1 vài câu hỏi sau đây trước khi đọc câu trả lời.

    Câu hỏi 1: Làm thế nào để SuperApp có thể mở được 1 MiniApp?
    Câu trả lời đó là source code của super app phải chứa code của mini app. Nhưng source code của mini app này sẽ được đóng gói dưới dạng framework.
    Team MiniApp sẽ build service mà họ muốn được tích hợp vào super app thành framework, và gửi cho team super app để team super app embed vào source code của họ. Các framework này có những yêu cầu sau:

    • Phải cung cấp 1 public function trả ra 1 View – view này là đầu vào của mini app.
    • Team MiniApp sẽ chỉ build phần service mà họ muốn đưa vào super app thành framework, chứ không build toàn bộ application của họ thành framework. Bởi nếu build toàn bộ app thành framework -> size của framework sẽ tăng lên đáng kể -> size của super app cũng sẽ tăng lên 1 tương đương. Vậy thì mỗi super app sẽ có size lên đến hàng nghìn MB chứ không còn là 300MB nữa.
      Ví dụ: Zalo muốn đưa Service Zalo Pay vào 1 super app khác, thì họ sẽ chỉ build service này thành framework, và đưa cho bên super app tích hợp, chứ ko phải build cả app Zalo thành 1 framework.

    Câu hỏi 2: Nếu mỗi mini app cung cấp 1 kiểu function có tên khác nhau, thì super app phải xử lí thế nào?
    Team SuperApp sẽ tạo ra 1 Interface / Protocol. Interface này sẽ là chuẩn chung cho toàn bộ MiniApp. Sau đó, team SuperApp sẽ đóng gói interface này thành 1 framework, và đưa cho phía mini app implement.

    Triển khai

    Step 1: Xây dựng 1 interface làm chuẩn chung cho các mini app, và dóng gói nó lại thành 1 framework và đưa cho mini app sử dụng.

    Step 2: Team MiniApp implement interface đó, và sau đó build source code thành framework và gửi cho team SuperApp.

    Step 3: SuperApp lấy ra input view mà team MiniApp đã cung cấp để hiển thị.

    Kết quả:

    Tổng kết:

    • Bài viết này chỉ xây dựng một demo đơn giản về xây dựng 1 super app để đem lại 1 cái nhìn tổng quan về cách thức super app và mini app kết nối với nhau.
    • Trên thực tế, để xây dựng 1 super app còn gặp nhiều khó khăn khác như: Authen giữa super app và mini app, thanh toán giữa super app và mini app, … Đó cũng là những bài toán hay cần phải giải quyết.

    Reference

    https://hoangatuan.medium.com/create-a-xcframework-for-ios-986c4fc1421e: Cách tạo framework
    https://www.brandsvietnam.com/congdong/topic/27240-WeChat-da-khoi-nguon-thuat-ngu-Super-App-nhu-the-nao: Lịch sử của super app

    Source code:

  • Giao thức bảo mật HTTPS và MITM attack(Secure Coding P1)

    Giao thức bảo mật HTTPS và MITM attack(Secure Coding P1)

    Giao thức bảo mật HTTPS và MITM attack(Secure Coding P1)

    Table of contents

    Giao thức bảo mật HTTPS và MITM attack

    Như tất cả lập trình viên đều biết: HTTPS là một giao giức bảo mật, dữ liệu được mã hóa trên đường truyền, các bước bắt tay (handshake) để mã hóa được dữ liệu của nó tóm gọn bởi các bước bên dưới

    Https handshake

    Các bước handshake này sẽ đảm bảo dữ liệu giữa client và server được mã hóa bởi một key mà chỉ có client và server biết. Sẽ không ai có thể đọc trộm hoặc sửa đổi các gói tin giữa client và server

    Tuy nhiên, nhìn sơ đồ trên chúng ta có thể thấy mắt xích yếu nhất của các bước HandShake chính là step 1 và step 2

    Về mặt logic: Nếu như ở Step 1 client say hello với "ai đó" ko phải server, Step 2 server response với "ai đó" ko phải là client thì sao? nếu như client và sever ko làm làm việc trực tiếp với nhau mà thông qua "ai đó" thì Masterkey ở step 7 đã bị "ai đó" lấy – và "ai đó" có khả năng tóm được gói tin, có khả năng giải mã được gói tin, có khả năng gói tin sẽ bị sửa đổi trong quá trình khi gửi nhận giữa client và server?

    Nếu điều này xảy ra, đó chính là MTTM attack – Man In The Middle Attack – một hình thức tấn công chen vào giữa đường truyền để lấy dữ liệu, sửa đổi, giả mạo gói tin

    Hầu hết các kết nối hiện tại của web, mobile đều đang sử dụng HTTPS để gửi nhận dữ liệu trên đường truyền. Điều này khiến đại đa số lập trình viên yên tâm và hài lòng với về mức độ an toàn này. Nếu sử dụng cách chặn gói tin thông thường thì cái mà attacker thu được chỉ là một gói dữ liệu đã mã hóa – không có giá trị gì.

    Nhưng thực tế có đúng như thế ko?

    Không, tất nhiên là không, hiện thực tàn khốc hơn thế rất nhiều

    Trên thực tế, dữ liệu gửi từ A sang B trên Internet ko thực sự đi trực tiếp từ A sang B, mà có thể nó sẽ còn phải qua rất nhiều server Trung gian khác.

    Quay lại lý thuyết về các bước HandShake – với hai mắt xích yếu nhất là Step1 và Step2 – Giả sử chúng ta lừa được client(ở đây là mobile) rằng ServerXXX mới là con server, lừa nốt server(ở đây là Server mà chúng ta cần kết nối đến) rằng ServerXXX mới là client thì sao?

    Bingo!!!

    Tức là thực tế dữ liệu đã gửi đến 1 con ServerXXX trung gian, sau đó forward cho client hoặc server – nghĩa là dữ liệu này hoàn toàn có thể được đọc, được giải mã, được sửa đổi ở server ServerXXX?

    Chúng ta có thể kiểm chứng suy luận này một cách dễ dàng theo cách bên dưới.

    (Mình xin nhấn mạnh lại một lần rằng, đây chỉ là một trò chơi ở level Kiddy có thể dùng trong việc kiểm thử – còn trên thực tế, sẽ tàn khốc hơn thế này rất nhiều. Nên việc chú trọng vào Security khi thiết kế, lập trình ứng dụng là cực kỳ quan trọng)

    MITM attack với WebProxy

    Chuẩn bị

    • Cài Web Proxy trên máy tính: Fiddler (Window), Charles (Mac)
    • Chuẩn bị 1 con điện thoại Android
    • Kết nối Máy tính, Android vào cùng 1 giải mạng
    • Cài đặt proxy của điện thoại Android trỏ vào IP của máy tính: mục đích mọi gói tin đi qua android đều đi qua proxy là máy tính

    Mô hình mạng sẽ như sau

    network

    Do Mobile nhận PC là proxy, nên toàn bộ việc gửi nhận dữ liệu – nói một cách chính xác là: toàn bộ hoạt động liên quan đến internet của Mobile đều bị Proxy – ở đây là PC giám sát Khi mở Webproxy trên máy tính – ở đây mình dùng Fiddler – chúng ta có thể nhìn thấy các gói tin

    Fidder1

    Tất nhiên, các gói tin này nếu dùng HTTPS thì sẽ là gói tin mã hóa – Không có giá trị gì

    Vậy làm sao để giải mã các gói tin này? Easy, follow 4 steps bên dưới nhé

    1. Setting Proxy của điện thoại trỏ vào PC(Giả sử IP của PC là 192.168.0.100)

    setting0

    2. Setting WebProxy để capture HTTPS Connect & Decrypt data

    Vào setting của WebProxy, chọn 2 mục

    • Capture HTTPS CONNECTs
    • Decrypt HTTPS traffic

    setting1

    Sang thẻ connection

    • Port :ở đây mình chọn port 8888 – đây chính là port để setup proxy cho Mobile
    • Chọn Allow remote computers to connect

    setting2

    3. Export Cert của webproxy và cài vào điện thoại

    Export root cert của webproxy sau đó copy vào điện thoại – cài cert như bình thường

    setting2

    Mục đích của việc này chính là để WebProxy và điện thoại có thể "hiểu nhau" – sau hành động mọi kết nối đến internet từ điện thoại – thông qua proxy đã ko còn bí mật – Proxy sẽ nhìn được toàn bộ dữ liệu của điện thoại

    4. Kết quả test thử với việc đăng nhập account Fsoft

    fsoft

    Dù là giao thức HTTPS nhưng dữ liệu username/password vẫn phơi thân ra như ảnh dưới Mọi người có thể thử với 1 số ngân hàng, ví điện tử, không phải ngân hàng/ví điện tử nào cũng áp dụng các cơ chế để phòng tránh MITM attack. Mình đã thử với 1 số ví điện tử/ngân hàng(mà ko tiện kể tên ra) thì thấy dữ liệu dạng này vẫn phơi thân ra mời gọi đầy quyến rũ 😛

    Replay gói tin

    replay

    Một tính năng cực kỳ hay ho của các tool Web Proxy là chúng ta có thể Replay gói tin, thậm chí sửa dữ liệu trước khi replay. Tức là ví dụ Chuyển 10 đồng thì chúng ta có thể sửa lại thành 200 đồng rồi replay gói tin. Tình cờ 1 cách đen đủi bạn code backend ko tính khả năng này là chúng ta đã có 200 đồng rồi. Đây cũng chính là thủ thuật áp dụng để trick điểm một cách quang minh chính đại trên Server GST – Hero. VD mình chơi được 30 điểm thì mình có thể sửa lại dữ liệu thành 50 điểm rồi đẩy lên server bằng tính năng Replay

    Yeah, câu hỏi quan trọng nhất: Phòng chống MITM attack thế nào?

    Client(mobile, web..etc) cần làm gì, Server cần làm gì?

    Nếu bạn là 1 developer, nếu bạn có nhiều hơn 2 năm kinh nghiệm, nếu lĩnh vực chính của bạn là client server, ví điện tử, financial…etc thì chúng ta sẽ buộc phải quan tâm đến vấn đề này

    Mình post bài lấy chỉ tiêu nên chúng ta hẹn nhau ở post sau nhé 😛

    senior

  • Hướng dẫn tạo plugin cho dự án Cordova/Ionic

    Hướng dẫn tạo plugin cho dự án Cordova/Ionic

    Table of contents

    • Tại sao cần tạo plugin cho Cordova
    • Tạo plugin bằng plugman
    • Hoàn thiện plugin

    Tại sao cần tạo plugin cho Cordova

    Về cơ bản, thì Cordova là framework phát triển các app iOS/Android (là chính) sử dụng html/js/css làm UI, và các bộ plugin làm cầu nối để call xuống source native của platform (iOS/Android)

    Cordova bao gồm:

    • Bộ html/js/css làm UI.
    • Native webview engine làm bộ render hiển thị UI
    • Cordova framework chịu trách nhiệm cầu nối giữa function call js và funtion native.
    • Source code native làm plugin cùng các config và các pulic js method.
    Cordova project struct

    Bình thường đối với người làm Cordova thì chủ yếu họ sẽ focus vào tầng UI bằng html/js/css. Việc sử dụng các chức năng native của platform thì sẽ sử dụng các plugin được cung cấp sẵn. Vậy nên về cơ bản, một lập trình viên làm Cordova chỉ cần làm được html/js/css là đủ.

    Tuy nhiên trong một số trường hợp, các plugin có sẵn không đảm bảo giải quyết được vấn đề bài toán, lúc này việc phát triển riêng một plugin thực hiện được logic của project và support được các platform là điều cần phải làm.

    Trong một vài trường hợp khác, có thể dự án đã có sẵn source native, tuy nhiên cần chuyển sang Cordova để support multi platform và tận dụng source code native có sẵn.

    -> Đo đó, hiểu biết về cách tạo một plugin để giải quyết nhu câu bài toán sẽ nảy sinh. Bài viết này sẽ tập trung vào việc

    • Làm thế nào để tạo plugin
    • Luồng xử lý từ js xuống native source của plugin như nào
    • Install plugin vào Cordova project
    • Build và test plugin trên iOS và Androd

    Tạo plugin bằng plugman

    Tạo Cordova project

    Để tạo Cordova plugin sample thì trước tiên cần có một Cordova project để test việc add plugin và kiểm tra hoạt động của plugin trên từng platform.

    • Install Cordova CLI: sudo npm install -g cordova
    • Create Cordova project: cordova create SamplePlugin com.nhathm.samplePlugin SamplePlugin

    Install plugman và tạo plugin template

    plugman là command line tool để tạo Apache Cordova plugin. Install bằng command: npm install -g plugman

    Create plugin:

    • Command: plugman create –name pluginName –plugin_id pluginID –plugin_version version
    • Ví dụ: plugman create –name GSTPlugin –plugin_id cordova-plugin-gstplugin –plugin_version 0.0.1

    Thêm platform mà plugin sẽ hỗ trợ:

    • plugman platform add –platform_name android
    • plugman platform add –platform_name ios

    Sau khi cài đặt xong thì thư mục plugin sẽ có struct như dưới.

    .
    └── GSTPlugin
    ├── plugin.xml
    ├── src
    │ ├── android
    │ │ └── GSTPlugin.java
    │ └── ios
    │ └── GSTPlugin.m
    └── www
    └── GSTPlugin.js

    Ở đây, plugin.xml là file config cho plugin, bao gồm các thông tin như tên của plugin, các file assets, resources. Define js-module như file js của plugin, define namespace của plugin, define các plugin phụ thuộc của plugin đang phát triển…

    Hoàn thiện plugin

    Cùng view file plugin.xml của plugin mới tạo:

    <?xml version='1.0' encoding='utf-8'?>
    <plugin id="cordova-plugin-gstplugin" version="0.0.1"
    xmlns="http://apache.org/cordova/ns/plugins/1.0"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <name>GSTPlugin</name>
    <js-module name="GSTPlugin" src="www/GSTPlugin.js">
    <clobbers target="cordova.plugins.GSTPlugin" />
    </js-module>
    <platform name="android">
    <config-file parent="/*" target="res/xml/config.xml">
    <feature name="GSTPlugin">
    <param name="android-package" value="cordova-plugin-gstplugin.GSTPlugin" />
    </feature>
    </config-file>
    <config-file parent="/*" target="AndroidManifest.xml" />
    <source-file src="src/android/GSTPlugin.java" target-dir="src/cordova-plugin-gstplugin/GSTPlugin" />
    </platform>
    <platform name="ios">
    <config-file parent="/*" target="config.xml">
    <feature name="GSTPlugin">
    <param name="ios-package" value="GSTPlugin" />
    </feature>
    </config-file>
    <source-file src="src/ios/GSTPlugin.m" />
    </platform>
    </plugin>

    Trong file này có một vài điểm cần hiểu như dưới:

    • <clobbers target="cordova.plugins.GSTPlugin" /> đây là namespace phần js của plugin. Từ file js, call xuống method native của plugin thì sẽ sử dụng cordova.plugins.GSTPlugin.sampleMethod
    • <param name="android-package" value="cordova-plugin-gstplugin.GSTPlugin" /> đây là config package name của Android, cần đổi sang tên đúng => <param name="android-package" value="com.gst.gstplugin.GSTPlugin" />. Như vậy Cordova sẽ tạo ra file GSTPlugin.java trong thư mục com/gst/gstplugin.

    Trong sample này, chúng ta sẽ sử dụng Swift làm ngôn ngữ code Native logic cho iOS platform chứ không dùng Objective-C, do đó phần platform iOS cần update.

    • Trong thẻ <platform name="ios> thêm tag <dependency id="cordova-plugin-add-swift-support" version="2.0.2"/>. Đây là plugin support việc import các file source Swift vào source Objective-C. Mà bản chất Cordova sẽ generate ra source Objective-C cho platform iOS.
    • Vì sử dụng Swift nên ta thay thế <source-file src="src/ios/GSTPlugin.m" /> bằng <source-file src="src/ios/GSTPlugin.swift" />. Và đổi tên file GSTPlugin.m sang GSTPlugin.swift

    Tiếp theo, chỉnh sửa các file js và native tương ứng cho plugin.

    File GSTPlugin.js

    • File này export các public method của plugin. Update file như dưới
    var exec = require('cordova/exec');
    
    exports.helloNative = function (arg0, success, error) {
        exec(success, error, 'GSTPlugin', 'helloNative', [arg0]);
    };
    • File này sẽ export method helloNative ra js và call method helloNative của native platform tương ứng.

    File GSTPlugin.swift

    • File này chứa logic và implementation cho iOS platform. Chỉnh sửa file như dưới
    @objc(GSTPlugin) class GSTPlugin : CDVPlugin {
        @objc(helloNative:)
        func helloNative(command: CDVInvokedUrlCommand) {
            // If plugin result nil, then we should let app crash
            var pluginResult: CDVPluginResult!
    
            if let message = command.arguments.first as? String {
                let returnMessage = "GSTPlugin hello \(message) from iOS"
                pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: returnMessage)
            } else {
                pluginResult = CDVPluginResult (status: CDVCommandStatus_ERROR, messageAs: "Expected one non-empty string argument.")
            }
    
            commandDelegate.send(pluginResult, callbackId: command.callbackId)
        }
    }

    File GSTPlugin.java

    package com.gst.gstplugin;
    
    import org.apache.cordova.CordovaPlugin;
    import org.apache.cordova.CallbackContext;
    
    import org.json.JSONArray;
    import org.json.JSONException;
    import org.json.JSONObject;
    
    import android.util.Log;
    
    public class GSTPlugin extends CordovaPlugin {
    
        @Override
        public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
            if (action.equals("helloNative")) {
                String message = args.getString(0);
                this.helloNative(message, callbackContext);
                return true;
            }
            return false;
        }
    
        private void helloNative(String message, CallbackContext callbackContext) {
            if (message != null && message.length() > 0) {
                callbackContext.success("GSTPlugin hello " + message + " from Android");
            } else {
                callbackContext.error("Expected one non-empty string argument.");
            }
        }
    }

    Tại thư mục của plugin, chạy command plugman createpackagejson . và điền các câu trả lời, phần nào không có thì enter để bỏ qua

    Switch sang thư mục chứa project Cordova SamplePlugin đã tạo, run command cordova plugin add --path-to-plugin.

    -> Ví dụ cordova plugin add /Users/nhathm/Desktop/Cordova-plugin_sample/GSTPlugin/GSTPlugin

    Bây giờ, plugin đã được add vào project Cordova. Tiếp theo sẽ chỉnh sửa source của index.html và index.js để test hoạt động của project.

    File index.html

    <div class="app">
          <h1>Apache Cordova</h1>
          <div id="deviceready" class="blink">
              <p class="event listening">Connecting to Device</p>
              <p class="event received">Device is Ready</p>
          </div>
    
          <button id="testPlugin">Test Plugin</button><br/>
    </div>

    File index.js

    • Add vào onDeviceReady()
    document.getElementById("testPlugin").addEventListener("click", gstPlugin_test);

    Thêm function:

    function gstPlugin_test() {
        cordova.plugins.GSTPlugin.helloNative("NhatHM",
            function (result) {
                alert(result);
            },
            function (error) {
                alert("Error " + error);
            }
        )
    }

    Sau khi chỉnh sửa hoàn chỉnh, build source cho platform iOS và Android

    • cordova platform add ios
    • cordova platform add android

    Build project native đã được generate ra và kiểm tra kết quả

    cordova plugin sample

    Đối với việc tạo UI cho project Cordova, chúng ta nên dùng các framework support như Ionic: https://ionicframework.com/

    Note: các dự án hybrid rất hạn chế về mặt performance, do đó nên cân nhắc khi bắt đầu dự án mới bằng hybrid

  • Các bước hướng dẫn chi tiết submit ứng dụng lên Google Store

    Các bước hướng dẫn chi tiết submit ứng dụng lên Google Store

    Khi bạn đã có trong tay một sản phẩm tốt và đầy tâm huyết. Vậy làm cách nào để có thể giới thiệu ứng dụng của mình đến với nhiều người dùng? Để mọi người biết đến và có thể sử dụng nó? Để nhận được những góp ý giúp cải thiện hơn, hay cũng là một nguồn thu nhập của bạn, thì bạn sẽ phải làm gì?. Chắc chắn, với một developer phát triển về mobile thì bạn sẽ có những câu hỏi như vậy, phải không?

    Để giải đáp cho câu hỏi đó thì iOS bạn sẽ sử dụng App Store để có sumit ứng dụng của bạn, App Store chính là market chính và có nhiều người iOS biết tới nhất.
    Với Android, đó chính là Google Play Store. Google Play Store cũng là market chính và có nhiều người dùng Android biết tới nhất. Vậy làm cách nào để có thể submit ứng dụng lên Google Store? Đừng lo, bài viết này mình sẽ giới thiệu tới các bạn luôn đây. Let’s start…

    Nội dung chính của bài viết:

    • Bạn cần chuẩn bị gì trước khi submit ứng dụng lên Google Store?
    • Cần tối ưu kích thước APK file
    • Từng bước submit ứng dụng lên Google Store
    • Đăng kí tài khoản Google Developer
    • Tạo ứng dụng mới và điền thông tin mô tả ứng dụng
    • Upload logo và screenshot ứng dụng
    • Tiến hành upload APK lên Store
    • Hoàn thành đánh giá Content rating
    • Đăng kí ứng dụng miễn phí hay trả phí

    Bạn cần chuẩn bị gì trước khi submit ứng dụng lên Google Store?

    Khác với Apple, khi mà App Store có quá trình review rất chặt chẽ. Các ứng dụng khi submit lên App Store đều trải qua quá trình review thủ công. Điều này sẽ đảm bảo ứng dụng trên App Store có chất lượng tốt nhất trước khi tới tay người dùng.

    Google Play Store thì quá trình review ứng dụng thường làm bằng máy. Do vậy, ứng dụng của bạn có nhiều cơ hội được approve hơn rất nhiều.

    Tuy nhiên, để ứng dụng thành công với hàng nghìn, thậm chí hàng triệu lượt tải thì bạn cần bỏ công sức ra tối ưu cũng như chuẩn bị kĩ càng trước khi submit.

    Những điều bắt buộc phải làm khi submit ứng dụng lên Google Store

    • Tạo một Bundle ID cho ứng dụng
    • Tạo một APK or Android App Bundles có sign key
    • Và tất nhiên là phải có một tài khoản Google Developer( chi phí để tạo là 25$)

    Cần tối ưu kích thước APK file

    Nếu ứng dụng của bạn có kích thước lớn thì nên chia ra thành nhiều module. Cách làm giống như các Game hay làm vậy. Các bạn chỉ đưa phần chính của ứng dụng lên Store. Sau khi người dùng tải ứng dụng về thì sẽ tiếp tục tải data.

    Tuy nhiên, mình khuyến khích là kích thước APK càng nhỏ càng tốt vì điều đó tốt cho ASO( App Store Optimization). Google Play Store ưu tiên các ứng dụng nhỏ nhẹ, nhưng chất lượng tốt.

    Lưu ý: Google Play supports compressed app downloads of only 150 MB or less.

    Đăng kí tài khoản Google Developer

    Đầu tiên, bạn cần đăng kí trở thành nhà phát triển ứng dụng ở đây: Google Play Console.

    Bạn đăng nhập bằng tài khoản Google như bình thường. Tick vào ô bên dưới Developer Agreement để chuyển sang màn hình thanh toán.

    Sau khi thanh toán bằng thẻ VISA/ MasterCard xong thì bạn cần điền các thông tin cần thiết cho nhà phát triển như: developer name, email address, website, phone number.

    Cuối cùng là nhấn vào nút COMPLETE REGISTRATION.

    Tạo ứng dụng mới và điền thông tin mô tả ứng dụng

    Như vậy là bạn đã trở thành nhà phát triển ứng dụng rồi đấy. Công việc tiếp theo là tạo ứng dụng mới bằng cách nhấn vào nút CREATE APPLICATION

    Lưu ý: Tiêu đề là phần hiển thị trên Google Play chứ không phải tên ứng dụng khi cài vào điện thoại. Nên bạn có thể khéo léo đưa từ khóa vào tiêu đề để tối ưu ASO.

    Tiếp theo, chúng ta cần điền thông tin mô tả ứng dụng: short description và full description.

    Rồi nhấn nút SAVE DRAFT. khi đã điền xong.

    Upload logo và screenshot ứng dụng

    Phần này chúng ta sẽ cần upload ảnh logo, screenshot của ứng dụng. Lưu ý là logo cần có kích thước là 512x512px.

    Nhấn vào Add high-res icon để tải logo lên.

    Chọn Add feature graphic. Đây là ảnh promo được hiển thị trên đỉnh của trang ứng dụng trên Google play.

    Nói chung, bạn có ảnh hay video nào có thể promo được cho ứng dụng thì upload hết lên đây nhé. Càng nhiều càng tốt…

    Cuối cùng là nhấn SAVE DRAFT. để tiếp tục.

    Phía dưới của màn hình có phần chọn kiểu ứng dụng là : App hay Game. Chọn Category phù hợp với ứng dụng: Tool, Productivity, Entertainment…

    Ở màn này có một mục là Content rating. Phần này mình sẽ hướng dẫn chi tiết ở phía dưới bài viết nhé.

    Điền URL tới file privacy policy. Nếu bạn chưa biết cách viết privacy policy như thế nào thì có thể sử dụng công cụ sau để tạo tự động App Privacy Policy Generator

    Bạn sẽ được chuyển đến màn hình như bên dưới.

    Tiến hành upload APK lên Store

    Bạn chọn MANAGE PRODUCTION để tiếp tục hoàn thành các bước tiếp theo.

    Ngoài ra, bạn cũng có thể lựa chọn chạy alpha hay beta testing trước khi thực sự publish ứng dụng cho tất cả người dùng.

    Ở bài viết này, mình không đề cập đển việc chạy alpha hay beta testing. Chúng ta publish chính luôn.

    Nhấn nút CREATE RELEASE.

    Nếu bạn đã tạo APK có sẵn sign key rồi thì không cần phải làm gì cả, chọn luôn opt out.

    Tiếp tục là chọn browse files để upload APK từ máy tính lên.

    Bạn có thể sửa release name, nhưng mình thì cứ để mặc định là số phiên bản của ứng dụng.

    Điền các thông tin chính cho bản apk này (thường thì người ta hay điền các tính năng mới mà cho lần upgrade ứng dụng) rồi nhấn REVIEW.

    APK cũng đã upload lên. Việc tiếp theo như mình nói ở trên là hoàn thành đánh giá Content rating.

    Hoàn thành đánh giá Content rating

    Phần Content rating này, bạn cứ trả lời thật với những câu hỏi của họ là ổn. Các câu hỏi kiểu như: Ứng dụng có liên quan đến SEX không? Ứng dụng có kích động, phản động hay liên quan đến Phát xít không? … Cứ trả lời thật nhé

    Đăng kí ứng dụng miễn phí hay trả phí

    Phần cuối cùng là Pricing & distribution

    Bạn cần cân nhắc là ứng dụng của bạn sẽ phát hành miễn phí hay là bán cho người dùng.

    Mình chỉ lưu ý là: Một khi đã chọn là ứng dụng miễn phí thì bạn không thể chuyển thành ứng dụng trả phí được nữa. Nhưng ngược lại thì được.

    Nếu bạn chọn là ứng dụng trả phí thì cần phải cài đặt phương thức nhận tiền để Google còn thanh toán cho bạn chứ.

    Tất cả đã xong. Việc của bạn bây giờ là quay trở lại App releases và nhấn nút START ROLLOUT TO PRODUCTION và chờ đợi Google review và appove cho ứng dụng của bạn.

    Theo kinh nghiệm của mình thì thời gian review sẽ tầm khoảng 4 giờ. Trong lúc chờ đợi thì hãy đón đọc các bài tiếp theo cũng mình nhé :))