Author: cuongnv83

  • Dependence Injection với Dagger trong Android

    Dependence Injection với Dagger trong Android

    Giới thiệu

    bài trước mình đã giới thiệu về việc tiêm phụ thuộc thủ công trong android. Việc chèn phụ thuộc thủ công hoặt service locators trong ứng dụng Android có thể có vấn đề tùy thuộc vào quy mô dự án của bạn. Bạn có thể giới hạn sự phức táp dự án của bạn khi nó tăng lên bằng cách sử dụng Dagger để quản lý các sự phụ thuộc.

    Dagger tự động tạo code mà bắt chước code bạn sẽ viết thủ công. Bời vì code được sinh tự động tại compile time. Dagger hilt được giới thiệu trong android jetpack nó giúp đơn giản hóa code DI hơn so với Dagger. Nhưng do nó dựa trên Dagger nên trong bài này mình sẽ giới thiệu sơ lược về Dagger.

    Lợi ích khi sử dụng Dagger

    Dagger giúp bạn từ viết mã tẻ nhạc và mã soạn sẵn dễ tạo ra lỗi bằng cách:

    • Sinh AppContainer code(application graph) mà bạn đã triển khai thủ công trong phân DI.
    • Tạo factories cho các class có sẵn trong application graph.
    • Quyết định xem có nên sử dụng lại một phụ thuộc hoặc tạo mới một instance mới thông qua việc sử dụng các scopes

    Dagger tư động làm tất cả điều này tại thời giơn build miễn là bạn khai báo các sự phụ thuộc của một class và chỉ định cách đáp ứng chúng bằng cách sử dụng các annotation. Dagger sinh ra code tương tự những gì bạn viết thủ công. Dagger tạo một graph của các đối tượng, cái mà nó có thể tham chiếu để tìm kiếm cách cung cấp một instance của một class. đối với mọi lớp trong graph Dagger tạo một factory-type class mà nó sử dụng trong nội bộ để lấy các instance của loại đó.

    Tại thời điểm xây dựng, Daggeer xem qua mã của bạn và:

    • Builds và Validates các biểu đồ phụ thuộc, đảm bảo rằng:

      • Mọi đối trượng phụ thuộc có thể được thỏa mãn, vì vậy không có runtime exception
      • Không có chu trình phụ thuộc nào tồn tại, vì vậy không có vòng lặp vô hạn.
    • Sinh các class mà được sử dụng tại runtime để tạo các đối tượng thực tế và các phụ thuộc của chúng.

    Một usecase đơn giản trong Dagger: Generating một factory

    Tạo một factory đơn giản cho class UserRepository hiển thị trong sơ đồ sau:

    Screenshot from 2021-09-30 23-41-28.png

    Định nghĩa UserRepository như sau:

    class UserRepository(
        private val localDataSource: UserLocalDataSource,
        private val remoteDataSource: UserRemoteDataSource
    ) { ... }
    

    Thêm một @Inject annotation vào contructor của UserRepository vì thế Dagger biết cách tạo một UserRepository:

    // @Inject lets Dagger know how to create instances of this object
    class UserRepository @Inject constructor(
        private val localDataSource: UserLocalDataSource,
        private val remoteDataSource: UserRemoteDataSource
    ) { ... }
    

    Trong đoạn mã ở trên, bạn đang nói với Dagger rằng:

    1. Cách để tạo một instance UserRepository bằng phương thức khởi tạo có annotate @Inject.
    2. Các phụ thuộc của nó là: UserLocalDataSourceUserRemoteDataSource.

    Bây giờ Dagger biết cách tạo một instance của UserRepository, nhưng nó không biết cách tạo các phụ thuộc của nó. Nếu bạn cũng chú thích các lớp khác, Dagger biết cách để tạo chúng:

    class UserLocalDataSource @Inject constructor() { ... }
    class UserRemoteDataSource @Inject constructor() { ... }
    

    Dagger component

    Dagger có thể tạo một graph các phụ thuộc trong dự án của bạn mà nó có thể sử dụng để tìm ra nơi mà nó nên lấy những phụ thuộc đó khi họ cần. Để Dagger làm được điều này, bạn cần tạo một interface và annotate nó với @Component. Dagger tạo một container khi bạn sẽ làm việc với việc tiêm phụ thuộc thủ công.

    Bên trong interface Component, bạn có thể định nghĩa các function mà trả về các instance của class mà bạn cần(ví dụ. UserRepository). @Component nói Dagger sinh ra một container với tất cả các phụ thuộc được yêu cầu để đáp ứng. Đây được gọi là Dagger component ; nó chứa một graph mà bao gồm các đối tượng mà Dagger biết cách để cung cấp và các phụ thuộc tương ứng của chúng.

    // @Component makes Dagger create a graph of dependencies
    @Component
    interface ApplicationGraph {
        // The return type  of functions inside the component interface is
        // what can be provided from the container
        fun repository(): UserRepository
    }
    

    Khi bạn build project, Dagger sinh ra một implementation của interface ApplicationGraphcho bạn là: DaggerApplicationGraph. với bộ xử lý annotation của nó, Dagger tạo một dependence graph mà bao gồm các quan hệ giữa ba class(UserRepository, UserLocalDataSource, UserRemoteDataSource) với chỉ một ertry point: getting một instance UserRepository. Bạn có thể sử dụng nó như sau:

    // Create an instance of the application graph
    val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
    // Grab an instance of UserRepository from the application graph
    val userRepository: UserRepository = applicationGraph.repository()
    

    Dagger tạo một new instance của UserRepository mỗi khi nó được yêu cầu.

    val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
    
    val userRepository: UserRepository = applicationGraph.repository()
    val userRepository2: UserRepository = applicationGraph.repository()
    
    assert(userRepository != userRepository2)
    

    Thi thoảng, bạn cần có một instance duy nhất của một dependency trong một container. Bạn có thể muốn điều này cho một vài lý do:

    1. Bạn muốn các loại khác mà có loại này làm một dependency để chia sẻ cùng một instance, như là nhiều đối tượng ViewModel trong luông đăng nhập sử dụng cùng LoginUserData.
    2. Một đối tượng là đắt giá để tạo và bạn không muốn tạo một new instance mỗi lần nó được khai báo như một phụ thuộc(cho ví dụ, một JSON parser).

    Trong ví dụ, bạn có thể muốn có một instance UserRepository duy nhất có sẵn trong graph để mỗi khi bạn yêu cầu một UserRepository, bạn luôn lấy cùng một instance. Điều này hữu ích trong ví dụ của bạn vì trong một ứng dụng thực tế có graph phức tạp hơn, bạn có thể có nhiều đối tượng ViewModel phụ thuộc vào UserRepository và bạn không muốn tạo một instance mới của UserLocalDataResourceUserRemoteDataResource mỗi lần UserRepository cần được cung cấp.

    Trong tiêm phụ thuộc thủ công, bạn đã làm điều này bằng cách pass sung một instance của UserRepository vào contructors của các classs ViewModel; nhưng trong Dagger, bởi vì bạn không viết thủ công, bạn nói cho Dagger biết răng bạn muốn sử dụng same instance. Điều này có thể hoàn thành với scope annotations.

    Scoping với Dagger

    Bạn có thể sử dụng scope annotations để giới hạn tồn tại của một object trong suốt thời gian tồn tại component của nó. Điều này có nghĩa là cùng instance của một phụ thuộc được sử dụng mỗi khi kiểu đó cần được cung cấp cho một class nhận nó làm phụ thuộc.

    Dể có một instance duy nhất của UserRepository khi bạn yêu cầu repository trong ApplicationGraph, sử dụng same scope annotation cho interface @ComponentUserRepository. Bạn có thể sử dụng annotation @Singleton cái mà đã đi kèm với gói javax.inject mà Dagger sử dụng.

    @Singleton
    @Component
    interface ApplicationGraph {
        fun repository(): UserRepository
    }
    
    // Scope this class to a component using @Singleton scope
    @Singleton
    class UserRepository @Inject constructor(
        private val localDataSource: UserLocalDataSource,
        private val remoteDataSource: UserRemoteDataSource
    ) { ... }
    

    Trong cả hai trường hợp, đối tượng được cung cấp cùng một phạm vi được sử dụng để chú thích giao diện @Component. Do đó, mỗi khi bạn gọi applicationGraph.repository(), bạn sẽ nhận được cùng một phiên bản của UserRepository.

    val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

    val userRepository: UserRepository = applicationGraph.repository()
    val userRepository2: UserRepository = applicationGraph.repository()
    
    assert(userRepository == userRepository2)
    

    Chú ý: Đối với các class như Activity hay fragment không thể inject qua contructor bời vì các class này là hệ thống tự gọi => sử dụng field inject:

    class LoginActivity: Activity() {
        @Inject lateinit var loginViewModel: LoginViewModel
    }
    

    Kết luận

    Dagger giúp việc tiêm phụ thuộc trở lên đơn giản hơn bằng cách sinh code tự động như chúng ta làm bằng tay, việc quản lý các phụ thuộc cũng dễ dàng hơn khi sử dụng Dagger. Bài viết này đã nếu một vài điều cơ bản về cách hoạt động của Dagger, bài viết sau mình sẽ giới thiệu về Dagger Hilt cái mà giúp cho việc tiêm phụ thuộc còn đơn giản hơn Dagger.

  • Dependency Injection trong Android

    Dependency Injection trong Android

    Giới thiệu

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

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

    Dependency injection là gì?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Kết luận

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

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

    Map, FlatMap,… trong Kotlin

    Giới thiệu

    Kotlin sinh ra extension function làm cho việc tuân thủ nguyên tắc open/close trở nên dễ dàng hơn. Với extension function ta không cần phải kế thừa từ một class khi muốn mở rộng chức năng.

    Có thể thấy extension function là thứ rất mạnh trong kotlin, cũng nhờ nó mà collections trong kotlin trở lên mạnh mẽ và làm cho việc lập trình đơn giản hơn rất nhiều.

    Bài viết này mình sẽ giới thiệu một số chức năng trong collections kotlin:

    1. filter
    2. map
    3. flatMap
    4. groupBy
    5. partition

    1. filter

    Chức năng này trả về một list mới gồm các phần tử thỏa mãn predicate.

    inline fun <T> Iterable<T>.filter(
        predicate: (T) -> Boolean
    ): List<T>
    

    Ví dụ trả về danh sách các số chẵn:

    listOf(1,2,3,4,5).filter { number->
        number%2 == 0
    }
    

    2. map

    Chức năng này giúp chuyển đổi một collection thành một list sau phép biến đổi hàm ánh xạ transform T -> R mà ta định nghĩa.

    inline fun <T, R> Iterable<T>.map(
        transform: (T) -> R
    ): List<R>
    

    Kiểu trả về của danh sách sau phép map phụ thuộc vào kiểu trả về của lambda transform.

    Ví dụ: Cho một danh sách sinh viên, điểm gpa cuối khóa sẽ được sử dụng để xét làm giảng viên. Một sinh viên được làm giảng viên nếu GPA > 3.8. Trả về danh sách giảng viên.

    Ta sẽ kết hợp filter và map để làm ví dụ xàm này.

    data class Student(val name:String, val gpa: Float)
    
    data class Teacher(val name:String)
    
    val students = getStudents()
    val teachers = students.filter { it.gpa > 3.8 }
            .map { Teacher(it.name) }
    

    Có thể thấy dùng filter + map làm code ngắn gọn dễ hiểu hơn nhiều đúng không nào.

    3. flatMap

    Nhìn thằng này chả khác gì thằng map ngoài thêm prefix flat, với một thằng ngu english như mình sau khi dịch flat thì chị google cho mình kết quả là phẳng, phẳng như nào thì cùng xem tiếp nhé.

    Đây là hàm flatMap

    inline fun <T, R> Iterable<T>.flatMap(
        transform: (T) -> Iterable<R>
    ): List<R>
    

    flatMap khác map duy nhất chỗ kiểu trả về của lambda transform là Iterable<R> thay vì R.

    flatMap sẽ map mỗi phần tử kiểu T thành một Iterable<R> . Cùng xem cách mà flatMap hoạt động dưới đây ta sẽ hiểu sao lại phẳng.

    Các list trả về sau khi mỗi phần tử transform sẽ được addAll vào một list duy nhất là destination. Nếu dùng map thì sẽ trả về một list mà các phần tử trong đó là các list có độ dài khác nhau.

    public inline fun <T, R> Iterable<T>.flatMap(transform: (T) -> Iterable<R>): List<R> {
        //Trả về giá trị của hàm flatMapTo
        //transform là phép biến đổi T -> Iterable<R> mà ta truyền vào
        return flatMapTo(ArrayList<R>(), transform)
    }
    
    public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.flatMapTo(destination: C, transform: (T) -> Iterable<R>): C {
        //Duyệt tất cả phần tử trong danh sách ban đầu
        for (element in this) {
    	//Với mỗi phần tử biến đổi thành một list bởi hàm transform
            val list = transform(element)
    	//Thêm vào arrayList
            destination.addAll(list)
        }
        return destination
    }
    

    flatMap rất hữu ích khi muốn transform một list với quan hệ 1-N.

    Ví dụ: Một ứng dụng nghe nhạc có chức năng cho người dùng chọn nhiều thể loại và trả về tất cả các bài hát trong các thể loại đó. Người dùng lại tài khoản vip nên mỗi bài hát lấy ra sẽ được chuyển định dạng thành vip.

    Ta có thể thấy thể loại và bài hát có quan hệ 1-N ta nghĩ ngay đến flatMap.

    data class Song(val name: String, val singer: String)
    
    data class VipSong(val name: String, val singer: String)
    
    data class Category(val name: String, val songs: List<Song>)
    ...
    val categories = listOf<Category>()
    val vipSongs = categories.flatMap { category ->
        category.songs.map { VipSong(it.name, it.singer) }
    }
    

    4. groupBy

    Hàm này có chức năng nhóm các phần tử trong collection theo selector và trả về một map collection với các key theo selector. Ví dụ: Liệt kê các học sinh theo tên cuong -> {Student("cuong", id = 1)}

    data class Grade(val math: Float, val physics: Float)
    
    data class Student(val name: String, val grade: Grade)
    ...
    
    val students = listOf(
            Student("Cuong1", Grade(1f,2f)),
            Student("Cuong2", Grade(3f,3f)),
            Student("Cuong3", Grade(1f,2f)),
            Student("Cuong5", Grade(1.2f,2.5f))
        )
    print(students.groupBy {
        it.grade
    })
    

    Kết quả sẽ trả về một map collection:

    {
    	Grade(math=1.0, physics=2.0)=[Student(name=Cuong1, grade=Grade(math=1.0, physics=2.0)), Student(name=Cuong3, grade=Grade(math=1.0, physics=2.0))],
    	Grade(math=3.0, physics=3.0)=[Student(name=Cuong2, grade=Grade(math=3.0, physics=3.0))], 
    	Grade(math=1.2, physics=2.5)=[Student(name=Cuong5, grade=Grade(math=1.2, physics=2.5))]
    }
    

    5. Partition

    Hàm này có chức năng phân chia collection thành hai list theo điều kiện, một list là thỏa mãn điều kiện, một list là không thỏa mãn điều kiện.

    Ví dụ: Cần lấy danh sách sinh viên được tốt nghiệp và sinh viên chưa được tốt nghiệp

    data class Student(val name: String, val gpa: Float, val credits: Int)
    
    fun isGraduated(student: Student) = student.run { gpa>=2.5 && credits == 150}
    
    fun main() {
        val students = listOf(
            Student("Lan", 4.0f, 140),
            Student("Phuong", 2.45f, 150),
            Student("MA", 2.7f, 150)
        )
        val (graduated, undergraduate) = students.partition(::isGraduated)
        println("""
            graduated: $graduated
            undergraduate: $undergraduate
        """.trimIndent())
    }
    

    Kết quả:

    graduated: [Student(name=MA, gpa=2.7, credits=150)]
    undergraduate: [Student(name=Lan, gpa=4.0, credits=140), Student(name=Phuong, gpa=2.45, credits=150)]
    

    Kết luận

    Các chức năng này giúp chúng ta giảm thiếu số lượng mã cũng như code clear hơn, nó cũng quan trọng và gặp thường xuyên khi làm việc với các thư viện reactive như Rx, flow coroutine.

  • Clean Architecture trong Android

    Clean Architecture trong Android

    Giới thiệu

    Clean Architecture là một kiến trúc lập trình được Robert C. Martin đề cập tại blog của ông vào năm 2012, đó là sự kiện quan trọng trong tư tưởng thiết kế phần mềm ảnh hưởng cho đến bây giờ.

    Clean Architecture có thể được sử dụng trong bất kỳ ứng dụng và nền tảng nào. Bài viết này mình xin được tập trung vào kiến trúc sạch trong phát triển ứng dụng android.

    Tổng quan kiến trúc sạch

    Có rất nhiều kiến trúc khác nhau trong lập trình nhưng chúng đều có một mục tiêu chung là tách biệt các mối quan tâm. Để đạt được sự tách biệt này cách chia phần mềm thành các layer đảm nhận các nghiệp vụ khác nhau.

    Một vài đặc điểm:

    • Dễ dàng test, các bussiness logic có thể test mà không có giao diện người dùng, cơ sở dữ liệu, bất kỳ yếu tố bên ngoài nào khác.
    • Giao diện người dùng có thể dễ dàng thay đổi mà không làm thay đổi phần còn lại của hệ thống.
    • Độc lập với các dữ liệu, ta có thể chuyển đổi các loại dữ liệu khác nhau mà không ảnh hưởng đến các layer khác.
    • Độc lập với các framework, điều này cho phép sử dụng các framework như các công cụ, thay vì phải nhồi nhét hệ thống của bạn phụ thuộc vào chúng.
    • Một layer chỉ biết layer phía dưới nó, layer trong cùng sẽ không biết gì về các layer khác(isolated) => tách biệt giữa các layer, layer business logic bên trong sẽ không phụ thuộc vào các layer khác.
      Mộ- Các vòng tròn đồng tâm càng ở trong càng cấp cao, module cấp cao không phụ thuộc vào module cấp thấp(Dependency Inversion principle). Ta sẽ rõ hơn trong chi tiết từng layer khi xây dựng ứng dụng android.
    • Áp dụng nguyên tắc SOLID.

    Clean Architecture trong ứng dụng Android

    Tùy thuộc vào đặc thù, quy mô dự án mà ta phân chia thành các layer khác nhau. Trong dự án android ta thường chia thành ba layer chính:

    • Domain
    • Data
    • Presenter

    Mỗi layer là một module trong Android Studio, giờ ta sẽ đi vào chi tiết từng layer.

    Domain layer

    • Domain layer là lớp chứa tất cả model và toàn bộ bussiness logic của ứng dụng, có thể coi đây là nơi chứa các policy còn các layer khác là nơi chứa các cơ chế.
    • Domain layer nằm trong cùng do đó sẽ không biết bất kỳ layer nào khác bên ngoài.
      Đây là module cấp cao, không phụ thuộc vào bất kỳ implementation của module cấp thấp nào mà chỉ phụ thuộc thông qua abstraction
    • Mỗi usecase đảm nhiệm một nhiệm vụ duy nhất (Single responsibility principle), ví dụ: GetData(), AddData(), …

    Tạo module domain với lib java or kotlin, do domain layer không phụ thuộc vào bất kỳ android framwork nào. Do domain layer chỉ chứa bussiness logic của app.


    Ta có thể thấy file gradle của module domain không chứa dependencies của android framework.

    Ở domain layer chỉ định nghĩa interface Repository, data layer chứa implementation mà domain đã định nghĩa.

    Presenter layer phụ thuộc usecase.

    Usecase tương tự như trong UML chứa một chức năng duy nhất, usecase chỉ phụ thuộc vào interface SchoolRepository (Nguyên tắc DI trong SOLID) mà không phụ thuộc trực tiếp repository tại data layer.

    class GetAllSchoolUseCase @Inject constructor(
        private val schoolRepository: SchoolRepository
    ) : IUseCase<Flow<List<School>>> {
    
        override fun invoke(): Flow<List<School>> = schoolRepository.getAllSchool()
    }

    Data layer

    Lớp này cung cấp cách thức để truy cập các nguồn dữ liệu trong room database hoặc internet. Các triển khai này sử dụng Repository pattern.

    Data layer được tạo là module library android.

    Tại data layer sẽ tạo class triển khai interface repository mà định nghĩa trong domain layer.

    class SchoolRepositoryImpl @Inject constructor(
        @DispatcherIO private val dispatcher: CoroutineDispatcher,
        private val schoolDao: SchoolDao,
        private val studentDao: StudentDao
    ) : SchoolRepository {
        ...
    }

    Để chuyển đổi qua lại giữa model và entity giữa domain layer và data layer ta dùng các function mapper.

    fun SchoolWithStudents.toSchool(): School {
        return this.run {
            School(
                schoolEntity.schoolID,
                schoolEntity.schoolName,
                schoolEntity.address,
                students.toStudents()
            )
        }
    }
    
    fun School.fromSchool(): SchoolEntity {
        return this.run {
            SchoolEntity(schoolID, schoolName, address)
        }
    }
    

    Presenter layer

    Tại đây chứa các code liên quan đến giao diện người dùng, layer này sẽ dependent các module khác.

    Do sử dụng một vài dependencies tại module data nên mình sử dụng api project(":data").

    Sử dụng MVVM

    • View: chịu trách nhiệm vẽ UI người dùng (Activity, Fragment, …). Lắng nghe hành động từ người dùng và gọi ViewModel sử lý, observe LiveData trong ViewModel.
    • Model: Chứa các bussiness login và data
    • ViewModel: Cầu nối giữa data và UI, ViewModel sẽ sử dụng các UseCase tại domain layer để thực hiện các nhiệm vụ.

    Mỗi màn hình(feature) sẽ là một subpackage trong package ui.

    ViewModel sử dụng các usecase để thực hiện các task khác nhau.

    @HiltViewModel
    class SchoolListViewModel @Inject constructor(
        private val getAllSchoolUseCase: GetAllSchoolUseCase,
        private val deleteSchoolUseCase: DeleteSchoolUseCase,
        private val deleteStudentUseCase: DeleteStudentUseCase,
        private val updateSchoolUseCase: UpdateSchoolUseCase,
        private val updateStudentUseCase: UpdateStudentUseCase
    ) : BaseViewModel<SchoolsViewState>() {
         ...
    }

    Kết luận

    Clean Architecture rất phù hợp với những dự án lớn phức tạp, có nhiều bussiness logic, giúp cho việc phát triển các tính năng và maintain dễ dàng hơn. Tuy nhiên với những dự án nhỏ và đơn giản hơn thì việc sử dụng clean architecture không mang lại hiệu quả và mất thời gian cho việc xây dựng ban đầu.

    Do chưa có kinh nghiệm nên bài viết này không thể tránh khỏi sai sót, rất mong nhận được góp ý từ các Anh.
    Author: CuongNV83