Android

Các Exceptions thường gặp khi lập trình Java và Android

Ai khi mới bắt đầu viết những dòng code Android đầu tiên thì cũng ít nhất trải qua một lần nhìn thấy cái Dialog rùng rợn “Unfortunately, app has stopped” hay “Rất tiếc, ứng dụng đã dừng lại” nếu ngôn ngữ thiết bị là Tiếng Việt. Lúc đó, tâm trí của người viết code rất là hoang mang vì không biết mình sai chỗ nào để mà sửa. Cách đơn giản nhất, là đọc phần Log trong console, vì theo mặc định thì mỗi lần ứng dụng bị dừng do hàm “quăng-ném-thảy” (từ gốc là throw) ra tên Exception (Ngoại lệ) thì nó sẽ log ra tên của Exception đó và lí do tại sao lại một hàm tại ví trí class và dòng nào lại “ném” ra cái ngoại lệ đó để bạn có hướng khắc phục. Do đó, bạn phải có một trình độ Tiếng Anh tốt. Tuy nhiên, phòng bệnh hơn chữa bệnh, chúng tôi sẽ đưa ra một vài Exception cụ thể và cơ bản nhất mà các bạn hay gặp, đặc biệt là các bạn có câu cửa miệng là “Em mới học” để tránh cũng như có hướng điều chỉnh cho phù hợp.

Unfortunately-app-has-stopped

1. Exception là gì?

Vào ngày xửa ngày xưa, xưa ơi là xưa, xưa thật là xưa, xưa quá là xưa, có một phú ông và một anh gia đinh. Một hôm, phú ông gọi anh gia đinh lại, và sai anh đi mua rượu mà không đưa anh đồng nào ngoài cái chai rỗng. Khi trả lời câu hỏi “Tiền đâu” của anh, ông đáp “Không tiền mà mua được rượu mới là người tài chứ”. Một lát sau, anh về, đưa lại cái chai rỗng cho phú ông, với lời đáp “Chai không mà uống rượu được mới là người tài chứ”.

Nếu xét dưới góc độ lập trình thì hai câu nói hay trên sẽ throw ra 2 cái Exception. Exception được thrown ra khi xuất hiện một trường hợp mà theo “tạo hóa” của ngôn ngữ lập trình, hoặc theo ý của lập trình viên, là không có cách xử lí. Chẳng hạn như tình huống “chai không mà uống rượu” kia. Ở ngoài đời thực thì quá hiển nhiên rồi, bạn không có cách xử lí, và trong chương trình được thực thi, thì nó sẽ throw ra Exception. Một trường hợp khác là do lập trình viên cố tình cho throw một Exception trong một tình huống cố ý rõ rệt để là hạn chế việc code chạy sai theo hướng không mong muốn.

Ví dụ:

Khi một hàm “quăng” ra một Exception thì mọi hoạt động về sau của chương trình sẽ bị dừng lại để tránh các sai sót làm code chạy bị sai, và cái Dialog rùng rợn bên trên sẽ xuất hiện và “ám toán” bạn cho tới khi bạn xanh mặt lên thì mới thôi. “Thật là khủng khiếp quá đi!”.  :mrgreen:  :mrgreen:  :mrgreen:  :mrgreen:  :mrgreen:

Số loại Exception và nguyên nhân thì nhiều vô số kể, nhưng đối với các bạn mới bắt đầu, thì những loại Exception dưới đây là phổ biến hơn cả.

2. NullPointerException

“Chai không mà uống rượu được” là ví dụ thực tế cho Ngoại lệ này. Một Object (không phải là kiểu dữ liệu nguyên thủy – primitive – như int, long) khi vừa khai báo xong mà không làm gì thêm, thì giá trị mặc định của nó là null. Chẳng hạn bạn có dòng dưới đây:

Nếu không làm gì khác, thì theo mặc định, mList sẽ có giá trị null, tức là tương đương với:

NULL là một trạng thái mang tính “hư vô”, tức là giá trị này là dạng “có cũng như không”, tương tự như lượng rượu trong cái chai rỗng vậy. Và vì bạn không uống được rượu trong cái chai rỗng, mọi Object có giá trị null thì không thể thực hiện được bất kì thao tác nào, ngoại trừ việc được gán giá trị mới. Nếu bạn gọi thực hiện một method nào từ đối tượng này, nó sẽ throw NullPointerException. Chẳng hạn:

