Kotlin và những cái mới mẻ – Phần 2

Ở phần 2, chúng ta sẽ khám phá sâu hơn về Kotlin: những cái hay, cái mới mà nó mang lại. Bạn nào chưa đọc có thể đọc tại đây:

Phần 2: Kotlin và những cái mới mẻ – Phần 1

Ở phần 3 này, chúng ta sẽ tiếp tục khám phá sâu hơn về Kotlin: những cái hay, cái mới mà nó mang lại.

Bài viết nằm trong Series Kotlin vi diệu, các bạn có thể xem tại:

Series Kotlin vi diệu: https://codecungtrung.com/kotlin/series-kotlin-vi-dieu/

Có những điều các bạn có thể không thấy như mình. Vậy nên hãy đọc rồi để lại comment nhé !!! Rất chờ đợi ý kiến từ các bạn.

I. Sealed class

1.Vấn đề

Hẳn bạn đã sử dụng enum trong Java, chẳng hạn như sau:

// Enum thể hiện các trạng thái của màn hình
public enum ScreenStateJava {
    ERROR,
    LOADING,
    DATA
}

Đơn giản phải không nào. Giờ đến ví dụ sử dụng Enum ở trên

// Set trạng thái cho màn hình
public void setDetailScreenType(ScreenStateJava screenState) {
    switch (screenState) {
        case ERROR:
            // Hiển thị lỗi nếu có
            break;
        case LOADING:
           // Hiển thị trạng thái loading của màn hình
            break;
        case DATA:
            // Có dữ liệu thì cho hiển thị lên
            displayData(screenData);
            break;
    }
}

// Hiển thị dữ liệu
private void displayData(ScreenData screenData) {
}

Như bạn thấy là ta sẽ phải có biến screenData để từ đó lấy dữ liệu hiển thị lên màn hình. Biến này sẽ được lấy ở một chỗ khác (kết quả từ request api, gọi cơ sở dữ liệu chẳng hạn), tách biệt hoàn toàn với “screenState”.

Java là OOP – lập trình hướng đối tượng phải không nào ? Ta đã có đối tượng ScreenStateJava lưu trạng thái màn hình, vậy sao không lưu thêm dữ liệu cho từng trạng thái đó nếu có luôn. Tạo một đối tượng sẽ giúp việc quản lý trạng thái, dữ liệu màn hình dễ dàng hơn.

Để giải quyết, dĩ nhiên enum cung cấp cho ta hàm khởi tạo như sau:

public enum ScreenStateJava {

    ERROR(new ScreenData(1, "Data 1")),
    
    LOADING(new ScreenData(1, "Data 2")),
    
    DATA(new ScreenData(1, "Data 3"));

    private ScreenData screenData;

    ScreenStateJava(ScreenData screenData) {
        this.screenData = screenData;
    }
}

Giờ lưu dữ liệu thôi 😀 Nhưng điều khó chịu ở đây là ta … phải cung cấp cùng một loại constructor cho mỗi loại ở trên. Vì sao ư ? Enum trong Java bảo vậy.

