Tag: Kotlin

  • Delegate trong Kotlin

    Delegate trong Kotlin

    Delegate là gì nhỉ? Tại sao lại là Delegate? Đúng như cái tên, Delegate là một design pattern mà bạn ủy quyền xử lý logic của Class hiện tại cho một Object/Class khác. Delegate thường được sử dụng để tách logic code theo việc của nó (separate concerns) hoặc common hóa một đoạn logic.
    Bài viết này sẽ giúp bạn tìm hiểu cơ bản về Delegate trong Kotlin và cách sử dụng khái niệm này. Trong Kotlin, có 2 cách để sử dụng Delegate
    – Interface/Class Delegation
    – Delegate Properties

    Delegate là cách bạn cho phép một object khác xử lý một logic cho object hiện tại

    Interface/Class Delegation

    Với cách thứ nhất, chúng ta sẽ sử dụng một interface làm abstract cho một object và truyền object đó vào phần khai báo implement thông qua keyword by. Bằng cách này, các abstract methods (method của interface) sẽ chạy code của delegating object!

    interface CameraOptimization {
        fun optimize()
    }
    
    object XiaomiDevicesOptimization : CameraOptimization {
        override fun optimize() {
            TODO("do something for Xiaomi devices")
        }
    
    }
    
    object DefaultDevicesOptimization : CameraOptimization {
        override fun optimize() {
            TODO("do something for others devices")
        }
    }
    
    class CameraManager(optimization: CameraOptimization) : CameraOptimization by optimization {
        fun cameraFocus() {
            //todo: focus camera
        }
    }

    Đây là cách setup cơ bản của Interface/Class Delegation. Như các bạn thấy thì implementation của method optimize() không trực tiếp xuất hiện ở trong class CameraManager mà sẽ được delegate đến object truyền vào bằng keyword by.

    class CameraActivity : BaseActivity() {
        private lateinit var _cameraManager: CameraManager
    
        private fun initView() {
            val vendor = android.os.Build.MANUFACTURER
            val config = when {
                vendor.equals("Xiaomi", ignoreCase = true) -> XiaomiDevicesOptimization
                else -> DefaultDevicesOptimization
            }
            _cameraManager = CameraManager(config)
            _cameraManager.optimize()
            _cameraManager.cameraFocus()
        }
    }


    Khi method optimize() được gọi, nó sẽ delegate đến Config của XiaomiDevicesOptimization hoặc DefaultDevicesOptimization tùy theo device đó là gì. Với cách tiếp cận này logic của CameraManager vẫn có khả năng tối ưu mà không cần phải quan tâm rằng nó sinh ra cho vendor cụ thể nào cả. Đồng thời cũng tăng khả năng mở rộng của Class này hơn. Nếu app của bạn quyết định support optimize thêm cả anh zai Samsung cũng oke luôn, code thêm 1 class và 1 dòng duy nhất.

    Delegate Properties

    Chắc bạn đã từng sử dụng rất nhiều lần lazy trong kotlin rồi đúng không? Delegate đó :v Delegate Properties là việc bạn implement operator getValue (có thể thêm cả setValue luôn nếu bạn muốn nó set được cả value) của một class. Hoặc một cách khác tường mình hơn là implement interface ReadWriteProperty/ReadOnlyProperty. Class đó sẽ trở thành Delegate. Vẫn là keyword by, chúng ta khai báo một biến với Delegate thông qua by.

    class IntNaturalSet : ReadWriteProperty<Any, Int> {
        private var _value: Int = 0
        override fun getValue(thisRef: Any, property: KProperty<*>): Int {
            return _value
        }
    
        override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) {
            _value = if(value < 0) 0 else value
        }
    }

    Trong ví dụ đơn giản này chúng ta đã ủy quyền getter setter của biến cho class IntNaturalSet can thiệp và xử lý logic. Bên cạnh việc ủy quyển xử lý logic getter (setter) thì Delegate cũng có thể access đến Class chứa biến được delegate thông qua param thisRef(Chính là generic T trong ReadWriteProperty/ReadOnlyProperty). Có thể là built-in delegate cho một Type hoặc ép kiểu thisRef trong logic của getter setter để có thể sử dụng param này.

    Ứng dụng của Delegate Properties rất rộng, chúng ta có thể custom cho logic in/out data như shared preferences, cache…
    Trong Androidx/Kotlin cơ bản cũng có vài delegate như lazy, viewModels, activityViewModels…

    Summary

    Delegate là một phương pháp khá hay trong lập trình giúp chúng ta tối ưu logic source code. Một source base có thể sẽ clean hơn nếu rút gọn các common logic hay boilerplate code. Một class có thể tăng tính mở rộng trong tương lai. Một project có thể sẽ triển khai nhanh hơn nhờ những common và khả năng scalable tốt. Lần tới nếu như bạn gặp phải một vấn đề có thể xử lý bằng Delegate, cứ thử xem sao nhé!
    Hi vọng bài viết giúp bạn có thêm chút kiến thức trong chặng đường của một Engineer :3!

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

  • Tại sao nên hạn chế sử dụng Singleton, static function(util class, Helper class)?

    Tại sao nên hạn chế sử dụng Singleton, static function(util class, Helper class)?

    Đây thực sự là một câu hỏi không dễ để trả lời… Một cách thông thường, trong suy nghĩ của hầu hết lập trình viên sẽ là: chỗ nào giống nhau, gọi lại nhiều lần thì nên gom thành common class, static cho dễ gọi.

    Ok, mọi thứ đều ổn, nhưng nếu ko để ý & quá tay một chút thì nó đã vi phạm nghiêm trọng đến design ứng dụng – nó phá vỡ ranh giới của các element,module hoặc class

    Tại sao lại như vậy? Bản chất Lập trình OOP là mô phỏng thực tế cuộc sống vào chương trình bằng ngôn ngữ lập trình. Thay vì dùng văn tả cảnh, thì lập trình viên tả lại cuộc sống bằng ngôn ngữ lập trình trên một framework nào đấy. Ở ngoài cuộc sống có gì, thì chương trình cũng có cái đó, chúng ta có Sinh viên, có Tài khoản ngân hàng, có Ô tô…etc.

    Nhưng nếu bạn để ý, trong cuộc sống Util class, Helper class, Common class… ko tồn tại. Ai đó tạo lên cuộc sống hẳn phải là một lập trình viên cực kỳ vĩ đại.

    Phân ranh giới. (Boundaries)

    Software architect là một nghệ thuật để tạo ra các ranh giới, nhằm phân tách các element, class, module..etc
    

    Cùng xem xét ví dụ sau:

    DisplayHelper – static function

    Alt

    ZxingDecoder cần lấy ra orientation device, hàm lấy orientation dùng ở rất nhiều nơi nên tạo sẵn một class DisplayHelper để dùng. Hàm get orientation cần context nên parameter của ZxingDecoder là context

    Display – interface

    Alt

    Mình tạo ra 1 interface là Display để có thể get orientation của device, parameter đầu vào của ZxingDecoder là display.

    Điểm khác biệt lớn nhất là ở cách viết 2 – ZxingDecoder đã tách khỏi(1 phần tách khỏi) framework android – khi nó xóa đi sự hiện diện của context, tức là mình đang cố gắng phân rõ ranh giới của ZxingDecoder khỏi android framework Nói một cách khác mình đang cố gắng decouple ZxingDecoder khỏi android framework

    ZxingDecoder là 1 bộ decode barcode – nó ko phụ thuộc vào framwork, việc decode này mang ý nghĩa – class mình viết có thể chạy được ở bất cứ đâu, không chỉ là trên android framework

    Ngoài ra, nếu sử dụng DisplayHelper – khi có càng nhiều nơi, càng nhiều class, layer, module gọi hàm get orientation, hoặc một function của DisplayHelper thì các bạn có thể tượng tượng rằng DisplayHelper như một sợi xích đâm xuyên tất cả các layer, module, class, và buộc chặt các thành phần này lại với nhau và hoàn toàn không thể tách rời. Nói cách khác, khi đó ko thể tạo ra các ranh giới Boundaries cho bất cứ thành phần nào dùng chung DisplayHelper. Tất cả đều phẳng, và dính chặt vào android framework thông qua context của DisplayHelper

    Dễ code(Create) – khác với việc dễ maintain(Update).

    Phân ranh giới. (Boundaries) – là target cho hầu hết các task refactor bạn phải làm

    Làm thế nào để phân ranh giới? à, cái ý mình ko dám nói 😀 (ở bài viết này)