Bảo đảm nó sẽ ném cái ngoại lện đó ra và bạn sẽ thấy cái Dialog rùng rợn kia sau khi chạy ứng dụng ngay và luôn.

Cách khắc phục: Như bạn đã biết rồi, để gán giá trị cho một Object, thì ta có thể khởi tạo qua Constructor, với cách làm này thì Object của bạn không bao giờ bị null cả, cho dù là nó chưa chứa bất kì nội dung nào. Chẳng hạn:

Cho dù là ArrayList này có dung lượng (capacity) là 0 đi nữa thì nó cũng sẽ không bao giờ ném ra Ngoại lệ NullPointerException, vì ta (tạm) nghĩ nó không còn là “hư vô” nữa. Hoặc có cách khác là bạn gán giá trị cho nó bằng một Object tương đương. Lưu ý là Object tương đương này có thể bị null và kéo theo Object bạn đang thao tác cũng bị null theo. Do đó, phải đảm bảo là Object tương đương đó phải không được null, hoặc trước khi thực hiện thao tác trên Object đó, bạn phải kiểm tra coi nó có bị null không. Chẳng hạn:

3. ActivityNotFoundException, UnknownServiceException, ReceiverCallNotAllowedException, ProviderNotFoundException.

Đã làm ứng dụng Android, thì nếu bạn hỏi “AndroidManifest.xml” là cái con cá con tôm gì thì tốt hơn là bạn nên bỏ ý định viết ứng dụng Android vì đó là điều cơ bản và sơ đẳng nhất. Manifest, dịch sang tiếng Việt là “Bảng kê khai”, là một text file chứa tất cả các thông tin cơ bản, kê khai tất cả các thành phần trọng yếu (app components) trong ứng dụng của bạn. Nếu ứng dụng của bạn có ít nhất một trong 4 thứ là Activity, Service, BroadcastReceiverContentProvider thì bạn phải kê khai hết tất cả các thành phần đó trong Manifest.

Thế nhưng, đôi lúc trong Android Studio, bạn không chọn New -> Activity để thêm một Activity mới. Thay vào đó, bạn chỉ tạo một class cho extends Activity (hoặc 1 sub class của Activity như AppCompatActivity hay ListActivity chẳng hạn), tự @Override phương thức onCreate(Bundle savedInstanceState) và tự viết code. Rồi trong một class khác, bạn cho startActivity(Intent intent) với tham số thứ hai trong Intent constructor là cái class bạn mới vừa tạo. Rồi, nhìn ngọt canh ngon cơm rồi đó, cho chạy thử thôi. Và rồi cái Dialog quỷ quái kia lại xuất hiện. Đọc log thì bạn sẽ thấy cái Ngoại lệ ActivityNotFoundException được ném ra. Chuyện gì đã xảy ra vậy?

Đơn giản thôi, tất cả các Activities có thể truy cập được đều phải được khai báo trong Manifest, để khi cài đặt ứng dụng từ tập tin APK, Hệ thống Android của thiết bị đích sẽ đọc qua AndroidManifest để nắm số lượng các Activities, Services, BroadcastReceivers và ContentProviders của ứng dụng của bạn, hệt như đi học quốc phòng thì nhà trường của bạn sẽ chuyển giao hết danh sách các sinh viên trong các lớp cho trường quân sự để họ nắm hết số lượng người, giới tính, ngày sinh… của các bạn.

Khi các chú, các anh bộ đội phát hiện một thành viên lạ không có trong danh sách mà trường học của bạn gửi cho thì họ sẽ… làm những điều mà họ phải làm. Tương tự như vậy,  khi Hệ thống Android được yêu cầu gọi 1 Activity, 1 Service, 1 BroadcastReceiver hay 1 ContentProvider không được khai báo trong Manifest thì nó sẽ cho dừng ứng dụng ngay và luôn, và trước khi dừng thì nó sẽ “quẳng” ra các ngoại lệ tương ứng.

Cách khắc phục: Đơn giản là khai báo đầy đủ các thành phần đó, chí ít là những cái sẽ được gọi, vào Manifest.

4. Các SQL(ite)Exception, chẳng hạn SQLiteConstraintException, SQLiteCantOpenDatabaseException.