Và bạn có để ý không ? Trường hợp trả về ERROR hoặc LOADING thì thực tế sẽ hoàn toàn không có dữ liệu cho biến screenData. Nhưng ta vẫn phải tạo constructor cho chúng và truyền dữ liệu nào đó vào để … nó không báo lỗi : ((

Chán chút thôi !!

Tóm lại, nếu bạn chỉ muốn làm gì đó đơn giản, không cần lưu dữ liệu hoặc mỗi loại một dữ liệu khác nhau thì enum bình thường đã ok rồi. Nhưng nếu bạn muốn nhiều hơn, thì Kotlin Sealed Class mà mình sẽ giới thiệu sau đây chính là giải pháp cho bạn.

2. Chi tiết

Bạn có thể hiểu sealed class là phiên bản mở rộng của enum. Chi tiết mình sẽ để ở link phần tham khảo nhé !!

Và giờ quay lại với ví dụ trên, ta viết lại như sau

sealed class ScreenStateSealedClass {

class Error : ScreenStateSealedClass()

class Loading : ScreenStateSealedClass()

class Data(val data: ScreenData) : ScreenStateSealedClass()

}

Code lúc khởi tạo ta sẽ gọi như sau

ScreenStateSealedClass.Error()
ScreenStateSealedClass.Loading()
ScreenStateSealedClass.Data(ScreenData(1, "3"))

Error và Loading hoàn toàn không có tham số gì cả và Data ta đang truyền dữ liệu vào ==> Hoàn toàn thỏa mãn cái ta cần giải quyết phải không nào !!!

Tips: thêm cho những bạn đã biết về Kotlin

  1. Class Error và Loading không lưu bất kì dữ liệu nào, và sẽ giống nhau ở mọi trường hợp, vậy nên hãy để nó ở dạng singleton
object Error : ScreenState()

object Loading : ScreenState()

2. Để class Data ở dạng data class để tận dụng hàm equals, dùng trong trường hợp cần so sánh.

data class Data(val data: ScreenData) : ScreenState()

Có gì hay nữa các bạn hãy để lại ở comment nhé 😀 Nào giờ chúng ta tiếp tục thôi !!!

II. Higher-order function

1.Vấn đề

Hẳn các bạn đã quen với viết Callback trong Java. Ví dụ như sau:

public class MyClass {

    private Listener listener;

    public MyClass(Listener listener) {
        this.listener = listener;
    }

    // Gọi đối tượng listener để sử dụng
    void someFunction() {
        listener.doThis();
        listener.doThat(12);
    }

    // Tạo một interface tên là Listener
    interface Listener {
        void doThis();

        void doThat(int number);
    }
}

class UserClass {

    // Khởi tạo đối tượng listener
    MyClass.Listener listener = new MyClass.Listener() {
        @Override
        public void doThis() {
            // Do something here
        }

        @Override
        public void doThat(int number) {
            // Do something here
        }
    };
    // Truyền đối tượng qua hàm khởi tạo
    MyClass myClass = new MyClass(listener);
}

Ta tạo một interface là Listener, khởi tạo đối tượng cho nó rồi … truyền nó qua hàm khởi tạo của class MyClass.

Rồi bên class MyClass ta gọi hàm của interface này thì chính là ta đang gọi hàm của nó trong class UserClass kia. Đơn giản phải không nào 😀

Tuy nhiên một số vấn đề hay xảy ra ở đây là:

  • Tương tối khó hiểu với người mới tiếp cận: mình biết tới cũng là do người khác chỉ và quả thật nó khá khó hiểu. Mình cũng chỉ cho một số người khác và họ cũng bảo như vậy.
  • Nhiều bước phải làm: nào thì tạo interface, viết hàm cho nó, khởi tạo đối tượng hoặc thích thì implement lại, truyền đối tượng sang class khác, tạo biến để lưu instance của interface rồi chỗ nào cần thì gọi.
  • Lãng phí: một interface bạn có thể dùng ở nhiều chỗ và không hẳn lúc nào bạn cũng dùng hết các hàm đó.

Còn vấn đề nào khác các bạn gặp phải nữa thì share lại vs mình ở comment nhé 😀

Nhưng nếu code quen rồi thì những vấn đề trên cũng chả to tát gì cả phải không :)) Và thường thì cũng không phí nhiều lắm :v

Mình cũng nghĩ vậy, và trong Kotlin mình vẫn code callback ầm ầm :v

Rồi đến một ngày đẹp trời đang đọc thêm về Kotlin, đọc cái gì mà Higher-order functions – nghe chả hiểu quái gì. Đọc hiểu được cách code, làm được một số cái ví dụ linh tinh.

Hôm khác quay lại đọc thêm thì thấy ghi nó có thể dùng để thay thế cho callback được. Chả tin nên search trên google một lô bài rồi code thử thấy khá ok. Bạn thử search theo từ khóa “higher order function vs callback in kotlin” xem.

Lằng nhằng ghê !!!

Chuyển đoạn code trên theo nó như sau:

// Đây là cách viết hàm khởi tạo trong kotlin,
// truyền vào what ??????
class MyClass(val doThis: () -> Unit, val doThat: (number: Int) -> Unit = {}) {

    fun someFunction() {
       // Đoạn này gọi phương thức như phần callback đó
       // mà không cần callback đâu :))
        doThis()
        doThat(12)
    }
}

class UserClass {
    // Hàm khởi tạo của class MyClass, truyền cái :: ??
    val myClass = MyClass(::doThis, ::doThat)

    private fun doThis() {
       // Do something here
    }

    private fun doThat(number: Int) {
       // Do something here
    }
}

