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ị.
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ó.
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.
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
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.
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.
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ị.
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.
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.
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:
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
Hôm nay mình xin chia sẻ một chút kiến thức của mình liên quan đến Dependency Injection.
Đầu tiên mình xin phép nói sơ qua về khái niệm của Dependency Injection. Vậy Dependency Injection là gì ?
Cụ thể dựa vào DI trong Principle, Dependency Injection cơ bản là cung cấp đối tượng phụ thuộc như 1 tham số. Ứng dụng phụ thuộc này khi các class đã được xây dựng hoặc chuyển chúng (các đối tượng phụ thuộc) vào các function cần mỗi phụ thuộc.
Cách triển khai chúng theo lý thuyết là lấy đấy tượng phụ thuộc và cung cấp chúng vào class thay vì tạo ra thể hiện của chúng trực tiếp trong class bị phụ thuộc.
Ưu điểm:
Khả năng tái sử dụng lại các class và tách rời các phụ thuộc (dependencies): Dễ dàng hơn trong việc thay đổi một dependency. Việc tái sử dụng code được cải thiện do Inversion of Control, và các class không còn kiểm soát việc tạo ra các dependencies như thế nào, thay vào đó nó có thể làm việc với bất kỳ cấu hình nào.
Dễ tái cấu trúc: Các dependencies trở thành những phần có thể kiểm tra như API. Hoàn toàn có thể kiểm tra lúc tạo đối tượng, lúc biên dịch chứ không bị ẩn đi.
Dễ dàng cho việc Testing: Các class không quản lý các dependencies của nó. Vì thế khi testing chúng ta có thể truyền các dependency khác nhau và xử lý được nhiều test case
Tiếp đến mình sẽ nói phần chính của bài này liên quan đến Annotation in Android Hilt (Một framework trong Android support việc triển khai Dependency Injection trong Project).
Chúng ta có 3 annotation thường dùng để inject các object trong Hilt:
@Inject: annotation dùng ở constructor của class
@Provides: annotation dùng ở Module
@Binds: một annotation khác cũng dùng ở Module
Vậy khi nào dùng chúng, đặc biệt là giữa @Provide và @Binds?
Inject
Chúng ta dùng @Inject annotation ở tất cả các constructor mà mình cần inject đối tượng (Object). Ví dụ như ViewModel, Repository, UseCase, DataSource, thậm trí là Android classes (ex. Activity, Fragment, …)
class ConnectRepositoryImpl @Inject constructor(
private val connectManager: ConnectManager
) : ConnectRepository {
fun doSomething() {}
}
class ConnectUseCaseImpl @Inject constructor(
private val connectRepository: ConnectRepository,
) : ConnectUseCase {
fun doSomething() {
// Using connectRepository
}
}
Qua ví dụ trên sau khi khởi tạo class ConnectRepositoryImpl với @Inject constructor. Chúng ta có thể dễ dàng sử dụng ConnectRepository ở các class khác (như ViewModel, Android class, UseCase) bằng cách inject chúng vào nơi cần chúng.
Tuy nhiên thì chúng ta lại chỉ có thể sử dụng annotation này để annotate constructor của những class mà mình tự define.
Provide
Vậy thì để khắc phục hạn chế chỉ có thể sử dụng annotation này để annotate constructor của những class mà mình tự define của @Inject, inject object của những class mà mình không define (Ví dụ như Retrofit, OkHttpClient hoặc Room database), chúng ta cùng đến với @Provides. Trước tiên, chúng ta cần tạo một @Module để chứa các dependency với annotation @Provides.
@InstallIn(SingletonComponent::class)
@Module
class DatabaseModule {
@Provides
@Singleton
fun provideGSDatabase(@ApplicationContext context: Context): GSDatabase {
return Room.databaseBuilder(context, GSDatabase::class.java, GS_DATABASE_NAME)
.fallbackToDestructiveMigration()
.build()
}
@Provides
fun provideSpeakerDao(gsDatabase: GSDatabase): SpeakerDao {
return gsDatabase.speakerDao()
}
}
Như đoạn code ở trên, mình khởi tạo đối tượng RomDatabase và một đối tượng Dao của chúng, đó là khởi tạo object không phải code của chúng ta define, hơn nữa trong trường hợp này còn khởi tạo theo kiểu Builder pattern, nên chúng ta không thể dùng @Inject annotation mà bắt buộc phải dùng @Provides. Bây giờ, chúng ta đã có thể inject object của interface GSDatabase và SpeakerDao ở bất cứ đâu.
Bind
Đối với interface, chúng ta không thể dùng annotation @Inject, vì nó không có constructor function. Tuy nhiên, nếu bạn có một interface mà chỉ có duy nhất một implementation (một class implement interface đó), thì bạn có thể dùng @Binds để inject interface đó. Việc inject interface thay vì class là trong những cách triển khai được Recommend sử dụng bởi vì nó tuân theo nguyên tắc Dependence Inversion của SOLID, từ đó dễ dàng cho việc testing và maintain mai này.
interface ConnectRepository {
fun doSomething()
}
class ConnectRepositoryImpl @Inject constructor(
private val connectManager: ConnectManager
) : ConnectRepository {
fun doSomething() {}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class ConnectDeviceModule {
@Binds
@Singleton
abstract fun provideConnectRepository(
connectRepository: ConnectRepositoryImpl
): ConnectRepository
}
class ConnectUseCaseImpl @Inject constructor(
private val connectRepository: ConnectRepository,
) : ConnectUseCase {
fun doSomething() {
// Using connectRepository
}
}
Cụ thể trong ví dụ trên, mình khai báo 1 interface ConnectRepository thể hiện cho đối tượng ConnectRepositoryImpl. Từ đó khi sử dụng ConnectRepositoryImpl chúng ta sẽ sử dụng và gọi nó thông qua ConnectRepository.
Ưu điểm của việc dùng @Binds thay cho @Provides là nó giúp giảm lượng code được generate, như là Module Factory class. Từ đó tăng thời gian build và tăng kích thước file .apk và .aab của Project.
Trên đây là chia sẻ của mình về 1 số Annotation của Hilt Android. Mong sẽ giúp ích được ít nhiều mọi người. Hẹn mọi người ở bài viết sắp tới
Hôm nay mình xin chia sẻ một chút kiến thức của mình liên quan đến Dependency Injection.
Đầu tiên mình xin phép nói sơ qua về khái niệm của Dependency Injection. Vậy Dependency Injection là gì ?
Cụ thể dựa vào DI trong Principle, Dependency Injection cơ bản là cung cấp đối tượng phụ thuộc như 1 tham số. Ứng dụng phụ thuộc này khi các class đã được xây dựng hoặc chuyển chúng (các đối tượng phụ thuộc) vào các function cần mỗi phụ thuộc.
Cách triển khai chúng theo lý thuyết là lấy đấy tượng phụ thuộc và cung cấp chúng vào class thay vì tạo ra thể hiện của chúng trực tiếp trong class bị phụ thuộc.
Ưu điểm:
Khả năng tái sử dụng lại các class và tách rời các phụ thuộc (dependencies): Dễ dàng hơn trong việc thay đổi một dependency. Việc tái sử dụng code được cải thiện do Inversion of Control, và các class không còn kiểm soát việc tạo ra các dependencies như thế nào, thay vào đó nó có thể làm việc với bất kỳ cấu hình nào.
Dễ tái cấu trúc: Các dependencies trở thành những phần có thể kiểm tra như API. Hoàn toàn có thể kiểm tra lúc tạo đối tượng, lúc biên dịch chứ không bị ẩn đi.
Dễ dàng cho việc Testing: Các class không quản lý các dependencies của nó. Vì thế khi testing chúng ta có thể truyền các dependency khác nhau và xử lý được nhiều test case
Tiếp đến mình sẽ nói phần chính của bài này liên quan đến Annotation in Android Hilt (Một framework trong Android support việc triển khai Dependency Injection trong Project).
Chúng ta có 3 annotation thường dùng để inject các object trong Hilt:
@Inject: annotation dùng ở constructor của class
@Provides: annotation dùng ở Module
@Binds: một annotation khác cũng dùng ở Module
Vậy khi nào dùng chúng, đặc biệt là giữa @Provide và @Binds?
Inject
Chúng ta dùng @Inject annotation ở tất cả các constructor mà mình cần inject đối tượng (Object). Ví dụ như ViewModel, Repository, UseCase, DataSource, thậm trí là Android classes (ex. Activity, Fragment, …)
<pre>
class ConnectRepositoryImpl @Inject constructor(
private val connectManager: ConnectManager
) : ConnectRepository {
fun doSomething() {}
}
</pre>
<pre>
class ConnectUseCaseImpl @Inject constructor(
private val connectRepository: ConnectRepository,
) : ConnectUseCase {
fun doSomething() {
// Using connectRepository
}
}
</pre>
Qua ví dụ trên sau khi khởi tạo class ConnectRepositoryImpl với @Inject constructor. Chúng ta có thể dễ dàng sử dụng ConnectRepository ở các class khác (như ViewModel, Android class, UseCase) bằng cách inject chúng vào nơi cần chúng.
Tuy nhiên thì chúng ta lại chỉ có thể sử dụng annotation này để annotate constructor của những class mà mình tự define.
Provide
Vậy thì để khắc phục hạn chế chỉ có thể sử dụng annotation này để annotate constructor của những class mà mình tự define của @Inject, inject object của những class mà mình không define (Ví dụ như Retrofit, OkHttpClient hoặc Room database), chúng ta cùng đến với @Provides. Trước tiên, chúng ta cần tạo một @Module để chứa các dependency với annotation @Provides.
<pre>
@InstallIn(SingletonComponent::class)
@Module
class DatabaseModule {
Như đoạn code ở trên, mình khởi tạo đối tượng RomDatabase và một đối tượng Dao của chúng, đó là khởi tạo object không phải code của chúng ta define, hơn nữa trong trường hợp này còn khởi tạo theo kiểu Builder pattern, nên chúng ta không thể dùng @Inject annotation mà bắt buộc phải dùng @Provides. Bây giờ, chúng ta đã có thể inject object của interface GSDatabase và SpeakerDao ở bất cứ đâu.
Bind
Đối với interface, chúng ta không thể dùng annotation @Inject, vì nó không có constructor function. Tuy nhiên, nếu bạn có một interface mà chỉ có duy nhất một implementation (một class implement interface đó), thì bạn có thể dùng @Binds để inject interface đó. Việc inject interface thay vì class là trong những cách triển khai được Recommend sử dụng bởi vì nó tuân theo nguyên tắc Dependence Inversion của SOLID, từ đó dễ dàng cho việc testing và maintain mai này.
<pre>
interface ConnectRepository {
fun doSomething()
}
</pre>
<pre>
class ConnectRepositoryImpl @Inject constructor(
private val connectManager: ConnectManager
) : ConnectRepository {
fun doSomething() {}
}
</pre>
<pre>
@Module
@InstallIn(SingletonComponent::class)
abstract class ConnectDeviceModule {
@Binds
@Singleton
abstract fun provideConnectRepository(
connectRepository: ConnectRepositoryImpl
): ConnectRepository
}
</pre>
<pre>
class ConnectUseCaseImpl @Inject constructor(
private val connectRepository: ConnectRepository,
) : ConnectUseCase {
fun doSomething() {
// Using connectRepository
}
}
</pre>
Cụ thể trong ví dụ trên, mình khai báo 1 interface ConnectRepository thể hiện cho đối tượng ConnectRepositoryImpl. Từ đó khi sử dụng ConnectRepositoryImpl chúng ta sẽ sử dụng và gọi nó thông qua ConnectRepository.
Ưu điểm của việc dùng @Binds thay cho @Provides là nó giúp giảm lượng code được generate, như là Module Factory class. Từ đó tăng thời gian build và tăng kích thước file .apk và .aab của Project.
Trên đây là chia sẻ của mình về 1 số Annotation của Hilt Android. Mong sẽ giúp ích được ít nhiều mọi người. Hẹn mọi người ở bài viết sắp tới
Xin chào các bạn, lại là DaoNM2 đây! Để tiếp tục series về Architecture patterns thì hôm nay mình xin giới thiệu cho các bạn một mẫu kiến trúc được sử dụng khá nhiều khi phát triển các ứng dụng di động đó là VIPER.
VIPER là gì?
VIPER là một mẫu kiến trúc để phát triền phần mềm, nó được sử dụng khá nhiều khi xây dựng các ứng dụng di động trên ngôn ngữ lập trình Swift. Nó được xây dựng dựa trên Clean Design Architecture. Các Modules trong VIPER được định hướng theo Protocol và mỗi chức năng, các thuộc tính input và output được thực hiện bằng các bộ quy tắc giao tiếp cụ thể.
Các thành phần chính của VIPER architecture pattern
VIPER là viết tắt của các chứ cái đầu trong các thành phần của nó, nó bao gồm View, Interactor, Presenter, Entity và Router. Các thành phần này sẽ tương tác với nhau như sơ đồ dưới đây:
View
Bao gồm các thành phần trong UIKit và ViewController, nó là nơi để hiển thị nội dung cho người dùng và nhận các tương tác từ người dùng sau đó gửi cho presenter để xử lí tiếp logic hiển thị. Trong mẫu kiến trúc này Presenter là tầng duy nhất có liên kết với View.
Interactor
Là nơi xử lý business logic của ứng dụng, nó sẽ thao tác với Entity, model, API fetcher và datastore. Khi nhận được request từ Presenter lúc này Interactor sẽ thực hiện logic để lấy dữ liệu tương ứng và trả về cho presenter.
Trong VIPER mỗi một Interactor sẽ tương ứng với một Use case, nó tách biệt hoàn toàn với View vì vậy khả năng kiểm thử độc lập trên Interactor khá dễ dàng.
Presenter
Là nơi xử lý logic hiển thị của ứng dụng, khi nhận được request thay đổi hoặc hiển thị thông tin từ View nó sẽ thực hiện logic tương ứng để yêu cầu Interactor trả về data. Sau khi nhận được data nó sẽ format lại dữ liệu và trả về cho View để hiển thị chúng lên màn hình. Khi nhận được yêu cầu di chuyển màn hình Presenter sẽ thực hiện call Router để nó làm nốt nhiệm vụ điều hướng
Entity
Đây là các Data model, nó có nhiệm vụ tương tác với Interactor để trả dữ liệu về cho Presenter.
Router
Là nơi xử lí luồng của ứng dụng, nó làm nhiệm vụ điều hướng ứng dụng đến nơi mà người dùng cần. Khi Presenter nhận yêu cầu chuyển màn hình từ View, nó sẽ thực hiện logic hiển thị và thực hiện tương tác với Router để xử lí di chuyển luồng đúng với yêu cầu của View.
Ưu điểm
VIPER được chia nhỏ thành nhiều phần, các phần đảm nhiệm các vai trò và nhiệm vụ cố định, các thành phần tương tác với nhau dựa trên các quy định cụ thể vì vậy nó có khá nhiều ưu điểm
Các nhiệm vụ được chia đều ra cho các thành phần vì vậy việc maintain không còn quá rắc rối.
Việc kiểm thử (Unit test) cũng trở nên dễ dàng hơn vì giờ đây các thành phần đã được chia nhỏ và không liên kết chặt chẽ với View
Cấu trúc source trở nên dễ hiểu và rõ ràng hơn vì nó được chia theo từng use case và các phần được chia nhiệm vụ và trách nhiệm rõ ràng
Không gặp phải trường hợp một file có nội dung quá dài, vì vậy việc đọc source code của người cũng trở nên dễ hiểu hơn
Khá là hữu dụng với các ứng dụng lớn với team size lớn
Dễ dàng để mở rộng và bảo trì, các developer có thể đồng thời làm việc trên nó một cách trơn tru
Giảm số lượng conflict khi merge source code
Nhược điểm
Do có nhiều thành phần và tương tác với nhau nên số file quản lý sẽ nhiều hơn so với các mẫu kiến trúc khác
Không dễ sử dụng cho người mới vì có nhiều ràng buộc và quy tắc cho từng phần, vì vậy cần thời gian để các member có thể tìm hiểu và thích nghi với mẫu kiến trúc này.
Một số thư viện bên thứ 3 không hỗ trợ kiến trúc này, vì vậy nếu không có lựa chọn nào khác lúc này nếu áp dụng thư viện vào ứng dụng nó sẽ phá vỡ kiến trúc ở các tính năng mà sử dụng thư viện này.
Tổng kết
Như mình đã phân tích ở trên VIPER có rất nhiều ưu điểm vì vậy nó rất đáng để các bạn tìm hiểu và sử dụng cho các dự án sắp tới. Tuy nhiên theo mình thì mẫu kiến trúc VIPER chỉ nên sử dụng cho những ứng dụng có kích thước vừa và lớn thì nó mới phát huy được tối đa sự hiệu quả. Đối với các dự án nhỏ nếu sử dụng VIPER architecture pattern thì nó lại trở nên quá cồng kềnh và không cần thiết.
Mình hi vọng bài viết mình chia sẻ sẽ giúp các bạn có thêm lựa chọn khi bắt đầu một dự án mới!
Chào các bạn, tiếp tục loạt bài vè chủ đề BLE, hôm nay mình tiếp tục trình bày về Discovery Service và Transfer BLE Data
Discovery Service
Sau khi kết nối thành công, để phục vụ việc Transfer dữ liệu, điều đầu tiên cần làm khi bạn kết nối với GATT Server trên thiết bị BLE là thực hiện Discovery Service. Điều này cung cấp thông tin về các Available Service trên remote device cũng như các service characteristics và thông tin của chúng. Cụ thể là



3 thông số trên thì device sẽ có một mã code riêng phục vụ việc Discovery và mở cổng transfer dữ liệu giữa App và thiết bị.
Turning notifications on and off
Để thực hiện việc Read và Write dữ liệu giữa ứng dụng và Device BLE chúng ta cần Turning notifications sau khi Discovery Service thành công.
Cụ thể là việc Call setCharacteristicNotification(). Điều này các bạn hiểu đơn giản là sẽ báo cho ngăn xếp Bluetooth về cái mong đợi nhận thông báo cho characteristic cụ thể của Device BLE đó.
Nếu không có điều này hoàn bạn bạn không thể nhận được dữ liệu response từ Device BLE cho dù bạn có thể gọi thành công writeCharacteristic.
Cụ thể về việc gọi Turning notifications. Chú ý về việc thực hiện sau khi Discovery Service thành công. Bạn có thể làm điểu này bằng Callback. Nhưng trong ví dụ của mình thì mình sử dụng 1 SingleSubject của Rx để thực hiện nó.



Read and Write dữ liệu
Đầu tiên các bạn clear rằng, kiểu dữ liệu giao tiếp giữa ứng dụng và Device BLE là ByteArray. Và để nhận biết việc nhận gửi đó giữa ứng dụng và Device BLE nào thì nó sẽ thông qua outputCharacteristic và inputCharacteristic tương ứng với việc Write và Read. 2 thông số outputCharacteristic và inputCharacteristic lấy được từ đối tượng BluetoothGattService khi sau khi chúng ta Discovery Service.
Data ByteArray gửi lên được define cụ thể cho từng Device BLE cụ thể. Đa phần sẽ thuộc về team Hardware và Firmware cung cấp file command request và response tương ứng của thiết bị đó.
Có 1 điểm lưu ý là trong quá trình trao đổi dữ liệu giữa ứng dụng và Device BLE, chúng ta sẽ gửi nhận liên tục. Hoàn toàn bài toán của việc gửi dữ liệu, Device BLE chưa trả về và mình lại đã gửi tiếp dữ liệu 1 lần nữa. Để giải quyết bài toán này, các bạn nên quản lý việc gửi nhận dữ liệu (hay ở đây mình hay gọi là command request ) trong 1 Queue tương ứng. Điều đó sẽ giúp các bạn quản lý dễ dàng cho dù bạn muốn việc thực hiện tuần tự, hay loại nào khác cũng sẽ dễ dàng hơn.
Đây là quá trình gửi dữ liệu từ ứng dụng sang Device BLE. Thông qua việc gọi writeCharacteristic qua đối tượng bluetoothGatt.

Sau khi gọi thành công. System sẽ trả về hàm callback tương ứng, thông về status đã gửi thành công hay thất bại đến Device BLE.
Tiếp đến là quá trình nhận dữ liệu sau khi request gửi đến Device BLE thành công. Thiết bị sẽ trả về response tương ứng tại 1 trong 2 functions.

Sau khi nhận được dự liệu kiểu ByteArray. Chúng ta sẽ thực hiện việc xử lý dữ liệu tương ứng.
Lưu ý:
Vì BLE là asynchronous nên là có nhiều Thread được tạo ra và thực thi. Cụ thể ở đây là khi nhận callbacks on BluetoothGattCallback, thì các dòng code này sẽ thực thực hiện **Binder** threads. Các hàm mình muốn nói đến ở đây ví dụ như các hàm **onCharacteristicWrite()**, **onCharacteristicRead()**, **onCharacteristicChanged()**, … đây là 1 số functions quan trọng trong việc xử lý và thực thi gửi nhận dữ liệu.
Vậy tại sao nên tránh xử lý trên Binder threads? Khi bạn nhận được nội dung nào đó trên **Binder** Thread, Android sẽ không gửi bất kỳ lệnh gọi lại mới nào cho đến khi code của bạn hoàn thành trên **Binder** Thread. Vì vậy, nói chung, bạn nên tránh thực hiện nhiều thao tác trên các luồng **Binder** vì bạn đang chặn các cuộc gọi lại mới khi bạn đang sử dụng nó.
Đây là những chia sẻ mang tính overview của mình về BLE của Android. Mong ít nhiều có thể chia sẻ cho mọi người.
Chào các bạn, để tiếp tục series về Architecture pattern thì hôm nay mìn sẽ giới thiệu đến một mô hình có thể giải quyết được một số nhược điểm của các mô hình cũ như MVC, MVP.
Nếu các bạn chưa tiếp cận hoặc chưa tìm hiều về các Architecture Pattern bao giờ thì có thể xem lại các bài viết của mình về MVC hoặc MVP tại đây: iOS Architecture Patterns: Cocoa MVC MVP Architecture Pattern và biến thể MVP-C Để khi đi vào bài viết này chúng ta sẽ dễ dàng hiểu được nội dung bài viết truyền tải.
Lịch sử hình thành và phát triển
MVVM được viết đầy đủ là Model View ViewModel, MVVM là một biến thể của mẫu thiết kế Presentation Model của Martin Fowler. Nó được sáng lập ra bởi các kiến trúc sư của Microsoft tên là Ken Cooper và Ted Peters, nó đặc biệt được sinh ra để làm đơn giản việc lập trình hướng sự kiện (event-driven programming). MVVM được tích hợp vào Windows Presentation Foundation (WPF) (hệ thống đồ họa .NET của Microsoft) và Silverlight, dẫn xuất ứng dụng Internet của WPF. John Gossman, một kiến trúc sư Microsoft WPF và Silverlight, đã công bố MVVM trên blog của mình vào năm 2005.
MVVC là gì?
MVVC là một mẫu kiến trúc giúp tách biệt source code của bạn ra thành nhiều thành phần khác nhau. Nó giúp code của bạn có các thành phần độc lập, giúp cho quá trình phát triển và kiểm thử ứng dụng trở nên rõ dàng và dễ dàng hơn.
Cấu tạo của MVVM
MVVM gồm 3 phần chính là Model, View và ViewModel.
MVVM
Model
Là nơi chứa dữ liệu và xử lí business logic, model sẽ thực hiện các công việc như lưu trữ các data được lấy về từ API, local storage, v.v. Nó độc lập so với View và tương tác với View thông qua ViewModel.
View
Là nơi hiển thị giao diện cho người dùng, nhận các sự kiện từ người dùng, xử lí và gửi các yêu cầu của người dùng cho ViewModel xử lí. View trong iOS thì thông thường là các thành phần của UIKit, storyboard, xib …, ở MVVM trong iOS thì View bao gồm cả các View Controller, nó sẽ là thành phần cài đặt cho View và gửi và nhận thông tin từ ViewModel.
ViewModel
Là nơi xử lí các logic hiển thị(presentation logic), nó là cầu nối giữa View và Model. ViewModel sẽ nhận yêu cầu từ View và lấy dữ liệu từ model về xử lí sau đó trả lại cho View thứ mà nó cần để hiển thị lên màn hình cho người dùng.
Trong khi MVC thì Controller, MVP thì có Presenter làm trung gian giữa View và Model. Ở MVVM thì ViewModel cũng tương tự, nó là thành phần trung gian giúp kết nối View với Model.
Ưu điểm của MVVM
Vì MVVM là mô hình nâng cấp của MVC, cho nên nó giúp app vẫn duy trì cấu trúc của mô hình MVC và bao gồm các ưu điểm của MVC
Giảm tải lượng code chứa trong View và View Controller.
Khi đó View và View Controller trở nên đơn giản hơn khi những logic. Ví dụ như logic về quy định cách hiển thị của dữ liệu, được chuyển hết sang ViewModel. Điều này khiến cho code trở nên dễ hiểu và dễ maintain hơn.
Sự liên lạc giữa các thành phần trong mô hình rõ ràng, khiến nó hoạt động tốt hơn với cơ chế binding dữ liệu.
Có thể thực hiện UnitTest lên tầng ViewModel.
Nhiệm vụ được chia đều cho các tầng
Nhược điểm của MVVM
Nhiều file nên source code lại nhiều thêm
Tương tác giữa các thành phần phức tạp hơn các mẫu kiến trúc khác như MVC, MVP vì vậy người mới khó tiếp cận và thực hiện hơn.
Rắc rối trong việc phản hồi lại yêu cầu hơn so với các mẫu kiến trúc khác
Đối với nhưng dự án nhỏ thì nó lại quá cồng kềnh để thực hiện
Tổng kết
MVVM là một mẫu kiến trúc rất tốt khi bạn triển khai những ứng dụng có kích thước vừa và lớn, nó hỗ trợ UnitTest khá hiệu quả. Trong MVVM thì nhiệm vụ được chia đều cho các tầng vì vậy sẽ không quá khó để quản lý source code. Mình hi vọng bài viết giúp các bạn có thể dễ dàng hơn khi chon mẫu kiến trúc cho các dự án mới. Chúc các bạn thành công!
Thời gian gần đây, như các bạn có thể thấy cứ bước chân ra khỏi nhà là thấy đâu đâu cũng có những ô mã QR. Từ việc đi đá bát phở cũng quét thanh toán QR, đăng nhập zalo hay telegram cũng có thể quét QR, hay thậm chí trà đá vỉa hè cũng có QR luôn…. Điều đó chứng tỏ mã QR đang dần được ứng dụng rất rộng rãi vào tất cả các lĩnh vực trong cuộc sống, thay thế cho giấy tờ truyền thống cũng như giúp cuộc sống trở nên tiện lợi hơn.
Tuy nhiên nếu chỉ có mỗi mã QR thì cũng không giúp ích được gì khi mà không có những thiết bị đọc và giải mã những chiếc QR vi diệu này. Và những thiết bị để đọc mã QR cũng chẳng đâu xa ngay chính trên chiếc smart phone mà lúc nào cũng theo các bạn 24/7. Việc tích hợp QR Scan vào các ứng dụng di động ngày nay như một tính năng không thể thiếu cũng như giúp ứng dụng trở nên đa nhiệm hơn
Thời gian vừa qua mình cũng có cơ duyên được trải qua một dự án về ngân hàng và phát triển tính năng QRPay, một tính năng mà như các bạn có thể thấy lúc nào cũng xuất hiện trên các ứng dụng Internet Banking cũng như các ví điện tử. Vậy nên ở bài viết này, mình xin chia sẻ các bạn cách tạo một ứng dụng Scan QR đơn giản và những framework liên quan trên iOS bằng ngôn ngữ lập trình Swift. Let’s go !
Phần 1: Tìm hiểu về mã QR
QR là viết tắt của Quick response có thể tạm dịch là mã phản hồi nhanh. Đây là dạng mã vạch có thể đọc được bởi một máy đọc chuyên dụng hoặc bằng smartphone có chức năng chụp ảnh kèm với ứng dụng cho phép quét mã. Mã QR còn có thể được gọi là Mã vạch ma trận (Matrix-barcode) hoặc Mã vạch 2 chiều (2D), là một dạng thông tin đã được mã hóa và có thể hiển thị để máy quét mã có thể đọc được.
Mã QR là một mã vạch ma trận được phát triển bởi công ty Denso Wave vào năm 1994. Denso Wave là công ty con của Toyota. Mã QR gồm những chấm đen và các ô vuông trên nền trắng, nó thể chứa đa dạng các thông tin như URL, thông tin cá nhân, thời gian, địa điểm của một sự kiện nào đó, mô tả, giới thiệu một sản phẩm nào đó,…
Phần 2: AV Foundation Framework trong Swift
Như các bạn có thể thấy, để làm việc với Media trên các thiết bị iOS, Apple đã cung cấp rất nhiều các framework mạnh mẽ. Ở tầng cao nhất (high-level) là UIKit và AVKit, tầng thấp nhất (low-level) là CoreAudio, Core Media, Core Animation và ở giữa chính là nhân vật chính của chúng ta – AVFoundation
UIKit framework giúp dễ dàng kết hợp tính năng chụp ảnh tĩnh và quay video cơ bản vào ứng dụng. Cả Mac OS X và iOS đều có thể sử dụng thẻ HTML5 <audio> và <video> bên trong WebView hoặc UIWebView để phát nội dung âm thanh và video.
Ngoài ra còn có AVKit framework, giúp đơn giản hóa việc xây dựng các ứng dụng phát video hiện đại. Tất cả các framework này đều thuận tiện và dễ sử dụng và nên thường được dùng khi thêm chức năng phương tiện vào ứng dụng. Tuy nhiên, mặc dù các framework này rất tiện lợi nhưng chúng thường thiếu tính linh hoạt và khả năng kiểm soát cần thiết cho các ứng dụng nâng cao hơn.
Ở tầng thấp nhất, chúng ta có các low-level framework, cung cấp chức năng hỗ trợ và được sử dụng bởi tất cả các framework cấp cao hơn. Hầu hết chúng đều là các framework cấp độ thấp, cực kỳ mạnh mẽ và hiệu quả, nhưng rất phức tạp để tìm hiểu và sử dụng, đồng thời yêu cầu chúng ta phải hiểu rõ về cách phương tiện được xử lý ở cấp độ phần cứng.
Chính vì vậy, vị cứu tính của chúng ta – AV Foundation nằm ở giữa low-level và high-level framework. Mang trong mình sức mạnh cũng như hiệu năng của các low-level framework nhưng lại dễ tiếp cận, dễ đọc hơn cho các developer. Nó có thể làm việc trực tiếp với các framework cấp thấp như Core Media và Core Audio và hoạt động với các high-level framework như Media Player và Assets Library. Như vậy chúng ta có thể đủ thấy sức mạnh của AVF mạnh như nào phải k ạ ^^
AV Foundation là framework đầy đủ tính năng để làm việc với phương tiện nghe nhìn trên iOS, macOS, watchOS và tvOS. Sử dụng AV Foundation, chúng ta có thể dễ dàng phát, tạo và chỉnh sửa phim QuickTime và các tệp MPEG-4, HLS streams và xây dựng chức năng truyền thông mạnh mẽ vào ứng dụng.
Với AV Foundation, chúng ta có thể tạo ra các ứng dụng liên quan đến chụp ảnh và quay video, phát nhạc,… nói chúng tất cả những thứ liên quan đến cụm từ Media trên mobile và ngoài ra nó còn kèm theo rất nhiều thứ hay ho như điều khiển đèn flash phía trước và phía sau, âm thanh cho video…vv.
Như vậy có thể thấy AV Foundation framework khá lớn nên trong bài viết này, mình chỉ tìm hiểu một ứng dụng nhỏ của framework này đó là sử dụng camera trên thiết bị của Apple để đọc mã QR Code. Và chi tiết cách xây dựng ở phần 3 dưới đây.
Phần 3: Xây dựng ứng dụng QR Scan đơn giản bằng AV Foundation framework trên Swift
Đầu tiên chúng ta sẽ tạo một class QRScanCustomView như sau:
Để có thể sử dụng thư viện, chúng ta phải import thư viện “import AVFoundation” và khởi tạo “captureSession” để quản lý session capture. Có thể hiểu đơn giản rằng captureSession giúp quản lý việc sử dụng camera của chúng ta.
Ngoài ra, vì ứng dụng của chúng ta sử dụng camera để capture mã QR nên phải khai báo Camera Usage Description trong Info.plist. Nếu không khai báo ứng dụng của chúng ta sẽ không thể chạy vì vi phạm policy của Apple
Tiếp theo chúng ta sẽ khởi tạo AVCaptureVideoPreviewLayer:
public override var layer: AVCaptureVideoPreviewLayer {
if let layer = super.layer as? AVCaptureVideoPreviewLayer {
return layer
}
return AVCaptureVideoPreviewLayer()
}
Layer này chính là một layer hiển thị lên màn hình có vai trò thực hiện việc sử dụng camera để scan mã QR
Tạo ra các func để quản lý start, stop capture session:
Ở func commonInit(), chúng ta sẽ tạo khởi tạo videoCaptureDevice để sử dụng kiểu capture video sử dụng camera của chúng ta và add vào kiểu input của captureSession, ở đây có thể hiểu là chúng ta đang khởi tạo đầu vào cho ứng dụng của chúng ta sử dụng camera để scan QR
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
return
}
let videoInput: AVCaptureDeviceInput
do {
videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
} catch {
delegate?.onCameraAccessDenied()
return
}
if captureSession?.canAddInput(videoInput) ?? false {
captureSession?.addInput(videoInput)
} else {
scanningDidFail()
return
}
Sau khi thực hiện khởi tạo đầu vào, chúng ta sẽ khởi tạo đầu ra cho captureSession qua class AVCaptureMetadataOutput, ở đây mình đã custom một khoảng trắng giữa màn hình để tạo vùng scan bằng thuộc tính “rectOfInterest”. Nếu không gắn thuộc tính này, mặc định cả màn hình capture của chúng ra sẽ là vùng scan:
let metadataOutput = AVCaptureMetadataOutput()
if captureSession?.canAddOutput(metadataOutput) ?? false {
captureSession?.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
metadataOutput.metadataObjectTypes = [.qr]
} else {
scanningDidFail()
return
}
layer.session = captureSession
layer.videoGravity = .resizeAspectFill
layer.frame = UIScreen.main.bounds
captureSession?.startRunning()
// MARK: Set scan Area
let rectView = layer.metadataOutputRectConverted(fromLayerRect: rectForScannerRange())
metadataOutput.rectOfInterest = rectView
Để hứng được output của captureSession. Chúng ta sẽ kế thừa delegate của AVCaptureMetadataOutput. Delegate này sẽ giúp chúng ta get được String decode được từ mã QR:
extension QRScanCustomView: AVCaptureMetadataOutputObjectsDelegate {
public func metadataOutput(_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection) {
stopScanning()
if let metadataObject = metadataObjects.first {
guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else {
return
}
guard let stringValue = readableObject.stringValue else {
return
}
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
scanSuccess(code: stringValue)
}
}
}
Ngoài ra mình còn tạo ra các func để custom màn hình và tạo animation quét QR để trông ứng dụng của chúng ta trông đẹp mắt hơn. Các bạn có thể tham khảo ở phần source đầy đủ bên trên.
Về cơ bản chỉ cần khởi tạo đủ AVCaptureSession, tạo ra input và output cho AVCaptureSession và kế thừa delegate để hứng output là chúng ta có thể tạo ra một ứng dụng scan QR đơn giản.
Bài viết của mình vẫn đang trong quá trình update để đầy đủ và chi tiết hơn, nếu có góp ý gì mọi người có thể comment bên dưới để mình bổ sung và cải thiện thêm nhé.
Cảm ơn mọi người đã quan tâm đến bài viết của mình. Chúc mọi người có một ngày làm việc hiệu quả !