Month: December 2022

  • Code sướng tay: Bước 1

    Có một vấn đề mà ai code cũng cần làm, đó là đặt tên (cho lớp, hàm, biến…).

    Ai cũng có thể viết code máy hiểu được, nhưng chỉ số ít viết code mà người hiểu được.

    “I’m not a great programmer, I’m just a good programmer with great habits.”

    Martin Fowler (tác giả sách Refactoring)

    Hồi còn đi học, chúng ta hay làm toán và đặt tên các biến là x,y,z,t… các hằng là a,b,c,…m,n…. Chúng ta hay giải lí với các biến là U, I, g, … Tương tự với các môn cần đến toán như hóa, sinh… Và tin học cũng là một môn dùng đến toán như vậy. Chúng ta tiếp tục lặp lại những gì mình từng biết:

    int a, b, c;

    float x, y, z;

    double aa, bb, xx, yy;

    Đặt những cái tên mà chúng ta thấy là đúng đắn, nhưng trông thật vô nghĩa. Trước mình cũng code rất nhiều những thuật toán, mà giờ đọc lại, không còn hiểu được ngày xưa mình code gì thế này, hàm này để làm gì, giải quyết bài toàn gì? Đừng đặt cho biến của bạn những cái tên quá ngắn như thế, vì nó chẳng thể hiện được tí ý nghĩa nào của biến đó hết.

    Đố ai hiểu code đang nghĩ gì :v

    Sau một chút thì mình học được cách thêm comment vào code để dễ hiểu hơn, như thế này:

    code cùng một chút comment (l=left, r=right)

    Tuy nhiên, sau này mấy anh khóa trên đi làm xong đến lớp bảo: “Ở công ty code giờ về lớp code nhìn cứ ngứa mắt. Đi làm code là tên biến nó cứ dài dài cơ. Càng dài càng tốt ae nhề, dài nhìn mới thuận mắt (nói với mấy anh cùng đi làm).” Lúc đấy mình cũng cười trừ thôi, vì thấy code của mình đọc dễ hiểu lắm rồi, đặt tên dài làm gì đâu. Hồi đó mình còn học các cách để rút gọn/ viết tắt tên biến như là lấy chữ cái đầu, bỏ đi nguyên âm, lấy 3 kí tự đầu… Mãi sau mới thấy, IDE gợi ý đến tận răng, tiếc gì mấy phím gõ mà phải học viết tắt quá mức như vậy nhỉ (thật ra học cái này cũng hữu ích khi mà đọc code người khác họ viết lst còn biết là list chả hạn .-.) Đừng cố gắng viết tắt tên biến, trừ khi là những từ viết tắt mà ai cũng biết và hiểu rõ.

    Hóa ra là, hàng tốt thì không cần quảng cáo, code tốt thì không cần comment, tự những cái tên trong code cần phải làm việc của nó khiến đoạn code dễ đọc, dễ hiểu (trừ những logic phức tạp và khó hiểu). Khi tên một hàm thể hiện nó làm gì, người ta sẽ không cần đọc đến đoạn code bên trong nó nữa. Trong cuộc đời 1 coder, thời gian đọc code luôn nhiều hơn thời gian viết ra nó (vì code thường sẽ cần được sửa), nên nếu đặt một cái tên tốt, sau này đọc lại bạn sẽ đỡ áp lực hơn khi đọc lại code của chính mình (và đỡ muốn đấm bản thân hơn, đỡ mất thời gian đọc comment, đỡ phải vò đầu bứt tai suy nghĩ ôi cái đoạn code này dùng để làm gì thế nhỉ).

    Nếu biến của bạn có đơn vị, hãy cố gắng thêm đơn vị vào tên của nó. Ví dụ thay vì hàm startAnimation(int delayTime) hãy đặt là startAnimation(int delayTimeInSeconds) hoặc nếu tốt hơn nữa, hãy thay kiểu của nó thành một kiểu mà sẽ khó bị nhầm lẫn, ví dụ như Duration trong Dart, bạn sẽ không cần quan tâm đến đơn vị của nó nữa. Khi code, nếu bạn phát hiện ra một cái tên khó hiểu hoặc dễ hiểu lầm và bạn có thể đặt cho nó cái tên tốt hơn, hãy đổi nó ngay khi bạn có thể (miễn là nó không ảnh hưởng tới đồng nghiệp của bạn, nếu có ảnh hưởng, bạn có thể hỏi ý kiến họ trước hoặc thêm một chút comment để sau này không tốn thời gian quá nhiều để hiểu nó nữa). Nhỏ thôi nhưng sẽ khiến bạn của sau này phải cảm ơn bạn đấy :3

    Một số cách đặt tên mà bạn nên tham khảo:

    • Nên dùng tiếng anh để đặt tên vì hầu hết các kí hiệu trong code đều là tiếng anh và tiếng anh không dễ bị nhầm lẫn như tiếng việt không dấu
    • Tuân thủ nguyên tắc đặt tên của ngôn ngữ/ dự án mà bạn làm
    • Đặt tên cho hàm bằng các động từ, cho các lớp bằng danh từ
    • Chỉ sử dụng biến bắt đầu bằng is/ are/ has… để đặt cho các biến/ thuộc tính/ hàm trả lời cho câu hỏi yes/no
    • Khi bạn không nghĩ ra được một cái tên cho biến/hàm/lớp bạn cần vì nó dễ bị nhầm lẫn với những biến/hàm/lớp khác, đây là lúc nên đặt lại tên cho cả những biến/hàm/lớp khác kia (chứ đừng thay bằng một từ đồng nghĩa, điều này sẽ khiến mọi người bối rối vì không biết chúng khác nhau ở đâu .-.)

    Tóm tắt các nguyên tắc:

    1. Tuân thủ convention đặt tên của ngôn ngữ/dự án đang làm
    2. Tên biến nên thể hiện ý nghĩa của nó, đừng cố gắng viết tắt nó
    3. Tham khảo một số nguyên tắc chung

    Lần đầu viết bài, mong là sẽ có lần sau hehe~

  • ANDROID BLUETOOTH CLASSIC – PART 4

    ANDROID BLUETOOTH CLASSIC – PART 4

    Chào các bạn, hôm nay mình tiếp tục chuỗi bài liên quan đến Bluetooth Classic trong Android. Bài hôm nay mình tập trung vào phần Transfer Bluetooth Data.

    Sau khi bạn đã kết nối thành công với thiết bị Bluetooth, mỗi thiết bị có một BluetoothSocket được kết nối. Sau đó bạn có thể chia sẻ thông tin giữa các thiết bị bằng cách thông qua đối tượng BluetoothSocket, về cơ bản các bước đọc ghi dữ, để truyền dữ liệu như sau:

    • Get InputStream và OutputStream xử lý truyền qua Socket bằng cách sử dụng getInputStream() và getOutputStream() tương ứng.
    • Đọc và ghi dữ liệu vào các luồng sử dụng read(byte[]) và write(byte[]).

    Mình sẽ tạo một ví dụ minh hoạ, với ứng dụng Chat giữa client và server bằng Bluetooth Classic.

    Đầu tiên chúng ta tạo một Object chứa phương thức đọc, ghi. Xử lý Read, Write thông qua 2 object InputStream và OutputStream.

    private val bluetoothAdapter: BluetoothAdapter = adapter
    private val handler: Handler = mHandler
    
    private lateinit var bluetoothSending: BluetoothSending
    
    inner class BluetoothSending(bluetoothSocket: BluetoothSocket?) : Thread() {
    
        private val inputStream: InputStream?
        private val outputStream: OutputStream?
    
        init {
            var tempIn: InputStream? = null
            var tempOut: OutputStream? = null
            try {
                tempIn = bluetoothSocket?.inputStream
                tempOut = bluetoothSocket?.outputStream
            } catch (e: IOException) {
                e.printStackTrace()
            }
            inputStream = tempIn
            outputStream = tempOut
        }
    
        override fun run() {
            val buffer = ByteArray(1024)
            var bytes: Int
    
            // Keep listening to the InputStream until an exception occurs.
            while (true) {
                try {
                    bytes = inputStream?.read(buffer)!!
                    handler.obtainMessage(Contstants.STATE_MESSAGE_RECEIVED, bytes, -1, buffer)
                        .sendToTarget()
                } catch (e: IOException) {
                    Log.e("PhongPN", "Bluetooth Reading Data Error", e)
                    e.printStackTrace()
                }
            }
        }
    
        fun write(bytes: ByteArray?) {
            try {
                outputStream?.write(bytes)
            } catch (e: IOException) {
                Log.e("PhongPN", "Bluetooth Writing Data Error", e)
                e.printStackTrace()
            }
        }
    }
    

    Tiếp đến chúng ta tạo một Server Socket. Lắng nghe các kết nối đến nó.

    inner class ServerClass : Thread() {
    
        private var serverSocket: BluetoothServerSocket? = null
    
        init {
            try {
                serverSocket =
                    bluetoothAdapter.listenUsingRfcommWithServiceRecord(
                        "PhongPN Transfer Data Bluetooth Classic", UUID.fromString(
                            Contstants.UUID
                        )
                    )
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not listen RFCOMM Sockets", e)
                e.printStackTrace()
            }
        }
    
        override fun run() {
            var socket: BluetoothSocket? = null
            while (socket == null) {
                try {
                    val message = Message.obtain()
                    message.what = Contstants.STATE_CONNECTING
                    handler.sendMessage(message)
                    socket = serverSocket?.accept()
                } catch (e: IOException) {
                    e.printStackTrace()
                    val message = Message.obtain()
                    message.what = Contstants.STATE_CONNECTION_FAILED
                    handler.sendMessage(message)
                }
                if (socket != null) {
                    val message = Message.obtain()
                    message.what = Contstants.STATE_CONNECTED
                    handler.sendMessage(message)
                    bluetoothSending = BluetoothSending(socket)
                    bluetoothSending.start()
                    break
                }
            }
        }
    
        fun cancel() {
            try {
                serverSocket?.close()
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not close the connect socket", e)
                e.printStackTrace()
            }
        }
    }
    

    Cuối cùng, chúng ta tạo một Client tương tác và trao đổi dữ liệu với Server Socket.

    inner class ClientClass(device: BluetoothDevice) : Thread() {
    
        private var socket: BluetoothSocket? = null
    
        init {
            try {
                socket = device.createRfcommSocketToServiceRecord(UUID.fromString(Contstants.UUID))
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not create RFCOMM Sockets", e)
                e.printStackTrace()
            }
        }
    
        override fun run() {
            try {
                socket?.connect()
                val message = Message.obtain()
                message.what = Contstants.STATE_CONNECTED
                handler.sendMessage(message)
                bluetoothSending = BluetoothSending(socket)
                bluetoothSending.start()
            } catch (e: IOException) {
                e.printStackTrace()
                val message = Message.obtain()
                message.what = Contstants.STATE_CONNECTION_FAILED
                handler.sendMessage(message)
            }
        }
    
        fun cancel() {
            try {
                socket?.close()
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not close the connect socket", e)
                e.printStackTrace()
            }
        }
    }
    

    Chúng ta xử lý send data tới nơi hiển thị dữ liệu (View) qua đối tượng Handle.

    private val handler: Handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(message: Message) {
            when (message.what) {
                Contstants.STATE_LISTENING -> binding.txtStatus.text = "Listening"
                Contstants.STATE_CONNECTING -> binding.txtStatus.text = "Connecting"
                Contstants.STATE_CONNECTED -> binding.txtStatus.text = "Connected"
                Contstants.STATE_CONNECTION_FAILED -> binding.txtStatus.text = "Connection Failed"
                Contstants.STATE_MESSAGE_RECEIVED -> {
                    val readBuff = message.obj as ByteArray
                    val tempMsg = String(readBuff, 0, message.arg1)
                    binding.txtReceivedMessage.text = tempMsg
                }
            }
        }
    }
    

    Các bạn chú ý là phương thức close() của luồng cho phép bạn chấm dứt kết nối bất kỳ lúc nào bằng cách đóng BluetoothSocket. Các nên gọi phương thức này khi bạn sử dụng xong kết nối Bluetooth. Vì thế trong 2 class client vs server mình luôn có 2 hàm đó để sẵn sàng trong việc đóng kết nối.

    Trên đây là chuỗi bài viết của mình liên quan đến Bluetooth Classic trong Android. Mong đâu đấy chút chia sẻ của mình có thế giúp mọi người có cái nhìn sơ qua về Bluetooth Classic, cũng như là có thể ít nhiều giúp các bạn nếu có gặp phải bài toán liên quan đến nó trong tương lai.

    Cảm ơn mọi người đã theo dõi. Hẹn gặp lại trong các bài viết sắp tới.

  • Hướng dẫn cách Gen Document Design bằng code Python

    Hướng dẫn cách Gen Document Design bằng code Python

    Hi các bạn, có bao giờ các bạn gặp phải tình huống khi kết thúc dự án thì phải làm Document design cho dự án của mình? Yêu cầu phải liệt kê hết các func trong project ra file excel và giải thích nó làm gì. Nếu câu trả lời là có thì các bạn có thể đọc tiếp bài viết này để xem cách thực hiện nó như thế nào nhé.

    Yêu cầu

    • Cần liệt kê hết tất cả các func trong souce code và giải thích func đó dùng để làm gì
    • Định dạng yêu cầu theo file excel

    Chuẩn bị

    Do tool được code trên nền tảng python nên mọi người cần cài đặt Python3 trước để có thể thực hiện gen document. Để cài đặt thì mọi người có thể làm theo hướng dẫn sau:

    1. Homebrew

    Link tham khảo: https://brew.sh v/bin/bash -c “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)”

    2. Install Python 3 on Mac – Brew Install

    Link tham khảo: https://www.freecodecamp.org/news/python-version-on-mac-update/ vbrew install pyenv // Install vpyenv install 3.9.2 // Update version

    3. Install pip: pip la một package của Python

    Link tham khảo: https://pip.pypa.io/en/stable/

    Download get-pip.py provided by https://pip.pypa.io hoặc sử dung terminal: vcurl https://bootstrap.pypa.io/get-pip.py -o get-pip.py vsudo python3 get-pip.py

    4. Openpyxl install: Cài đặt openpyxl sử dụng pip.

    Link tham khảo: https://openpyxl.readthedocs.io/en/stable/ vsudo pip install openpyxl

    5. Tải sẵn file template ở đây:

    File design là file excel template
    File DocC+.py là file code python nhằm mục đích gen gen souce và update file excel.

    NOTE: Đây là code dùng để gen dự án sử dụng ngôn ngữ lập trình swift, nếu các bạn cần gen trên ngôn ngữ khác chúng ta sẽ cần thực hiện chỉnh sửa file DocC+.py cho phù hợp với ngôn ngữ mà bạn sử dụng.

    Hướng dẫn sử dụng

    Bước 1: Copy file DocC+.py và design.xlsx vào trong thư mục cần gen.

    Bước 2: Bật Terminal -> navigate đến project folders chữa file DocC+.py

    Bước 3: Chạy câu lệnh dưới đây: python3 DocC+.py

    Vậy là chúng ta đã gen xong file liệt kê tất cả các func có trong thư mục mà các bạn chọn. Rất nhanh gọn và tiện lợi đúng không?

    Ưu điểm

    • Tiết kiệm rất nhiều thời gian thực hiện, thay vì phải copy bằng tay mất rất nhiều thời gian và khiến người thực hiện khá stress thì tool giúp chúng ta làm nó trong vài phút
    • Có thể sử dụng cho tất cả các ngôn ngữ lập trình
    • Quá dễ sử dụng

    Tổng kết

    Vậy là bài viết trên mình đã giới thiệu, chia sẻ và hướng dẫn các bạn sử dụng một tool cực kì hữu dụng và dễ sử dụng. Mình hi vọng nó sẽ giúp các bạn giải quyết bài toán mà bạn gặp phải. Chúc các bạn thành công!

  • Lottie Animation: Biến hình màn hình Login của bạn

    Lottie Animation: Biến hình màn hình Login của bạn

    Hi mọi người, ở bài viết trước mình đã hướng dẫn các bạn cách làm sao để thêm và ứng dụng Lottie vào ứng dụng của bạn. Có thể khi đọc bài viết trước các bạn sẽ chưa hình dung được sức mạnh của Lottie Animation. Để chứng minh sức mạnh của Lottie Animation hôm nay mình sẽ hướng dẫn các bạn ứng dụng nó vào màn hình Login và biến nó trở thành 1 màn hình thú vị hơn. Để hiểu rõ hơn về cách cài đặt Lottie animation bạn có thể xem lại bài viết Hướng dẫn cách sử dụng Lottie Animation trong Swift

    Chuẩn bị

    Để có thể bắt đầu dễ dàng và nhanh chóng hơn thì chúng ta cần chuẩn bị sẵn những thứ sau:

    1. Kiến thức về Lottie, tham khảo tại đây
    2. Project demo các bạn tải tại đây

    Bắt đầu

    Đầu tiên chúng ta sẽ kéo UI vào để thực hiện thay đổi màn hình Login như sau:

    Trong màn hình Login của chúng ta sẽ có các thành phần sau:

    1. ImageView làm background cho ứng dụng
    2. 2 Lottie Animation View, 1 cái làm thành phần chính nằm trên nửa màn hình và một cái nằm ở góc nhằm trang trí thêm cho màn hình
    3. 2 UITextField để cho người dùng nhập user name và password
    4. 1 button Login

    Sau khi sử dụng interface builder thực hiện kéo các view vào màn hình thì các bạn kéo reference cho các item vào view controller để thực hiện code.

    Mở ViewController lên và import Lottie

    import UIKit
    import Lottie

    Viết thêm một số func để cài đặt lottie và cài đặt view như sau

    extension ViewController {
        func setupUI() {
            tfAccount.layer.borderWidth = 1
            tfAccount.layer.borderColor = UIColor.orange.cgColor
            tfAccount.backgroundColor = UIColor.orange.withAlphaComponent(0.2)
            tfAccount.layer.cornerRadius = 5
            tfAccount.attributedPlaceholder = NSAttributedString(string: "  Account",
                                                                 attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
            tfAccount.placeholderRect(forBounds: CGRect(x: 5, y: 5, width: 100, height: 44))
            tfPassword.layer.borderWidth = 1
            tfPassword.layer.borderColor = UIColor.orange.cgColor
            tfPassword.backgroundColor = UIColor.orange.withAlphaComponent(0.2)
            tfPassword.layer.cornerRadius = 5
            tfPassword.attributedPlaceholder = NSAttributedString(string: "  Password",
                                                                 attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
            btnLogin.layer.cornerRadius = 5
            loadingView.backgroundColor = UIColor.black.withAlphaComponent(0.4)
        }
        
        func setupLottie() {
            // 1. Set animation content mode
            animationLottieView.contentMode = .scaleAspectFit
            animationLottieView2.contentMode = .scaleAspectFit
    
            // 2. Set animation loop mode
    
            animationLottieView.loopMode = .loop
            animationLottieView2.loopMode = .loop
    
            // 3. Adjust animation speed
    
            animationLottieView.animationSpeed = 0.5
            animationLottieView2.animationSpeed = 0.5
    
            // 4. Play animation
            animationLottieView.play()
            animationLottieView2.play()
        }
        
        private func showLoading() {
            // 1. Create lottie animation view
            let lottieView: LottieAnimationView = LottieAnimationView(name: "loading_crypto")
            lottieView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
            
            // 2. Set animation content mode
            lottieView.contentMode = .scaleAspectFit
            
            // 3. Set animation loop mode
            lottieView.loopMode = .loop
            
            // 4. Adjust animation speed
            lottieView.animationSpeed = 0.5
            
            loadingView.addSubview(lottieView)
            lottieView.center = CGPoint(x: UIScreen.main.bounds.width / 2, y:  UIScreen.main.bounds.height / 2)
            view.addSubview(loadingView)
            // 5. Play animation
            lottieView.play()
            
        }
        
        @objc private func hideLoading() {
            loadingView.subviews.forEach { $0.removeFromSuperview() }
            loadingView.removeFromSuperview()
        }
    }

    Thêm loading view để thực hiện màn hình loading như sau

    class ViewController: UIViewController {
        @IBOutlet weak var tfAccount: UITextField!
        @IBOutlet weak var tfPassword: UITextField!
        @IBOutlet weak var animationLottieView: LottieAnimationView!
        @IBOutlet weak var btnLogin: UIButton!
        @IBOutlet weak var animationLottieView2: LottieAnimationView!
        
        private var loadingView: UIView = UIView.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            setupUI()
            setupLottie()
        }
        
        @IBAction func login(_ sender: Any) {
            showLoading()
            perform(#selector(hideLoading), with: nil, afterDelay: 5)
        }
    }

    ở viewDidLoad chúng ta sẽ thực hiện setUpUI và setup lottie, tại func login chúng ta sẽ thực hiện showLoading và để nó ẩn đi sau 5 giây.

    Bây giờ chúng ta thực hiện cmd + R để chạy ứng dụng, kết quả thu được sẽ như sau:

    Kết quả thật tuyệt vời đúng không? Đối với các màn hình có animation phức tạp như vậy để code sử dụng animation gần như ta quá phức tạp và tốn quá nhiều effort để thực hiện, tuy nhiên nếu dùng lottie các bạn chỉ mất một chút thời gian để căn chỉnh layout.

    Vậy là chúng ta đã hoàn thành việc ứng dụng Lottie vào màn hình Login để tăng trải nghiệm của người dùng. Ngoài ra chúng ta còn có thể ứng dụng Lottie vào rất nhiều các tính năng khác để ứng dụng trở nên thú vị hơn. Ví dụ như: Màn hình launch screen với animation logo, animation trên notification, animation Tabbar icon, v.v.

    Cách sử dụng Lottie phụ thuộc rất lớn vào trí tưởng tượng và óc sáng tạo của người dùng, nếu bạn chỉ nắm vai trò là front end developer nhiều khi bạn sẽ không có quyền quyết định ứng dụng sẽ như nào, nhưng nếu bạn có ý tưởng hay ho bạn có thể đưa ý kiến lên Team leader, designer, BA, và PM của dự án để cải thiện trải nghiệm của người dùng.

    Chúc các bạn thành công với các dự án sắp tới.

  • ANDROID BLUETOOTH CLASSIC – PART 3

    ANDROID BLUETOOTH CLASSIC – PART 3

    Chào các bạn, hôm nay mình tiếp tục chuỗi bài liên quan đến Bluetooth Classic trong Android. Bài hôm nay mình tập trung vào phần Connect giữa các thiết bị Bluetooth.

    Để tạo kết nối Bluetooth giữa 2 thiết bị chúng ta cần thiết lập lập kết nối giống như việc kết nối giữa máy chủ (Server) và máy khách (Client). Chúng giao tiếp với nhau thông qua RFCOMM sockets, cụ thể thông qua đối tượng BluetoothSocket Socket trong Android. Cơ chế như nhau, máy khách (Client) cung cấp Socket information khi nó mở một RFCOMM Channel đến máy chủ (Server), máy chủ (Server) nhận Socket information khi kết nối đến được Accept. Server và Client được coi là đã kết nối với nhau khi chúng có một BluetoothSocket được kết nối trên cùng một kênh RFCOMM.

    Có 2 kết nối mà bạn cần quan tâm khi tạo Connect trong Bluetooth Classic

    • Connect như là một Client
    • Connect như là một Server

    Với từng bài toán, từng requirement cụ thể thì ta sẽ sử dụng chúng theo cách phù hợp.

    1. Connect như là một Client

      Cách connect đầu tiên này thường dùng cho bài toán kết nối với một thiết bị Bluetooth có sẵn. Từ đó kết nối, điều khiển, transfer dữ liệu với thiết bị đó. Đầu tiên bạn phải có được một đối tượng BluetoothDevice đại diện cho thiết bị Bluetooth. Đây là công đoạn bạn Scan (Discovery) và lấy thông thiên của thiết bị. Sau đó, bạn phải sử dụng BluetoothDevice để có được BluetoothSocket và bắt đầu kết nối.

      Các bước cụ thể như sau:

      1. Tạo một BluetoothSocket (RFCOMM Sockets) từ BluetoothDevice bằng cách gọi hàm createRfcommSocketToServiceRecord(UUID). Các bạn có hiểu UUID là một mã string, kiểu như một Port Number. Mã này của Client truyền vào hàm này, phải khớp với mã UUID mà máy chủ cấp. Thường với bài toán kết nối với thiết bị Bluetooth, thì thiết bị Bluetooth có cung cấp một chuỗi UUID chuẩn để ứng dụng có thể fix cứng dưới code và gửi lên khi tạo connect.

      2. Gọi BluetoothSocket.connect(). Sau khi gọi hàm, hệ thống sẽ thực hiện tra cứu SDP để tìm thiết bị từ xa có UUID phù hợp. Nếu tìm kiếm và khớp thành công và thiết bị từ xa chấp nhận kết nối, nó sẽ chia sẻ kênh RFCOMM để sử dụng trong quá trình kết nối và trả về phương thức connect(). Nếu kết nối không thành công hoặc nếu phương thức connect() timeout (sau khoảng 12 giây), thì phương thức này sẽ đưa ra một IOException. Bạn có thể tham khảo mã code sau

        class BluetoothClassicConnection(device: BluetoothDevice): Thread() {
        
        private val bluetoothSocket: BluetoothSocket? by lazy {
            device.createRfcommSocketToServiceRecord(UUID)
        }
        
        override fun run() {
            // Cancel discovery because it otherwise slows down the connection.
            if (BluetoothAdapter.getDefaultAdapter()?.isDiscovering == true) {
                BluetoothAdapter.getDefaultAdapter()?.cancelDiscovery()
            }
        
            try {
                bluetoothSocket?.let { socket ->
                    socket.connect()
        
                    // Handle Transfer Data 
                }
            } catch (e: IOException) {
                try {
                    bluetoothSocket?.close()
                } catch (e: IOException) {
                    Log.e(TAG, "Could not close the client socket", e)
                }
                Log.e(TAG, "Could not connect", e)
            }
        }
        
        // Closes the client socket and causes the thread to finish.
        fun cancel() {
            try {
                bluetoothSocket?.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the client socket", e)
            }
        }
        
        companion object {
            private const val UUID = "UUID code provide from Server Site"
         }
        }
        

      Note:

      • Việc duy trì kết nối Bluetooth Classic khá tốn tài nguyên của máy, đặc biệt như là PIN. Các bạn cần lưu ý đóng socket khi không cần dùng nữa, bằng cách gọi close() của đối tượng BluetoothSocket. Sau khi gọi Socket sẽ ngay lực tức được đóng lại và giải phóng tất cả các tài nguyên nội bộ có liên quan.
      • Chú ý đến việc huỷ Discovery() trước khi kết nối để đảm bảo việc connect được diễn ra thành công.
    2. Connect như một Server

      Với loại connect này được thực hiện nếu bạn muốn connect 2 thiết bị devices với nhau. Một thiết bị trong 2 thiết bị đó phải hoạt động như một Server bằng cách nắm giữ BluetoothServerSocket. Cụ thể BluetoothServerSocket lắng nghe các kết nối, sử dụng để thu nhận kết nối bluetoothSocket (RFCOMM sockets). Khi kết nối được thiết lập, máy chủ socket không còn cần thiết nữa và có thể đóng lại thông qua hàm close().

      Các bước cụ thể như sau:

      1. Lấy thông tin BluetoothServerSocket bằng cách gọi hàm listenUsingRfcommWithServiceRecord(String, UUID).

      2. Bắt đầu lắng nghe các yêu cầu kết nối bằng cách gọi accept(). Khi thành công, accept() trả về BluetoothSocket được kết nối và sau đó chúng ta sẽ sử dụng nó để trao đổi dữ liệu với thiết bị kết nối.

        class BluetoothClassicServerConnection: Thread() {
        private val serverSocket: BluetoothServerSocket? by lazy {
            BluetoothAdapter.getDefaultAdapter()?.listenUsingInsecureRfcommWithServiceRecord(NAME, UUID)
        }
        
        private var bluetoothSocket: BluetoothSocket? = null
        
        override fun run() {
            // Keep listening until exception occurs or a socket is returned.
            var shouldLoop = true
        
            while (shouldLoop) {
                val socket: BluetoothSocket? = try {
                    bluetoothSocket = serverSocket?.accept()
                } catch (e: IOException) {
                    Log.e(TAG, "Socket's accept() method failed", e)
                    shouldLoop = false
                    null
                }
                socket?.also {
                    
                    // Handle Transfer Data  
        
                    serverSocket?.close()
                    shouldLoop = false
                }
            }
        }
        
        // Closes the connect socket and causes the thread to finish.
        fun cancel() {
            try {
                serverSocket?.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the connect socket", e)
            }
        }
        
        companion object {
            private const val NAME = "The name we provide the service with when it is added to the SDP (Service Discovery Protocol)"
            private const val UUID = "UUID code provide from Server Site"
          }
        }
        

      Note:

      • Khi call accept() trả về BluetoothSocket, Socket đã được kết nối. Vì vậy, không nên gọi connect(), giống như mình làm ở phía Client.

    Vậy là mình cũng vừa trình bày xong phần liên quan đến Connect Bluetooth Classic trong Android. Các loại và các trường hợp sử dụng nó. Bài tiếp theo mình sẽ trình bày về việc Transfer Data (Read and Write) của Bluetooth Classic.

    Hẹn gặp lại các bạn trong bài viết sắp tới.

  • Android Bluetooth Classic – Part 2

    Android Bluetooth Classic – Part 2

    Tiếp tục trong chuỗi bài liên quan đến Bluetooth Classic trong Android. Mình xin chia sẻ phần liên quan đến Tìm kiếm (Scanning – Discovery) và Enable discoverability thiết bị.

    1. Tìm kiếm (Scanning – Discovery) thiết bị Bluetooth

      Để bắt đầu tìm kiếm (Scanning) thiết bị, hàm startDiscovery() thông qua đối tượng BluetoothAdapter được gọi. Quá trình này bất đồng bộ (asynchronous) và trả về một giá trị boolean cho biết quá trình discovery đã bắt đầu thành công hay chưa. Theo như mình tìm hiểu thì quá trình scan thường được truy vấn trong khoảng 12 giây, sau đó hiển thị output lên màn hình hoặc nơi hiển thị kết quả.

      Để nhận thông tin về từng thiết bị được quét (tìm thấy) thành công, ứng dụng của bạn phải register một BroadcastReceiver với ACTION_FOUND intent. Hệ thống sẽ broadcasts intent này cho từng thiết bị. Trong intent này sẽ chứa công tin về device thông qua các extra fields như EXTRA_DEVICE và EXTRA_CLASS.

      Cụ thể:

    class LegacyPairingFlowBluetoothDeviceReceiver(
        private val mListener: Listener
    ) : BroadcastReceiver() {
    
        companion object {
            private const val RESTART_DISCOVERY_DELAY = 5_000L // ms
        }
    
        private val handler = Handler(Looper.getMainLooper())
        private val restartDiscovery = Runnable {
            if (!cancelScan) {
                BluetoothAdapter.getDefaultAdapter()?.startDiscovery()
            }
        }
    
        var cancelScan: Boolean = false
            set(value) {
                field = value
                if (value) {
                    handler.removeCallbacks(restartDiscovery)
                }
            }
    
        override fun onReceive(context: Context, intent: Intent) {
            val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
            if (state == BluetoothAdapter.STATE_ON && !cancelScan) {
                BluetoothAdapter.getDefaultAdapter()?.startDiscovery()
            }
    
            val action = intent.action
            if (BluetoothDevice.ACTION_FOUND == action) {
                val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) as? BluetoothDevice
                if (device != null) {
                    mListener.onFoundBluetoothDevice(device)
                }
            } else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED == action) {
                mListener.onDiscoveryStarted()
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED == action) {
                if (!cancelScan) {
                    handler.postDelayed(restartDiscovery, RESTART_DISCOVERY_DELAY)
                }
                mListener.onDiscoveryFinished()
            }
    
            if (BluetoothAdapter.STATE_ON == state) {
                mListener.onBluetoothStateOn()
            }
        }
    
        interface Listener {
            fun onFoundBluetoothDevice(device: BluetoothDevice)
    
            fun onDiscoveryStarted()
    
            fun onDiscoveryFinished()
    
            fun onBluetoothStateOn() {}
        }
    }
    
    Để bắt đầu kết nối với thiết bị Bluetooth, bạn gọi getAddress() của BluetoothDevice để truy xuất địa chỉ MAC được liên kết.
    

    Note: Thực hiện tìm kiếm (gọi discovery) thiết bị tiêu tốn rất nhiều tài nguyên. Sau khi bạn đã tìm thấy một thiết bị để kết nối, hãy chắc chắn rằng bạn dừng việc tìm kiếm lại bằng function cancelDiscovery() trước khi thử connect với thiết bị đó. Ngoài ra, bạn không nên thực hiện tìm kiếm (gọi discovery) khi được kết nối với thiết bị vì quá trình tìm kiếm làm giảm đáng kể băng thông khả dụng cho bất kỳ kết nối hiện có.

    1. Enable discoverability

      Nếu bạn chỉ cần kết nối từ ứng dụng của bạn với một thiết bị Bluetooth, transfer data với chúng thì bạn không cần phải Enable discoverability của thiết bị. Enable discoverability chỉ cần thiết khi bạn muốn ứng dụng của mình là một host a server socket chấp nhận các kết nối đến, vì các thiết bị Bluetooth phải có thể khám phá các thiết bị khác trước khi khởi động kết nối với các thiết bị khác.

      Để thực hiện điều đó, chúng ta gọi hàm startActivityForResult(Intent, int) với action Intent tương ứng là ACTION_REQUEST_DISCOVERABLE. Theo mặc định, thiết bị có thể được discover trong hai phút. Bạn có thể xác định thời lượng khác, tối đa một giờ, bằng cách thêm EXTRA_DISCOVERABLE_DURATION extra cho Intent đó.

        val requestCode = 1;
        val discoverableIntent: Intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
           putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 500)
        }
        startActivityForResult(discoverableIntent, requestCode)
    

    Note: Nếu Bluetooth chưa được bật trên thiết bị, thì việc Enable discoverability sẽ tự động bật Bluetooth.

    Bài tiếp theo mình sẽ trình bày về việc Connect và Transfer giữa ứng dụng và thiết bị Bluetooth. Hẹn gặp lại các bạn ở bài viết sắp tới.

  • Android Bluetooth Classic

    Android Bluetooth Classic

    Tiếp tục trong chuỗi bài liên quan đến Bluetooth Classic trong Android. Mình xin chia sẻ phần liên quan đến Tìm kiếm (Scanning – Discovery) và Enable discoverability thiết bị.

    1. Tìm kiếm (Scanning – Discovery) thiết bị Bluetooth

      Để bắt đầu tìm kiếm (Scanning) thiết bị, hàm startDiscovery() thông qua đối tượng BluetoothAdapter được gọi. Quá trình này bất đồng bộ (asynchronous) và trả về một giá trị boolean cho biết quá trình discovery đã bắt đầu thành công hay chưa. Theo như mình tìm hiểu thì quá trình scan thường được truy vấn trong khoảng 12 giây, sau đó hiển thị output lên màn hình hoặc nơi hiển thị kết quả.

      Để nhận thông tin về từng thiết bị được quét (tìm thấy) thành công, ứng dụng của bạn phải register một BroadcastReceiver với ACTION_FOUND intent. Hệ thống sẽ broadcasts intent này cho từng thiết bị. Trong intent này sẽ chứa công tin về device thông qua các extra fields như EXTRA_DEVICE và EXTRA_CLASS.

      Cụ thể:

    class LegacyPairingFlowBluetoothDeviceReceiver(
        private val mListener: Listener
    ) : BroadcastReceiver() {
    
        companion object {
            private const val RESTART_DISCOVERY_DELAY = 5_000L // ms
        }
    
        private val handler = Handler(Looper.getMainLooper())
        private val restartDiscovery = Runnable {
            if (!cancelScan) {
                BluetoothAdapter.getDefaultAdapter()?.startDiscovery()
            }
        }
    
        var cancelScan: Boolean = false
            set(value) {
                field = value
                if (value) {
                    handler.removeCallbacks(restartDiscovery)
                }
            }
    
        override fun onReceive(context: Context, intent: Intent) {
            val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
            if (state == BluetoothAdapter.STATE_ON && !cancelScan) {
                BluetoothAdapter.getDefaultAdapter()?.startDiscovery()
            }
    
            val action = intent.action
            if (BluetoothDevice.ACTION_FOUND == action) {
                val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) as? BluetoothDevice
                if (device != null) {
                    mListener.onFoundBluetoothDevice(device)
                }
            } else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED == action) {
                mListener.onDiscoveryStarted()
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED == action) {
                if (!cancelScan) {
                    handler.postDelayed(restartDiscovery, RESTART_DISCOVERY_DELAY)
                }
                mListener.onDiscoveryFinished()
            }
    
            if (BluetoothAdapter.STATE_ON == state) {
                mListener.onBluetoothStateOn()
            }
        }
    
        interface Listener {
            fun onFoundBluetoothDevice(device: BluetoothDevice)
    
            fun onDiscoveryStarted()
    
            fun onDiscoveryFinished()
    
            fun onBluetoothStateOn() {}
        }
    }
    
    Để bắt đầu kết nối với thiết bị Bluetooth, bạn gọi getAddress() của BluetoothDevice để truy xuất địa chỉ MAC được liên kết.
    

    Note: Thực hiện tìm kiếm (gọi discovery) thiết bị tiêu tốn rất nhiều tài nguyên. Sau khi bạn đã tìm thấy một thiết bị để kết nối, hãy chắc chắn rằng bạn dừng việc tìm kiếm lại bằng function cancelDiscovery() trước khi thử connect với thiết bị đó. Ngoài ra, bạn không nên thực hiện tìm kiếm (gọi discovery) khi được kết nối với thiết bị vì quá trình tìm kiếm làm giảm đáng kể băng thông khả dụng cho bất kỳ kết nối hiện có.

    1. Enable discoverability

      Nếu bạn chỉ cần kết nối từ ứng dụng của bạn với một thiết bị Bluetooth, transfer data với chúng thì bạn không cần phải Enable discoverability của thiết bị. Enable discoverability chỉ cần thiết khi bạn muốn ứng dụng của mình là một host a server socket chấp nhận các kết nối đến, vì các thiết bị Bluetooth phải có thể khám phá các thiết bị khác trước khi khởi động kết nối với các thiết bị khác.

      Để thực hiện điều đó, chúng ta gọi hàm startActivityForResult(Intent, int) với action Intent tương ứng là ACTION_REQUEST_DISCOVERABLE. Theo mặc định, thiết bị có thể được discover trong hai phút. Bạn có thể xác định thời lượng khác, tối đa một giờ, bằng cách thêm EXTRA_DISCOVERABLE_DURATION extra cho Intent đó.

        val requestCode = 1;
        val discoverableIntent: Intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
           putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 500)
        }
        startActivityForResult(discoverableIntent, requestCode)
    

    Note: Nếu Bluetooth chưa được bật trên thiết bị, thì việc Enable discoverability sẽ tự động bật Bluetooth.

    Bài tiếp theo mình sẽ trình bày về việc Connect và Transfer giữa ứng dụng và thiết bị Bluetooth. Hẹn gặp lại các bạn ở bài viết sắp tới.

  • Android Bluetooth Classic

    Android Bluetooth Classic

    Tiếp tục chuỗi bài liên quan đến việc tìm hiểu kết nói giữa ứng dụng và thiết bị thông qua Bluetooth. Các bạn có thể tìm hiểu về BLE mình đã chia sẻ tại đây, giống như BLE, hôm nay mình cũng xin phép overview qua và chia sẻ kiến thức mình biết được liên quan đến Bluetooth Classic.

    Chuỗi bài chia sẻ này mình xin chia sẻ một số phần chính

    • Setup Bluetooth Classic trong Android
    • Lấy danh sách các thiết bị ghép nối
    • Tìm kiếm (Scanning) thiết bị
    • Connect và Transfer giữa Ứng dụng và Thiết bị thông qua Bluetooth Classic
    1. Setup Bluetooth Classic trong Android

    Để sử dụng các tính năng Bluetooth trong ứng dụng của mình, bạn phải khai báo vs xin một số quyền để được sử dụng. Tại file AndroidManifest chúng ta yêu cầu các quyền.

    <manifest>
        <!-- Request legacy Bluetooth permissions on older devices. -->
        <uses-permission android:name="android.permission.BLUETOOTH"
                         android:maxSdkVersion="30" />
        <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
                         android:maxSdkVersion="30" />
    
        <!-- Needed only if your app looks for Bluetooth devices.
             If your app doesn't use Bluetooth scan results to derive physical
             location information, you can strongly assert that your app
             doesn't derive physical location. -->
        <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    
        <!-- Needed only if your app makes the device discoverable to Bluetooth
             devices. -->
        <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
    
        <!-- Needed only if your app communicates with already-paired Bluetooth
             devices. -->
        <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    
        <!-- Needed only if your app uses Bluetooth scan results to derive physical location. -->
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
        ...
    </manifest>
    

    Như các bạn thấy ở trên:

    • BLUETOOTH permission cho phép ứng dụng kết nối, ngắt kết nối, và truyền dữ liệu với các thiết bị Bluetooth khác.
    • BLUETOOTH_ADMIN permission cho phép ứng dụng phát hiện ra các thiết bị Bluetooth mới và thay đổi cài đặt Bluetooth của thiết bị.
    • BLUETOOTH_SCAN permission phục vụ cho việc Scan thiết bị cho cả Bluetooth Classic và BLE
    • BLUETOOTH_CONNECT cho phép ứng dụng communicates với thiết bị đã được Pair sẵn trong System trước đó
    • BLUETOOTH_ADVERTISE cho phép các thiết bị hiện tại có thể discoverable với các thiết bị khác

    Đối với Android 12 or higher, các bạn chú ý them:

    • Đối với các khai báo quyền liên quan đến Bluetooth cũ, hãy đặt android:maxSdkVersion thành 30. Bước compatibility ứng dụng này giúp hệ thống chỉ cấp cho ứng dụng của bạn các quyền Bluetooth mà ứng dụng đó cần khi cài đặt trên các thiết bị chạy Android 12 trở lên.
    • ACCESS_FINE_LOCATION quyền này chỉ cần thiết nếu ứng dụng của bạn sử dụng kết quả quét Bluetooth để xác định vị trí thực tế. Về quyền này, đặc biệt là các quyền liên quan đến Location, mình sẽ trao đổi rõ hơn ở bài viết tiếp theo liên quan đến Companion Device Manager.

    Note: Trong các quyền trên thì BLUETOOTH_SCAN, BLUETOOTH_CONNECT, BLUETOOTH_ADVERTISE là các runtime permissions. Các bạn chú ý việc xử lý code về việc nhận approve từ người dùng.

    1. Lấy danh sách các thiết bị ghép nối

    Cụ thể mình lấy các thiết bị Bluetooth đã được ghép nối và hiển thị chúng trong một danh sách. Tại các trạng thái mà thiết bị Bluetooth hiển thị trong ứng dụng của mình sẽ là:

    • unknown
    • paired (đã ghép nối).
    • connected (đang kết nối).

    Có một lưu ý là chúng ta cần phải phân biệt giữa paired và connected của một thiết bị Bluetooth. Thiết bị đã paired là chỉ biết về sự tồn tại của nhau và sẵn sàng kết nối thông qua một mã code. Mã code được sử dụng để xác thực và dẫn đến một kết nối. Đầu tiên chúng ta gọi lớp BluetoothAdapter để giao tiếp với Bluetooth. Tạo một object của cuộc gọi bằng cách gọi statics method getDefaultAdapter().

    fun getBluetoothAdapter(context: Context?): BluetoothAdapter? {
        val manager =
            if (context == null) null else context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        return if (manager == null) {
            BluetoothAdapter.getDefaultAdapter()
        } else {
            manager.adapter
        }
    }
    

    Sau khi chúng ta get danh sách Devices đã từng Paired.

    Set<BluetoothDevice> pairedDevices = getBluetoothAdapter(context).getBondedDevices();
    

    Nếu trong trường hợp ứng dụng chưa bật Bluetooth, để cho phép bật Bluetooth của thiết bị, chúng ta gọi intent với hằng số Bluetooth ACTION_REQUEST_ENABLE.

    Intent turnOn = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(turnOn, 0);
    

    Ngoài ra Android còn cung cấp các hằng số khác, các bạn có thể xem phía dưới đây:

    • ACTION_REQUEST_DISCOVERABLE: Hằng số này sử dụng cho việc bật discovering của bluetooth
    • ACTION_STATE_CHANGED: Hằng số này sẽ thông báo rằng trạng thái Bluetooth sẽ được thay đổi
    • ACTION_FOUND: Hằng số này dùng để nhận thông tin về mỗi device mà được discover

    Bài tiếp theo mình sẽ trình bày về việc tìm kiếm Bluetooth Devices, cũng như Connect và Transfer giữa ứng dụng và thiết bị.

  • Hướng dẫn cách sử dụng Lottie Animation trong Swift

    Hướng dẫn cách sử dụng Lottie Animation trong Swift

    Có lẽ tất cả mọi người trên thế giới đều yêu thích cái đẹp và mình cũng không ngoại lệ. Việc xây dựng những ứng dụng có UI đẹp mắt, animation mượt mà và thú vị khiến hàng triệu người dùng trầm trồ là mong muốn của rất nhiều những Developer trên toàn thế giới. Là một developer nếu bạn có tìm thấy bài viết này của mình thì chắc hẳn bạn cũng có khát khao làm những điều thú vị cho ứng dụng của mình có đúng không? Vậy chúng ta cùng đi tiếp vào bài viết này để làm cách nào để có thể làm ứng dụng của các bạn trở nên thú vị hơn nhé.

    Trong một dịp mình có cơ hội làm việc trong một dự án có sử dụng rất nhiều Animation để làm cho ứng dụng trở nên đẹp và thú vị hơn với người dùng. Lúc mới vào dự án khi sử dụng một số tính năng có sử dụng animation làm mình thấy rất hứng thú, thật khó để cưỡng lại sự đẹp đẽ và hoa mỹ của nó. Trong khi làm việc trong dự án thì mình cũng đã phát hiện ra một thư viện hỗ trợ cho việc thực hiện animation rất hay đó chính là Lottie. Vì vậy ngày hôm nay mình muốn chia sẻ với các bạn về thư viện này và cách thêm nó vào ứng dụng của các bạn.

    Lottie là gì?

    Lottie là định dạng tệp hoạt hình dựa trên JSON cho phép các nhà thiết kế gửi animation trên bất kỳ nền tảng nào dễ dàng như vận chuyển nội dung tĩnh. Bạn có thể đọc thêm về nó ở đây.

    Lottie được sử dụng khi nào?

    Lottie khá là linh hoạt và nó có thể sử dụng được trên nhiều nền tảng khác nhau từ iOS, Android, Web …. Vì vậy nó giúp đồng bộ animation trên tất cả các nền tảng mà không xảy ra sai sót nào.

    Lottie hỗ trợ làm animation rất tốt, nó có thể làm được những animation rất phức tạp mà việc code animation không thể làm được hoặc làm đơn giản hoá việc thêm animation vào trong ứng dụng.

    Hiện nay nó đang được sử dụng khá phổ biến trên các ứng dụng.

    Làm sao để sử dụng Lottie cho ứng dụng của bạn?

    Bước 1: Tạo dự án mới

    Nếu bạn muốn thêm vào dự án có sẵn của mình thì bỏ qua bước này nhé.

    Đầu tiên chúng ta mở Xcode lên và tạo một ứng dụng demo để có thể test Lottie chạy trên ứng dụng một cách dễ dàng hơn.

    Tạo ứng dụng mới

    Bước 2: Thêm thư viện Lottie vào ứng dụng

    Để thêm thư viện Lottie vào ứng dụng chúng ta có thể sử dụng bằng nhiều cách khác nhau như Cocoa Pods, Carthage hoặc Swift Package Manager. Nếu bạn chưa biết cách thêm thư viện Lottie vào dự án thì bạn có thể tham khảo hướng dẫn tại đây.

    Ở bài viết này mình sẽ hướng dẫn các bạn sử dụng Swift Package Manager để thêm vào ứng dụng như sau:

    Bạn mở dự án của bạn trên Xcode, chọn File -> Add Packages… Trên đầu bên phải của popup hiện ra có công cụ tìm kiếm các bạn đánh link: https://github.com/airbnb/lottie-ios.git để tìm thư viện. Sau đó bấm vào Add Package và chờ một lúc để Xcode tải thư viện bạn, sau khi tải xong bạn bấm Add để hoàn tất

    Bước 3: Thêm Lottie vào dự án

    Đầu tiên chúng ta cần chuẩn bị file Lottie JSON, nếu bạn chưa có thì bạn có thể download nó tại đây. Bạn nhớ tải file JSON nhé.

    Bạn cũng có thể sử dụng ứng dụng Adobe After Effect để tạo file Lottie của riêng mình.

    Khi đã có file lottie JSON rồi chúng ta sẽ thực hiện thêm file vào trong dự án bằng cách kéo thả nó trực tiếp vào thư mục trong Xcode của bạn, tại nơi mà bạn muốn lưu nó. Thông thường chúng ta tạo mới thư mục Resouces/LottieJSON và lưu nó trong đó.

    Add file lottie vào dự án

    Hãy nhớ bạn tích Copy Items if needed và các mục như trên hình nhé.

    Thêm Lottie bằng cách sử dụng code

    Đầu tiên để sử dụng được thư viện Lottie hãy nhớ import thư viện Lottie vào màn hình mà bạn sử dụng

    import Lottie

    Tiếp theo chúng ta tạo func addLottieAnimation() để thực hiện nhiệm vụ add lottie vào view hiện tại. Bạn cũng có thể custom một func riêng để handle xử lí triệt để các logic của Lottie animation. Trong bài này mình chỉ hướng dẫn cơ bản để các bạn có thể thêm Lottie vào ứng dụng của mình.

        func addLottieAnimation() {
            // 1. Create lottie animation view
            let lottieView: LottieAnimationView = LottieAnimationView(name: "404-Notfound")
            lottieView.frame = view.bounds
            
            // 2. Set animation content mode
            lottieView.contentMode = .scaleAspectFit
            
            // 3. Set animation loop mode
            lottieView.loopMode = .loop
            
            // 4. Adjust animation speed
            lottieView.animationSpeed = 0.5
            
            view.addSubview(lottieView)
            
            // 5. Play animation
            lottieView.play()
        }

    Sau đó bạn call func này ở viewDidLoad thì sẽ nhận được kết quả như sau:

    Thêm Lottie bằng cách sử dụng Builder Interface

    Đầu tiên bạn mở file giao diện của bạn lên băng Interface Builder, kéo một UIView và thực hiện constraint cho nó.

    Thêm UIview vào để sử dụng Lottie

    Sau đó ở trên cùng của tab bên phải bạn chọn Identity Inspector và thay đổi giá trị như hình dưới

    Thay đổi class sang LottieAnimationView

    Tiếp tục chuyển sang tab Attributes Inspector để điền tên file Lottie mà bạn muốn

    Tạo một liên kết giữa view của bạn với file controller để sử dụng, sau đó bạn thực hiện code như dưới đây để Lottie có thể hoạn động.

    import UIKit
    import Lottie
    
    class ViewController: UIViewController {
        @IBOutlet weak var animationLottieView: LottieAnimationView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            // 1. Set animation content mode
            animationLottieView.contentMode = .scaleAspectFit
    
            // 2. Set animation loop mode
    
            animationLottieView.loopMode = .loop
    
            // 3. Adjust animation speed
    
            animationLottieView.animationSpeed = 0.5
    
            // 4. Play animation
            animationLottieView.play()
        }
    }

    Chạy ứng dụng và bạn sẽ nhận được kết quả như mong đợi!

    Vậy là mình đã giới thiệu và hướng dẫn cho các bạn một thư viện khá hiệu quả cho các bạn sử dụng để bổ trợ cho các bạn việc thực hiện animation tốt hơn, dễ dàng hơn, và nhanh hơn so với cách code truyền thống. Từ giờ các bạn có thể tha hồ sáng tạo trên những ứng dụng sắp tới của bản thân và khiến mọi người sử dụng thích thú.

    Mình hi vọng nó sẽ giúp các bạn làm ra những ứng dụng có trải nghiệm tuyệt vời, giúp những người sử dụng ứng dụng của các bạn phải trầm trồ khi sử dụng nó. Chúc các bạn thành công với những dự án sắp tới của mình.

  • Architecture pattern: Clean Swift

    Architecture pattern: Clean Swift

    Xin chào mọi người! Lại là DaoNM2 đây! Tiếp tục với series về Architecture pattern hôm nay mình sẽ giới thiệu cho các bạn một kiến trúc khá mới so với các mẫu kiến trúc hiện tại đó là Clean Swift (VIP). Trước đây mình đã có cơ hội tiếp cận với kiến trúc này, khi tham gia vào việc xây dựng một ứng dụng rất lớn cho một công ti rất nổi tiếng về ô tô xe máy ở Việt Nam. Khi đó mình cũng đã tích luỹ được một số kinh nghiệm về kiến trúc này, vì vậy mình muốn chia sẻ với các bạn một số thông tin cũng như kinh nghiệm mà mình đã tích luỹ được khi làm việc với mẫu kiến trúc này.

    Đầu tiên nếu bạn là người mới hoặc bạn mới tìm hiều về các mẫu kiến trúc trong lập trình bao giờ đây là bài đầu tiên bạn đọc, thì để có thể hiểu kiến trúc này tốt hơn thì bạn có thể tham khảo các bài viết về các mẫu thiết kế trước khi tiếp tục ở link dưới đây:

    Bối cảnh hình thành

    Clean swift lần đầu tiên được giới thiệu bởi Raymond Law trên website clean-swift.com của anh ấy. Ý tưởng hình thành mẫu kiến trúc này là do anh ấy đã quá chán với các vấn đề của MVC(một mẫu kiến trúc mà Apple khuyên dùng) vì vậy anh ấy đã nghĩ ra Clean Swift để giải quyết các vấn đề mà các mẫu kiến trúc trước đây chưa làm được. Clean Swift được Raymond Law xây dựng dựa trên Clean Architecture của Uncle Bob.

    Clean Swift là gì?

    Clean Swift là một mẫu kiến trúc xây dựng dựa trên Clean Architecture của Uncle Bob để áp dụng cho việc xây dựng các ứng dụng iOS và MacOS.

    Trong Clean Swift Architecture pattern thì tất cả logic của ứng dụng sẽ được chia đều ra 3 thành phần chính của nó là View controller, Interactor và Presenter. Mỗi phần sẽ đảm nhiệm một số logic cụ thể, và chúng liên lạc với nhau bằng các liên kết 1 chiều, vì vậy source code của bạn sẽ luôn luôn đi theo một chiều chứ không đa chiều như các architecture pattern khác.

    Khi ứng dụng Clean Swift vào trong dự án của bạn, nó sẽ được cấu trúc theo từng màn hình của ứng dụng(scenes).

    Các thành phần của Clean Swift

    Mẫu kiến trúc Clean Swift được cấu tạo bởi các phần như sau:

    • View
    • View Controller
    • Router
    • Presenter
    • Interactor
    • Worker(optional)
    • Model(optional)
    Clean Swift architecture pattern

    Clean Swift gồm 3 phần chính là ViewController, Presenter và Interactor. 3 phần này có liên kết 1 chiều và tạo thành 1 vòng tròn. Khi View controller nhận được request nó sẽ gọi sang Interactor để nó xử lí logic, khi xong logic Interactor sẽ gửi dữ liệu sang bên Presenter để nó thực hiện format lại dữ liệu rồi trả về cho ViewController làm nhiệm vụ update lên View cho người dùng. Các thành phần này sẽ được kết nối với nhau bằng protocol.

    View

    Là các thành phần nằm trong UIKit hoặc bất kể thứ gì liên quan tới UI ví dụ như: storyboard, xib, UIView, UIControl …

    View Controller

    Định nghĩa các màn hình(scenes), nó có thể chứa 1 hoặc nhiều View

    Nó sẽ giữ các instances của Interactor và Router

    Là nơi nhận các tương tác của người dùng và gọi đến Interactor hoặc Router để xử lý, nó cũng nhận output của Presenter làm input và truyền nó lên view để hiển thị cho người dùng.

    Interactor

    Chứa các business logic của màn hình

    Giữ instance của Presenter và các Workers(nếu có)

    Nhận thông tin input từ ViewController và xử lý hoặc yêu cầu Worker làm việc để truyền kết quả sang cho Presenter

    Interactor sẽ không được import UIKit để đảm bảo source không có liên kết trực tiếp với View

    Presenter

    Giữ một tham chiếu yếu đến View Controller để truyền dữ liệu sang View Controller

    Là nơi xử lý logic hiển thị, khi nhận được input từ Interactor nó sẽ thực hiện format lại dữ liệu và truyền sang cho ViewController để nó hiển thị thông tin cho người dùng.

    Worker

    Là nơi được coi là trung tâm dữ liệu, nó sẽ thực hiện các nhiệm vụ liên quan tới việc lấy dữ liệu từ API hoặc LocalDB

    Là thành phần phụ nên ở các màn hình đơn giản không có tương tác với dữ liệu chúng ta có thể bỏ qua worker

    Router

    Nó giữ một tham chiếu yếu tới View Controller, nhằm mục đích tránh việc tham chiếu lẫn nhau(retain cycles) dẫn đến không thể release các đối tượng này khi nó không còn được sử dụng -> Lack memory.

    Router sinh ra để giảm tải công việc cho View Controller nó sẽ làm nhiệm vụ điều hướng trong ứng dụng.

    Model

    Nó là nơi định nghĩa các đối tượng cho ứng dụng, nó chỉ làm nhiệm vụ định nghĩa các đối tượng và không có xử lí logic hay liên kết trực tiếp với các thành phần khác của kiến trúc.

    Các đối tượng trong model sẽ được khai báo theo value type(struct, enum)

    Tương tự như Worker, một số màn hình đơn giản không tương tác với dữ liệu sẽ không cần đến Model

    Ưu điểm

    • Dễ maintain, fixbugs vì liên kết 1 chiều giữa các thành phần
    • Hỗ trợ viết Unit test một cách dễ dàng
    • Viết các phương thức ngắn hơn với trách nhiệm duy nhất
    • Tách được business logic sang cho Interactor xử lí
    • Có thể tái sử dụng các Workers và Services
    • Có thể áp dụng được cho các dự án lớn để giảm tình trạng conflict khi merge source.

    Nhược điểm

    • Quá nhiều các protocol với các nhiệm vụ riêng biệt, làm cho việc đặt tên trở nên khó khăn và không cẩn thận sẽ gây khó hiểu cho người đọc
    • Kích thước ứng dụng lớn do chứa nhiều protocol và nhiều file
    • Cần thời gian để cho các thành viên dự án có thể hiểu và tuân theo

    Tổng kết

    Clean swift là một mẫu kiến trúc không phổ biến như các mẫu khác, tuy nhiên ưu điểm của nó mang lại là rất lớn. Để vận hành các dự án lớn có yêu cầu viết Unitest chúng ta có thể coi Clean Swift là một trong những ứng cử viên sáng giá. Mình hi vọng bài viết có thể giúp các bạn có thêm kiến thức về mẫu kiến trúc Clean Swift và giúp các bạn có thể chọn được mẫu kiến trúc ưng ý cho những dự án sắp tới.

    Nếu các bạn muốn biết thêm nhiều thông tin hơn về Clean Swift Architecture Pattern thì các bạn có thể tham khảo tại link sau: Clean Swift