Cái ta thấy ở đây là gì ??

  • Hai hàm doThis(), doThat() không viết ở trong interface mà viết ngay trong class UserClass.
  • Tham số truyền vào hàm khởi tạo của class MyClass dưới dạng :: + tên phương thức. Bạn chỉ cần hiểu đơn giản là … truyền phương thức vào hàm khởi tạo nhé. Vâng mình đang nói là … truyền phương thức vào hàm :)))
  • Bên class MyClass ta lấy các phương thức truyền vào qua hàm khởi tạo và có thể sử dụng như lúc gọi callback đó. Đã code thử nhé, mất mấy ngày lận mà :))
  • Và hay nhất là thấy nó ít bước hơn, nên phần nào cũng dễ hiểu hơn.
Khi nghe truyền phương thức vào hàm :)))

2. Chi tiết

Hay nhể ? Higher-order function, truyền phương thức vào phương thức rồi lấy ra sử dụng như thường ??

Sở dĩ như vậy được là vì trong Kotlin, function là first-class, ý là bạn có thể lưu trong biến và cấu trúc dữ liệu, truyền vào như tham số và trả về từ higher-order function khác.

Higher-order function được định nghĩa là một function … nhận tham số là một function, hoặc trả về function – chỗ này bạn có thể hiểu lúc return sẽ trả về kết quả khi gọi tiếp một function khác, sử dụng cú pháp của higher-order function.

Về lợi ích khi dùng mình đã lấy ví dụ ở phần 1 rồi đó 😀

Đương nhiên cái gì cũng có giá của nó và hẳn bạn đã nghe, Kotlin sẽ làm tăng dung lượng code và code khi compile ra sẽ khá phức tạp. Higher-order function cũng không ngoại lệ.

Tuy nhiên mình sẽ không trình bày ở trong bài này, hẹn các bạn ở phần cuối của series nhé 😀

III. Collection trong Kotlin

1.Vấn đề

Khi làm việc với collection trong Java, bạn hẳn sẽ hay cần tìm, sắp xếp các phần tử thỏa mãn điều kiện cho trước, biến đổi phần tử đã thành một dạng khác rồi lấy phần tử list mới đó để dùng …

Và những gì bạn cần làm đương nhiên là … tự viết các phương thức ra như sau

// Tìm phần tử thoả mãn điều kiện
for (String name : listName) {
    if (name.startsWith("A")) {
        System.out.println(name);
    }
}

// Biến đổi phần tử sang dạng khác rồi lấy list đó để sử dụng
ArrayList<String> upperListName = new ArrayList<>();

for (String name : listName) {
    upperListName.add(name.toUpperCase());
}

for (String upperName : upperListName) {
    System.out.println(upperName);
}


// Sắp xếp - theo tên chẳng hạn

Comparator<Employee> compareById = new Comparator<Employee>() {
    @Override
    public int compare(Employee o1, Employee o2) {
        return o1.getId().compareTo(o2.getId());
    }
};

List<Employee> listEmployee = new ArrayList<>();
Collections.sort(listEmployee, compareById);

Điều ta thấy ở đây là:

  • Ta hay phải duyệt qua các phần tử của List và vòng for là cái ta rất hay phải viết ==> Lặp đi lặp lại code
  • Code ở mỗi lần viết thường khá dài ==> Dài sẽ dễ gây ra lỗi hơn
  • Khả năng cao là mỗi người sẽ có một cách viết khác nhau cho cùng một kiểu logic ==> Dễ gây khó hiểu cho người đọc code

Như đã nói ở phần 1 – Kotlin và Java: kế thừa và phát triển, Kotlin ngắn gọn hơn Java, đồng thời cũng mang lại nhiều điều mới, những công cụ mạnh mẽ hơn cho developer. Và Collection trong Kotlin là một trong những cái đó.

Ta viết lại đoạn trên như sau

// Tìm phần tử thỏa mãn điều kiện

listName
// chỗ này thay cho  if (name.startsWith("A"))
.filter { it.startsWith("A") } 
// Chỗ này là duyệt qua từng phần tử nè
.forEach {
    print(it)
}

// Biến đổi phần tử sang dạng khác rồi lấy list đó để sử dụng

listName
// map sẽ thực hiện sự biến đổi từng phần tử sang dạng uppercase
.map {
    it.toUpperCase()
}
// Biến đổi xong rồi đó, giờ in ra là toàn uppercase 
.forEach {
    print("Username is " + it)
}