Cái tên nói lên tất cả, các Exception này được ném ra khi có một trục trặc diễn ra trong quá trình thao tác với SQL(ite)Database. Chẳng hạn SQLiteConstraintException được ném ra khi các SQL(ite) constraints của bạn bị sai, còn nếu bạn thấy SQLiteCantOpenDatabaseException thì là do ứng dụng không mở được một tập tin SQLite Database được chỉ định – có thể là do tập tin đó nằm trong ExternalStorage (xin xem Environment) mà bạn chưa yêu cầu quyền WRITE_EXTERNAL_STORAGE trong Manifest. Tuy nhiên, phổ biến nhất là SQLiteException(“No such table”) và điều này khá thô lỗ, giống như ai đó yêu cầu bạn vào siêu thị Thế giới di động để mua thịt, làm sao bạn có thể mua vì trong đó có ai bán thịt đâu! Tương tự như vậy, nếu bạn cố tình INSERT dữ liệu vào một TABLE không có trong SQLite Database của bạn thì hệ thống sẽ ném ra ngoại lệ này với hàm ý “Có cái bảng nào với tên đó đâu mà bắt tôi chèn dữ liệu vào?”.

Cách khắc phục: Không có cách khắc phục nào khác ngoài việc tự kiểm tra lại các câu schema, các phương thức, và hơn nữa là các quyền trong Manifest.

5. ParseException

Ngoại lệ này được ném ra khi hệ thống không parse được URL từ String, chẳng hạn bạn nhập String url = “Hello world” và dùng Uri#parse để chuyển String đó thành địa chỉ trang web để mở nó trong trình duyệt hoặc WebView. Đây cũng là một ngoại lệ rất thô lỗ và bạn phải kiểm tra lại (các) String về URL, mà phần nhiều là do bạn quên một vài kí tự nào đó.

6. ClassCastException

Type casting là một điều hoàn toàn bình thường và việc thao tác với nó diễn ra như cơm bữa. Quen thuộc nhất chính là hàm Activity#findViewById(int id) return View hoặc View#findViewById(int id) return View mà bạn hay sử dụng để liên kết các thành phần trong layout.xml vào phần Logic trong Java.  Xếp thứ hai là các thao tác với Fragment. Chẳng hạn:

Tuy nhiên, nếu như bạn quên cho Activity implements cái Fragment interface thì Ngoại lệ này sẽ hiện ra cùng với bảng Dialog quen thuộc kia. Đơn giản là cái MyActivity kia đã “dính dáng” gì tới cái MyFragment.Listener đâu mà cast được. Hiện tại, chúng đang là “bà con khác cha và khác ông nội” luôn mà.

Cách khắc phục: Tùy theo từng trường hợp cụ thể mà ta có hướng xử lí phù hợp. Ở đây tôi chỉ đưa ra gợi ý về trường hợp Fragment-Activity và AlertDialog vì tôi thấy chúng phổ biến nhất. Về Fragment-Activity, thì bạn phải xử lí như trong ví dụ bên trên. Còn về AlertDialog, nếu bạn đang sử dụng Theme ứng dụng là AppCompat, thì các class để bạn làm Activity phải extends AppCompatActivity, và khi truyền tham số Context vào trong android.support.v7.app.AlertDialog.Builder(Context), thì bạn phải truyền hẳn 1 instance đại diện cho cả cái class extends AppCompatActivity (thông thường là từ khóa this quen thuộc).

Cũng có một vài trường hợp do các bạn không cẩn thận mà ra, chẳng hạn như trong phần layout.xml, bạn gán ID cho một đối tượng View nào đó, xong trong Java thì bạn lại khai báo và cast nó thành một đối tượng hoàn toàn khác không liên quan. Đối với kiểu này thì bạn phải rà soát lại code từ đầu. Chẳng hạn:

7. NoSuchMethodException

Có một lần tôi gặp trường hợp này khi dùng thử ứng dụng mã nguồn mở của một người khác trên GitHub. Sau khi cài đặt xong tập tin APK của anh ấy cung cấp sẵn thì ứng dụng bị dừng với Exeption “hi hữu” này. Lạ lùng thay là Exception báo không tìm thấy 1 method trong 1 class của anh ấy tự viết, và bình thường thì lẽ ra Gradle (của anh ấy) đã phải không build được file APK rồi. Nên tôi cho rằng có lẽ Gradle của anh đã bị lỗi và build nhầm, vì Gradle của tôi báo đúng lỗi sai method và dừng build. Lỗi, rất đơn giản là anh ấy gõ sai tên method.

Cái tên nói lên tất cả, Exception này phát sinh khi bạn gọi một hàm không có trong (các) class của Object đó. Chẳng hạn:

