Month: December 2022

  • Annotation in Android Hilt

    Annotation in Android Hilt

    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?

    1. 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.

    1. 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.

    1. 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

  • Annotation in Android Hilt

    Annotation in Android Hilt

    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?

    1. 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.

    1. 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 {

    @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()
    }
    

    } </pre>

    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.

    1. 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

  • Architecture Pattern: VIPER trong iOS

    Architecture Pattern: VIPER trong iOS

    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úc các bạn thành công!

  • Android Bluetooth Low Energy (BLE) – Part 3

    Android Bluetooth Low Energy (BLE) – Part 3

    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

    1. 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à

      image

      image

      image

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

    2. 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ó. 

      image

      image

      image  

    3. 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 outputCharacteristicinputCharacteristic tương ứng với việc Write và Read. 2 thông số outputCharacteristicinputCharacteristic 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.

      image

    • Đâ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.

      image

    • 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.

      image

    • 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.

      image  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.

  • Architecture Pattern: MVVM trong iOS

    Architecture Pattern: MVVM trong iOS

    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!

  • AVFoundation trên Swift và ứng dụng để xây dựng tính năng QR Scan

    AVFoundation trên Swift và ứng dụng để xây dựng tính năng QR Scan

    Xin chào tất cả các bạn

    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:

    import Foundation
    import UIKit
    import AVFoundation
    
    protocol QRScanCustomViewDelegate: AnyObject {
        func onDecodeFailed()
        func onDecodeSuccess(_ decodeString: String)
        func onCameraAccessDenied()
    }
    
    public class QRScanCustomView: UIView {
        weak var delegate: QRScanCustomViewDelegate?
        private var captureSession: AVCaptureSession?
        private var scanAreaWidth: CGFloat = 300.0
        private var scanAreaHeight: CGFloat = 300.0
        private var scanAreaXpos: CGFloat = 0.0
        private var scanAreaYpos: CGFloat = 0.0
        private var scanAreaCornerRadius: CGFloat = 8.0
        private var scanViewBackgroundColor = UIColor.black.withAlphaComponent(0.9)
        private var isAnimationFromTop: Bool = true
        private var animationTimer = Timer()
        private var animationView = UIView()
        private let gradientBackgroundView = UIView()
        private var gradientView = UIView()
        private let animationContainerView = UIView()
        
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        
        public override var layer: AVCaptureVideoPreviewLayer {
            if let layer = super.layer as? AVCaptureVideoPreviewLayer {
                return layer
            }
            return AVCaptureVideoPreviewLayer()
        }
    }
    
    extension QRScanCustomView {
        func startScanning() {
            captureSession?.startRunning()
        }
        
        func stopScanning() {
            captureSession?.stopRunning()
        }
        
        func startAnimation() {
            runAnimation()
            animationTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(runAnimation), userInfo: nil, repeats: true)
        }
        
        func stopAnimation() {
            animationTimer.invalidate()
        }
        
        func setSizeOfScanArea(width: CGFloat, height: CGFloat) {
            scanAreaWidth = width
            scanAreaHeight = height
        }
        
        private func commonInit() {
            self.clipsToBounds = true
            captureSession = AVCaptureSession()
            
            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
            }
            
            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 AVAILABLE RANGE TO SCAN
            let rectView = layer.metadataOutputRectConverted(fromLayerRect: rectForScannerRange())
            metadataOutput.rectOfInterest = rectView
        }
        
        // MARK: Get rect of scan range
        private func rectForScannerRange() -> CGRect {
            scanAreaXpos = (UIScreen.main.bounds.size.width - scanAreaWidth) / 2
            scanAreaYpos = (UIScreen.main.bounds.size.height - scanAreaHeight) / 2
            let newRect = CGRect(x: scanAreaXpos, y: scanAreaYpos, width: scanAreaWidth, height: scanAreaHeight)
            return newRect
        }
        // MARK: Add view with clear scanner range
        func addQRScannerOverlayView(view: UIView) {
            view.addSubview(overlayView())
            view.layer.addSublayer(createCornerFrame(with: rectForScannerRange()))
            // Set authorize here to run it after qrScannerDelegate was declared
            setupScanAnimationView(view: view)
        }
        // MARK: Create scan range
        private func overlayView() -> UIView {
            let overlayView = UIView(frame: frame)
            overlayView.backgroundColor = scanViewBackgroundColor
            let path = CGMutablePath()
            path.addRoundedRect(in: rectForScannerRange(), cornerWidth: scanAreaCornerRadius, cornerHeight: scanAreaCornerRadius)
            path.closeSubpath()
            path.addRect(CGRect(origin: .zero, size: overlayView.frame.size))
            
            let maskLayer = CAShapeLayer()
            maskLayer.backgroundColor = UIColor.black.cgColor
            maskLayer.path = path
            maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
            overlayView.layer.mask = maskLayer
            overlayView.clipsToBounds = true
            
            return overlayView
        }
        // MARK: Draw focus corner frame
        private func createCornerFrame(with rect: CGRect) -> CAShapeLayer {
            let length: CGFloat = 24.0
            let radius: CGFloat = 8.0
            let offset: CGFloat = 8.0
            let lineWidth: CGFloat = 4.0
            let distance: CGFloat = rect.size.width - offset * 2 - length * 2 - radius * 2
            
            let path = CGMutablePath()
            
            var start = CGPoint(x: rect.origin.x + offset, y: rect.origin.y + offset + radius + length)
            var end = CGPoint(x: start.x + radius + length, y: start.y - length - radius)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x, y: start.y - length))
            path.addArc(tangent1End: CGPoint(x: start.x, y: start.y - length - radius),
                        tangent2End: CGPoint(x: start.x + radius, y: start.y - length - radius),
                        radius: radius)
            path.addLine(to: end)
            
            start = CGPoint(x: end.x + distance, y: end.y)
            end = CGPoint(x: start.x + length + radius, y: start.y + radius + length)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x + length, y: start.y))
            path.addArc(tangent1End: CGPoint(x: start.x + length + radius, y: start.y),
                        tangent2End: CGPoint(x: start.x + length + radius, y: start.y + radius),
                        radius: radius)
            path.addLine(to: end)
            
            start = CGPoint(x: end.x, y: end.y + distance)
            end = CGPoint(x: start.x - radius - length, y: start.y + length + radius)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x, y: start.y + length))
            path.addArc(tangent1End: CGPoint(x: start.x, y: start.y + length + radius),
                        tangent2End: CGPoint(x: start.x - radius, y: start.y + length + radius),
                        radius: radius)
            path.addLine(to: end)
            
            start = CGPoint(x: end.x - distance, y: end.y)
            end = CGPoint(x: start.x - length - radius, y: start.y - radius - length)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x - length, y: start.y))
            path.addArc(tangent1End: CGPoint(x: start.x - length - radius, y: start.y),
                        tangent2End: CGPoint(x: start.x - length - radius, y: start.y - radius),
                        radius: radius)
            path.addLine(to: end)
            
            let shape = CAShapeLayer()
            shape.path = path
            shape.strokeColor = UIColor.orange.cgColor
            shape.lineWidth = lineWidth
            shape.fillColor = UIColor.clear.cgColor
            shape.lineCap = .round
            return shape
        }
        // MARK: Create scan animation view, size of inputView = size of QRScanView
        private func setupScanAnimationView(view: UIView) {
            animationContainerView.backgroundColor = .clear
            animationContainerView.frame = rectForScannerRange()
            view.addSubview(animationContainerView)
            
            animationView.frame = CGRect(x: animationContainerView.bounds.minX - 8,
                                         y: animationContainerView.bounds.minY,
                                         width: animationContainerView.bounds.width + 16,
                                         height: 3.0)
            animationView.layer.cornerRadius = animationView.frame.height / 2
            animationView.backgroundColor = UIColor.orange
            animationContainerView.addSubview(animationView)
            animationContainerView.bringSubviewToFront(animationView)
            
            gradientBackgroundView.frame = animationContainerView.bounds
            gradientBackgroundView.backgroundColor = .clear
            animationContainerView.addSubview(gradientBackgroundView)
            animationContainerView.bringSubviewToFront(gradientBackgroundView)
            gradientBackgroundView.clipsToBounds = true
        }
        
        @objc
        private func runAnimation() {
            animationView.clipsToBounds = false
            if isAnimationFromTop {
                for subView in gradientBackgroundView.subviews {
                    subView.removeFromSuperview()
                }
                gradientBackgroundView.addSubview(createGradientViewEffect(rect: CGRect(x: 0,
                                                                                        y: -20,
                                                                                        width: gradientBackgroundView.bounds.width,
                                                                                        height: 20),
                                                                           isRevertColor: false))
                UIView.animate(withDuration: 2.0, animations: {
                    self.animationView.transform = CGAffineTransform(translationX: 0,
                                                                     y: self.animationContainerView.frame.size.height)
                    self.gradientView.transform = CGAffineTransform(translationX: 0,
                                                                    y: self.animationContainerView.frame.size.height)
                    Timer.scheduledTimer(timeInterval: 1.6, target: self, selector: #selector(self.hideGradientView), userInfo: nil, repeats: false)
                })
                
            } else {
                for subView in gradientBackgroundView.subviews {
                    subView.removeFromSuperview()
                }
                gradientBackgroundView.addSubview(createGradientViewEffect(rect: CGRect(x: 0,
                                                                                        y: gradientBackgroundView.bounds.height,
                                                                                        width: gradientBackgroundView.bounds.width,
                                                                                        height: 20),
                                                                           isRevertColor: true))
                UIView.animate(withDuration: 2.0, animations: {
                    self.animationView.transform = CGAffineTransform(translationX: 0,
                                                                     y: 0)
                    self.gradientView.transform = CGAffineTransform(translationX: 0,
                                                                    y: 0)
                    Timer.scheduledTimer(timeInterval: 1.6, target: self, selector: #selector(self.hideGradientView), userInfo: nil, repeats: false)
                })
            }
            isAnimationFromTop = !isAnimationFromTop
        }
        
        @objc
        private func hideGradientView() {
            UIView.animate(withDuration: 0.4, animations: {
                self.gradientView.alpha = 0
            })
        }
        
        private func createGradientViewEffect(rect: CGRect, isRevertColor: Bool) -> UIView {
            gradientView.layer.sublayers?.removeAll()
            gradientView.frame = rect
            let gradientLayer: CAGradientLayer = CAGradientLayer()
            gradientLayer.frame = gradientView.bounds
            let firstColor = UIColor.orange.withAlphaComponent(0.01).cgColor
            let secondColor = UIColor.orange.withAlphaComponent(1).cgColor
            gradientLayer.colors = isRevertColor ? [secondColor, firstColor] : [firstColor, secondColor]
            gradientLayer.locations = [0.0, 1.0]
            gradientView.layer.insertSublayer(gradientLayer, at: 0)
            gradientView.alpha = 0.6
            return gradientView
        }
        
        func scanningDidFail() {
            delegate?.onDecodeFailed()
            captureSession = nil
        }
        
        func scanSuccess(code: String) {
            delegate?.onDecodeSuccess(code)
        }
    }
    
    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)
            }
        }
    }

    Để 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 startScanning() {
            captureSession?.startRunning()
        }
        
        func stopScanning() {
            captureSession?.stopRunning()
        }

    Ở 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ả !

  • Android Bluetooth Low Energy (BLE) – Part 2

    Android Bluetooth Low Energy (BLE) – Part 2

    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ề Connection, Disconnect BLE

    1. Connection

    Sau khi bạn đã Scan thấy thiết bị của mình bằng cách quét tìm thiết bị, bạn phải kết nối với thiết bị đó bằng cách gọi connectGatt(). Nó trả về một đối tượng BluetoothGatt mà sau đó bạn sẽ sử dụng cho tất cả các hoạt động liên quan đến GATT như đọc ghi dữ liệu thông qua BLE (Read and Write). Các bạn chú ý, có 2 version của phương thức connectGatt()

    image

    Hay cụ thể hơn

    image

    Nào chúng ta cùng đi vào sâu hơn một số đối số trong hàm Connect này.

    1. “autoConnect” parameter: Đối số chỉ ra rằng bạn có muốn kết nối ngay lập tức hay không.
    • Trong trường hợp này mình set giá trị là “False” tức là ‘connect immediately’. Theo mình tìm hiểu, Android sẽ cố gắng connect trong 30s, nếu không thành công đẩy ra lỗi Timeout (Thường với mã lỗi là 133). Có một lưu ý là bạn chỉ có thể tạo một kết nối tại một thời điểm bằng cách sử dụng false, vì Android sẽ hủy mọi kết nối khác có giá trị false, nếu có.
    • Vậy nếu mình set là “True” thì sao nhỉ? Android sẽ kết nối bất cứ khi nào nó nhìn thấy thiết bị và cuộc gọi này sẽ không bao giờ Timeout. Theo mình tìm hiểu, bên trong stack sẽ tự quét và khi nhìn thấy thiết bị, nó sẽ kết nối với thiết bị đó. Bạn có thể cân nhắc đến bài toán Reconnect device trong trường hợp này. Bạn chỉ cần tạo một đối tượng BluetoothDevice và gọi connectGatt với value là “True” tương ứng với đối số autoConnect.
    1. “transport” parameter: Đưa ra các mode cho việc connect giữa Device và ứng dụng. Nếu trong trường hợp của việc kết nối BLE thì các bạn nên sử dụng value TRANSPORT_LE để tránh những lỗi không mong muốn trong quá trình connect. Có một giá trị mà các bạn cũng cần lưu ý là TRANSPORT_AUTO, hỗ trợ cho việc connection giữa điện thoại và thiết bị hỗ trợ kết nối cả BLE và Bluetooth classic.

    Sau khi mình gọi việc connect, System sẽ đẩy kết quả và các callback tương ứng thông qua BluetoothGattCallback

    image

    2. Disconnection

    Mình có tìm hiểu có rất nhiều source mẫu handle việc gọi disconnect BLE sai cách. Sau đây là các đúng mà mình tìm hiểu được nếu bạn muốn Disconnect BLE

    • Call disconnect()
    • Đợi Callback onConnectionStateChange với trạng thái Disconnect
    • Call close()
    • Dispose gatt object

    image

    image

    Lệnh disconnect() thực sự sẽ thực hiện ngắt kết nối và cũng sẽ cập nhật trạng thái kết nối nội bộ của ngăn xếp Bluetooth. Sau đó, nó sẽ kích hoạt gọi lại onConnectionStateChange để thông báo cho bạn rằng trạng thái mới hiện đã bị ‘ngắt kết nối’.

    Lệnh gọi close() sẽ hủy đăng ký BluetoothGattCallback của bạn và giải phóng ‘client interface’.

    Cuối cùng, việc xử lý đối tượng BluetoothGatt sẽ giải phóng các tài nguyên khác liên quan đến kết nối.

    Ở biết tiếp theo mình sẽ tiếp tục với việc Discovery Service BLE sau khi Connection thành công và Transfer BLE Data của BLE. Hẹn các bạn trong bài viết sắp tới.

  • MVP Architecture Pattern và biến thể MVP-C

    MVP Architecture Pattern và biến thể MVP-C

    Là một Developer, chắc hẳn các bạn đã trải qua nhiều dự án khác nhau. Thông thường khi bạn càng làm nhiều dự án bạn càng có nhiều cơ hội tiếp cận đến các loại Architecture pattern khác nhau như MVC, MVP, MVVM, VIPPER, … 

    Sau khi chinh chiến ở các dự án lớn nhỏ khác nhau mình cũng tích luỹ được một chút kiến thức về MVP Architecture pattern, vì vậy mình muốn viết một bài để chia sẻ một số kiến thức nho nhỏ mà mình đã học được về MVP cho những bạn chưa có cơ hội làm việc với MVP Architecture pattern.

    Lịch sử hình thành và phát triển

    MVP là viết tắt của Model View Presenter, nó bắt nguồn từ đầu những năm 1990 tại Talligent, một liên doanh của Apple, IBM và Hewlett-Packard. MVP là mô hình lập trình cơ bản để phát triển ứng dụng trong môi trường CommonPoint dựa trên C++ của Taligent. Sau này nó đã được Taligent chuyển sang Java.

    Đến năm 1998 thì Taligent giải thể, Andy Bower và Blair McFlashan của Dolphin Samlltalk đã điều chỉnh MVP để tạo cơ sở cho Smalltalk của họ.

    Đến năm 2006 thì Microsoft cũng bắt đầu kết hợp MVP vào tài liệu và ví dụ về lập trình giao diện người dùng trong .NET Framework.

    Đến nay thì MVP được sử dụng khá là rộng rãi vì những lợi ích mà nó đem lại cho các lập trình viên. Ngoài ra MVP cũng có rất nhiều biến thể để cải thiện những nhược điểm của nó.

    MVP là gì?

    MVP là một mẫu kiến trúc giao diện người dùng(user interface architecture pattern) được thiết kế để tạo điều kiện thuận lợi cho Automated Unit Testing(Chạy Unit Test tự động) và cải thiện việc phân tách các thành phần trong trình bày logic(presentation logic).

    MVP sinh ra dựa trên kiến trúc MVC, nó hướng tới mục tiêu cải thiện kiến trúc MVC.

    MVP được thể hiện băng hình ảnh sau:

    Model: là một interface xác định dữ liệu được hiển thị hoặc dữ liệu này được thực hiện trong giao diện người dùng.

    View: là một interface thụ động dùng để hiện thị dữ liệu của Model và định hướng các lệnh người dùng (events) tới Presenter để Presenter hành động dựa trên các dữ liệu đó.

    Presenter: hành động theo Model và View. Presenter lấy dữ liệu từ kho lưu trữ (Model), sau đó định dạng dữ liệu và hiển thị lên View.

    Ưu điểm của MVP

    Như đã nói ở trên do MVP được xây dựng dựa trên kiến trúc MVC nên nó sẽ có các ưu điểm tương tự như MVC. Các bạn có thể xem thêm về MVC ở bài viết sau: iOS Architecture Patterns: Cocoa MVC

    Mục đích cao cả của MVP sinh ra là để cải thiện những nhược điểm của kiến trúc MVC vì vậy nó giúp giảm tải lượng lớn logic nằm ở tầng Model so với mô hình MVC

    Kiến trúc MVP có tầng Presenter chuyên để xử lý các logic hiển thị, nó là thành phần trung gian tương tác với View và Model qua interface nên nó có thể viết Unit testing một cách dễ dàng.

    Nhược điểm của MVP

    Cũng như MVC, kiến trúc MVP cũng có những nhược điểm. Nhược điểm lớn nhất của MVP là càng về sau Presenter của MVP sẽ càng phình to nếu logic được thêm mới. Khí đó bạn sẽ rất khó để chia nhỏ khi presenter quá lớn.

    Biến thể MVP-C trong iOS

    Khái niệm Coordinator lần đầu tiên được đưa ra bởi Khanlou vào năm 2015, nó là một giải pháp để xử logic luồng cho View Controller.

    Dựa trên điều này kiến trúc MVP-C được ra đời với C là Coordinator làm nhiệm vụ xử lý luồng cho ứng dụng và các tầng cũ là Model, View và Presenter vẫn giống như MVP được mô tả ở trên.

    Ưu điểm của MVP-C

    View controller có thể tập trung vào mục tiêu chính của chúng. Giúp phân chia rõ ràng vai trò của View.

    Giúp giảm tải các logic trên các tầng khác, ta có thể đưa một số logic như phân luồng di chuyển màn hình từ presenter vào coordinator để giúp presenter đỡ trở nên cồng kềnh khi có quá nhiều logic. Nó đã cải thiện được nhược điểm của kiến trúc MVP truyền thống.

    Ngoài ra Coordinator cũng được ứng dụng vào các kiến trúc khác như MVC để tạo ra MVC-C và MVVM tạo ra MVVM-C.

    Tổng kết

    Đó là những kiến thức mà mình đã tích luỹ được khi làm việc với các dự án được thực hiện theo kiến trúc MVP. Mình hi vọng nó sẽ giúp ích cho các bạn khi cần thiết.

  • iOS Architecture Patterns: Cocoa MVC

    iOS Architecture Patterns: Cocoa MVC

    Là một iOS developer chắc hẳn các bạn không lạ gì với Cocoa MVC. Nó được coi là một trong những architecture pattern để phát triển ứng dụng iOS phổ biến nhất. Nó rất dễ sử dụng và được chính Apple khuyên dùng. iOS, MacOS và watchOS đều sử dụng cấu trúc này làm kiến ​​trúc mặc định để phát triển. Tuy được rất phổ biến và được Apple khuyên dùng nhưng nó cũng có những ưu điểm và nhược điểm, vì vậy bài viết này mình sẽ giới thiệu và giải thích cho các bạn về Cocoa MVC, ưu điểm, nhược điểm và khi nào nên chọn Cocoa MVC sử dụng cho ứng dụng của bạn.

    Giải thích về Cocoa MVC

    Cocoa MVC là viết tắt của Cocoa Model View Controller. Cocoa MVC gán các đối tượng trong ứng dụng iOS bằng một trong 3 vai trò sau: Model, View hoặc Controller. Mẫu kiến trúc này không chỉ xác định vai trò của các đối tượng trong ứng dụng mà nó còn xác định cả cách các đối tượng giao tiếp với nhau. Ba loại đối tượng này được phân tách khỏi các loại khác bằng các ranh giới trừu tượng và giao tiếp với các đối tượng thuộc các loại khác thông qua các ranh giới đó. Tập hợp các đối tượng của một loại MVC nhất định trong một ứng dụng đôi khi được gọi là một Layer, ví dụ: Layer model.

    Cocoa MVC

    Model

    Những đối tượng được gán với vai trò model trong mẫu kiến trúc Cocoa MVC sẽ làm nhiệm vụ đóng gói dữ liệu cụ thể cho một ứng dụng và xác định logic và tính toán để thao tác và xử lý dữ liệu đó. Những đối tượng này có thể có một hoặc nhiều mối quan hệ với các đối tượng mô hình khác và do đó, đôi khi lớp Model của một ứng dụng thực sự là một hoặc nhiều đối tượng. Phần lớn dữ liệu đều nằm ở Model sau khi nó được tải vào ứng dụng bằng các cách khác nhau(API, Files, …). Vì Model đại diện cho kiến thức và chuyên môn cho một vấn đề cụ thể nên nó có thể được tái sử dụng khi có các trường hợp tương tự. Các Model sẽ không có liên kết trực tiếp với View và View cũng không được trực tiếp sửa dữ liệu của Model mà nó sẽ phải thực hiện thông qua Controller.

    Giao tiếp: Các hành động của người dùng trên View sẽ gọi đến Controller, khi này Controller sẽ gọi đến Model tương ứng để thực hiện cập nhật dữ liệu cho Model. Khi Model thay đổi (ví dụ: dữ liệu mới được nhận qua kết nối mạng), nó sẽ thông báo cho Controller, lúc này Controller sẽ cập nhật các Views thích hợp.

    View

    View là một đối tượng trong ứng dụng mà người dùng có thể nhìn thấy. Một đối tượng View biết cách tự vẽ và có thể phản hồi các hành động của người dùng. Mục đích chính của View là hiển thị dữ liệu từ Model của ứng dụng và cho phép chỉnh sửa dữ liệu đó. Mặc dù vậy, View thường được tách rời khỏi Model trong ứng dụng MVC.

    Bởi vì bạn thường sử dụng lại và cấu hình lại chúng, nên View cung cấp tính nhất quán giữa các ứng dụng. Cả UIKit và AppKit framework đều cung cấp các bộ sưu tập View classes và Trình tạo giao diện(Interface Builder) cung cấp hàng tá view objects trong Thư viện của nó.

    Giao tiếp: View tìm hiểu về các thay đổi trong dữ liệu của Model thông qua Controller của ứng dụng và truyền đạt các thay đổi do người dùng. ví dụ: văn bản được nhập vào TextField thông qua Controller đến Model của ứng dụng.

    Controller

    Controller vai trò trung gian giữa một hoặc nhiều View của ứng dụng và một hoặc nhiều Model của nó. Do đó, cController là một đường dẫn mà qua đó View tìm hiểu về những thay đổi trong Model và ngược lại. Controller cũng có thể thực hiện các tác vụ thiết lập và điều phối cho một ứng dụng và quản lý vòng đời của các đối tượng khác.

    Giao tiếp: Controller diễn giải các hành động của người dùng được thực hiện trong View và truyền dữ liệu mới hoặc dữ liệu đã thay đổi tới Model. Khi Model thay đổi, Controller sẽ giao tiếp dữ liệu Model mới đó với View để chúng có thể hiển thị nó.

    Ưu điểm của Cocoa MVC

    Cocoa MVC có khá nhiều ưu điểm như sau:

    1. Dễ hiểu và dễ sử dụng, vì vậy ai cũng có thể làm việc với nó một cách dễ dàng kể cả người mới
    2. Được Apple khuyên dùng, vì vậy nó rất phổ biến khi gặp vấn đề sẽ dễ xử lí.
    3. Giúp developer tách source của họ ra làm các đối tượng khác nhau với 3 vai trò riêng biệt. Khi có lỗi xảy ra chúng ta sẽ khoanh vùng được nơi xảy ra lỗi.
    4. Tránh việc phải tạo một file quá dài, ảnh hưởng tới việc maintain ứng dụng
    5. Có thể tái sử dụng và mở rộng

    Nhược điểm của Cocoa MVC

    1. Phân chia nhiệm vụ giữa các vai trò không đồng đều, View chỉ làm nhiệm vụ hiển thị và nhận action từ người dùng, Controller thì chỉ là trung gian điều hướng giữa View-Model, trong khi đó Model phải làm quá nhiều việc từ lưu dữ liệu, xử lí dữ liệu, thực hiện Business Logic của ứng dụng, … Đó là lí do Model còn hay được gọi với cái tên khác là Massive
    2. Không hỗ trợ tốt cho UnitTest bởi View phải phụ thuộc vào cả Controller và Model. View sẽ không thể xử lý được vấn đề gì bởi View không thể nhận yêu cầu và cũng không có dữ liệu để hiển thị. Để tiến hành UnitTest trên View, chúng ta cần giả lập cả Controller và Model.
    3. Đối với các ứng dụng quy mô lớn, quy trình xử lý nghiệp vụ có tính phức tạp cao, lượng dữ liệu lớn thì mô hình MVC trở nên rất cồng kềnh và khó để thực hiện.

    Khi nào bạn nên sử dụng Cocoa MVC

    Như đã phân tích ở trên, các bạn cũng đã nhìn thấy cách vận hành của mô hình này, các ưu điểm và nhược điểm của nó. Vậy khi nào thì chúng ta nên sử dụng Cocoa MVC cho ứng dụng của mình. Theo mình thì sẽ các tiêu chí như sau:

    1. Khi bạn không biết về các architecture patterns khác tốt hơn, bạn chỉ hiểu rõ về MVC hoặc bạn và member trong team là người mới thì nên chọn Cocoa MVC cho dự án của mình.
    2. Dự án của bạn có kích thước vừa và nhỏ, số iOS developer có số lượng ít, có ít hiểu biết về các architecture patterns khác.

    Tổng kết

    Qua bài viết trên mình đã giới thiệu cho các bạn về một architecture pattern rất phổ biến trong lập trình ứng dụng iOS. Ngoài ra cũng giúp các bạn hiểu rõ về cách hoạt động cũng như ưu điểm và nhược điểm của Cocoa MVC. Mình hi vọng bài viết này sẽ giúp các bạn có thể hiểu rõ hơn về Cocoa MVC cũng như có những lựa chọn tốt nhất cho từng dự án mà sắp tới các bạn phát triển.

  • Mutating func trong Struct và enum – Swift

    Mutating func trong Struct và enum – Swift

    Như mọi người đã biết, struct và enum trong Swift là value types(kiểu giá trị), mặc định thì các thuộc tính của kiểu giá trị thì không thể được sửa đổi ở bên trong các phương thức thể hiện của nó(instance methods).

    Tuy nhiên nếu chúng ta cần phải chỉnh sửa các thuộc tính của struct hoặc enum trong một phương thức cụ thể, thì chúng ta sẽ đặt mutating trước các func, nó sẽ giúp các func của bạn có thể thay đổi được các thuộc tính bên trong func và khi kết thúc func giá trị sẽ được nghi lại vào các thuộc tính của struct ban đầu. Phương thức này cũng có thể gán lại một instance mới cho thuộc tính self của nó và nó sẽ được thay thế khi phương thức kết thúc.

    Mutating trong Struct

    Trong ví dụ này mình sẽ tạo một Struct có tên là Counter và tạo ra một func lấy giá trị của thuộc tính count trong Counter như sau:

    struct Counter {
        private var count: Int = 0
        
        func getCount() -> Int {
            count
        }
    }

    Đây là một ví dụ bình thường về struct, hàm getCount() ở đây không thực hiện thay đổi giá trị của struct Counter mà nó chỉ lấy giá trị của thuộc tính count theo cách thông thường.

    Vậy khi chúng ta muốn viết một hàm increase() để tăng giá trị count thông thường chúng ta sẽ viết như sau:

    Nếu là class thì sẽ không vấn đề gì vì class là reference type. Ở trường họp này do chúng ta đang viết một func chỉnh sửa thuộc tính count của struct Counter nên xCode sẽ báo lỗi rằng self ở đây là immutable(không thể thay đổi), như đã giải thích ở trên thì struct là value type nên mặc định sẽ không thể thay đổi được thuộc tính của nó trong các func của struct đó.

    func getCount() không bị báo lỗi vì func này không làm thay đổi thuộc tính trong struct.

    Để func increase() không bị báo lỗi chúng ta cần thêm mutating đằng trước func để xCode biết là func này có thể thay đổi được thuộc tính của struct:

    struct Counter {
        private var count: Int = 0
        
        func getCount() -> Int {
            count
        }
        
        mutating func increase() {
            count += 1
        }
    }
    
    var counter = Counter() // count = 0
    counter.increase() // count = 1

    Để có thể thay đổi được thuộc tính của instance counter thì chúng ta cần phải khai báo nó là var, vì func increase() sẽ thay đổi giá trị của counter vì vậy cần khai báo là var để mutating func có thể gán lại giá trị mới cho instance counter.

    Nếu chúng ta để là let Xcode sẽ thông báo lỗi không thể sử dụng mutating func trên giá trị không thể thay đổi, counter đang là một “let” constant. Do struct là value type nên nó là immutable có nghĩa là không thay đổi được, nếu chúng ta cố tình khai báo let count Xcode sẽ thông báo lỗi.

    Lỗi khi khai báo let counter để call mutating func

    NOTE: Để hiểu rõ hơn các bạn có thể xem thêm thông tin ở đây: Stored Properties of Constant Structure Instances

    Mutating func không chỉ thay đổi được thuộc tính của struct mà nó còn có thể thay đổi cả giá trị của chính instance (self)

    struct Counter {
        private var count: Int = 0
        
        func getCount() -> Int {
            count
        }
        
        mutating func increase() {
            count += 1
        }
        
        mutating func resetCounter() {
            self = Counter(count: 0)
        }
    }
    
    var counter = Counter() // count = 0
    counter.increase() // count = 1
    counter.resetCounter() // count = 0

    mutating func resetCounter() là một ví dụ, ở trong func này chúng ta thực hiện tạo ra một instance Counter mới với giá trị khởi tạo là 0 và gán lại cho chính instance gọi func này.

    Mutating func trong enum

    Tương tự như struct, enum cũng là value type và để thay đổi giá trị trong func chúng cũng cần phải sửa dụng mutating cho func đó.

    Để hiểu rõ hơn ta đi vào ví dụ sau:

    Chúng ta cần tạo ra một công tắc quạt với một tính năng là mỗi khi bấm nút thì sẽ làm thay đổi tốc độ quay của quạt một cách tuần tự và lặp đi lặp lại. Để làm theo yêu cầu chúng ta sẽ tạo enum như sau:

    enum FanStateSwitch {
        case off, low, high
        mutating func next() {
            switch self {
            case .off:
                self = .low
            case .low:
                self = .high
            case .high:
                self = .off
            }
        }
    }
    
    var fanSwitch = FanStateSwitch.off
    fanSwitch.next() // fanSwitch is low
    fanSwitch.next() // fanSwitch is high
    fanSwitch.next() // fanSwitch is off

    Tương tự như struct khi khởi tạo enum hãy nhớ khởi tạo nó với var thay vì let.

    Hi vọng bài viết sẽ giúp các bạn hiểu rõ hơn về mutating func và cách sử dụng, ứng dụng nó vào trong dự án.

    Chúc các bạn thành công!