// sắp xếp phần tử - giờ ngắn vậy thôi nè

val listEmployee = mutableListOf<Employee>()
listEmployee.sortedBy { it.name }

Viết lại bằng Kotlin thì khác gì nhỉ ?

  • Không phải viết những vòng for nhàn chán nữa
  • Như bạn thấy đấy code sẽ ngắn hơn
  • Cung cấp sẵn những phương thức thực hiện các logic trên list, từ đó việc hiểu sẽ dễ dàng hơn – tuy nhiên bạn viết như bên Java vẫn ok nhé :)) Nhưng có sẵn và tiện hơn tại sao lại không dùng nhể :v

2.Chi tiết

Collection trong Kotlin cung cấp một kho các tool để làm việc với collection.

Ở trên mình đã sử dụng các phương thức filter, map, sortedBy, … Chúng chỉ là một phần nhỏ trong kho các phương thức mà Kotlin mang lại cho chúng ta. Bạn muốn xem thêm mình sẽ để ở link cuối bài nhé 😀

Nào giờ bạn hãy để ý thanh cuộn bên phải

Bên Java

Giờ sang Kotlin, nhiều phương thức hơn phải không nào :)) Nhiều lắm, cứ lấy ra mà dùng thôi. Thực sự là trong khi code bằng Kotlin mình thấy cực đủ dùng, thậm chí thấy thừa, nhiều hàm không dùng tới luôn.

Bên Kotlin

Còn nhiều như tìm cái lớn nhất, lọc, lấy phần tử, chuyển đổi, kết nối, … Ở đây mình lấy ví dụ cho List. Trong Kotlin còn có Map, Set nữa cũng tương tự nhé.

Kotlin còn tách Collection ra làm hai loại:

  • Mutable Collection: collection có thể thay đổi kích thước dữ liệu, có thể thêm, sửa, xóa…
  • Immutable Collection: collection chỉ có nhiệm vụ readOnly, dùng để hiển thị thông tin. Do đó nếu như bạn chỉ muốn hiển thị thông tin thì nên dùng List.

Immutable Collection lợi hơn Mutable Collection rất nhiều:

  • Tối ưu bộ nhớ hơn so với Mutable Collection.
  • Có thể dễ dàng chia sẻ giữa các thread mà không lo bị ConcurrentModificationException hoặc bất kì lỗi liên quan tới list có thể bị thay đổi.

Chi tiết hơn bạn có thể xem tại đây: https://github.com/Kotlin/kotlinx.collections.immutable/blob/master/proposal.md

Tóm lại

Ở phần 3 này chúng ta đã tìm hiểu về 3 khái niệm mới trong Kotlin là:

  • Sealed class
  • Higher-order function
  • Collection trong Kotlin

Chúng ta cũng đã so sánh với code bên Java, phân tích, so sánh code giữa hai bên.

Khen Kotlin quá nhiều rồi, ở phần sau chúng ta sẽ đi chê, chỉ ra những điều chưa hoàn thiện của Kotlin nhé 😀

Bài viết nằm trong Series Kotlin vi diệu, hãy xem tại đây

Nhiều cty cũng bắt đầu code Kotlin rồi, nhiều người bắt đầu code bằng Kotlin, cộng đồng Kotlin đang phát triển mạnh.

Làn gió mới Kotlin – hãy thử ngay thôi nào 😀

Bạn nào có nhu cầu học Kotlin có thể tham khảo khóa học sau của Duy Thanh – một cái tên rất nổi tiếng nhé, rất chi tiết và bổ ích đó !

Nếu bạn thấy nội dung này có giá trị, hãy ủng hộ để blog Code cùng Trung có thể duy trì và phát triển hơn nữa nhé.Ủng hộ blog

Tham khảo

Đọc chi tiết các phần tại trang chủ

  1. Sealed class: https://kotlinlang.org/docs/reference/sealed-classes.html
  2. Higher-order function: https://kotlinlang.org/docs/reference/lambdas.html
  3. Collection trong Kotlin: https://kotlinlang.org/docs/reference/collections-overview.html

Tham khảo khác

4. https://codeascraft.com/2018/04/12/sealed-classes-opened-my-mind/

5. https://proandroiddev.com/kotlin-sealed-classes-enums-with-swag-d3c4b799bcd4

3 thoughts on “Kotlin và những cái mới mẻ – Phần 2”

Để lại comment