Rõ ràng là trong SomeClass không hề có method nào có tên doSomething() và bản thân SomeClass không hề extends bất kì một class nào khác. Và nếu bạn cố tình yêu cầu SomeClass instance thực hiện doSomething() thì rõ ràng Android Studio sẽ báo lỗi và tô đỏ các chữ, và dù bạn bắt nó generate APK thì nó cũng la oai oái rằng “Đùa tôi à? Có cái method tên ấy đâu mà bảo tôi build?”. Do đó, lỗi này không xuất phát từ các bạn, mà có thể là do IDE của các dev trên GitHub gặp lỗi mà build nhầm.

Tuy nhiên, cũng có thể do ứng dụng được cài đặt lên một thiết bị có mức API nhỏ hơn mức minAPI của bạn, và bản thân các framework classes trong phiên bản Android đó không có hàm mới vừa được đưa vào, và phát sinh Exception này là “một tất yếu khách quan”. Chẳng hạn hàm Context#getColor(int color) chỉ vừa xuất hiện không lâu, trong API 23 trở về sau. Nếu bạn sử dụng hàm này, build APK xong, cài lên thiết bị chạy API 16 thì việc ứng dụng không bị dừng khi gọi hàm trên là điều vô cùng hi hữu.

Tuy nhiên, lỗi lớn nhất là việc dùng Annotations. Giả sử bạn đang override nội dung của một method cho ứng dụng đang có minSdk là 16, trong đó lại gọi một method khác chỉ có trên API23. Vậy là bạn sẽ làm theo cái hướng dẫn của nó là thêm cái if (SDK_INT … ). Tuy nhiên, nhiều lúc bạn lại quên và dùng cái Annotation là RequireApi hay TargetApi cho parent method. Điều này là vô cùng nguy hiểm, vì mặc dù ở các mức APIs thấp vẫn có method này nhưng hệ thống vẫn không gọi tới và thảy ra cái Exception này. Vì sao tôi biết ư? Vì tôi “bị” rồi. Chẳng hạn:

Bảo đảm nhìn “không có gì sai” nhưng chắc chắn khi compile ra thì từ các máy JellyBean tới KitKat sẽ hiện ra cái bảng kia với cái NoSuchMethodException kia, mặc dù mức API nào mà chẳng có onCreate(Bundle). Rất tiếc, quá nhọ.

8. ClassNotFoundException

Cũng tương tự như Ngoại lệ bên trên, lần này là thiếu luôn nguyên cả một class hẳn hoi. Cách khắc phục không có gì khác ngoài việc kiểm tra từng class một trong project của bạn. Và khả năng rất cao là do Annotation. Một trường hợp khác là việc truyền sai tên class khi dùng reflector.

9. ArrayIndexOutOfBoundsException

Exception này hay gặp khi bạn làm ListView + Adapter, nhưng không chỉ giới hạn ở đó. Nguyên nhân là có sự sai khác giữa tổng số lượng các items bạn báo trước cho Adapter “chuẩn bị” trong getItemCount(), tuy nhiên số lượng các items trong ArrayList hay Array nguồn không được đủ con số đó. Ví dụ:

Chắc chắn Ngoại lệ này sẽ được ném ra, vì sau khi hàm getView chạy 5 lần (từ position 0 -> 4) thì mList đã hết, nhưng bạn đã “báo quân số” cho Adapter biết là có tới 10 items, và do vậy hàm getView phải chạy đủ 10 lần. Một tình huống vô cùng trớ trêu.

Vì bản chất của nó là như vậy, thì bạn phải kiểm tra size của List, hay length của Array có đúng với số lượng bạn mong muốn chưa, hoặc tốt hơn hết thì cứ cho getItemCount return ra size hoặc length của List hay Array nguồn.

#########################

Trên đây chỉ là một số Exception thường gặp ở các bạn mới bắt đầu viết ứng dụng. Rất mong sau khi đã đọc bài viết này thì bạn sẽ tránh được những lỗi cơ bản nhất. Và vui lòng đừng chụp ảnh màn hình với cái Dialog rùng rợn đó mà đi hỏi ở các forum là “Máy em nó hiện thông báo này thì làm sao ạ?”, vì tất cả các Exception đều đi kèm với cái bảng đó. Thay vào đó, bạn hãy mở phần console lên, đọc các phần Log mà Hệ thống Android đưa ra, đặc biệt là các dòng chữ màu đỏ. Lúc nào cũng vậy, các dòng đầu sẽ đưa ra tên Exception cùng nguyên nhân của nó, và bạn hãy căn cứ vào đó để điều chỉnh code của mình. Happy coding.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.