Viết Ứng dụng Android bằng Kotlin: Lambda, Tránh Null và Hơn thế nữa
Vietnamese (Tiếng Việt) translation by Dai Phong (you can also view the original English article)
Trong loạt bài gồm ba phần này, chúng ta đã tìm hiểu sâu về Kotlin, ngôn ngữ lập trình hiện đại cho các ứng dụng Android chạy trong Máy ảo Java (JVM).
Mặc dù Kotlin chưa thể là ngôn ngữ lập trình thay thế được thiết kế để chạy trên JVM nhưng nó có rất nhiều thứ để cung cấp cho các nhà phát triển Android. Vì vậy, nếu bạn cảm thấy thất vọng với Java trong quá trình phát triển Android, thì bạn sẽ muốn dùng thử Kotlin.
Trong hai phần đầu tiên, chúng ta đã đề cập đến những vấn đề sau:
Java so với Kotlin: Bạn có nên sử dụng Kotlin cho phát triển Android? Trong bài giới thiệu này, chúng ta đã tìm hiểu những gì mà Kotlin cung cấp cho các nhà phát triển Android, cũng như một số nhược điểm tiềm ẩn mà bạn cần phải biết trước khi quyết định liệu Kotlin có phù hợp với bạn hay không.
Viết Ứng dụng Android bằng Kotlin: Làm quen. Trong bài viết này, chúng ta đã tìm hiểu cách cấu hình Android Studio để hỗ trợ cho code Kotlin, tạo ra một ứng dụng Android được viết hoàn toàn bằng Kotlin, nắm vững cú pháp cơ bản của Kotlin và thậm chí thấy rằng việc chuyển đổi sang Kotlin có thể giúp bạn không bao giờ phải viết lại một
findViewById
nữa .
Bây giờ bạn đang ở nơi mà bạn có thể thay thế các thành phần Java trong dự án bằng code Kotlin để cho ra kết quả tương tự, hãy tiếp tục và tìm hiểu một số khía cạnh mà Kotlin có lợi thế hơn so với Java.
Trong phần cuối cùng này, chúng ta sẽ khám phá một số tính năng nâng cao của Kotlin cho phép bạn thực hiện các tác vụ mà có thể dài dòng hơn hoặc không thể đạt được nếu viết bằng Java.
Khi kết thúc bài viết này, bạn sẽ biết cách sử dụng Kotlin để giảm bớt một số code không cần thiết khỏi các dự án của bạn, mở rộng các lớp hiện có với các tính năng mới, và làm cho nỗi sợ NullPointerException
trở thành dĩ vãng.
Không còn Null nữa
Đã bao nhiêu lần bạn gặp NullPointerException
khi kiểm thử code của bạn? Ngày nay, rõ ràng là việc cho phép các nhà phát triển gán null cho một tham chiếu đối tượng không phải là sự lựa chọn thiết kế tốt nhất! Cố gắng sử dụng một tham chiếu đối tượng có một giá trị null là nguồn gốc làm phát sinh các lỗi lớn trên nhiều ngôn ngữ lập trình khác nhau, bao gồm Java. Trên thực tế, NullPointerException
là nguồn gốc gây nên phiền toái lớn từ đó Java 8 đã thêm chú thích @NonNull
đặc biệt để cố gắng và khắc phục lỗ hổng này trong hệ thống kiểu của nó.
Vấn đề nằm ở thực tế rằng, Java cho phép bạn thiết lập bất kỳ biến kiểu tham chiếu nào thành null—nhưng nếu tại một thời điểm nào đó bạn cố gắng sử dụng một tham chiếu trỏ tới null, thì biến sẽ không biết nơi nào để tìm kiếm vì đối tượng không thật sự tồn tại. Lúc này, JVM không thể tiếp tục thực thi như bình thường—ứng dụng của bạn sẽ bị crash, và bạn sẽ gặp phải một NullPointerException
. Cố gắng gọi một phương thức trên một tham chiếu null hoặc cố gắng truy cập một trường của tham chiếu null sẽ dấn đến một NullPointerException
.
Nếu bạn đã từng gặp phải nỗi đau NullPointerException
(và không phải tất cả chúng ta, ở một số thời điểm?) thì có tin tốt đây: tránh null (hay còn gọi là null safety) được tích hợp vào ngôn ngữ Kotlin. Trình biên dịch Kotlin không cho phép bạn gán một giá trị null cho một tham chiếu đối tượng, vì nó đặc biệt kiểm tra code của bạn để tìm ra sự có mặt của các đối tượng null có thể có trong quá trình biên dịch. Nếu trình biên dịch Kotlin phát hiện ra rằng một NullPointerException
có thể xảy ra tại runtime, thì code của bạn sẽ không biên dịch thành công.
Kiểu thiết kế tránh null của Kotlin có nghĩa là bạn (hầu như) không bao giờ gặp phải vấn đề null và NullPointerException
bắt nguồn từ code Kotlin. Ngoại trừ duy nhất là nếu bạn gặp phải một NullPointerException
do sử dụng sai một trong những toán tử đặc biệt của Kotlin, hoặc nếu bạn sử dụng một trong những toán tử này để cố ý kích hoạt NullPointerException
(chúng ta sẽ khám phá các toán tử chi tiết hơn trong phần sau của bài viết này).
Tránh Null trong Thực tế
Trong Kotlin, mặc định tất cả các biến đều được xem là không thể null, vì vậy cố gắng gán một giá trị null cho một biến sẽ dẫn đến một lỗi biên dịch. Ví dụ, code sau đây sẽ không biên dịch:
1 |
var example: String = null |
Nếu bạn muốn một biến chấp nhận một giá trị null, thì bạn sẽ cần phải đánh dấu một cách rõ ràng rằng biến đó là có thể null. Đây là sự đối lập hoàn toàn so với Java, nơi mà mọi đối tượng được xem là có thể null theo mặc định, đó là một lý do rất lớn giải thích tại sao NullPointerException
lại rất phổ biến trong Java.
Nếu bạn muốn khai báo một cách rõ ràng rằng một biến có thể chấp nhận một giá trị null, thì bạn sẽ cần phải nối thêm một dấu ?
vào kiểu biến. Ví dụ, code sau đây sẽ biên dịch:
1 |
var example: String? = null |
Khi trình biên dịch nhìn thấy kiểu khai báo này, nó nhận ra rằng đây là một biến có thể null và sẽ xử lý nó hơi khác một chút, đáng chú ý nhất là ngăn bạn khỏi việc gọi một phương thức hoặc truy cập vào một thuộc tính trên tham chiếu có thể null này, một lần nữa giúp bạn tránh những NullPointerException phiền phức . Ví dụ, code sau đây sẽ không biên dịch:
1 |
var example : String? = null |
2 |
example.size |
Mặc dù thiết kế của Kotlin có nghĩa là rất khó gặp phải NullPointerException bắt nguồn từ code Kotlin, nhưng nếu dự án của bạn có pha trộn giữa Kotlin và Java thì phần Java của dự án vẫn có thể là một nguồn gây ra NullPointerException.
Nếu bạn đang làm việc với một sự pha trộn giữa các tập tin Kotlin và Java (hoặc có lẽ bạn đặc biệt cần phải đưa ra các giá trị null vào code Kotlin của bạn) thì Kotlin bao gồm một loạt các toán tử đặc biệt được thiết kế để giúp bạn xử lý một cách nhẹ nhàng các giá trị null mà bạn gặp phải.
1. Toán tử Gọi An toàn
Toán tử Gọi An toàn ?.
cung cấp cho bạn một cách để đối phó với các tham chiếu mà có thể tiềm ẩn một giá trị null, trong khi vẫn đảm bảo rằng bất kỳ cuộc gọi đến tham chiếu này sẽ không dẫn đến một NullPointerException
.
Khi bạn thêm toán tử Gọi An toàn vào một tham chiếu, thì tham chiếu đó sẽ được kiểm tra giá trị null. Nếu giá trị là null, thì null được trả về, nếu không tham chiếu đối tượng sẽ được sử dụng như dự định ban đầu. Mặc dù bạn có thể đạt được hiệu quả tương tự khi sử dụng câu lệnh if
, nhưng toán tử Gọi An toàn cho phép bạn thực hiện công việc tương tự với lượng code ít hơn nhiều.
Ví dụ, code sau đây sẽ trả về a.size
chỉ khi a
không null—nếu không, nó sẽ trả về null:
1 |
a?.size |
Bạn cũng có thể nối các toán tử Gọi An toàn lại với nhau:
1 |
val number = person?.address?.street?.number |
Nếu bất kỳ biểu thức nào trong chuỗi biểu thức này là null, thì kết quả sẽ null, nếu không kết quả sẽ là giá trị được yêu cầu.
2. Toán tử Elvis
Đôi khi bạn sẽ có một biểu thức tiềm ẩn một giá trị null, nhưng bạn không muốn sinh ra một NullPointerException
ngay cả khi giá trị này thật sự là null.
Trong những tình huống này, bạn có thể sử dụng toán tử Elvis ?:
của Kotlin để cung cấp một giá trị thay thế sẽ được sử dụng bất cứ khi nào giá trị là null, đây cũng là một cách tốt để ngăn chặn việc truyền các giá trị null trong code của bạn.
Hãy nhìn vào một ví dụ về toán tử Elvis trong thực tế:
1 |
fun setName(name: String?) { |
2 |
username = name ?: "N/A" |
Ở đây, nếu giá trị ở bên trái của toán tử Elvis không phải là null thì giá trị ở phía bên trái sẽ được trả về (name
). Nhưng nếu giá trị bên trái của toán tử Elvis là null thì biểu thức bên phải sẽ được trả về, trong trường hợp này là N/A
.
3. Toán tử !!
Nếu bạn muốn buộc code Kotlin sinh ra một NullPointerException theo kiểu Java thì bạn có thể sử dụng toán tử !!
. Ví dụ:
1 |
val number = firstName!!.length |
Ở đây, chúng ta đang sử dụng toán tử !!
để khẳng định rằng biến firstName
không phải là null. Chừng nào firstName
còn chứa một tham chiếu hợp lệ, thì biến number còn được thiết lập thành length
(chiều dài) của chuỗi. Nếu firstName
không chứa một tham chiếu hợp lệ, thì Kotlin sẽ đưa ra một NullPointerException
.
Biểu thức Lambda
Một biểu thức lambda đại diện cho một hàm ẩn danh. Lambda là một cách tuyệt vời để giảm thiểu số lượng code cần thiết để thực hiện một số tác vụ phát sinh tại mọi thời điểm trong phát triển Android—ví dụ như viết listener và callback.
Java 8 đã giới thiệu các biểu thức lambda gốc và chúng giờ đây được hỗ trợ trong Android Nougat. Mặc dù tính năng này không phải là cái gì đó độc đáo trong Kotlin, nhưng vẫn đáng để tìm hiểu chúng. Hơn nữa, nếu bạn đang làm việc trên một dự án có chứa cả code Kotlin lẫn Java, thì bạn có thể sử dụng các biểu thức lambda trên toàn bộ dự án của bạn!
Một biểu thức lambda bao gồm một tập hợp các tham số, một toán tử lambda (->
) và thân hàm, được sắp xếp theo định dạng sau:
1 |
{ x: Int, y: Int -> x + y } |
Khi xây dựng biểu thức lambda trong Kotlin, bạn cần phải tuân theo các quy tắc sau:
Biểu thức lambda nên được bao quanh bởi các dấu ngoặc nhọn.
Nếu biểu thức có chứa bất kỳ tham số nào, thì bạn cần phải khai báo chúng phía trước biểu tượng mũi tên
->
.Nếu bạn đang làm việc với nhiều tham số, thì bạn nên tách chúng bằng dấu phẩy.
Phần thân theo sau dấu
->
.
Lợi ích chính của biểu thức lambda đó là chúng cho phép bạn định nghĩa các hàm ẩn danh và sau đó chuyển các hàm này ngay lập tức dưới dạng một biểu thức. Điều này cho phép bạn thực hiện nhiều tác vụ phát triển thông dụng một cách ngắn gọn hơn so với những gì bạn có thể thực hiện trong Java 7 và phiên bản cũ hơn vì bạn không cần phải viết đặc tả của hàm trong một lớp hoặc giao diện trừu tượng. Thực tế, việc thiếu lambda trong Java 7 trở về trước là một phần quan trọng giải thích tại sao việc viết các listener và callback đã trở nên lỗi thời trong Android.
Chúng ta hãy tìm hiểu một ví dụ thông dụng: thêm một click listener vào một nút. Trong Java 7 trở về trước, việc này thường đòi hỏi code sau đây:
1 |
button.setOnClickListener(new View.OnClickListener() { |
2 |
|
3 |
@Override
|
4 |
public void onClick(View v) { |
5 |
Toast.makeText(this, "Button clicked", Toast.LENGTH_LONG).show(); |
6 |
}
|
7 |
});
|
Tuy nhiên, hàm lambda trong Kotlin cho phép bạn thiết lập một click listener bằng một dòng code:
1 |
button.setOnClickListener({ view -> toast("Button clicked") }) |
Rồi, điều này trở nên ngắn gọn và dễ đọc hơn nhiều, nhưng chúng ta có thể nâng cao hơn nữa—nếu một hàm nhận một hàm khác làm tham số cuối cùng, thì bạn có thể truyền nó bên ngoài danh sách dấu ngoặc đơn:
1 |
button.setOnClickListener() { toast("Button clicked") } |
Và nếu hàm đó chỉ có một tham số là một hàm, thì bạn có thể xoá bỏ hoàn toàn các dấu ngoặc đơn:
1 |
button.setOnClickListener { toast("Button clicked") } |
Các Hàm Mở rộng
Tương tự như C#, Kotlin cho phép bạn thêm các chức năng mới vào các lớp hiện tại mà bạn sẽ không thể sửa đổi. Vì vậy, nếu bạn nghĩ rằng một lớp thiếu một phương thức hữu ích thì tại sao không tự mình thêm nó, thông qua một hàm mở rộng?
Bạn tạo một hàm mở rộng bằng cách thêm tiền tố tên của lớp mà bạn muốn mở rộng vào tên của hàm mà bạn đang tạo.
Ví dụ:
1 |
fun AppCompatActivity.toast(msg: String) { |
2 |
|
3 |
Toast.makeText(this, msg, Toast.LENGTH_LONG).show() |
4 |
}
|
Xin lưu ý rằng, từ khoá this
bên trong hàm mở rộng tương ứng với đối tượng của AppCompatActivity
mà .toast
được gọi.
Trong ví dụ này, bạn chỉ cần import các lớp AppComptActivity
và Toast
vào tập tin .kt của bạn và sau đó bạn đã sẵn sàng để gọi ký hiệu .
trên các đối tượng của lớp được mở rộng.
Khi đề cập đến phát triển Android, bạn có thể thấy các hàm mở rộng đặc biệt hữu ích trong việc việc cung cấp cho các ViewGroup
khả năng tự inflate, ví dụ:
1 |
fun ViewGroup.inflate( |
2 |
@LayoutRes layoutRes: Int, |
3 |
attachToRoot: Boolean = false): View { |
4 |
|
5 |
return LayoutInflater |
6 |
.from(context) |
7 |
.inflate(layoutRes, this, attachToRoot) |
8 |
}
|
Vì vậy, thay vì phải viết như sau:
1 |
val view = LayoutInflater |
2 |
|
3 |
.from(parent) |
4 |
.inflate(R.layout.activity_main, parent, false) |
Bạn chỉ cần sử dụng hàm mở rộng của bạn:
1 |
val view = parent.inflate(R.layout.activity_main) |
Đối tượng Singleton
Trong Java, việc tạo ra các singleton đã trở nên khá dài dòng, yêu cầu bạn tạo một lớp với một hàm xây dựng riêng tư và sau đó tạo ra đối tượng đó như là một thuộc tính riêng tư.
Kết quả cuối cùng thường là như sau:
1 |
public class Singleton { |
2 |
|
3 |
private static Singleton singleton = new Singleton( ); |
4 |
|
5 |
private Singleton() { } |
6 |
|
7 |
public static Singleton getInstance( ) { |
8 |
return singleton; |
9 |
}
|
Thay vì khai báo một lớp, Kotlin cho phép bạn định nghĩa một đối tượng duy nhất, có nghĩa là về mặt ngữ nghĩa giống như một singleton, trong một dòng code:
1 |
object KotlinSingleton {} |
Sau đó bạn có thể sử dụng singleton này ngay lập tức, ví dụ:
1 |
object KotlinSingleton { |
2 |
fun myFunction() { [...] } |
3 |
}
|
4 |
KotlinSingleton.myFunction() |
Tóm tắt
Trong bài viết này, chúng ta đã tìm hiểu sâu về thiết kế tránh null (hay còn gọi là null safety) của Kotlin và đã học được cách để đảm bảo rằng các dự án Android của bạn vẫn có thể tránh null ngay cả khi chúng có chứa một hỗn hợp code Java và Kotlin bằng cách sử dụng một loạt các toán tử đặc biệt. Chúng ta cũng đã khám phá cách bạn có thể sử dụng biểu thức lambda để đơn giản hóa một số tác vụ phát triển thông thường trong Android, và cách thêm chức năng mới vào các lớp hiện có.
Kết hợp với những gì mà chúng ta đã học được trong Phần 1: Java so với Kotlin và Phần 2: Làm quen, giờ đây bạn đã có mọi thứ cần thiết để bắt đầu xây dựng các ứng dụng Android hiệu quả bằng ngôn ngữ lập trình Kotlin.
Hãy thưởng thức ngôn ngữ Kotlin, và tại sao không xem qua một số các khóa học và hướng dẫn khác về lập trình Android?