Giới thiệu về android

Công ty cổ phần thương mại Vạn Tín Việt

Giới Thiệu Về Android & Môi Trường Phát Triển Phần Mềm

Xin chào các bạn. Bạn đang ở đây và đang đọc các dòng mở đầu cho các bài viết về lập trình Android của mình, chứng tỏ bạn đang quan tâm đến lĩnh vực lập trình cho các thiết bị Android này. Dù cho bạn có đang là một người mới muốn làm quen và thử sức với lập trình Android, hay bạn là người đã có kinh nghiệm nhất định về Android và đang muốn tham khảo thêm các thông tin và ý kiến từ những lập trình viên khác, thì việc bạn chú ý đến các bài viết này, theo mình nghĩ, đều là các quyết định sáng suốt.

Với chuỗi bài viết về lập trình Android này, mình sẽ cố gắng mang đến cho các bạn các thông tin cơ bản và cơ sở nhất, cùng những xây dựng từng bước, giúp các bạn mới làm quen với Android có thể dễ dàng tiếp cận và thực hiện mong ước xây dựng một ứng dụng Android hoàn chỉnh của các bạn. Đồng thời mình sẽ tìm hiểu các bài viết từ các nguồn khác để nói rộng hơn một chút về Android cho các bạn đã hiểu về lĩnh vực này có cơ hội cùng chia sẻ kiến thức.

Tuy cố gắng trong việc xây dựng một bộ kiến thức ổn về Android là vậy, nhưng thực sự với tốc độ phát triển và sự nâng cấp nhiệt tình từ chính Google dành cho hệ điều hành của họ, cũng khiến mình nhiều phen “hụt hơi”“chới với” (trong việc viết và cập nhật trên blog). Do đó nếu bạn có đang đọc các bài viết trong chuỗi bài này của mình mà thấy kiến thức đã cũ, thì vui lòng giúp mình để lại comment ở mỗi bài viết, đồng thời cũng hãy chủ động tìm kiếm thông tin mới mẻ hơn từ các trang web khác hoặc trang “chính chủ” Google nhé, dĩ nhiên mình luôn để link đến trang web gốc ở các mục liên quan rồi.

Hãy bắt đầu nào. Trước tiên, để có thể yêu thích lập trình Android, thì mình mời bạn tìm hiểu về Android trước.

Giới Thiệu Android

Vâng, Android là một hệ điều hành, và hệ điều hành này là một hệ điều hành mã nguồn mở.

Biểu tượng hình chú robot của hệ điều hành Android
Biểu tượng hình chú robot của hệ điều hành Android

Ban đầu hệ điều hành này được xây dựng hướng đến việc sử dụng trên các điện thoại di động thông minh, sau này nó tiếp tục được phát triển để sử dụng rộng rãi trên các máy tính bảng, TV, thiết bị đeo được, xe hơi,…

Liệt kê ra như vậy để bạn thấy rằng, tiếp cận vào lập trình Android không chỉ gói gọn trong một lĩnh vực là viết ứng dụng cho các điện thoại thông minh, mà bạn còn có cơ hội mở rộng hơn kiến thức bản thân cho các nền tảng khác, như TV chẳng hạn. Hơn nữa với việc đây là hệ điều hành mã nguồn mở, bạn có thể còn phát triển được các ứng dụng chạy trên rất nhiều thiết bị phần cứng của nhiều hãng sản xuất khác nhau. Về cơ bản tất cả các hệ điều hành liên quan này đều dựa trên nền tảng Android, nhưng mỗi nhà sản xuất lại đưa vào những cập nhật riêng, mang đến các diện mạo khác nhau đôi chút hoặc rất nhiều tùy từng nhà sản xuất (sự đa dạng này vừa là lợi thế nhưng cũng gây ra các vấn đề tương thích khá hóc búa cho các lập trình viên chúng ta, nhưng không sao, hãy cùng nhau trải nghiệm thôi).

Tại Sao Lại Chọn Lập Trình Android?

Sau khi nhìn “sơ” qua Android là gì, chúng ta đến một bước lựa chọn nữa khi tìm hiểu sâu hơn về Android dưới con mắt của một lập trình viên.

Dễ Tiếp Cận

Có thể nói trở thành một lập trình Android là một quyết định khá dễ dàng, chỉ cần bạn thích và… bùm, bắt tay vào thôi.

Có được điều này thứ nhất phải kể đến Trang thiết bị. Chắc chắn để lập trình thì bạn phải trang bị máy tính rồi, nhưng máy tính nào thì lập trình được Android? Thì có thể nói là, hầu như bạn chẳng cần quá bận tâm về nó. Dù máy tính bạn có chạy được Windows, Mac, Linux hay thậm chí Chrome OS, bạn đều thoải mái bắt đầu với Android. Còn về một điện thoại di động thông minh để kiểm thử ứng dụng ư? Điều đó cũng không quá khó khi mà các điện thoại Android trên thị trường hiện nay đa số đều có giá khá “mềm”, bạn rất dễ để tìm mua một chiếc nếu chưa có thiết bị này trong tay.

Thêm nữa, các lập trình viên Android được Google trang bị cho một công cụ lập trình có tên Android Studio, bạn sẽ được làm quen công cụ này sau. Công cụ lập trình này phải nói là khá mạnh mẽ, và như mình nói ở trên, tương thích được với nhiều loại hệ điều hành, khiến chúng ta vừa dễ cài đặt, dễ tiếp cận, lại yên tâm code vì rõ ràng ở những bước đầu tiên này chúng ta đã cảm nhận được sự trang bị đầy đủ từ Google như nào rồi ha.

Nhìn giao diện hiện đại này của Android Studio bạn có thấy kích thích không?
Nhìn giao diện hiện đại này của Android Studio bạn có thấy kích thích không?

Điều tiếp theo dẫn đến lập trình Android trở nên dễ tiếp cận, đó là Ngôn ngữ lập trình. Nếu bạn đã biết Java, điều đó thật tuyệt, bạn sẽ chỉ cần học thêm về cách thức tương tác với hệ điều hành thông qua Java. Nhưng nếu như bạn chưa từng biết đến Java, thì việc học Java cũng không quá khó khăn, đặc biệt nếu bạn đã từng biết đến một ngôn ngữ lập trình Hướng đối tượng nào khác như C++ hay C# (bạn có thể tìm hiểu về Java qua các bài viết của mình ở đây). Hoặc một lựa chọn tuyệt vời hơn, đó là bạn tìm hiểu luôn Kotlin, một ngôn ngữ khá hiện đại và mạnh mẽ. Tóm lại nếu bạn biết Java hay Kotlin đều có thể bắt tay vào lập trình Android ngay được rồi.

Biết Java hay Kotlin đều có thể lập trình Android
Biết Java hay Kotlin đều có thể lập trình Android

Tính Tương Thích

Mục này cũng có thể hiểu với nhiều ý.

Thứ nhất, có thể thấy sự tương thích nằm ở Hệ điều hành. Android là một hệ điều hành mở, lại là một hệ điều hành nổi tiếng số một trên thế giới hiện nay. Việc Android được ứng dụng trên nhiều lĩnh vực khác nhau mang đến cho bạn có cơ hội suy nghĩ đến việc phát triển một ứng dụng trên thiết bị điện thoại thông minh, lại có thể nhanh chóng mở rộng ra để chạy trên các thể loại thiết bị khác, như TV hay các thiết bị giải trí khác.

Sự tương thích cũng nằm ở Ngôn ngữ lập trình cho hệ điều hành này. Không cần phải nói đến việc ngôn ngữ Java được sử dụng rộng rãi cho các lĩnh vực khác nhau như thế nào vì ai cũng biết, điều này cho thấy nếu bạn từng lập trình Android bằng Java, bạn cũng có thể nhanh chóng ứng dụng logic hay nền tảng của ứng dụng này cho các lĩnh vực khác cũng dùng ngôn ngữ Java. Hay nếu bạn biết Kotlin, bạn cũng dễ dàng phát triển ra các ứng dụng cho iOS hay các ứng dụng Web khác cũng bằng ngôn ngữ Kotlin này.

Ứng Dụng Của Bạn Được Đưa Lên Google Play

Chắc hẳn bạn cũng biết cái “chợ” cho các ứng dụng Android này. Nó được gọi là Google Play, hay Google Play store. Không cần phải là một lập trình viên, bạn vẫn thường xuyên sử dụng nó, và bạn cũng nhận ra rằng Google Play là một nơi chứa vô số các ứng dụng khác nhau.

Giao diện Google Play quen thuộc
Giao diện Google Play quen thuộc

Đúng vậy, mình đã từng nghe Google Play đã cán mốc 2,6 triệu ứng dụng được đưa lên đây. Và bạn hãy tưởng tượng mà xem, trong số 2,6 triệu ứng dụng này, cũng có vài ứng dụng của bạn trên đó thì sao. Khi đó, có khi có đến hàng trăm, hoặc hàng ngàn, hoặc hơn nữa, người dùng trên khắp thế giới đang sử dụng ứng dụng của bạn hàng ngày thì sao. Nghĩ thôi đã thấy thích rồi.

Ngoài việc dễ dàng tải ứng dụng xuống, thì việc đưa ứng dụng lên Google Play cũng khá dễ dàng. Bạn chỉ cần bỏ ra $25 cho một lần duy nhất trong đời để mua tài khoản trên đây, rồi sau đó thoải mái phát triển các ứng dụng của riêng bạn và xuất bản.

Thêm nữa, bên cạnh có nhiều người dùng trên thế giới biết đến và sử dụng ứng dụng của bạn, bạn còn có khả năng kiếm thêm tiền nhờ vào việc tận dụng hiển thị quảng cáo trong ứng dụng, hoặc xây dựng các chức năng có trả phí, hoặc xây dựng các ứng dụng cần người dùng phải mua mới dùng được. Thật tuyệt đúng không nào.

Chưa hết, khi bạn đã quyết định đưa ứng dụng lên Google Play thì chỉ mất vài giờ để ứng dụng của bạn đến với người dùng trên toàn thế giới (con số này so với Apple App Store là vài ngày, thậm chí có khi đến cả tuần). Việc đưa các bản cập nhật hay sửa lỗi lên Google Play chính vì vậy cũng khá là nhanh chóng và dễ thở hơn nhiều.

Môi Trường Phát Triển Phần Mềm

Chúng ta đã hiểu về Android qua tìm hiểu các ý trên đây rồi. Nhưng bài hôm nay sẽ không dừng lại sớm vậy, hãy cùng xem thêm chút nữa về cái gọi là Môi trường phát triển phần mềm là gì nhé.

Môi trường phát triển phẩm mềm là một môi trường mà ở đó nhà Phát Triển Phần Mềm có được những công cụ cần thiết nhất để viết ra một ứng dụng hoàn chỉnh. Vì bài học liên quan đến Android, do đó chúng ta sẽ tập trung vào tìm hiểu Môi trường phát triển phần mềm Android (Android Development Environment) sẽ bao gồm những công cụ cần thiết gì tiếp theo đây.

Hệ Điều Hành (Operating System)

Như mình có nói trên kia, ứng dụng Android có thể được lập trình trên hầu hết các nền tảng hệ điều hành phổ biến nhất hiện nay như Windows, Mac, Linux hay thậm chí Chrome OS.

Dù bạn có đang dùng hệ điều hành nào thì cũng đừng quá lo lắng
Dù bạn có đang dùng hệ điều hành nào thì cũng đừng quá lo lắng

Java Development Kit (JDK)

Bộ Công Cụ Phát Triển Cho Java, chắc chắn rồi, vì ứng dụng Android được viết dựa trên ngôn ngữ Java mà, do đó chúng ta cần phải có bộ JDK này để các công cụ khác có thể dùng nó để biên dịch ra mã Java, rồi từ Java sẽ biên dịch tiếp thành các mã máy. Ngay cả như nếu bạn dùng Kotlin để lập trình Android, thì bạn vẫn cần đến bộ JDK này, vì Kotlin vẫn tận dụng máy ảo JVM bên trong JDK mà thôi.

JDK
JDK

Android Software Development Kit (Android SDK)

Tương tự như JDK, Android SDK là Bộ Công Cụ Phát Triển Cho Android. SDK này sẽ cung cấp cho chúng ta một bộ các thư viện và công cụ cần thiết để chúng ta có thể build, kiểm tra và debug cho các ứng dụng Android mà chúng ta sắp lập trình đây.

Android SDK
Android SDK

Android Studio

Cuối cùng chúng ta phải cần công cụ này, như đã nói ở trên, đây là công cụ mà chúng ta sẽ tương tác trực tiếp và dài lâu. Android Studio cung cấp cho chúng ta một giao diện trực quan để chúng ta có thể viết code, chỉnh sửa, biên dịch, debug, quản lý bộ nhớ,… tất cả mọi thứ cần thiết để chúng ta có thể tạo nên một phần mềm trên đó.

Android Studio
Android Studio

Kết Luận

Chúng ta vừa có cái nhìn sơ lược nhất về việc xây dựng ứng dụng trên hệ điều hành Android này. Hi vọng các thông tin căn bản này giúp tạo cho bạn một kiến thức và một động lực nhất định để xây dựng ước mơ lập trình trên nền tảng di động của chính bạn.

Cài Đặt Các Công Cụ Phát Triển Cho Android

Chúng ta bắt đầu bắt tay vào xây dựng một môi trường lập trình Android qua các bước sau đây.

Cài Đặt JDK

Như mình đã nói ở bài trước, JDK chính là chữ viết tắt của Java Development Kit – Bộ Công Cụ Phát Triển Cho Java. Nó cũng là một công cụ trong Bộ Công Cụ Phát Triển Cho Android. Vì JDK giúp chúng ta có thể biên dịch sang mã Java, rồi từ Java sẽ biên dịch tiếp thành các mã máy, và thực thi chúng trên nền máy ảo JVM. Và nếu bạn nào đang lập trình Android với Kotlin chứ không phải Java, thì bạn vẫn phải download và cài đặt bộ công cụ này trước tiên.

Để cài đặt JDK bạn hãy vào trang của Oracle để download file cài đặt theo đường link này (hoặc bạn có thể search Google với từ khóa “JDK Download”). Khi mở link bạn sẽ được dẫn đến trang download JDK của Oracle như sau.

Trang download JDK của Oracle
Trang download JDK của Oracle

Chú ý

Có thể giao diện và chức năng ở trang download JDK của Oracle có sự khác nhau giữa máy tính của bạn và screenshot của mình do Oracle luôn nâng cấp và chỉnh sửa giao diện của họ. Đồng thời phiên bản JDK cũng có thể khác với hướng dẫn ở thời điểm này của mình do thư viện này cũng được nâng cấp thường xuyên.
Nếu có bất cứ khó khăn nào khiến bạn không thể hoàn thành việc download và cài đặt JDK thì hãy để lại lời nhắn cho mình ở cuối bài viết hoặc chat với Facebook fan page nhé.

Bạn hãy lựa chọn version của JDK (thường thì bạn cứ chọn version mới nhất) và gói cài đặt phù hợp với hệ điều hành của máy tính bạn đang dùng (với macOS hay Windows mình khuyên bạn nên download gói Installer). Như với máy mình thì mình sẽ chọn như sau.

Lựa chọn version của JDK và gói cài đặt phù hợp với hệ điều hành máy tính
Lựa chọn version của JDK và gói cài đặt phù hợp với hệ điều hành máy tính

Sau khi download JDK về thì bạn tiến hành cài đặt như bình thường nhé, khi cài đặt xong bạn có thể qua bước tiếp theo.

Cài Đặt Android Studio

Nếu bạn nào để ý chút, là sau khi cài đặt JDK, hiển nhiên chúng ta cũng cần phải cài Android SDK, như bài trước cũng có nhắc đến, nó chính xác là một Bộ Công Cụ Phát Triển Cho Android. Nhưng thực ra bạn hãy yên tâm, với bước này, ngay sau khi cài đặt Android Studio thành công, công cụ này cũng sẽ giúp chúng ta cài đặt và quản lý Android SDK một cách tự động. Android Studio quả là tuyệt vời mà chúng ta sẽ cùng tìm hiểu thêm các chức năng khác của nó ở các bài học tiếp theo nữa.

Nhiệm vụ bây giờ của chúng ta là vào link này để download Android Studio. Phiên bản Android Studio ở thời điểm mà bài viết này được chỉnh sửa có tên Electric Eel.

Trang download Android Studio Electric Eel
Trang download Android Studio Electric Eel

Chú ý

Cũng như JDK. Có thể giao diện, chức năng và phiên bản Android Studio ở trang download có sự khác nhau giữa máy tính của bạn và screenshot của mình do Goole cũng thường xuyên nâng cấp và chỉnh sửa giao diện của trang Web cũng như của ứng dụng Android Studio này.
Nếu có bất cứ khó khăn nào khiến bạn không thể hoàn thành việc download và cài đặt Android Studio thì hãy để lại lời nhắn cho mình ở cuối bài viết hoặc chat với Facebook fan page nhé

Khi vào đến trang download trên, hãy tìm nút Download và tiến hành cài đặt Android Studio về máy nhé.

Chú ý

Bạn cũng nên để ý đến cấu hình máy tính tối thiểu để có thể cài đặt và sử dụng Android Studio, mình xin liệt kê lại cho bạn nắm như sau.

  • Nếu là Windows thì phải là các phiên bản 8/10/11 64-bit (hoặc các phiên bản mới hơn sau này).
  • Nếu là macOS thì phải từ phiên bản 10.14 trở lên, chip Intel hay Apple đều được.
  • Nếu là Chrome OS thì thiết bị phải chạy với chip Intel i5 trở lên.
  • Nếu là các hệ thống Linux khác thì phải từ phiên bản 2.31 trở lên của thư viện GNU C (glibc).
  • Máy phải trang bị tối thiểu 8GB RAM.
  • Đĩa cứng phải còn trống khoảng 8GB.
  • Độ phân giải màn hình không quá bắt buộc nhưng tốt nhất cũng nên tối thiểu 1280 x 800.

Sau khi cài xong bạn mở ứng dụng Android Studio lên.

Lần đầu tiên mở Android Studio, có thể bạn sẽ được hỏi như hình bên dưới với 2 tùy chọn.

  • Tùy chọn 1: “I want to import…”. Nếu bạn đã từng cài Android Studio trước đó rồi, thì tùy chọn này sẽ mặc định được chọn, vì bạn sẽ có cơ hội dùng lại tất cả các cài đặt của bản trước.
  • Tùy chọn 2: “I do not have…”. Có nghĩa là đây là lần đầu máy tính của bạn cài đặt Android Studio. Nó sẽ chọn sẵn tùy chọn này cho bạn.

Bạn cứ chọn lựa, nhưng chúng cũng không quan trọng lắm. Dù tùy chọn nào được chọn, bạn cũng có thể nhấn OK để đi tiếp.

Các tùy chọn ban đầu bạn có thể gặp
Các tùy chọn ban đầu bạn có thể gặp

Bây giờ bạn đến màn hình Welcome, hãy nhấn Next.

Màn hình Welcome
Màn hình Welcome

Tiếp theo bạn sẽ phải chọn loại gói cài đặt nào, chúng ta cứ để như mặc định gói Standard. Sau đó bạn lại nhấn Next.

Chọn lựa gói
Chọn lựa gói

Bước kế tiếp là bước chọn Theme. Với dân lập trình, thường thì mình thấy họ rất thích Theme Dracula. Hay còn gọi là dark mode. Với Theme này thì mọi thứ sẽ tối thui ngoại trừ code của bạn phát sáng lên để bạn dễ dàng tập trung vào code hơn, đồng thời cũng giảm tình trạng mỏi mắt khi code lâu nữa đấy. Và từ giờ trở đi mình cũng sẽ dùng theme Dracula này.

Chọn theme
Chọn theme

Đến màn hình cuối cùng, hãy nhấn Finish để bắt đầu cài đặt.

Bắt đầu cài đặt
Bắt đầu cài đặt

Có thể mất từ 5 đến 10 phút tùy cấu hình máy cho việc cài đặt. Sau khi tiến trình cài đặt hoàn tất, bạn sẽ đến màn hình sau.

Màn hình Welcome
Màn hình Welcome

Đến bước này thì mình xin chúc mừng bạn, bạn đã tạo được cho mình một môi trường để lập trình Android rồi đó. Bạn có thấy dễ không nào. Nếu bạn nào còn vướng mắc gì đó thì nhớ để lại bình luận bên dưới để mình có thể hỗ trợ nhé.

Tạo Mới Project & Làm Quen Với Android Studio

Tạo Mới Project

Bạn thân mến, trước hết mình xin nói sơ qua điều này một tí. Để cho các bài giảng được mạch lạc và logic với nhau, mình xin giới thiệu với bạn một project lớn mà với nó, mình và các bạn sẽ cùng nhau đi xuyên suốt qua các bài giảng trong chương trình. Project của chúng ta có tên là TourNote. Mình sẽ nói rõ về project này ở bài kế tiếp, còn bây giờ, đừng chần chừ nữa, hãy cùng mình tạo project TourNote bằng Android Studio qua các bước sau đây nhé.

Đầu tiên nếu chưa mở Android Studio thì bạn hãy mở lên. Sau khi đã mở Android Studio xong, nếu bạn đang ở màn hình Welcome như dưới đây, thì hãy đảm bảo danh sách bên trái như hình dưới đang chọn là Projects, sau đó chọn tùy chọn New Project ở thành phần bên phải.

New Project từ màn hình Welcome
New Project từ màn hình Welcome

Còn nếu bạn đang ở màn hình chính của Android Studio thì hãy nhấn vào menu File > New > New Project… như hình dưới đây.

New Project từ màn hình chínhhttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2023/02/Screenshot-2023-02-20-at-11.31-1.png?resize=300%2C233&ssl=1 300w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2023/02/Screenshot-2023-02-20-at-11.31-1.png?resize=1024%2C797&ssl=1 1024w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2023/02/Screenshot-2023-02-20-at-11.31-1.png?resize=768%2C597&ssl=1 768w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2023/02/Screenshot-2023-02-20-at-11.31-1.png?resize=1536%2C1195&ssl=1 1536w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2023/02/Screenshot-2023-02-20-at-11.31-1.png?resize=1099%2C855&ssl=1 1099w" data-lazy-loaded="1" sizes="(max-width: 1000px) 100vw, 1000px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px; vertical-align: bottom;">
New Project từ màn hình chính

Bắt đầu từ cửa sổ New Project xuất hiện tiếp theo như hình bên dưới. Bạn sẽ được đưa đến các câu hỏi liên quan đến project bạn sắp tạo ra. Vì như mục này của bài trước có nói rằng ngoài việc bạn có thể lập trình ra các ứng dụng trên điện thoại, thì Android còn có thể chạy trên TV, Wearable, Xe hơi… các kiểu. Và bạn hoàn toàn có thể sử dụng Android Studio để viết các ứng dụng ở các nơi mà Android có mặt này. Chính vì vậy việc trả lời các câu hỏi từ những bước đầu tiên này khá là quan trọng.

Ở bước đầu tiên này bạn sẽ lựa chọn một Template, ở bên dưới mình sẽ giải thích chút về Template sau. Còn bước này bạn hãy đảm bảo tùy chọn Phone and Tablet ở danh sách bên trái được chọn như hình sau.

Tùy chọn Phone and Tablet
Tùy chọn Phone and Tablet

Bạn có thể thắc mắc rằng ngoài Phone and Tablet thì các tùy chọn khác bên trái khác là gì, mình xin giải thích một lượt như sau.

  • Phone and Tablet: chọn mục này nếu bạn muốn viết ứng dụng chạy trên điện thoại và máy tính bảng. Và bạn đã hiểu tại sao TourNote của chúng ta lại chọn tùy chọn này.
  • Wear OS: chọn vào đây nếu bạn muốn viết ứng dụng chạy trên các thiết bị đeo được, chẳng hạn như đồng hồ thông minh (smart watch) hay các thiết bị kiểm tra sức khỏe khác.
  • Android TV: chắc chắn là viết ứng dụng chạy trên smart TV rồi.
  • Automotive: viết ứng dụng chạy được trên thiết bị nghe nhìn của xe hơi.

Ở phiên bản cũ hơn của Android Studio mình còn thấy có tùy chọn Android Things nữa nhưng phiên bản Android Studio Electric Eel đã không còn thấy. Tùy chọn này giúp bạn có thể tạo ra các project chạy được trên các thiết bị IoT (viết tắt của Internet of Things). Tiếng Việt gọi là Vạn vật kết nối. Các thiết bị IoT đều có thể kết nối và chia sẻ dữ liệu qua lại với nhau. Các project ở tùy chọn này thuộc một lĩnh vực khá mới mẻ mà thú thật mình cũng chưa có dịp tìm hiểu kỹ.

Quay lại với tab Phone and Tablet của chúng ta. bạn có thể nhìn thấy khá nhiều các màn hình mẫu trong tab này, như giới thiệu trên kia, người ta gọi đây là các Template. Bạn sẽ phải chọn cho mình một Template trong số đó. Với mỗi lựa chọn Template này, source code được tạo ra ban đầu từ Android Studio sẽ khác nhau. Các source code tự sinh ra này đảm bảo rằng nếu bạn vừa tạo xong project và thực thi ứng dụng lên thiết bị ngay và luôn, thì giao diện của ứng dụng sẽ đúng như Template mà bạn đã chọn đó. Tuy nhiên, là người mới bắt đầu, hoặc không muốn phiền nhiễu về các source code dựng sẵn, thì bạn nên tìm và chọn Template Empty Activity. Template này chỉ đơn giản tạo sẵn một TextView bên trong project để hiển thị sẵn một câu chào lên màn hình khi bạn thực thi ứng dụng ở bước tiếp theo mà thôi. Nếu bạn đã tìm ra và chọn xong Template Empty Activity rồi thì hãy nhấn nút Next để qua bước tiếp theo.

Bước này muốn bạn khai báo một số thông tin về project. Bạn hãy điền vào các thông tin như hình. Mình sẽ giải thích một chút các thông tin này ở bên dưới hình.

Khai báo các thông tin cho project
Khai báo các thông tin cho project
Tên của ứng dụng sẽ được hiển thị như thế này
Tên của ứng dụng sẽ được hiển thị như thế này
  • Name: là tên của ứng dụng, tên này sẽ xuất hiện ở màn hình chính của thiết bị Android khi người dùng cài đặt ứng dụng của bạn lên đó. Các bạn có thể nhìn vào hình nhỏ bên cạnh sẽ thấy tên của các ứng dụng xuất hiện phía dưới icon của ứng dụng đó. Các bạn có thể viết hoa tên ứng dụng, hay để khoảng trắng tùy thích, nhưng nhớ là đừng quá dài hay quá ngắn, làm sao cho xúc tích và dễ nhớ.
  • Package name: là tên package của ứng dụng (nếu bạn nào từng biết qua Java thì khái niệm package trong Android là tương đương với Java. Nếu bạn muốn tìm hiểu về package của Java thì có thể đọc link này nhé). Ngoài ra thì với Android, package còn là định danh cho từng ứng dụng nữa. Package nên là duy nhất và đặc thù nhất của một ứng dụng, sẽ không thể có hai ứng dụng với cùng một package được cài lên cùng một thiết bị. Vậy đặt tên package như thế nào? Thường thì người ta sẽ đảo ngược tên miền của công ty lại và thêm vào tên của project để tạo thành một package. Như ví dụ bạn thấy, mình có trang web yellowcode.com chẳng hạn, project là TourNote, thì package sẽ lấy là com.yellowcode.tournote, đảm bảo không trùng đúng không các bạn. Còn như bạn là cá nhân, không có trang web cũng chẳng có công ty gì cả, thì có thể lấy tên bạn để tạo package, chẳng hạn như, com.duymanh.tournote, hay com.honghoahoi.tournote tùy thích.
  • Save location: là đường dẫn đến thư mục chứa project của bạn, bạn có thể để mặc định hoặc tạo đường dẫn đến nơi tùy thích trong ổ cứng của bạn.
  • Language: ngôn ngữ mà bạn dùng để viết ứng dụng của bạn. Với TourNote ở các bài học Android này mình sẽ cố gắng diễn đạt ở cả 2 ngôn ngữ Java và Kotlin. Tuy nhiên bạn hãy xác định và chọn cho bạn một ngôn ngữ cụ thể.
  • Minimum SDK: mục này báo cho hệ thống biết ứng dụng được tạo ra sẽ hỗ trợ ngược tối đa đến hệ điều hành cũ nhất nào. Nên nhớ là việc ứng dụng càng hỗ trợ hệ điều hành cũ hơn thì bạn càng phải giải quyết các bài toán tương thích ngược hơn và do đó bạn sẽ càng mất thời gian và đau đầu hơn trong việc phát triển các ứng dụng. Các bạn có thể nhấn vào Help me choose để xem thống kê về số lượng người sử dụng các version của hệ điều hành để có sự chọn lựa tốt nhất cho bước này. Nhưng theo mình, trừ khi có đòi hỏi đặc biệt nào đó, nếu không bạn cứ để mặc định ở tùy chọn này.
  • Cuối cùng bạn sẽ thấy có thêm một checkbox Use legacy android.support libraries: tùy chọn này gây nhiều hoang mang cho những ai mới chập chững bước vào Android. Ban có thể để trống checkbox này và không cần tìm hiểu gì thêm. Nhưng nếu bạn có thắc mắc thì mình cũng diễn đạt sơ thế này. Ở các thế hệ hệ điều hành Android xưa, để giúp các lập trình viên mang các chức năng và các thư viện mới mẻ được giới thiệu ở các hệ điều hành mới ra, hỗ trợ ngược lại cho các ứng dụng chạy trên các hệ điều hành cũ hơn, Android thường đóng gói các thư viện hỗ trợ này vào một gói có tên android.support.*. Kể từ Android 9.0 (API level 28) trở đi Google có trình làng một gói thư viện hỗ trợ khác có tên là AndroidX, gói hỗ trợ này là một phần trong dự án Jetpack. AndroidX này thay thế hoàn toàn android.support.* đã không còn được xây dựng bởi Google nữa. Và dĩ nhiên từ đó về sau chúng ta chỉ nên biết duy nhất một gói AndroidX mà thôi.

Sau khi bạn đã điền tất cả các thông tin đã nói ở trên rồi thì hãy nhấn Finish để kết thúc các tùy chọn. Chúc mừng bạn, Project của bạn đã được tạo thành công rồi đó.

Tổng Quan Về Project

Giao Diện Chung

Giờ đây sau khi tạo mới một project, bạn đang đứng trong giao diện chính của Android Studio, nhìn một cách tổng quan giao diện này được chia làm các phần chính sau.

Tổng quan các thành phần chính của Android Studio
Tổng quan các thành phần chính của Android Studio
  1. Toolbar: thanh công cụ. Nơi đây bạn có được các nút điều khiển chính, chẳng hạn như các nút Mở project (Open), Lưu project (Save All),. Hoặc đặc thù hơn với lập trình có các nút Khởi chạy ứng dụng (Run), Debug ứng dụng (Debug),. Hoặc các quản lý cấp cao như các nút Chạy chương trình quản lý Android SDK (SDK Manager), Chạy chương trình quản lý máy thật và máy ảo (Device Manager),.
  2. Navigation bar: thanh điều hướng. Giúp bạn theo dõi file nào đang được mở, đường dẫn file đó trong project của bạn như thế nào.
  3. Editor window: cửa sổ soạn thảo, là nơi bạn chỉnh sửa các dòng code. Đặc biệt hơn ở cửa sổ này đó là, tùy vào loại source code, cửa sổ này sẽ xuất hiện khác nhau với từng loại để bạn có thể xem và chỉnh sửa source code dễ dàng. Chẳng hạn như khi bạn mở một file java/kotlin code, sẽ khác với khi bạn mở một file XML, và khác với khi bạn mở một file ảnh,… Chúng ta sẽ từ từ làm quen cửa sổ này sau.
  4. Tool window bar: các điều khiển cho các công cụ khác. Các công cụ khác chính là các công cụ cho bạn can thiệp vào các công cụ quản lý của hệ thống. Chẳng hạn như Quản lý log (Logcat), Quản lý quá trình debug (Debug), Quản lý kết quả tìm kiếm (Find), Xem cây thư mục của project (Project),. Tuy nhiên dàn nút trên đây chỉ là cho phép bạn tắt mở các công cụ tương ứng mà thôi. Và sự xuất hiện của mỗi Tool window cũng tùy vào ngữ cảnh mà bạn tương tác. Ví dụ ở hình trên bạn không thấy các Tool window Debug hay Find đâu cả, nhưng khi bạn tìm kiếm hay bắt đầu vào quá trình debug bạn sẽ thấy chúng. Cuối cùng thì nếu nhấn vào mỗi loại công cụ được liệt kê ở đây sẽ được mở ra ở dạng cửa sổ cụ thể như mục số 5.
  5. Tool window: chính là các cửa sổ được điều khiển tắt mở từ thanh số 4 mà mình có nói đến trên đây.
  6. Status bar: thanh trạng thái, hiển thị trạng thái của project và của chính trình biên dịch Android Studio này. Bạn sẽ thấy thông báo ứng dụng đang được thực thi, có thành công không, có lỗi gì không,…

Máy Ảo, Máy Thật & Khởi Chạy Ứng Dụng

Máy Ảo

Máy ảo (Emulator) là một phần mềm giả lập, nó được tạo ra với cấu hình và hoạt động giống như máy thật nhất có thể. Câu hỏi là nếu bạn đã có trong tay một máy thật Android lúc này rồi thì sao? Câu trả lời là: không gì tốt bằng. Nhưng không phải vì vậy mà bạn lại không tạo cho riêng mình một máy ảo. Vì sao? Có hai lý do chính. Lý do thứ nhất, đó là không phải lúc nào bạn cũng dùng máy thật để chạy đi chạy lại ứng dụng mà bạn đang làm dở dang chưa ổn định, điều này có thể làm hư cái máy thật của bạn. Lý do thứ hai, số lượng máy thật của bạn sẽ chỉ có một hoặc rất ít, thì việc bạn có thêm một hay nhiều máy ảo giả lập các cấu hình phần cứng khác mà các máy thật của bạn chưa có, giúp bạn kiểm tra kỹ hơn sự tương thích của ứng dụng trên nhiều phần cứng và màn hình khác nhau trước khi “xuất xưởng”.

Ngày xưa khi nhắc đến máy ảo Android, bạn thường có nhiều cân nhắc chọn lựa giữa các nhà cung cấp máy ảo khác nhau. Nhưng kể từ nâng cấp đáng giá từ Google khiến cho các máy ảo của họ tích hợp các Google service (như Google Play, Google Maps,…) thì chúng ta hoàn toàn có thể tin tưởng và chọn dùng duy nhất một loại máy ảo do chính Google cung cấp. Máy ảo này lại có sẵn khi bạn cài đặt Android Studio. Và Google đặt còn đặt cho nó cái tên không gì có thể khó đoán hơn, đó là AVD (viết tắt của từ Android Virtual Device).

Giới thiệu AVD

Như nói ở trên, AVD là một máy ảo Android được hỗ trợ chính thức từ Google. Vì là bản “chính chủ” nên máy ảo này sẽ có tính ổn định cao. Chẳng hạn như nó sẽ tiêu tốn bộ nhớ của máy tính ít hơn các máy ảo khác. Nó còn hỗ trợ giả lập tất cả các loại thiết bị, từ điện thoại, máy tính bảng, thiết bị đeo được, và kể cả Android TV nữa đấy. Ngoài ra nó còn cho phép chúng ta tùy chỉnh các giả lập, như camera, GPS, kiểu màn hình gập, cảm biến chuyển động,…

Cài Đặt AVD

Đầu tiên, đảm bảo bạn đã mở Android Studio lên rồi. Từ màn hình chính của Android Studio, có hai cách để khởi động AVD, bạn có thể đi từ menu Tools > Device Manager, hoặc tìm kiếm icon  trên thanh công cụ (toolbar).

Thanh quản lý thiết bị (Device Manager) sẽ được mở ra ở một tool window như sau.

Thanh quản lý thiết bị
Thanh quản lý thiết bị

Bạn có thể thấy cửa sổ trên có 2 tab, Virtual và Physical. Vì đây là thanh quản lý thiết bị chung, nên nó cũng giúp quản lý cả các thiết bị ảo (Virtual) hay thật (Physical). Mục này chúng ta tập trung nói về máy ảo, do đó bạn hãy đảm bảo tab Virtual được chọn như hình trên.

Có thể ở máy bạn đã có sẵn các máy ảo được tạo ra trong quá trình cài đặt Android Studio rồi. Nhưng cho dù có hay chưa có máy ảo nào thì mình cũng mời bạn cùng đi qua các bước tạo mới một máy ảo nhé. Đầu tiên bạn hãy tìm và nhấn vào nút Create device, bạn sẽ được dẫn tới một cửa sổ cho bạn chọn các thiết bị giả lập.

Cửa sổ chọn lựa thiết bị
Cửa sổ chọn lựa thiết bị

Chú ý

Hình ảnh của mình và ở máy tính của bạn trong bài học hôm nay có thể có sự khác nhau. Đó là do mình vẫn còn dùng hình cũ khi thấy rằng vẫn có thể dùng được, để giảm thời gian chỉnh sửa lại nội dung bài. Tuy nhiên sự khác biệt này không ảnh hưởng đến trải nghiệm của bạn trên giao diện ứng dụng mới của bạn đâu nhé.

Mình sẽ điểm sơ qua các thành phần của cử sổ này, để giúp bạn có một chọn lựa máy ảo hợp lý (nếu bạn lỡ tạo lầm một máy ảo không ưng ý cũng không sao, bạn cứ tạo và chạy lên thử, nếu không thích hoàn toàn có thể gỡ bỏ và tạo lại máy ảo khác một cách nhanh chóng).

  • Khung bên trái (Category): khung này cho phép bạn chọn loại máy ảo như điện thoại (Phone), máy tính bảng (Tablet), thiết bị đeo được (Wear OS), hay TV, thiết bị trên ô tô (Automotive). Với ứng dụng TourNote, chúng ta sẽ trải nghiệm ứng dụng trên điện thoại trước, do đó bạn hãy chọn Phone như hình trên.
  • Khung lớn ở giữa: chính là các thiết bị giả lập tương ứng với từng loại thiết bị bên trái, có vài thiết bị được tạo ra dựa vào thiết bị thật đang được kinh doanh phổ biến trên thị trường, như các dòng Pixel, Nexus hay Galaxy (hiển thị ở cột Name). Cột Play Store cho biết máy ảo này có hỗ trợ ứng dụng Google Play hay không, nếu có, một icon Máy ảo máy thật - Icon Google Play sẽ xuất hiện, mình khuyến khích các bạn nên chọn các loại máy ảo có xuất hiện icon này. Cột Size cho biết kích cỡ màn hình mà máy ảo giả lập, kích cỡ này tính theo đơn vị inch như ngoài thực tế. Cột Resolution là độ phân giải của màn hình, độ phân giải này được tính theo đơn vị pixel, chính là điểm ảnh theo các chiều ngang & dọc. Density cho biết mật độ điểm ảnh của màn hình, căn cứ vào kích cỡ màn hình (size) và độ phân giải (resolution) mà ta có tỷ lệ tương ứng như mdpihdpixhdpixxhdpi420dpi. Thông số density này bạn sẽ được làm quen ở các bài học sau, chẳng hạn như bài học về dimen này.
  • Khung bên phải: như một tóm tắt trực quan cho chọn lựa của bạn ở các khung khác.

Như hình trên, chúng ta sẽ chọn con máy ảo là điện thoại Nexus 5X, có sẵn Google Play, màn hình 5.2″, độ phân giải 1080×1920, có density là 420dpi. Sau khi bạn đã chọn máy ảo, bạn hãy nhấn Next và xem màn hình kế tiếp.

Cửa sổ lựa chọn hệ điều hành
Cửa sổ lựa chọn hệ điều hành

Đến bước như hình trên đây chính là bước mà bạn sẽ cài hệ điều hành Android vào cho con Nexus 5X ảo mà bạn đã chọn. Lưu ý là bạn phải đảm bảo tab Recommended đang được chọn, đây chính là tab mà hệ thống sẽ gợi ý gói hệ điều hành Android tốt nhất cho bạn. Có khá nhiều loại hệ điều hành, nhưng để dễ dàng kiểm thử xem ứng dụng của bạn trông như thế nào trên các giao diện hệ điều hành mới thì bạn nên chọn loại mới nhất, cho đến thời điểm hiện tại, hệ điều hành mới nhất đáng để trải nghiệm chính là Tiramisu(API Level 33).

À nếu bạn chưa cài đặt gói hệ điều hành mong muốn từ trước, thì nó sẽ xuất hiện nút để bạn download bên cạnh tên hệ điều hành như hình trên (hoặc icon mũi tên hướng xuống với phiên bản Android Studio mới hơn). Đồng thời nút Next ở bước này bị mờ đi, khi đó phải down gói hệ điều hành mà bạn cần về trước. Sau khi nhấn vào nút download thì cửa sổ download và install hệ điều hành sẽ xuất hiện như hình sau.

Cài đặt hệ điều hành Android
Cài đặt hệ điều hành Android

Sau khi download xong, bạn sẽ thấy chọn lựa của chúng ta không còn nút download kế bên nữa, và chúng ta hoàn toàn có thể nhấn nút Next để qua bước cuối cùng.

Bước tiếp theo như hình dưới, sẽ là bước cho bạn vài tùy chỉnh cuối cùng trước khi hoàn thành. Bạn có thể đổi tên máy ảo (ở mục AVD Name), tỷ lệ scale, hiển thị màn hình theo chế độ mặc định là đứng/ngang, dung lượng Ram và bộ nhớ dành cho máy ảo này, camera cho máy ảo,…. Nhưng tốt nhất bạn nên để mặc định và nhấn Finish.

Cửa số tổng kết thông tin máy ảo
Cửa số tổng kết thông tin máy ảo

Sau khi kết thúc quá trình cài đặt bạn sẽ nhìn thấy máy ảo mà bạn vừa tạo sẽ hiển thị trong danh sách máy ảo của tool window Device Manager mà bạn đã mở trước đó. Ở tool window này, bạn cũng có thể tạo thêm nhiều máy ảo khác, hoặc sử chữa hay xóa bất kỳ máy ảo nào đang tồn tại.

Danh sách máy ảo
Danh sách máy ảo

Trường hợp bạn muốn khởi chạy máy ảo lên, hãy nhấn vào nút có hình tam giác. Máy ảo sẽ khởi động lên ở một tool window khác có tên là Running Devices. Cửa sổ này hiển thị tất cả các thiết bị, cả ảo lẫn thật, đang được kết nối với máy tính của bạn mà Android Studio có thể nhận ra.

Hình dạng máy ảo
Hình dạng máy ảo

Chú ý

Có một vài ghi chú đối với việc sử dụng AVD mà mình muốn bạn tham khảo, dành cho người mới làm quen với máy ảo Android.

  • Tuy máy ảo được hiển thị trong một tool window của một project. Bạn có cảm tưởng như máy ảo này chỉ dành riêng cho một project, khi tạo project mới hay khi chuyển qua project khác bạn phải khởi chạy lại máy ảo. Không phải nhé, đây là máy ảo chung của Android Studio, bạn hoàn toàn có thể chuyển đổi từ project này sang project khác nhưng khi mở Running Devices ra thì nó sẽ giống nhau ở các project.
  • Nếu bạn muốn máy ảo hiển thị tách ra khỏi Android Studio như một ứng dụng riêng thay vì chạy trong Running Devices, bạn có thể vào menu của Android Studio File > Settings… (với máy Mac là Android Studio > Settings…), sau đó chọn Tools > Emulator ở danh sách bên trái cửa sổ. Ở các chọn lựa bên phải cửa sổ bạn hãy tìm và bỏ chọn Launch in a tool window. Sau đó thoát và khởi động lại máy ảo bạn sẽ thấy sự khác biệt (đọc ý dưới đây để biết cách thoát máy ảo).
  • Khi thoát Android Studio thì máy ảo cũng sẽ thoát, nó giống như là bạn tắt nguồn điện thoại đi vậy. Nhưng khi khởi chạy lại máy ảo thì việc máy ảo có khởi động lại từ đầu hay mở lại chính màn hình mà bạn vừa “tắt nguồn” sẽ tùy thuộc vào tùy chỉnh Cold boot hay Quick boot mà khi tạo máy ảo bạn đã chọn (mặc định nó sẽ chọn Quick boot cho bạn, tức là nó sẽ mở lại màn hình bạn đang thao tác trước khi thoát máy ảo, bạn hoàn toàn có thể thay đổi chọn lựa này khi Edit máy ảo).
  • Nếu muốn thoát máy ảo trong khi đang mở Android Studio thì bạn có thể nhấn vào dấu X ở tab của máy ảo đang chọn trong cửa sổ Running Devices.

Sau khi khởi động xong, màn hình máy ảo sẽ trông như hình dưới đây. Bạn nên mở ứng dụng Google Play (Play Store) có sẵn trong máy ảo lên và đăng nhập vào tài khoản Gmail của bạn.

Máy ảo khởi động hoàn chỉnh
Máy ảo khởi động hoàn chỉnh

Thực Thi Ứng Dụng Lên AVD

Đảm bảo Android Studio đang mở. Đảm bảo máy ảo AVD vẫn đang mở. Nếu bạn đang ở màn hình Welcome của Android Studio như hình dưới thì chọn vào project TourNote ở danh sách bên phải để vào màn hình chính của Android Studio. Nếu không thấy danh sách ứng dụng nào cả ở cửa sổ này thì bạn có thể chọn Open và tìm đến đường dẫn chứa project TourNote mà bạn đã tạo ở bài trước.

Thực thi ứng dụng từ màn hình Welcomehttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2023/03/Screenshot-2023-03-22-at-14.59.20.png?resize=300%2C222&ssl=1 300w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2023/03/Screenshot-2023-03-22-at-14.59.20.png?resize=1024%2C758&ssl=1 1024w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2023/03/Screenshot-2023-03-22-at-14.59.20.png?resize=768%2C568&ssl=1 768w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2023/03/Screenshot-2023-03-22-at-14.59.20.png?resize=1536%2C1137&ssl=1 1536w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2023/03/Screenshot-2023-03-22-at-14.59.20.png?resize=2048%2C1515&ssl=1 2048w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2023/03/Screenshot-2023-03-22-at-14.59.20.png?resize=1140%2C844&ssl=1 1140w" data-lazy-loaded="1" sizes="(max-width: 1000px) 100vw, 1000px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px; vertical-align: bottom;">
Thực thi ứng dụng từ màn hình Welcome

Khi màn hình chính của Android Studio được mở, bạn hãy đảm bảo tên của máy ảo bạn muốn thực thi ứng dụng lên đó đang được chọn như hình dưới đây. Sau đó bạn có thể nhấn vào nút Run  Máy ảo máy thật - Icon run ở kế bên tên máy ảo trên thanh công cụ (toolbar).

Chú ý chọn máy ảo khi thực thi ứng dụng
Chú ý chọn máy ảo khi thực thi ứng dụng

Sau khi Run thì bạn hãy mở lại tool window Running Devices ra và đợi một phút. Xin chúc mừng, bạn đã thành công với việc khởi chạy ứng dụng đầu tiên của mình lên thiết bị (ảo)!!!

Máy ảo đã khởi chạy TourNote
Máy ảo đã khởi chạy TourNote

Máy Thật

Với máy thật thì dễ dàng hơn nhiều, bạn sẽ không tốn công chọn lựa và cài đặt, việc duy nhất lúc này là bạn cần mở chế độ Developer options mà thôi.

Khi bạn mua máy về, thì khi vào Settings của máy, bạn sẽ không thấy Developer options ở đâu. Đơn giản vì hệ thống đã giấu tùy chọn này, chỉ những nhà phát triển (developer) mới biết cách cho nó hiện ra. Để hiện ta tùy chọn này bạn làm như sau, mở ứng dụng Settingstìm đến mục About phone (một số máy bạn phải vào tiếp mục Software info). Khi đó bạn sẽ thấy mục Build number như hình sau.

Bạn tìm đến Build number trong Settings của thiết bị
Bạn tìm đến Build number trong Settings của thiết bị

Nếu bạn dùng ngôn ngữ tiếng Việt cho điện thoại thì tìm theo Cài đặt > Giới thiệu điện thoại > Số hiệu bản dựng (hoặc đại loại vậy tùy theo bản dịch của từng nhà sản xuất, mình không chắc có khớp với máy của mình hay không).

Lúc này bạn hãy nhấn nhanh nhiều lần lên Build number (hay Số hiệu bản dựng), cho đến khi bạn thấy thông báo dạng toast (một thông báo nhỏ ở phía dưới màn hình) với nội dung Developer mode has been turned on hoặc You are now a developer! thì đã thành công. Khi này quay lại màn hình Settings bạn đã thấy mục Developer options như hình dưới đây.

Bạn đã bật Developer options thành công
Bạn đã bật Developer options thành công

Sau đó bạn hãy vào Developer options, và check chọn USB debugging như hình này.

Nhớ chọn USB debugging
Nhớ chọn USB debugging

Từ giờ trở đi khi bạn cắm thiết bị thật Android vào máy tính thông qua dây cáp, bạn sẽ thấy thay vì tên máy ảo xuất hiện, nơi đây sẽ là máy thật của bạn.

Tên máy thật sẽ xuất hiện ở đây
Tên máy thật sẽ xuất hiện ở đây

Bạn chỉ cần nhấn nút Run Máy ảo máy thật - Icon run giống như bạn khởi chạy với máy ảo ở trên. Đến đây bạn hoàn toàn có thể thấy ứng dụng của mình hiển thị trên máy thật.

Tuy nhiên nếu bạn gặp trục trặc như hai tình huống dưới đây thì bạn phải làm tiếp vài bước nhỏ nữa, còn các bạn nào đã chạy ứng dụng được lên máy thật rồi thì không cần đọc hai ý dưới này nhé.

1. Tôi không thấy tên thiết bị xuất hiện ở toolbar như hình trên. Có thể bạn đang lập trình Android trên Windows và Windows của bạn không có sẵn driver cho thiết bị Android này rồi, bạn đọc bài này để install driver nhé https://developer.android.com/studio/run/oem-usb.html.

2. Tôi không dùng Windows. Hoặc. Tôi đã thử tìm và cài đặt driver cho Windows rồi nhưng vẫn không thấy tên thiết bị xuất hiện. Điều này xảy ra với thiết bị Android lần đầu tiên kết nối với máy tính của bạn, bạn hãy nhìn lại màn hình thiết bị Android xem nếu có xuất hiện hộp thoại sau thì hãy check chọn vào Always allow from this computer rồi nhấn OK như hình dưới nhé, khi đó tên của thiết bị sẽ xuất hiện thôi.

Nhớ chú ý màn hình này trên Android nhé
Nhớ chú ý màn hình này trên Android nhé

Kết Luận

Bạn vừa trải qua một bài học dài về cách thiết lập máy ảo và khởi chạy ứng dụng trên máy ảo và máy thật. Từ giờ bạn đã có thể thoải mái chạy thử ứng dụng để xem kết quả như thế nào rồi nhé. Các bạn có thể chọn cho mình một máy ảo hoặc máy thật để kiểm thử, nhưng các bài học từ đây về sau mình sẽ dùng AVD để chạy ứng dụng và dùng để chụp màn hình ứng dụng cho các bạn cùng xem.

Hướng Dẫn Cài Đặt Google Play Cho Máy Ảo Genymotion

Tại Sao Phải Cài Google Apps Cho Genymotion?

Bởi vì bắt đầu từ bài 4 chúng ta đã làm quen với máy ảo Genymotion rồi. Chắc chắn bạn đã thử sử dụng và đang hài lòng với máy ảo này đúng không nào.

Nhưng chúng ta vẫn cần nâng cấp Google Apps cho máy ảo này, nhằm mục đích cạnh tranh với máy ảo AVD vốn dĩ đã hỗ trợ nhiều hơn các giả lập của Google APIs, ngoài ra thì việc nâng cấp này còn chuẩn bị cho các bài học kế tiếp sau này, để bạn có cái mà demo chạy thử, như các bài học liên quan tới maps chẳng hạn.

Bạn cũng nên biết là bài hôm nay do mình tổng hợp lại trên mạng, và các link cài đặt mình cũng lấy từ các trang đó, do đó sẽ có lúc các link này bị lỗi thời không truy cập được. Vậy nếu có trục trặc gì với link, hoặc bạn có link nào khác hay hơn thì cứ để lại cho mình bình luận bên dưới bài hôm nay nhé.

Mình cũng có một video demo cho cách cài đặt Google Play với hệ điều hành Android 7.0. Video này mình để ở cuối bài học hôm nay, các bạn xem video sẽ dễ hiểu hơn nhiều.

Các Bước Cài Google Apps Cho Genymotion

Download Những Gói Cần Thiết

Trước tiên thì bạn phải download hai gói sau đây, cứ down về thôi, bước tiếp theo chúng ta mới sử dụng đến các gói này.

1. ARM Translation v1.1 (link download)

2. Chọn một trong các gói sau. Đây chính là gói Google Apps, bạn chú ý chọn cho đúng gói của phiên bản hệ điều hành của máy ảo Android mà bạn đã tạo bởi Genymotion nhé.

– Google Apps cho hệ điều hành Android 7.0.
Lưu ý là với đường link này, bạn phải chọn các option trước khi download. Bạn nên chọn Platform x86Android 7.0, và Variant stock. Chi tiết cách cài đặt cho Android 7.0 này được mình mô tả kỹ ở video bên dưới bài học nhé.

– Google Apps cho hệ điều hành Android 6.0.

– Google Apps cho hệ điều hành Android 5.1.

– Google Apps cho hệ điều hành Android 5.0.

– Google Apps cho hệ điều hành Android 4.4.

– Google Apps cho hệ điều hành Android 4.3.

– Google Apps cho hệ điều hành Android 4.2.

– Google Apps cho hệ điều hành Android 4.1.

Khởi Chạy Máy Ảo Genymotion Và Bắt Đầu Cài Đặt Google Apps

Lưu ý là ở bước này nếu bạn vẫn chưa có máy ảo Genymotion thì có thể xem cách đăng ký và cài đặt máy ảo

Giờ đây với máy ảo đang mở, bạn hãy kéo và thả file .zip đã download ở bước đầu tiên (Genymotion-ARM-Translation_v1.1.zip) vào màn hình của máy ảo. Kéo và thả… nhẹ nhàng và duyên dáng như hình vẽ của mình dưới đây.

Screen Shot 2016-09-07 at 11.26.22.png

Sau khi thả file vào máy ảo xong, thì có một dialog như sau xuất hiện, bạn nhớ nhấn OK để máy ảo tiến hành cài đặt.

Screen Shot 2016-09-07 at 11.25.57.png

Gói ARM này cài đặt rất nhanh, bạn sẽ nhanh chóng nhìn thấy kết quả thành công ở dialog tiếp theo như sau. Khi này bạn nhấn OK để đóng hộp thoại này lại.

Screen Shot 2016-09-07 at 11.26.39.png

Tiếp theo người ta khuyên bạn nên tắt máy ảo Genymotion và khởi động lại. Đợi đến khi máy ảo khởi động xong thì bạn kéo/thả tiếo file .zip mà bạn download lần thứ hai vào máy ảo, cách kéo/thả cũng y như bước trên vậy.

Screen Shot 2016-09-07 at 11.27.34.png

Cũng như lần trước, một dialog xuất hiện và bạn lại nhấn OK để máy ảo tiếp tục cài đặt.

Screen Shot 2016-09-07 at 11.28.02.png

Lần này bạn đợi hơi lâu một tí do gói cài đặt này lớn. Sau khi cài đặt hoàn tất bạn lại nhận được một dialog thông báo cuối cùng và bạn cứ nhấn OK.

Screen Shot 2016-09-07 at 11.28.33.png

Lần này người ta cũng khuyên bạn nên khởi động lại máy ảo. Bạn cứ vâng lời đi, đâu tốn nhiều thời gian đâu đúng không nào. 🙂

Screen Shot 2016-09-07 at 11.29.18.png

Màn hình trên đây là khi bạn khởi động lại máy ảo, có thể sẽ phải đợi hơi lâu. Sau khi chờ đợi việc update thành công thì bạn có thể thấy ứng dụng Google Play xuất hiện. Hãy đăng nhập vào Google Play ngay và cài đặt các app cần thiết cho máy ảo nhé, như Maps, Gmail,… chẳng hạn.

Thỉnh thoảng bạn sẽ gặp thông báo lỗi ở những lần dầu cài đặt này (như mình có chụp lại ở màn hình dưới đây). Khi đó bạn đừng lo lắng, hãy vào lại Google Play kiểm tra và cập nhật mới cho tất cả các ứng dụng trong danh sách ứng dụng (như video bên dưới bài học). Hoặc bạn có thể mở từng ứng dụng của Google có sẵn trên máy ảo như Google Play Service và chọn Update (như hình).

Group 2.png

Nhìn chung thì việc cài đặt Google Apps cho Genymotion là một việc làm không nằm trong hỗ trợ chính của nhà sản xuất, chính vì vậy sẽ không hoàn toàn suôn sẻ cho các bạn mới thử lần đầu. Nhưng bạn cũng biết là mình cũng đã thử cả tỷ lần rồi (hơi nổ tí) và xác suất thành công là 100% nên bạn cứ yên tâm mày mò, vọc đi vọc lại nhé.

Chúc các bạn thành công.

Video Minh Họa Cài Đặt Google Apps Cho Máy Ảo Android 7.0

Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy đánh giá 5 sao nếu thấy thích bài viết, hãy comment bên dưới nếu có thắc mắc, hãy để lại địa chỉ email của bạn để nhận được thông báo mới nhất khi có bài viết mới, và nhớ chia sẻ các bài viết của Yellow Code Books đến nhiều người khác nữa nhé.

Dạo Quanh Một Chút Về Ứng Dụng Android

Tóm Tắt Quá Trình Tạo Một Ứng Dụng Android

Chúng ta đều đã biết các ứng dụng Android được viết trên ngôn ngữ lập trình Java hoặc Kotlin (mặc dù tới bài này chúng ta vẫn chưa đụng đến đoạn code Java hay Kotlin nào). Có khi bạn còn phải kết hợp cả ngôn ngữ C++ nữa, nhưng bạn đừng quá lo lắng, bạn chỉ cần biết hoặc Java hoặc Kotlin là đủ xây dựng ứng dụng Android rồi.

Sau khi viết ra một ứng dụng hoàn chỉnh bằng ngôn ngữ lập trình thông qua Android Studio, công cụ này sẽ dùng Android SDK để biên dịch mã code đó, kết hợp với vài dữ liệu khác, và đóng gói các resource kèm theo để tạo thành một file cài đặt, đó là file APK (viết tắt của từ Android Package).

Về sau Android có hỗ trợ một định dạng mới để chứa tất cả các thứ được biên dịch trên đây để mang đi “xuất xưởng”, và được gọi với cái tên Android App Bundle (viết tắt là AAB). Tuy nhiên AAB lại không phải là một file cài đặt cuối như APK. Nó chỉ chứa đầy đủ các nội dung cần thiết để rồi khi bạn đưa file này lên Google Play, chính Google Play sẽ quyết định thiết bị cài đặt ứng dụng của bạn cần những gì, nó sẽ tạo ra một APK cuối cùng để có thể cài đặt riêng và phù hợp nhất trên thiết bị đó.

Nếu bạn chọn biên dịch ra APK, thành phẩm cuối cùng bạn có là một file .apk. Nếu bạn chọn biên dịch ra AAB, bạn sẽ nhận được file .aab. File .apk hay .aab này chứa tất cả các thông tin cần thiết để có thể mang đi cài đặt lên các thiết bị khác (chỉ có .apk mới làm được chuyện này thôi), hoặc dùng để xuất bản lên Google Play.

Quá trình tóm tắt để tạo ra một ứng dụng Android như vậy, phần chi tiết hơn thì mình sẽ nói sau khi bạn “lành nghề” hơn với lập trình ứng dụng cho hệ điều hành này.

Vậy ngôn ngữ lập trìnhcác dữ liệu khác, hay các resource kèm theo được nhắc đến ở trên là gì? Chúng kết hợp lại với nhau như thế nào để Android SDK có thể hiểu và biên dịch ra file .apk hay .aab? Chúng ta sẽ hiểu phần nào ở mục sau đây (còn hiểu sâu hơn thì cần theo dõi tiếp bài sau để tiến hành xây dựng một ứng dụng nhé).

Các Thành Phần Của Một Ứng Dụng

Để dễ hình dung, mình sẽ ví dụ cho bạn thấy rằng việc xây dựng ứng dụng Android cũng giống như việc sản xuất ra một chiếc Ô tô vậy. Ô tô hay ứng dụng đều có những thành phần cơ bản không thể thiếu. Chẳng hạn nhé, ô tô cần phải có động cơ; Ô tô cần phải có bộ khung hay các trang trí khác để tạo thành bộ “giao diện”; Ô tô cần phải có hệ thống điện – điện tử để giải trí hay giám sát; Ô tô cần phải có hệ thống thu phát tín hiệu,… tất cả các thành phần đó tạo thành một thành phẩm ô tô. Một điều lưu ý là thành phẩm ô tô đó có thể có tất cả các thành phần cơ bản, khi đó tính năng của xe sẽ tốt và giá thành thì rất cao. Hoặc cũng có những ô tô chỉ cần một vài thành phần thôi cũng đáp ứng nhu cầu của bộ phận khách hàng nào đó.

Quay lại nói về một ứng dụng, chúng ta cũng có khái niệm các thành phần cơ bản của ứng dụng, cũng có khái niệm ứng dụng có hết tất cả các thành phần, hoặc chỉ cần một hay một vài thành phần là đủ. Tuy nhiên để có thể gọi là một ứng dụng Android thì nó phải có ít nhất một trong các thành phần sau. Chúng ta cùng điểm qua các thành phần đó của ứng dụng.

Activities

Bạn sử dụng Activity để tạo nên một giao diện màn hình. Đây là nơi mà người dùng sẽ nhìn thấy và bắt đầu tương tác với ứng dụng. Mình giả sử bạn đang muốn xây dựng một ứng dụng email, vậy bạn phải suy nghĩ ra các màn hình liên quan cho nó, bạn cần có một màn hình để hiển thị danh sách các email mới nhất. Và cũng cần một màn hình để hiển thị nội dung email. Và một màn hình khác giúp người dùng soạn thảo email mới. Và một màn hình khác…. Và…. Tất cả các “và” như vậy tạo thành một ứng dụng với nhiều màn hình, mỗi màn hình là một Activity. Mỗi Activity như vậy tuy độc lập nhưng lại có thể kết nối với nhau tạo thành một trải nghiệm của người dùng trong ứng dụng email của bạn.

Bạn đã hiểu sơ về khái niệm thành phần đầu tiên của một ứng dụng Android rồi đúng không nào. Chúng ta sẽ được học kỹ hơn về Activity ở bài tiếp theo, học cách làm sao để quản lý chu kỳ sống của một Activity, hay cách thức từ Activity này chuyển qua Activity khác, hay đóng một Activity,…

Services

Khác với Activity được dùng để hiển thị giao diện lên màn hình cho người dùng, Service lại được dùng cho các tác vụ chạy nền, tức là các tác vụ mà người dùng không hề trông thấy. Chẳng hạn như với một ứng dụng nghe nhạc, Activity sẽ hiển thị giao diện các bài nhạc cho người dùng chọn để chơi, còn khi người dùng đã chọn một bài thì thực chất công việc chơi nhạc là của Service. Bằng chứng là khi bạn chuyển qua ứng dụng khác, hay tắt màn hình điện thoại, tuy người dùng không còn nhìn thấy các Activity (các màn hình của ứng dụng) nữa nhưng Service vẫn đang hoạt động ở nền và luôn chơi bản nhạc đó, hay tự chuyển qua bản nhạc khác một cách tự động.

Content Providers

Một Content Providers sẽ lo việc chia sẻ dữ liệu của một ứng dụng. Dù cho ứng dụng của bạn có lưu dữ liệu ở file hệ thống, hay ở cơ sở dữ liệu SQLite, hay ở trên Web, hay bất cứ đâu mà nó có thể truy xuất được. Thì với Content Provider, ứng dụng khác cũng hoàn toàn có thể truy xuất vào dữ liệu của bạn, chỉnh sửa nó (nếu Content Provider cho phép). Một ví dụ điển hình là ứng dụng Quản Lý Danh Bạ của Android, ứng dụng này chứa đựng thông tin số điện thoại và các thông tin khác trên Danh Bạ người dùng, tuy vậy ứng dụng của bạn với khai báo các permission (các quyền cho phép) cần thiết vẫn có thể đọc hay chỉnh sửa thông tin đó.

Broadcast Receivers

Một Broadcast Receivers là một thành phần có khả năng thu nhận các thông báo của hệ thống. Bạn hãy tưởng tượng Broadcast Receiver giống như một cột ăng-ten thu sóng, và nguồn phát sóng chính là từ hệ thống. Hệ thống sẽ phát các tín hiệu như là, màn hình tắt, pin yếu, hay là vừa chụp một hình ảnh,… Tùy vào bạn dựng lên cột ăng-ten muốn lắng nghe loại sóng nào. Broadcast Receiver cũng giống như Service ở chỗ không hiển thị giao diện cho người dùng. Nhưng Broadcast Receiver có thể tạo một thông báo trên thanh Notification của thiết bị.

Một khía cạnh độc đáo khác của Android là việc cho phép một ứng dụng nào đó có thể khởi động một thành phần của một ứng dụng nào đó khác. Một minh chứng cho khía cạnh này là, ví dụ bạn đang xây dựng một ứng dụng mà trong đó có chức năng chụp ảnh dựa trên máy ảnh của thiết bị, thì thay vì bạn phải mày mò xây dựng chức năng này, ứng dụng của bạn hoàn toàn có thể gọi một dứng dụng có sẵn chức năng chụp ảnh để giúp bạn làm việc đó một cách dễ dàng nhanh chóng, bằng cách gọi thẳng đến Activity của ứng dụng chụp ảnh đó, ứng dụng chụp ảnh sẽ điều khiển máy ảnh, chụp ảnh và lưu ảnh lại, ứng dụng của bạn chỉ cần lấy ảnh vừa lưu lại mang ra sử dụng, điều này diễn ra một cách nhịp nhàng và người dùng hoàn toàn bị đánh lừa rằng chỉ có ứng dụng của bạn đang làm tất cả mọi chuyện.

Kích Hoạt Các Thành Phần

Chúng ta vừa dạo qua các thành phần giúp tạo nên một ứng dụng Android ở trên. Giờ chúng ta nói đến việc làm sao để kích hoạt, hay làm cách nào để các thành phần đó hoạt động.

Vậy thì bạn nên nhớ là (hoặc ghi chú lại sau này mình nhắc lại sẽ nhớ rõ), ba trong bốn thành phần kể trên bao gồm ActivityService và Broadcast Receiver được kích hoạt bởi một thông điệp bất đồng bộ (asynchronous message) có tên là Intent.

Trong một ứng dụng thì Intent như là thành phần trung gian kết dính các thành phần khác với nhau, chẳng hạn từ một Activity này bạn có thể kích hoạt một Activity khác, hoặc kích hoạt một Service hoạt động. Intent cũng giúp làm trung gian để từ một ứng dụng có thể kích hoạt thành phần của ứng dụng khác, như ví dụ về việc mở một ứng dụng có chức năng chụp ảnh trên đây.

Về phía phân loại Intent thì có hai dạng là tường minh và ngầm hiểu. Vậy thôi, bạn sẽ được biết cách sử dụng Intent một cách cụ thể ở các bài học sau.

File Manifest

Wow, chúng ta đã xem qua một ứng dụng Android có các thành phần gì, chúng ta cũng đã xem qua cách để kích hoạt các thành phần đó. Điều quan trọng cuối cùng trong một ứng dụng Android, đó là để sử dụng các thành phần mà chúng ta mong muốn, trước khi kích hoạt chúng, chúng ta cần phải “khai báo” chúng. Tất cả các khai báo đó đều được để trong một file, đó là file Manifest.

Ngoài việc giúp khai báo các thành phần sẽ được sử dụng trong ứng dụng, file Manifest còn là nơi bạn khai báo các mục đích khác như:

  • Đăng ký các user permission, tạm hiểu là các quyền hạn mà ứng dụng xin phép người dùng để sử dụng các tính năng của hệ thống, như là quyền được truy cập internet, được đọc danh bạ (contacts), được ghi file ra thẻ nhớ ngoài,…
  • Chỉ định Minimum Required SDK, là chỉ định thông số hệ điều hành thấp nhất của Android mà ứng dụng của bạn hỗ trợ, ngoài việc khai báo giá trị này trong file Manifest, chúng ta còn có thể khai báo trong file build.gradle mà mình sẽ nói ở các bài sau.
  • Chỉ định các tính năng phần cứng và phần mềm mà ứng dụng của ta muốn sử dụng đến, như camera, bluetooth, hay ngay cả tính năng multitouch.
  • Khai báo các thư viện bên ngoài (bên ngoài ở đây có thể hiểu là các thư viện không có sẵn trong các API Framework của Android) mà ứng dụng của bạn có sử dụng đến, như thư viện Google Maps, Google Play,… Và phần này cũng có thể được khai báo trong file build.gradle mà chúng ta cũng sẽ tìm hiểu sau.

Các Resource

Một ứng dụng Android không thể nào thiếu các resource. Bạn có thể hiểu các thành phần được nói trên đây là các source code, nó chứa đựng các logic cho ứng dụng được dựng lên bởi một ngôn ngữ lập trình nào đó. File Manifest như là một file cấu hình của ứng dụng. Thì các resource chính là các loại file còn lại bổ trợ thêm cho ứng dụng của bạn, chúng bao gồm các file hình ảnh, âm thanh, video, các file text, màu sắc,… hay thậm chí một file giao diện XML. Bạn sẽ có cơ hội được tìm hiểu sâu hơn về các resource bên trong một ứng dụng Android, và cách thức làm việc với file giao diện XML sau này.

Kết Luận

Mình nghĩ bài học thứ 5 nên kết thúc ở đây, với bài học này bạn có một cái nhìn tổng quát về cấu tạo của một ứng dụng. Bạn sẽ học cách sử dụng từng thành được nói ở trên, cách sử dụng Intent để kích hoạt các thành phần này, hay cách sử dụng file Manifest cũng như file build.gradle để khai báo các nhu cầu cần thiết cho ứng dụng của bạn ở các bài tiếp theo.

Dạo Qua Ứng Dụng TourNote & Thực Hành Debug Ứng Dụng

Chào mừng các bạn quay trở lại với bài học số 6 trong chương trình học Android 

Hẳn các bạn đã có một chuỗi dài chờ đợi diện mạo của ứng dụng mà chúng ta sắp sửa xây dựng – Ứng dụng TourNote – Bài hôm nay sẽ “vén màn” bí mật đó, và từ đây về sau chúng ta cùng từng bước xây ứng dụng này trong suốt các bài thực hành nhé.

Dạo Qua Ứng Dụng TourNote

Main Screen.png

Như bạn có thể thấy, ứng dụng TourNote của bạn, đúng rồi ứng dụng này sẽ là của bạn, nếu bạn cùng với mình đi đến cuối các bài giảng của chương trình Android này, ứng dụng của chúng ta sẽ có giao diện hiện đại theo chuẩn Material Design của Android.

Có một điều thú vị là trong khi thông tin về TourNote đang chuẩn bị trình làng cho mọi người ở bài hôm nay, thì Google cũng vừa tung ra một ứng dụng liên quan đến du lịch, đó là Google Trip. Có thể nói là trùng ý tưởng!!! Để thêm phần thú vị nữa thì bạn có thể xem TourNote là một bản thu nhỏ của Google Trip đi, mình cũng không chắc chúng sẽ giống nhau bao nhiêu phần trăm, nhưng có vẻ chúng giống nhau ở ý tưởng giúp ghi chú lại những gì bạn đã trải qua và sẽ được bạn thực hiện trong chuyến du lịch nào đó trong tương lai. Bạn vẫn có thể học hỏi Google Trip để bổ sung thêm cho TourNote của mình được hoàn hảo hơn cả các bài thực hành này của mình.

Có thể nói thêm là TourNote sẽ như một quyển sổ tay, giúp người dùng của bạn sưu tầm các địa chỉ ăn uống, du lịch, tham quan,… Đặc biệt hơn, bạn sẽ tận dụng dịch vụ map của Google để lưu vị trí, gợi nhắc người dùng khi ở gần vị trí nào đó,…

Trước mắt, không như Google Trip, ứng dụng TourNote sẽ được xây dựng dưới dạng offline, tức mọi thứ sẽ được lưu trữ trên thiết bị Android của bạn (hoặc máy ảo). Điều này cũng có nghĩa là nếu bạn xóa app, hay xóa data của thiết bị, thì mọi dữ liệu sẽ bị mất :(. Bạn có tùy chọn share các note của mình lên các công cụ online khác như Facebook, Twitter, hay gửi cho bạn bè qua email,… để lưu giữ lại. Mình hi vọng là ứng dụng sẽ có khả năng lưu trữ online ở mục nâng cao cuối chương trình.

Hiện tại như hình trên, mình có thiết kế sẵn 3 màn hình chính. Bao gồm màn hình Home (màn hình đầu tiên) chứa các chủ đề (là các tab ở trên màn hình) và danh sách các note theo từng chủ đề đó. Màn hình thứ hai là khi người dùng nhấn vào nút   fab, khi đó sẽ có các tùy chọn cho người dùng thêm/sửa Chủ Đề hay Ghi Chú. Cuối cùng là màn hình Thêm Ghi Chú. Các màn hình còn lại sẽ xuất hiện từ từ khi chúng ta đi vào chi tiết bài thực hành.

Trước khi đi vào xây dựng ứng dụng, bạn nhất thiết phải biết debug ứng dụng cái đã. Để biết debug ứng dụng là gì thì mời bạn xem qua phần dưới đây nhé.

Thực Hành Debug Ứng Dụng

Debug Là Gì?

Debug là một khái niệm trong lập trình, nó bao gồm 2 hành động, đó là Tìm Kiếm Lỗi và Sửa Lỗi. Thông thường khi có bất kỳ Lỗi nào phát sinh xảy đến cho ứng dụng của bạn – Tất nhiên Lỗi không thể do chủ ý của một người lập trình giỏi giang như bạn được, mà Lỗi xuất hiện bất ngờ và rất khó lường – Khi đó việc Tìm Kiếm xem Lỗi xảy ra do đâu là rất quan trọng. Sau khi tìm thấy Lỗi, bạn phải sửa được Lỗi đó. Kết hợp cả 2 quá trình Tìm Kiếm – Sửa Lỗi lại chúng ta tạo ra khái niệm Debug.

Vậy có tất cả các cách debug gì mà Môi Trường Lập Trình Android hỗ trợ cho chúng ta? Hãy cùng nhau tìm hiểu các cách sau.

Debug Bằng Cách Ghi Log

Log sẽ được hiển thị ở tab logcat trong cửa sổ Android Monitor. Nếu các bạn còn nhớ Bài 3, phần Tổng Quan Về Project có nhắc đến Tool windows (các cửa sổ công cụ), thì hôm nay chúng ta làm quen với Android Monitor – một trong các cửa sổ công cụ đó.

Screen Shot 2016-09-19 at 22.08.33.png

Trước khi đi vào cách thức debug với log, chúng ta làm quen với logcat một chút. Như bạn thấy ở trên, logcat chứa các dòng log liên quan đến hệ thống, và cả những dòng log từ phía ứng dụng của bạn nữa, là do chủ động bạn show ra hay được xây dựng sẵn từ các thư viện cấp thấp hơn. Logcat hiển thị log tức thời theo thời gian thực (realtime), và còn được lưu giữ lại nữa, nên chúng ta hoàn toàn có thể tận dụng loagcat để debug một ứng dụng.

Ẩn Hiện Android Monitor

Logcat được hiển thị trong cửa sổ Android Monitor, nhưng nếu bạn không thấy cửa sổ này đâu cả, thì hãy tìm và nhấn vào am-icon.png ở dưới cùng của cửa sổ chính.

Nhấn vào am-icon.png một lần nữa để ẩn cửa sổ này đi.

Hoặc bạn cũng có thể vào menu View > Tool Windows > Android Monitor để ẩn/hiện cửa sổ này.

Cách Ghi Log

Khi muốn ghi log, bạn hãy chọn một trong các hàm sau đây, việc phân biệt log theo các hàm ngoài mục đích log của bạn sẽ được xuất hiện đúng màu chủ đạo để dễ tìm trong một rừng log, thì bạn còn có thể lọc (filter) các log theo từng dạng mà chúng ta sẽ nhắc đến ở dưới. Nên nhớ là nếu bạn ghi log này ở đâu trong code, thì khi hệ thống thực thi đến dòng code đó, log của bạn sẽ xuất hiện ngay lập tức ở cửa sổ logcat. Các hàm ghi log được liệt kê như sau.

Để in ra log dạng lỗi (error)

Log.e(String, String);

Để in ra log dạng cảnh báo (warning)

Log.w(String, String);

Để in ra log kiểu cung cấp thông tin (information)

Log.i(String, String);

Để in ra log cho các nhu cầu debug

Log.d(String, String);

Và log cho các nhu cầu khác (verbose)

Log.v(String, String);

Mặc dù mình không tìm thấy bất kỳ sự ràng buộc nào cho việc lựa chọn các dạng ghi log trên đây, tất cả là nằm ở bạn, nhưng có một số lời khuyên, chẳng hạn như bạn đừng nên show ra các log dạng verbose hay debug khi tung ứng dụng ra thị trường, nó chỉ được dùng cho mục đích kiểm tra trong quá trình xây dựng ứng dụng mà thôi. Trong khi đó các log dạng errorwarning hay information thì hoàn toàn được phép xuất xưởng.

Như bạn thấy ở trên, hàm log nào cũng có 2 tham số truyền vào, tham số thứ nhất cho phép bạn đặt một nhãn gọi là tag, tag dùng để gom nhóm hay phân biệt các loại log với nhau; Tham số thứ hai là nội dung của log mà bạn muốn in ra. Chú ý tag sẽ được dùng đi dùng lại nhiều lần nên tốt nhất nên khai báo chúng là một biến static để tiện cho việc dùng lại.

Thực Hành Ghi Log Cho TourNote

Đến đây bạn mới bắt đầu code cho ứng dụng của mình, dù cho là các dòng code thử nghiệm, bạn sẽ phải xóa đi ở bài sau. Chú ý từ bây giờ về sau, nếu bạn thấy các nội dung được bao quanh bởi một khung màu vàng như thế này, thì đó là lúc bạn phải mở ứng dụng TourNote lên để thực hành. Điều này giúp bạn dễ dàng tra cứu các bước mà mình đã làm ở từng bài giảng khác nhau.

Với Android Studio đang mở, bạn tìm đến class MainActivity.java nằm trong project TourNote bạn đã tạo ở Bài 3 và click đúp vào để mở class này ra. Nếu không biết mở class này ở đâu, bạn hãy tìm trong cửa sổ Project ở bên trái màn hình. Nếu không thấy cửa sổ Project ở đâu, bạn có thể click vào menu View > Tool Windows > Project để hiện nó lên. Class MainActivity.java sẽ xuất hiện ở cửa sổ Project như một trong hai hình sau, tùy vào cách thức hiển thị mà bạn chọn (bạn chú ý thấy 2 cái tên Android và Project là 2 cách hiển thị cây thư mục nằm ở phía trên của cửa sổ này).

Group 2.png

Với class MainActivity.java đã mở, bạn hãy gõ thêm các dòng code sau, đầu tiên là dòng code liên quan đến khai báo tag để bên ngoài hàm onCreate(), tiếp đến là các hàm ghi log bạn để ở trong hàm onCreate() và để ở các dòng cuối trước khi đóng ngoặc kết thúc nhé. Nếu bạn nào lần đầu làm quen với lập trình và không hiểu lý do vì sao chúng ta code như vậy thì hãy cùng tìm hiểu ở các bài sau, hoặc bạn có thể xem qua các bài học về lập trình Java. Code của bạn sau khi thêm vào (các dòng mới được tô sáng) sẽ như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MainActivity extends AppCompatActivity {
  
    private static final String TAG = "TourNote_MainActivity";
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
  
        Log.e(TAG,"This is an Error log");
        Log.w(TAG,"This is a Warning log");
        Log.i(TAG,"This is an Information log");
        Log.d(TAG,"This is a Debug log");
        Log.v(TAG,"This is a Verbose log");
    }
}

Giờ bạn hãy chạy ứng dụng lên máy ảo hay máy thật. Khi màn hình TourNote vừa hiện lên trên thiết bị, bạn tìm đâu đó trong cửa sổ logcat sẽ thấy các dòng log mà bạn vừa code lúc nãy như sau.

Screen Shot 2016-09-22 at 11.25.55.png

Debug Qua Việc Phân Tích Stack Trace

Nói đến debug ứng dụng thì không thể không nhắc đến Stack Trace. Stack Trace là một tập hợp các dòng log đặc biệt, các dòng log này cũng được xuất hiện trong cửa sổ logcat như bao dòng log khác. Nhưng Stack Trace chỉ xuất hiện khi ứng dụng của bạn bị crash – Một trạng thái dừng đột ngột của ứng dụng vì bất cứ một lỗi nào đó xảy ra mà hệ điều hành không có khả năng cứu vãn, khi đó hệ điều hành tự động đóng ứng dụng của bạn lại và để lại một hộp thoại thông báo rằng ứng dụng đã bị dừng.

Stack Trace khi xuất hiện giống như sau.

android-monitor-logcat_2-2_2x.png

Bạn có thể thấy Stack Trace hiển thị danh sách các hàm có liên đới đến crash, đi kèm theo đó là tên file và dòng nào trong file đó gây ra crash. Bạn có thể nhấn vào bất cứ dòng nào có tô xanh, khi đó bạn được dẫn đến đúng file và dòng lỗi đó.

Nhấn vào Up the stack trace logcat-arrow-up.png hay Down the stack trace logcat-arrow-down.png để di chuyển nhanh lên/xuống các Stack Trace trong logcat.

Thực Hành Tạo Ra Stack Trace Và Tương Tác Với Nó

Nên nhớ là mục thực hành này mang tính giúp bạn làm quen với Stack Trace để sau này khi ứng dụng của bạn bị crash thì bạn sẽ biết cách tìm đến dòng lỗi một cách dễ dàng như thế nào.

Với MainActivity.java đang được mở, bạn thêm vào dòng code được tô sáng như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MainActivity extends AppCompatActivity {
  
    private static final String TAG = "TourNote_MainActivity";
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
  
        Log.e(TAG,"This is an Error log");
        Log.w(TAG,"This is a Warning log");
        Log.i(TAG,"This is an Information log");
        Log.d(TAG,"This is a Debug log");
        Log.v(TAG,"This is a Verbose log");
  
        throw new RuntimeException("This is a crash");
    }
}

Giờ đây khi bạn chạy lại ứng dụng… Booom! App bị crash khi vừa chạy xong. Và để ý ở logcat bạn sẽ thấy Stack Trace với nội dung crash là “This is a crash”, và MainActivity.java:23 được tô sáng, có nghĩa là lỗi crash xảy ra ở class MainActivity.java dòng 23. Bạn có thể click vào nơi được tô sáng, bạn sẽ được dẫn đến tận nơi như mình đã nói.

Screen Shot 2016-09-22 at 13.39.02.png

Debug Bằng Công Cụ

Ngoài việc xem log như trên, Android Studio còn hỗ trợ các bạn một công cụ debug hiệu quả nữa. Với công cụ này bạn có thể tạo ra các breakpoint (điểm dừng) trên source code. Khi ứng dụng của bạn chạy đến nơi có brakpoint, nó sẽ bị chặn lại, và công cụ debug của Android Studio sẽ giúp bạn xem các biến, các giá trị ngay tại thời điểm đó, thậm chí bạn có thể chụp hình, hay quay phim lại màn hình ứng dụng của bạn. Và cho đến khi bạn chưa cho phép, thì Android Studio vẫn dừng mãi ở breakpoint đó đến khi nào bạn nhấn nút đi tiếp. Trong một project bạn có thể đặt bao nhiêu breakpoint tùy thích. Và breakpoint chỉ hoạt động trong chế độ chạy debug mà thôi, các cách thực thi ứng dụng ở chế độ debug được liệt kê dưới đây.

Có 2 cách để thực thi ứng dụng ở chế độ debug.

1. Nếu ứng dụng của bạn đang chạy, nhấn vào nút toolbar-attach-debugger.png trên thanh công cụ sẽ giúp ứng dụng của bạn chuyển ngay sang chế độ debug, giữ nguyên màn hình và các chức năng đang chạy, mà không cần phải khởi động lại.
2. Nếu ứng dụng đang tắt, bạn hãy nhấn vào nút toolbar-debug.png để ứng dụng được chạy lên, tương tự như khi chạy với chế độ bình thường nhưng lại sẵn sàng để debug.

Thực Hành Debug Bằng Công Cụ

Với file MainActivity.java được mở. Trước hết chúng ta phải tạo ra một breakpoint để buộc Android Studio dừng lại tại đó khi chế độ debug được khởi động. Để tạo breakpoint, rất đơn giản, bạn chỉ cần nhấn chuột vào thanh bên trái của cửa sổ editor, ngay dòng bạn muốn dừng lại, như hình sau.

Screen Shot 2016-09-22 at 14.01.20.png

OK giờ mình chọn giải pháp khởi động lại ứng dụng bằng chế độ debug, để làm vậy bạn nhấn vào nút toolbar-debug.png trên thanh công cụ. Ứng dụng của bạn sẽ được khởi động lại trong giây lát.

Sau khi ứng dụng khởi động xong, bạn sẽ thấy chẳng có gì được vẽ lên màn hình thiết bị hết, đơn giản vì Android Studio đã phát hiện thấy breakpoint bạn vừa đặt, khi Android chuẩn bị “vẽ” ứng dụng của bạn ra màn hình thì đã bị chặn lại rồi.

Nhìn lại code editor thì thấy dòng có breakpoint được tô sáng, có vài thông tin màu xám xuất hiện trong code editor này, đó là dữ liệu được tạo ra ở thời điểm biên dịch đến breakpoint của bạn, và công cụ debug hiển thị nó lên cho bạn xem luôn.

Screen Shot 2016-09-22 at 14.09.31.png

Cũng với hình trên, ở phía dưới màn hình bạn sẽ thấy cửa sổ Debug tự động bật ra. Với cửa sổ Debug này bạn có thể nhấn Screen Shot 2016-09-22 at 14.15.53.png để ứng dụng tiếp tục chạy và sẽ dừng lại ở breakpoint kế tiếp (trong trường hợp của bài thực hành này chỉ có một breakpoint thôi nên nếu nhấn vào nút này, ứng dụng sẽ được chạy tiếp như bình thường), hoặc bạn có thể nhấn Screen Shot 2016-09-22 at 14.18.02.png để dừng chế độ debug. Bạn cũng có thể dùng dàn nút điều khiển như dưới đây, ý nghĩa của chúng theo thứ tự từ trái sang phải như sau.

Screen Shot 2016-09-22 at 14.23.03.png

– Step Over: nhảy qua dòng kế tiếp, nếu dòng đó là một hàm thì nhảy qua hàm đó. Dùng chức năng này khi bạn muốn debug tiếp các dòng tiếp theo với các biến được phát sinh sau đó, để tìm ra chính xác dòng nào bị lỗi.
– Step Into: nhảy vào trong hàm nào đó, nếu vệt sáng đang sáng ở hàm đó. Dùng chức năng này khi bạn nghi ngờ lỗi xảy ra bên trong hàm.
– Step Out: ra khỏi hàm. Dùng chức năng này khi bạn chắc rằng không còn lỗi trong hàm này nữa, bạn nhanh chóng ra khỏi hàm để kiểm tra các dòng tiếp theo sau hàm này.

Bạn có thể dùng cửa sổ Variables để xem các biến được khai báo đến thời điểm đặt breakpoint, giá trị của từng biến như thế nào.

Screen Shot 2016-09-22 at 14.09.31.png

Bạn cũng nên thử dùng cửa sổ Watches để xem các biến hay các hàm mà bạn nghi là bị lỗi. Khi nhấn vào dấu Screen Shot 2016-09-22 at 14.35.43.png ở cửa sổ này và gõ vào tên biến hay tên hàm kèm các tham số, bạn sẽ có kết quả của sự kiểm chứng đó. Chẳng hạn mình gõ TAG, là biến static đã được khai báo trong ví dụ ở trên với nội dung khai báo là “TourNote_MainActivity”, bạn thử nhìn Watches hiện ra mà xem, thật tuyệt!!!

Screen Shot 2016-09-22 at 14.33.18.png

Bạn vừa xem qua tất cả những cách debug lỗi, bạn hãy thực hành tất cả các cách này nhé, vì mình cũng đang dùng chúng và thực sự đó là những cách hiệu quả giúp chúng ta nhanh chóng tìm ra lỗi phát sinh ở đâu trong rừng code.

Tạo Giao Diện Người Dùng

Bài hôm nay chúng ta sẽ nói về Giao Diện Người Dùng  (Graphical User Interface – GUI), một thành phần quan trọng trong ứng dụng của các bạn. Nhắc lại một chút rằng, ở bài 5 chúng ta có nói qua các Thành Phần Của Ứng Dụng (Application Component), nhưng có bạn nào thắc mắc rằng giao diện sẽ được xây dựng như thế nào dựa trên các Thành Phần này không? Nếu bạn để ý kỹ bài học, thì mình có nói rằng trong 4 Thành Phần đó (ActivitiesServicesContent Providers và Broadcast Receiver) thì chỉ có duy nhất Activities là hiển thị giao diện cho người dùng. Vậy bài này các bạn sẽ được biết cách thức xây dựng giao diện cho một Activity. Activity là gì thì chúng ta sẽ tìm hiểu sau, điều các bạn cần quan tâm bây giờ là bạn cần hiểu Activity chính là cái màn hình mà bạn thực thi ứng dụng lên từ bài học số 4, màn hình in ra mỗi một dòng chữ “Hello World”. Và giao diện của nó gắn liền với một class có tên MainActivity.java.

Trước khi chính thức đi vào thực hành xây dựng giao diện, chúng ta nói qua một số khái niệm sau, chúng là những viên gạch cơ bản đầu tiên để xây dựng nên một “công trình” giao diện hoàn chỉnh, chúng hoành tráng hay nghèo nàn, kiên cố hay xiêu vẹo là ở những viên gạch cơ bản này đấy.

Khái Niệm Widget

Bạn ít khi nghe nói đến Widget trong giao tiếp thông thường, nhất là khi trao đổi tiếng Việt giữa các lập trình viên với nhau. Nhưng trong ngôn ngữ lập trình, Widget có khái niệm rõ ràng và được sử dụng trong các tài liệu tiếng Anh đấy nhé.

Vậy thì, Widget là một thành phần của Giao Diện Người Dùng, Widget giúp hiển thị các thông tin được sắp xếp, và có thể chỉnh sửa được tới người dùng. Mỗi Widget sẽ mang một kiểu thông tin nhất định, tập hợp chúng lại với nhau chúng ta tạo nên một Giao Diện Người Dùng. @@

Đọc các dòng trên có thể khiến bạn bị lùng bùng lỗ tai, nhưng thực ra Widget rất dễ hiểu, nó chính là cái Textbox, cái Label, cái Button,… trên ứng dụng của bạn mà thôi. Để dễ hiểu hơn bạn hãy nhìn vào hình ảnh rất đẹp của một ứng dụng sau. ^^

New - Edit Note.png

Bạn có thể thấy:

– Một số text, như Thêm Ghi ChúChủ Đề
– Một số chỗ để người dùng nhập text của họ vào, như Tiêu ĐềMiêu TảGhi Chú
– Một số check box, như Ăn UốngTham QuanMua Sắm
– Một số nút, như nút +, nút x
– Một số nơi để hiển thị hình ảnh
– …

Tất cả những thành phần được liệt kê ở trên đều là các Widget cả đấy, chúng được sắp đặt ở các vị trí cụ thể trên màn hình, mỗi Widget đảm nhận các vai trò khác nhau đến với sự tương tác với người dùng. Bạn thấy có dễ hiểu hơn không nào. Chúng ta sẽ làm quen với từng Widget ở bài học sau nhé, còn bây giờ hãy qua khái niệm kế tiếp.

Khái Niệm View

Bạn đã hiểu Widget là gì rồi, giờ ta sẽ tìm hiểu về khái niệm ViewView thực chất là nguồn gốc để tạo nên các Widget được nói đến ở trên.

View được hiểu như là các cấu tạo giống nhau, hay nói cách khác nó là các khuôn mẫu cơ bản để làm nền tảng tạo nên Widget. Nó tương tự như khái niệm về lập trình trong OOP ấy, trong đó View là class còn các Widget là các object được tạo ra từ class (View) đó. Do đó trong khi ở View chỉ có các thuộc tính chung nhất, thì Widget là các thể hiện khác nhau của View. Mỗi Widget chứa đựng các thuộc tính đặc thù của nó mà các Widget khác không có. Chẳng hạn Button sẽ có các thuộc tính khác với LabelCheckbox sẽ khác Radio Button,...

Khái Niệm ViewGroup

ViewGroup cũng gần như là View, nhưng đặc biệt hơn một chút, đó là, ViewGroup được tạo ra để chứa đựng các View hay các ViewGroup khác. Nếu như View là nguồn gốc để tạo ra các Widget, thì ViewGroup lại là nguồn gốc để tạo ra các Layout. Các layout mà chúng ta sẽ sớm làm quen, như ConstraintLayout, LinearLayoutRelativeLayoutFrameLayoutGridLayout,… Mỗi layout như một Thùng Chứa (Container) để chứa đựng các View hay các ViewGroup khác bên trong mình, các thành phần bên trong này được sắp xếp theo một quy tắc nhất định tùy theo từng loại ViewGroup.

Kết Hợp View Và ViewGroup Để Tạo Nên Giao Diện

Với khái niệm View và ViewGroup ở trên, các bạn có thể thấy rằng mối quan hệ của hai thành phần này giúp tạo thành một giao diện người dùng có bản chất là một cây phân cấp. Cây phân cấp này bao gồm các View và ViewGroup, chúng được sắp xếp hay lồng vào nhau theo những quy tắc nhất định. Như hình minh họa dưới đây.

viewgroup.png

Chỉnh Sửa Giao Diện Cho TourNote

Woohooo!!! Lý thuyết ở trên nặng đầu quá, nếu bạn chưa hiểu gì cả thì cũng không sao. Giờ chúng ta nên làm quen với thực hành tạo giao diện. Có một điều chúng ta phải thống nhất với nhau trước khi đi vào chi tiết thực hành, đó là việc xây dựng giao diện cho ứng dụng Android sẽ làm bằng cách kết hợp cả hai phương pháp, là “kéo thả”, và code. Kéo thả có nghĩa là bạn có được công cụ cần thiết để chỉ việc kéo các widget có sẵn vào màn hình thiết kế, việc kéo thả này sẽ làm phát sinh code XML. Còn code có nghĩa là bạn phải biết cách nhìn vào các dòng chữ được sắp xếp theo chuẩn XML sau các thao tác kéo thả của bạn. Việc kéo thả giao diện thì rất trực quan rồi, lát nữa đến bài thực hành bạn sẽ thấy nó dễ đến mức nào. Còn việc nhìn vào code XML là vì nhiều khi công cụ kéo thả không giúp bạn có thể đạt được mong muốn cuối cùng của giao diện, hoặc có bất kỳ lỗi nào đối với công cụ này mà bạn hoàn toàn không có khả năng sửa chữa nó bằng các phương pháp kéo thả, ngoại trừ nhìn vào code. Ngoài ra, khi nhìn vào code, các bạn còn có thể thấy sự kết hợp các View và ViewGroup như cây phân cấp ở trên đã trình bày, giúp bạn có sự hình dung rõ ràng nhất về việc giao diện của bạn tạo ra nó được xây dựng dựa trên cơ sở gì.

Đầu tiên, để đảm bảo chắc chắn code của bạn sẽ thực thi lên được (vì bài trước bạn đã code một dòng làm cho ứng dụng bị crash để trải nghiệm), thì bạn nên xóa các dòng đã code từ bài trước đó đi nhé, bảo đảm class MainActivity.java “sạch” giống như sau.

1
2
3
4
5
6
7
8
public class MainActivity extends AppCompatActivity {
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

Giờ bạn hãy tìm đến file activity_main.xml và click đúp để mở file này lên, đây chính là giao diện của class ActivityMain.java. Nhắc lại là nếu không biết mở class này ở đâu, bạn hãy tìm trong cửa sổ Project ở bên trái màn hình. File activity_main.xml sẽ xuất hiện ở cửa sổ Project như một trong hai hình sau, tùy vào cách thức hiển thị là Android hay Project.

Tạo giao diện người dùng - File giao diện XMLhttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2016/09/hinh1.png?resize=300%2C182&ssl=1 300w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2016/09/hinh1.png?resize=768%2C467&ssl=1 768w" data-lazy-loaded="1" sizes="(max-width: 796px) 100vw, 796px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px auto; clear: both; text-align: center;">

Cửa sổ của file activity_main.xml được mở lên sẽ rất khác với lúc bạn mở ActivityMain.java hôm trước. Vì mình cũng đã có nói đến ở Bài 3, rằng cửa sổ editor này rất đặc biệt, nó sẽ hiển thị nội dung theo đúng loại file đã mở, trong trường hợp này bạn đang mở file giao diện XML nên Android Studio sẽ hiển thị theo kiểu thiết kế giao diện như sau.

Tạo giao diện người dùng - File activity_main.xmlhttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2016/09/hinh2.png?resize=300%2C161&ssl=1 300w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2016/09/hinh2.png?resize=768%2C412&ssl=1 768w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2016/09/hinh2.png?resize=1024%2C549&ssl=1 1024w" data-lazy-loaded="1" sizes="(max-width: 1000px) 100vw, 1000px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px auto; clear: both; text-align: center;">

Giao diện ở cửa sổ này có thể khác với giao diện ở máy bạn một chút tùy vào version của Android Studio. Các bạn có thể chú ý ở phía dưới cửa sổ này có 2 tab: Design và Text (như hình dưới).

Tạo giao diện người dùng - Tab Design và tab Texthttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2016/09/hinh3.png?resize=300%2C137&ssl=1 300w" data-lazy-loaded="1" sizes="(max-width: 412px) 100vw, 412px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px auto; clear: both; text-align: center;">

Mặc định cửa sổ này hiển thị chế độ Design, tức là các bạn có thể kéo-thả giao diện, hệ thống sẽ sinh ra code XML một cách tự động cho bạn. Để hiểu hơn về cửa sổ Design này, và cách kéo thả sao cho ra một giao diện Android, thì bạn có thể xem trước loạt bài riêng về ContraintLayout này của mình nhé.

Và như mình nói, chúng ta phải học cách làm quen việc kéo thả giao diện, và cả cách code XML cho giao diện Android nữa. Nên bạn hãy đừng ngần ngại, hãy thử nhấn vài tab Text xem sao.

Tạo giao diện người dùng - Tab texthttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2016/09/hinh4.png?resize=300%2C161&ssl=1 300w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2016/09/hinh4.png?resize=768%2C412&ssl=1 768w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2016/09/hinh4.png?resize=1024%2C549&ssl=1 1024w" data-lazy-loaded="1" sizes="(max-width: 1000px) 100vw, 1000px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px auto; clear: both; text-align: center;">

Một lát nữa đây chúng ta sẽ thực hành với việc kéo thả giao diện. Nhưng trước khi thực hành chuyện đó, chúng ta nên xem qua một chút cấu tạo của XML ở bước này, để khi kết thúc việc kéo thả, chúng ta sẽ cùng nhau so sánh lại một lần nữa sự thay đổi ở XML nhé.

Giải Thích Một Chút Giao Diện XML

Bạn hãy chú ý dòng đầu tiên.

1
<?xml version="1.0" encoding="utf-8"?>

Bạn nên biết rằng đây không phải khái niệm của Android, nên bạn không cần quan tâm cũng được. Đây là định nghĩa của XML mà thôi, nó thông báo rằng cấu trúc bạn đang dùng là XML version 1.0, và text được mã hóa theo định dạng utf-8.

Đoạn kế tiếp.

1
2
3
4
5
6
7
8
9
10
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    ...
 
</android.support.constraint.ConstraintLayout>

Đoạn này cho thấy rằng giao diện đang được khai báo với thẻ gốc là ConstraintLayout. Lưu ý là nếu bạn nào chưa rành về XML được tổ chức như thế nào, thì có thể tìm hiểu thêm trên mạng. Rất nhiều bài viết mô tả rất chi tiết về cấu trúc XML đấy nhé, đơn cử như bài viết này đây.

Quay lại đoạn XML trên đây. Như bài học có nói, ConstraintLayout là một ViewGroup, do đó bên trong nó có thể chứa các View hoặc ViewGroup khác. Như trường hợp trên, TextView chính là View, thành phần này nằm bên trong ViewGroup ConstraintLayout. Ngoài ra, sở dĩ bạn thấy ConstraintLayout được viết hơi bị dài dòng, android.support.constraint.ConstraintLayout là vì hệ thống nêu đích danh nơi chứa ConstraintLayout này thôi, bạn đừng quan tâm cũng đừng chỉnh sửa gì hết nhé.

Các thuộc tính bên trong thẻ ConstraintLayout hay các thẻ khác xuất hiện trong giao diện hôm nay sẽ được mình trình bày riêng ở bài học khác. Hôm nay bạn hiểu sơ như thế này thôi.

Thực Hành Chỉnh Sửa Giao Diện Cho TourNote

Bây giờ chúng ta sẽ chính thức chỉnh sửa giao diện cho TourNote nhé. Vẫn ở màn hình của main_activity.xml, bạn hãy chọn lại tab Design.

Sau đó đảm bảo chữ “Hello World!” đang được chọn ở màn hình trực quan thiết kế trong cửa sổ này. Bạn có thể tham khảo hình sau.

Tạo giao diện người dùng - Tab Design

Chữ “Hello World!” mà bạn đang chọn đó là một TextViewTextView là gì ư? Đây là một widget giúp chúng ta hiển thị các text đến với người dùng. Text mà TextView này đang hiển thị chính là “Hello World!” đó. Giờ chúng ta sẽ thay đổi text này. Từng Widget cụ thể cũng sẽ được mình nói ở các bài học riêng lẻ luôn nhé. Với TextView đang được chọn, nhìn vào bên phải cửa sổ editor bạn thấy có một cửa sổ nhỏ có tên Attributes. Cửa sổ Attributes sẽ giúp bạn thay đổi các thuộc tính của các View hay ViewGroup mà bạn đang chọn.

Nhìn xuống phía dưới Attributes, bạn sẽ thấy một field có tên là text. Trong field text này có đang hiển thị sẵn text “Hello World”. Hãy thay thế text này thành text “Bạn chưa có ghi chú nào cho mình, hãy tạo mới ghi chú từ hôm nay”. Như hình sau.

Tạo giao diện người dùng - Sửa chữa TextView

Khi bạn sửa chữa nội dung cho TextView ở cửa sổ Attributes, bạn thấy trực quan màn của ứng dụng cũng thay đổi theo ở bên phải Attributes này, thật thuận tiện đúng không nào.

Tuy nhiên, cách hiển thị text ở màn hình ứng dụng chưa được đẹp lắm. Bạn mong muốn chúng được canh giữa màn hình cơ. Bạn hãy làm tiếp bước dưới đây.

Cuộn xuống đến cuối Attributes, bạn sẽ thấy dòng chữ View all attributes.

Tạo giao diện người dùng - View all attributes

Nhấn vào dòng chữ này hệ thống sẽ dẫn bạn đến một danh sách đầy đủ hơn các thuộc tính của TextView. Bạn thấy đấy, một cái TextView thôi mà có rất nhiều thuộc tính cho nó đúng không. Bạn có thể nhìn sơ qua danh sách các thuộc tính này, chúng cũng rất dễ hiểu thôi.

Tạo giao diện người dùng - All attributes

Hãy cuộn tìm trong các thuộc tính này một thuộc tính quan trọng, đó là gravity. Hãy nhấn vào hình tam giác bên trái chữ gravity này để bung ra nhiều tùy chọn nhỏ hơn nữa. Khi này bạn hãy check chọn vào thành phần center bên trong gravity này nhé. Bạn có thể xem hình dưới đây để hiểu rõ hơn.

Tạo giao diện người dùng - Chọn gravity center

Nếu bạn chưa biết gravity là gì thì có thể xem tiếp các bài học kế. Nhưng như bạn thấy đấy, sau khi check chọn vào center, bạn thấy ngay text ở TextView đã được canh chỉnh vào giữa rồi đúng không nào.

Tạo giao diện người dùng - Xem trước giao diện

Giờ thì bạn hãy nhấn lại tab Text của màn hình activity_main.xml để xem sự thay đổi về code XML nhé. Bạn có thấy dòng code nào xuất hiện thêm không. Để mình chỉ ra cho bạn xem bằng dòng lệnh được tô sáng như sau. Bài sau chúng ta sẽ tìm hiểu một chút về các dòng code XML này.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="Bạn chưa có ghi chú nào cho mình, hãy tạo mới ghi chú từ hôm nay"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
 
</android.support.constraint.ConstraintLayout>

Run Ứng Dụng

Lúc này bạn hãy thử run ứng dụng TourNote để xem thành quả nhé.

Làm Quen Với Android Resource

Ở bài trước chúng ta đã làm quen sơ qua cách thức tạo một giao diện cho Android rồi đúng không nào. Đến bài học hôm nay các bạn sẽ chưa cần phải tạo một giao diện nào khác đâu, mà hãy cùng mình nhìn lại cách thức chúng ta sử dụng các “nguyên liệu” để tạo thành một giao diện hoàn chỉnh của bài hôm trước. “Nguyên liệu” mà mình muốn nhắc đến đó chính là khái niệm Resource.

Resource Là Gì?

Trong lập trình Android, có một sự phân biệt rõ ràng giữa resource và source code. Nếu như source code là Java code, thì resource là những file còn lại không liên quan đến Java code đó, chẳng hạn như các file hình ảnh, âm thanh, video, các file text, màu sắc,… và thậm chí là file activity_main.xml mà bạn đã làm quen hôm trước, cũng chính là các resource mà chúng ta cần quan tâm.

Resource Để Ở Đâu Trong Project?

Nếu như source code được để trong thư mục java/ thì resource được để trong thư mục res/. Bạn hãy nhìn vào hai thư mục mà mình vừa nhắc đến bên dưới đây.

Resource - Nơi chứa đựng resource

Nếu bạn xổ thư mục res/ này ra, bạn sẽ nhìn thấy rất nhiều thư mục con trong đó. Những thư mục con này được tạo sẵn khi bạn tạo mới một project, số lượng thư mục tạo sẵn và tên các thư mục này phụ thuộc vào từng thời điểm của Android Platform. Ngoài các thư mục được tạo sẵn này bạn có thể tạo thêm nhiều thư mục khác nữa (tất nhiên theo một quy luật đặt tên nhất định mà chúng ta sẽ nói rõ hơn ở các bài sau).

Resource - Tổng quan thư mục res

Sử Dụng Resource Như Thế Nào?

Bạn nên ghi nhớ rằng, resource của Android được chia ra làm 2 dạng, Default Resource (resource theo mặc định) và Alternative Resource (resource có chọn lựa).

Default Resource

Default resource có thể dịch là resource theo mặc định. Các resource dạng này sẽ được hiển thị tùy theo chỉ định mặc định của máy.

Ví dụ như bạn để một ảnh Bitmap vào trong default resource, thì ảnh của bạn sẽ được dùng cho tất cả các kích cỡ màn hình ở ngoài thị trường, nó có thể sẽ trông đẹp ở một số màn hình, nhưng có thể sẽ mờ, hay vỡ ở một số màn hình khác.

Thêm một ví dụ nữa, chẳng hạn như bạn đang xây dựng ứng dụng hỗ trợ song ngữ Anh-Việt, thì với file ngôn ngữ được để ở default resource, file này sẽ được hệ thống dùng đến mà không hề có sự chọn lựa rằng khi nào nên hiển thị giao diện ứng dụng với ngôn ngữ tiếng Anh, khi nào tiếng Việt.

Nói vậy không có nghĩa là default resource vô dụng, vậy khi nào thì dùng default resource được? Chúng ta đi tiếp phần dưới đây để cùng nhau sáng tỏ.

Cách Sử Dụng Default Resource

Nếu bạn có một ảnh, hay một file âm thanh, hay một text, mà bạn không cần phải phân biệt chúng giữa các thiết bị khác nhau trên thị trường, thì bạn có thể để các file này ở default resource.

Mình ví dụ như resource là một file âm thanh notification của app. Ồ file này thì dù cho máy có màn hình kiểu nào, LG hay Samsung, chạy ở quốc gia nào cũng được. Cho file này vào default resource thôi.

Hay bạn có một ảnh dạng Vector, ảnh dạng này sẽ không bị bể khi bạn zoom nó như là ảnh Bitmap, do đó nó không cần phân biệt theo các loại màn hình khác nhau. Cho file ảnh này vào default resource thôi.

Thêm một ví dụ nữa, đó là nếu bạn có các resource dạng màu sắc. Ồ màn hình nào cũng hiển thị màu giống nhau, không cần phân biệt. Cho resource này vào default resource thôi.

Nhiêu đây ví dụ chắc bạn cũng đã hiểu khi nào thì dùng resource dạng default resource. Vậy thì, cho resource vào default resource là thế nào? Dễ lắm, bạn chỉ cần để các resource mà bạn có vào các thư mục con của thư mục res/ theo một quy tắc sau. Nên nhớ là các thư mục con này hoặc sẽ có sẵn khi bạn tạo mới project (như hình mình để ở trên kia), hoặc bạn tự tạo nhưng nhớ đúng với tên mình liệt kê ra đây (tất cả loại resource này sẽ được mình nói rõ ở từng bài học riêng biệt, bạn yên tâm nếu đọc sơ qua danh sách dưới đây mà không hiểu gì nhé).

– animator/: Thư mục này dùng để chứa các animation theo dạng XML. Đây là các resource định nghĩa về chuyển động cho các thành phần UI của ứng dụng.
– anim/: Thư mục này cũng dùng để chứa các animation theo dạng XML. Nhưng khác với animator, các resource theo dạng anim tạo chuyển động theo một số cấu trúc định sẵn, và vì vậy anim sẽ không linh hoạt bằng animator.
– color/: Thư mục này dùng để chứa định nghĩa về các màu sắc được biến đổi theo trạng thái (state list of colors) cho ứng dụng theo dạng XML.
– drawable/: Thư mục này dùng để chứa các ảnh bitmap (hỗ trợ các ảnh theo định dạng .png.9.png.jpg.gif); Chứa các ảnh biến đổi theo trạng thái (state list); Chứa các hình khối (shape) do bạn tự vẽ bằng XML; Và còn nhiều loại khác nhưng ít phổ biến hơn các loại mình vừa nêu.
– mipmap/: Thư mục này dùng để chứa các icon của ứng dụng.
– layout/: Thư mục này dùng để chứa đựng tất cả các giao diện màn hình được code bằng XML. Bạn nhớ lại đi, bài trước chúng ta thử code giao diện ở file activity_main.xml, file lày có phải nằm trong thư mục layout/ không nào?
– menu/: Thư mục này dùng để chứa đựng tất cả các định nghĩa cho menu của ứng dụng, bao gồm Options MenuContext Menu, hay Sub Menu.
– raw/: Thư mục dùng để chứa đựng các file mà bạn được phép định nghĩa nội dung tùy thích, không bị gò bó phải là XML hay Java code gì cả.
– values/: Thư mục này dùng để chứa đựng các file XML với các kiểu dữ liệu đơn giản, như stringintegercolor. Chúng ta sẽ nói rõ hơn chút xíu về thư mục values/ này ở phần tiếp theo của bài học hôm nay.
– xml/: Thư mục này dùng để chứa đựng các file XML tùy ý, tuy tùy ý nhưng phải theo cấu trúc XML.

Vậy thôi đấy, có nhiều lắm không bạn. Bạn yên tâm chúng ta sẽ nói rất cụ thể về từng loại resource trong từng thư mục default resource này sau.

Và có một điều nên nhớ rằng, trong Android thư mục res/ chỉ chứa duy nhất một cấp thư mục con thôi. Do đó bạn không thể tạo một thư mục con như này được res/values/my_res/.

Thực Hành Tạo Default Resource Cho TourNote

Chào mừng các bạn quay lại ứng dụng TourNote.

Trước hết, hãy nhớ lại, bài trước bạn đã cho hiển thị dòng chữ “Bạn chưa có ghi chú nào cho mình, hãy tạo mới ghi chú từ hôm nay” lên màn hình. Bạn làm rất đúng, và ứng dụng chạy lên thiết bị ngon lành.

Nhưng… hãy nhìn lại activity_main.xml, có một số dấu hiệu cảnh báo bên trong editor này. Nếu ở tab Design, đảm bảo TextView ở giữa màn hình được chọn, bạn sẽ thấy icon hình biển báo màu vàng với dấu chấm than bên trong, click vào icon này bạn sẽ thấy nội dung cảnh báo được liệt kê ở một cửa sổ nhỏ bên dưới nó.

Resource - Cảnh báo resource

Hay ở tab Text, bạn cũng thấy vệt vàng xuất hiện ở bên phải text, đưa chuột vào đó bạn cũng nhìn thấy rõ nội dung cảnh báo.

Resource - Cảnh báo resource

Các vệt vàng xuất hiện trong editor chính là các vệt cảnh báo, nó cho bạn biết là bạn đã làm gì đó chưa được đúng đắn. Mặc dù nó không phải là các vệt lỗi được hiển thị bằng màu đỏ, nếu có vệt đỏ bạn sẽ không build app được mà phải sửa ngay.

Quay lại một chút với nội dung các vệt vàng này. Bạn đều thấy cả hai nói rằng bạn đang hardcode, dịch ra tiếng Việt là code cứng, hay có thể hiểu là “code chuối”. Bởi vì như bạn thấy đó, các resource trong Android được tách bạch rõ ràng vai trò theo các thư mục. Nghĩa là sao? Bạn xem, activity_main.xml là file nằm trong thư lục layout/, chính vì vậy nó sẽ chỉ chứa thông tin về giao diện mà thôi. Do đó, việc bạn để hẳn một chuỗi trong file layout đó là tạm được, nhưng sẽ bị cảnh báo rằng bạn đang code cứng.

Vậy bạn đã hiểu mình phải làm gì để xóa bỏ lời cảnh báo rồi phải không nào, bạn phải khai báo chuỗi ở nơi khác, và gọi đến chúng từ file layout này. Lợi ích của việc làm này từ từ bạn sẽ hiểu khi chúng ta qua bước kế tiếp trong bài này.

Ở bước tiếp theo này, mình muốn các bạn nên tổ chức rằng cách chuỗi nên để trong file strings.xml. Để có thể hiểu rõ hơn tại sao chuỗi lại để trong đây thì đến các bài học về từng loại resource mình sẽ trình bày cụ thể hơn. Để mở file chứa chuỗi này ra thì bạn tìm trong thư mục values/ như hình sau, rồi click đúp vào file đó.

Resource - File strings

Khi file này được mở ra, các bạn có thể thấy có ít nhất một string được khai báo sẵn.

Resource - Strings có sẵnhttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2016/10/hinh6.png?resize=300%2C101&ssl=1 300w" data-lazy-loaded="1" sizes="(max-width: 485px) 100vw, 485px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px auto; clear: both; text-align: center;">

Cách thức khai báo một string cụ thể sẽ được mình nói rõ ở bài viết về resource loại này sau, còn bây giờ hãy thêm vào một dòng string mới.

1
2
3
4
<resources>
    <string name="app_name">TourNote</string>
    <string name="empty_note">Bạn chưa có ghi chú nào cho mình, hãy tạo mới ghi chú cho mình từ hôm nay</string>
</resources>

Bạn chỉ cần lưu ý rằng String bạn vừa khai báo có tên là empty_note, nội dung của nó nằm trong cặp dấu nháy kép sau khi khai báo tên. Giờ bạn hãy quay lại file activity_main.xml và thay thế chỗ bị cảnh báo vạch vàng lúc nãy bằng một trong hai cách sau.

Nếu bạn thao tác với tab Design. Hãy đảm bảo TextView ở giữa màn hình được chọn. Tìm đến field text mà bài học trước bạn đã sửa chuỗi bằng cách gõ thủ công, rồi tìm đến dấu ba chấm () bên góc phải của field này.

Resource - Sửa text choa TextView

Khi nhấn vào dấu ba chấm này, một cửa sổ nhỏ xuất hiện, nơi đây chứa đựng tất cả các resource dạng chuỗi có thể sử dụng được trong project của bạn. Bao gồm cả chuỗi có tên empty_note mà bạn vừa thêm vào ở bước trên. Bạn chỉ việc chọn đến chuỗi đó và nhấn OK thôi.

Resource - Lựa chọn chuỗi cần thêm

Khi này field text ở TextView đã có sự thay đổi. Thay vì nội dung “Bạn chưa có…” thì đã được thay thế bằng “@string/empty_note”. Nếu bạn chưa hiểu cách hiển thị chuỗi này như thế nào thì cũng đừng vội nản nhé, bài học sau chúng ta sẽ tìm hiểu kỹ. Lúc này đây bạn hãy nhìn lại editor của activity_main.xml xem sao. Icon cảnh báo đã biến mất, nhưng màn hình hiển thị trực quan giao diện vẫn như bài học trước.

Nếu bạn xem qua tab Text của cửa sổ này, bạn cũng sẽ thấy nội dung của code cũng thay đổi tương ứng, và cũng chẳng còn icon cảnh báo đâu nữa nhé.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="@string/empty_note"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
 
</android.support.constraint.ConstraintLayout>

Giờ đây bạn có thể thực thi lại chương trình để kiểm tra xem thao tác của bài học hôm nay có ảnh hưởng gì đến ứng dụng của bạn không nhé.

Alternative Resource

Alternative resource có thể dịch là resource có chọn lựa. Ngược với default resource là các resource mà bạn không cần quan tâm chúng hiển thị thế nào cho từng loại thiết bị. Thì alternative resource là các resource mà bạn chính là người phải chỉ định cho hệ thống biết khi nào nên dùng resource nào, tức là có sự chọn lựa resource theo ý bạn. Bạn hãy nhìn vào hình minh họa sau.

Resource - Minh họa Alternative Resource

Hình minh họa trên có nghĩa là, giả sử bạn có một layout theo chuẩn alternative resource, khi đó bạn có thể chỉ định rõ cho hệ thống rằng nếu ứng dụng chạy trên phone (như hình bên trái) thì hãy hiển thị layout đó dạng list, còn nếu ứng dụng chạy trên tablet (hình bên phải) thì hãy hiển thị layout dạng table.

Vậy cách sử dụng alternative resource có khác với default resource hay không? Mời bạn xem tiếp bước sau.

Cách Sử Dụng Alternative Resource

Cách sử dụng alternative resource đơn giản không kém cách sử dụng default resource, mình chắc chắn đó. Bởi vì bạn vẫn dựa vào cách thức tổ chức thư mục theo dạng default resource, rồi bạn chỉ cần thêm một điều kiện vào cuối tên của mỗi thư mục đó để hệ thống biết khi nào nên load resource nào mà thôi.

Công thức cụ thể cho alternative resource như sau.

1
<default_resource>-<config_qualifier>

Trong đó default_resource là các thư mục con của res/ mà mình có nhắc đến ở trên như drawable/layout/,… Còn config_qualifier chính là phần chỉ định cho hệ thống biết khi nào nên sử dụng resource nào. Hai thành phần này được ngăn cách nhau bởi dấu “-“.

Nhìn vào hình dưới đây, bạn sẽ thấy các thư mục được tô sáng chính là các thư mục alternative resource, vì chúng có chỉ định config_qualifier. Để biết rõ về các chỉ định config_qualifier này thì mời bạn đọc bài viết này để hiểu rõ hơn nhé..

Resource - Các Alternative Resource

Bạn hãy nhìn vào ví dụ các file ảnh ic_launcher.png được để vào trong các alternative resource như hình sau.

Resource - Tìm hiểu Alternative Resource

Khi đó giả sử ở một thiết bị nào đó, khi hệ thống muốn load ảnh ic_launcher.png này lên dùng, thì việc đầu tiên hệ thống sẽ phải xác định xem ic_launcher.png ở thư mục nào là thích hợp nhất cho thiết bị đó. Nếu hệ thống tìm thấy một thư mục thích hợp, ví dụ mipmap-xxxhdpi/ thì ic_launcher.png trong thư mục đó sẽ được load lên dùng. Còn nếu hệ thống không tìm thấy một thư mục thích hợp, nó sẽ tìm đến thư mục default resource, tức là tìm đến thư mục với tên mipmap/. Chính vì vậy bạn thường thấy có cả các default resource lẫn alternative resource được định nghĩa đồng thời trong cùng một ứng dụng.

Thực Hành Tạo Alternative Resource Cho TourNote

Bây giờ chúng ta sẽ nâng cấp resource string lúc nãy, sao cho app có thể hỗ trợ song ngữ Anh-Việt. Nếu thiết bị của user đang dùng là tiếng Việt, TourNote được khởi động cũng hiển thị tiếng Việt, ngược lại nếu thiết bị của user là tiếng Anh thì TourNote cũng tiếng Anh. Còn nếu thiết bị của user hiển thị ngôn ngữ khác với 2 ngôn ngữ mà bạn hỗ trợ, thì hiển thị mặc định là tiếng Anh.

Trước hết bạn cần phải tạo một alternative resource. Bạn hãy tạo một thư mục con của res/ với tên chính xác là values-vi/.

Chắc bạn cũng biết tại sao phải đặt tên như vậy, nhưng để chắc cú thì mình cũng xin nhắc lại. Chúng ta đã có sẵn một thư mục default resource có tên là values/, trong đó chúng ta chứa một file strings.xml, file này chứa đựng các text của ứng dụng, các text này vẫn chưa được đặt điều kiện config_qualifier do đó nó sẽ được đọc lên ở bất kỳ trường hợp nào của thiết bị. Giờ chúng ta thêm một thư mục có phần default_resource cũng là values, nhưng thêm vào config_qualifier là -vi ý muốn nói hệ thống hãy tìm đến thư mục alternative resource này ngay khi ngôn ngữ máy là vi (tiếng Việt). Còn nếu bạn nào muốn biết tại sao phải là -vi mà không phải -vn hay -vietname thì bạn nên biết là nó đều có quy tắc cả và mình nói hết tất tần tật quy tắc này ở đây rồi nhé.

Giờ đây mình tin chắc bạn đã hiểu rồi, vậy hãy tạo mới thư mục resource như đã nói bằng cách click chuột phải vào res/ chọn New > Directory và gõ vào values-vi.

Sau khi bạn tạo xong thư mục mới, bạn nên kiểm tra chắc chắn rằng thư mục mới đó phải nằm ở con của res/ và ngang cấp với values/ như hình sau nhé.

Resource - Tạo thư mục mới

Nếu bạn tạo sai vị trí của values-vi/ hoặc đặt sai tên, bạn cứ nhấn chuột phải vào thư mục sai đó và chọn delete rồi tạo mới lại theo bước trên đây.

Giờ thì với thư mục value-vi/ mới được tạo, chưa có gì trong đó cả, bạn cần có một file string với nội dung là các text tiếng Việt, bạn có thể copy strings.xml có sẵn trong values/ qua cho tiện.

Để copy strings.xml bên values/, bạn vào thư mục này, click phải chuột vào strings.xml chọn Copy.

Resource - Copy resoure string

Rồi click chuột phải lại vào values-vi/ chọn Paste. Một dialog xuất hiện hỏi bạn có đổi tên không, bạn hãy nhấn OK mà không đổi tên gì cả, vì chúng ta mong muốn 2 tên file đều là strings.xml.

Resource - Paste resource string

Bạn có nhận ra không. Hiện tại thì file strings.xml bên values/ đang sai vì chúng ta mong muốn thư mục này phải chứa resource default là tiếng Anh.

Vậy bạn hãy mở strings.xml bên values/ để chỉnh sửa thành tiếng Anh như sau.

Resource - Thêm resource tiếng Anh

Nên nhớ là name=”empty_note” là giống nhau ở cả 2 file string này.

Nào, giờ bạn thử vào settings của thiết bị (máy ảo Android hay máy thật mà bạn chuẩn bị run ứng dụng) chỉnh ngôn ngữ là tiếng Anh rồi run ứng dụng. Sau đó vào lại settings của thiết bị chỉnh ngôn ngữ thành tiếng Việt rồi run lại ứng dụng. Hình ảnh của 2 lần run như sau.

Nên nhớ là name=”empty_note” là giống nhau ở cả 2 file string này.

Nào, giờ bạn thử vào settings của thiết bị (máy ảo Android hay máy thật mà bạn chuẩn bị run ứng dụng) chỉnh ngôn ngữ là tiếng Anh rồi run ứng dụng. Sau đó vào lại settings của thiết bị chỉnh ngôn ngữ thành tiếng Việt rồi run lại ứng dụng. Hình ảnh của 2 lần run như sau.

Resource - Kết quả chạy cuối cùng

Đi Sâu Vào Widget

Chắc bạn còn nhớ ở bài 7, bạn đã làm quen với việc tạo một giao diện khá cơ bản cho TourNote, khi đó bạn đã biết về mối tương quan giữa View và Widget. Bài học hôm nay sẽ giúp bạn hiểu rõ hơn, cũng như sẽ biết cách sử dụng tốt hơn vài widget cơ bản nhất trong lập trình Android. Sau bài học này thì mình cũng có viết một bài mở rộng, nơi đó mình nói về tất cả các widget còn lại mà bài học hôm nay chưa kịp nói đến.

Nào trước khi vào từng widget cụ thể, chúng ta hãy đi qua các khái niệm sau.

Khái Niệm Thuộc Tính (Attribute)

Các Thuộc Tính của một view được dùng để định nghĩa các biểu hiện của một view đó. Chẳng hạn như vị trí của chúng, độ rộng & chiều cao của chúng, màu sắc, nội dung,…

Các thuộc tính này được hiển thị trong cửa sổ Attribute khi bạn nhấn chọn vào View này trong editor ở chế độ Design. Hoặc được định nghĩa thông qua các câu lệnh XML. Ở bài 7 bạn có thực hành sơ qua việc sử dụng các thuộc tính này, cụ thể là thuộc tính text.

Tạo giao diện người dùng - Sửa chữa TextView

Một số ít trường hợp còn lại các thuộc tính sẽ được định nghĩa bằng Java code, nhằm phục vụ một số ý đồ thay đổi diện mạo cho view đó ở runtime.

Một ví dụ để bạn biết nơi tham khảo tất cả các thuộc tính XML của một view, bạn hãy vào link này, đây là ví dụ cho TextView. Khi đã mở link ra thì bạn hãy nhìn vào bảng “XML attributes”, bảng này sẽ liệt kê tất cả các thuộc tính bằng XML của TextView. Kéo xuống bảng ở dưới một tí là “Inherited XML attributes”, bảng này liệt kê tiếp tất cả các thuộc tính XML mà TextView đó kế thừa từ view gốc, nếu bạn chưa hiểu khái niệm kế thừa là gì thì bạn cứ nhớ rằng tất cả các thuộc tính XML của TextView này sẽ nằm ở cả 2 bảng vừa nhắc đến trên đây. Dựa vào các thuộc tính này của XML, bạn cũng sẽ dễ dàng suy ra các giá trị bên trong cửa sổ Attribute của tab Design thôi, vì tên của chúng khá giống nhau, bạn so sánh đi nhé. Hơn nữa, ở dưới trang mình đã đưa còn một bảng hữu dụng nữa là “Public methods”, bảng này chứa các thuộc tính mà bạn có thể gọi đến ở Java code cho các mục đích thay đổi runtime được nhắc đến ở trên.

Một Số Thuộc Tính Quan Trọng Của View

Phần này mình sẽ nói đến các thuộc tính quan trọng hoặc hay sử dụng thôi nhé. Vì bạn cũng biết đó, mỗi View có rất nhiều thuộc tính, việc của bạn là đến các link liên quan đến các View đó như link mình đưa ra ở mục trước, để tự tìm hiểu các thuộc tính liên quan của View.

View Nào Cũng Nên Có ID

Bạn thử nghĩ xem nếu bạn muốn tô màu cho một View, hay set text cho chúng, cách tốt nhất để có thể phân biệt xem View nào là View nào, là đặt cho chúng một ID rồi gọi chúng thông qua ID này. ID thực ra là một cái tên do bạn đặt cho các View hay ViewGroup. Tất nhiên cũng có View không cần bạn phải đặt một ID, vì chúng luôn luôn hiển thị với một trạng thái duy nhất trong suốt quá trình sống của ứng dụng, chúng không bị thay đổi, và không ảnh hưởng đến vị trí hiển thị của view nào khác cả.

Định Nghĩa ID Cho Một View

Để định nghĩa một ID cho View (hay ViewGroup), bạn có thể làm bằng một trong hai cách, tùy vào kiểu hiển thị Design hay Text đối với View đó.

Nếu như ở tab Design của editor chỉnh sửa giao diện. Để định nghĩa ID cho View, bạn hãy đảm bảo View bạn cần đặt ID được chọn trong màn hình trực quan thiết kế. Rồi tìm đến cửa sổ Attribute bên cạnh, và tìm field có tên ID. Field này chính là nơi bạn đặt ID cho View hay ViewGroup được chọn đó. Bạn xem hình sau sẽ rõ. Còn việc đặt ID theo tiêu chuẩn gì thì bạn có thể tham khảo cách đặt tên ID ở code XML mình nói ngay dưới hình sau.

Tạo giao diện người dùng - Tạo ID

Nếu bạn muốn thêm ID cho View hay ViewGroup ngay bên trong code của tab Text, thì bạn nên nhớ cú pháp để định nghĩa một ID là.

android:id="@+id/id_của_view"

Trong đó.

– android:id – Thuộc tính này được dùng khi bạn muốn định nghĩa một ID cho View hay ViewGroup bằng code XML.

– @+id/  – Báo cho hệ thống biết bạn muốn tạo mới một ID. Nhớ là phải có dấu “+”.

 id_của_view  – Là tên bạn tự đặt (theo chuẩn đặt tên biến của Java, nếu bạn nào có nhu cầu có thể xem cách đặt tên biến ở bài học Java này).

Truy Xuất Đến ID Của Một View

Bạn vừa mới tạo một ID, nếu bạn muốn gọi đến view thông qua ID này thì có các trường hợp sau.

Trường hợp thứ nhất, nếu bạn dùng phương pháp kéo thả giao diện tab Design, thì bạn hầu như không cần quan tâm đến ID của các View. Vì sao. Vì bạn đã làm việc trực quan với giao diện của chúng rồi. Hệ thống sẽ tự động tìm kiếm và điền ID vào code XML cho bạn. Đến các bài thực hành kế tiếp bạn sẽ hiểu rõ vấn đề này. Nhưng cho dù cách thức lập trình giao diện này không hề liên quan đến ID, mình vẫn liệt kê ra đây, để cho bạn thấy rằng ID nó khá là quan trọng trong thiết kế giao diện.

Trường hợp thứ hai, nếu bạn thiết kế hay chỉnh sửa giao diện trong code XML, thì bạn sẽ đụng chạm nhiều đến ID hơi bị nhiều, khi đó bạn có thể dùng cú pháp sau để có thể gọi đến các ID của các View hay ViewGroup thông qua code XML này.

thuộc_tính_android="@+id/id_của_view"

Trong đó.

– thuộc_tính_android – Một số thuộc tính của Android. Như bạn sẽ làm quen ở ngay dưới phần thực hành của bài học này, hay cụ thể hơn ở Bài 10, khi đó với layout có tên là ConstraintLayout sẽ bắt bạn phải chỉ định vị trí tương quan giữa các view, và vì vậy bạn sẽ gọi đến ID của view khác thông qua rất nhiều thuộc tính, như app:layout_constraintBottom_toTopOf, hay app:layout_constraintStart_toEndOf,… như code minh họa sau.

1
2
3
4
5
6
<TextView
    ...
    app:layout_constraintBottom_toTopOf="@+id/id_của_view"
    app:layout_constraintEnd_toEndOf="@+id/id_của_view"
    app:layout_constraintStart_toEndOf="@+id/id_của_view"
    app:layout_constraintTop_toTopOf="@+id/id_của_view" />

– @id/ – Khác với khi định nghĩa một ID mới, lúc đó bạn phải dùng dấu “+” phía sau ký tự “@”. Lần này truy xuất đến ID đã định nghĩa, bạn có thể bỏ dấu “+” đi và chỉ cần gọi @id/ mà thôi.

– id_của_view – Chính là ID mà bạn đã định nghĩa.

Trường hợp thứ ba nếu bạn gọi đến ID của View hay ViewGroup trong Java code, bạn phải truyền R.id.id_của_view vào một số hàm, ví dụ hàm này findViewByID(R.id.id_của_view);.

View Nào Cũng Cần Có Kích Cỡ

Kích cỡ ở đây là các định nghĩa về chiều ngang & dọc của view. Tương ứng với điều đó chúng ta có 2 thuộc tính liên quan đến xác định kích cỡ cho view, đó là layout_width và layout_height (lưu ý là bạn có thể không cần định nghĩa ID cho view, nhưng bạn buộc phải định nghĩa kích cỡ, nếu không thì hệ thống sẽ báo lỗi).

Do trong lập trình mobile, bạn sẽ không biết chính xác ứng dụng của bạn sẽ được chạy trên kích thước màn hình nào, có hàng ngàn kích thước màn hình khác nhau trên thị trường, do đó để chỉ định được kích cỡ của một view sao cho hoàn hảo khi ứng dụng được chạy lên là điều không dễ. Nhưng Android đã hỗ trợ bạn, không chỉ với việc chỉ định kích thước cụ thể, mà còn có thể có các chọn lựa khác nhau cho các kích thước động nữa, mời bạn làm quen trước với các khái niệm kích thước sau.

1. Bạn có thể chỉ định giá trị match_parent, với giá trị này thì view của bạn sẽ thoải mái bung ngang hay bung dọc sao cho có thể dàn hết không gian mà view cha của view đó cho phép. Ví dụ một view được set thuộc tính android:layout_width=”match_parent” sẽ có chiều ngang hết cỡ trong khoảng chiều ngang của view cha, và nếu thuộc tính android:layout_height=”match_parent” thì sẽ có chiều cao hết cỡ trong khoảng chiều cao của view cha.

2. Bạn có thể chỉ định giá trị match_constraint. Nếu view của bạn nằm trong một ConstraintLayout, thì sẽ không có kiểu match_parent như trên đâu, mà là match_constraint. Cơ bản thì kiểu khoảng cách này gần gần giống với match_parent quen thuộc ở các layout khác. Chỉ khác ở chỗ Match Constraint không “độc ác” đến nổi đẩy các view khác ra khỏi màn hình mà chiếm lấy hết không gian ở view cha. Nó vẫn chiếm không gian của view cha nhưng chiếm trong khả năng cho phép, tức là vẫn tuân thủ theo quy luật mà các constraint đã tạo, vẫn chừa không gian cho các view khác xuất hiện. Và khi bạn chỉ định kiểu khoảng cách là Match Constraint thì code XML của view sẽ mang giá trị là 0dp.

3. Bạn có thể chỉ định giá trị wrap_content, với chỉ định này thì view của bạn sẽ có kích thước co dãn sao cho đủ không gian cho các thành phần con bên trong nó.

4. Chỉ định kích cỡ cụ thể. Hai cách trên khá tốt, với màn hình khác nhau bạn cũng không sợ, vì bạn có thể chỉ định một view rộng hết màn hình, hay rộng trong khoảng thành phần con của nó thôi. Tuy nhiên còn 1 cách nữa là bạn có thể chỉ định độ lớn mong muốn theo một giá trị dp, ví dụ bạn set android:layout_width=”125dp” (hoặc 125dip cũng được, 2 ký hiệu viết tắt này đều là density independent, biểu thị một giá trị ảo dựa trên tỉ lệ điểm ảnh của màn hình, giá trị này sẽ được mình nói đến ở bài sau vì nó cũng khá dài dòng).

Để định nghĩa kích thước cho View (hay ViewGroup), bạn cũng có thể làm bằng một trong hai cách, tùy vào kiểu hiển thị Design hay Text đối với View đó.

Nếu như ở tab Design của editor chỉnh sửa giao diện, bạn hãy đảm bảo View bạn cần đặt ID được chọn trong màn hình trực quan thiết kế. Ở cửa sổ Attribute bên cạnh hãy tìm đến các field có tên layout_width và layout_height và set vào đó một trong các giá trị mà mình vừa trình bày trên kia. Bạn có thể tham khảo hình sau.

Tạo giao diện người dùng - Thiết lập kích thước

Có một lưu ý nữa là, với cửa sổ Attribute này, bạn còn có thể có một cách khác thú vị hơn nữa để thay đổi kích thước của View, bằng cách click vào một trong các nơi được mình tô đỏ như hình dưới đây.

Tạo giao diện người dùng - Tùy chình kích thước

Về ý nghĩa của các ký hiệu kích thước ở hình trên đây, thì mình mời các bạn xem qua một chút ở link này, mình không nhắc lại ở bài học này để tránh rườm rà nhé.

Còn nếu như bạn không muốn sử dụng tab Design như trên đây, mà muốn định nghĩa kích thước cho View hay ViewGroup ngay bên trong code của tab Text, thì bạn có thể để ý đến hai thuộc tính sau.

1
2
3
4
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    ... />

Xây Dựng Một Số Widget Cho Tour Note

Nào giờ chúng ta sẽ thực hành thực tế việc chỉnh sửa các thuộc tính cho các View sẽ như thế nào nhé.

TextView – Nơi Hiển Thị Các Label

Lại nhắc đến bài số 7, khi đó bạn đã tạo một TextView bên trong file activity_main.xml rồi, với TextView này bạn đã biết công dụng của nó là hiển thị thông báo đến người dùng khi không có ghi chú nào trong ứng dụng được tạo. Đúng vậy, TextView chính là một label, widget này chuyên dùng để hiển thị các đoạn text mang tính thông báo mà người dùng không thể chỉnh sửa được.

Các Thuộc Tính Thường Dùng Của TextView

Ngoài các thuộc tính chung được nói ở mục trên kia, TextView còn có các thuộc tính thường dùng của riêng nó như sau.

– android:gravity – Dùng để canh chỉnh text của nó sao cho canh trái, phải, giữa,… so với không gian của chính nó. Ở bài học số 7 bạn đã thử canh chỉnh thuộc tính gravity này rồi.

– android:text – Bạn đã dùng đến thuộc tính này để set text cho TextView ở bài trước rồi đúng không nào.

– android:textColor – Hiển thị màu cho text, bài thực hành hiển thị màu sẽ được mình nói đến ở bài viết đi sâu hơn về resource.

– android:textSize – Kích cỡ của text, kích cỡ này được tính theo đơn vị sp. Định nghĩa và cách sử dụng đơn vị này sẽ được nói đến ở bài khác, cùng với các định nghĩa về dp và dip được nhắc đến ở trên.

– android:textAllCaps – Dùng để in hoa hết tất cả các ký tự của text nếu giá trị thuộc tính này là true.

– android:padding – Set khoảng cách giữa biên của view đến các thành phần con của nó. Đơn vị tính của thuộc tính này cũng là dp hoặc dip. Nếu bạn muốn khoảng cách riêng cho từng cạnh biên có thể dùng tách biệt từng thuộc tính cụ thể của padding như android:paddingTopandroid:paddingBottomandroid:paddingLeft và android:paddingRight.

– android:margin – Set khoảng cách giữa biên của view đến các thành phần bên ngoài của nó. Nếu như padding trên kia giúp cho các thành phần con cách xa cạnh biên của view ra, thì margin lại giúp các thành phần bên ngoài tránh xa cạnh biên của view. Bạn nên phân biệt hai giá trị này. Đơn vị tính của thuộc tính này cũng là dp hoặc dip. Tương tự bạn có thể set android:marginTopandroid:marginBottomandroid:marginLeft và android:marginRight đối với từng cạnh của view.

– android:ellipsize – Dùng thuộc tính này khi bạn muốn text của mình sẽ bị cắt và hiển thị “…” khi không đủ không gian để chứa hết text đó.

– android:background – Dùng để set màu nền hoặc ảnh nền cho View.

Thực Hành Hoàn Thiện Các Thuộc Tính Cho TextView Của TourNote

Bây giờ các bạn hãy mở lại project TourNote lên, chúng ta tiến hành chỉnh sửa lại TextView ở bài trước cho đúng đắn và chuyên nghiệp hơn.

Bạn mở lại file activity_main.xml ra, hãy để giao diện này đang ở tab Design, và đảm bảo TextView ở giữa màn hình được chọn, chúng ta sẽ thêm các thuộc tính sau vào trong TextView.

Trước hết, hãy thêm vào TextView này một ID và các kích thước, bạn hãy thêm vào các thông tin như hình sau rồi mình giải thích nhé.

Tạo giao diện người dùng - Chỉnh sửa TextView

– ID – Bạn đặt ID cho TextView này là activity_main_tv_empty, sở dĩ mình đặt dài dòng như vậy là vì muốn đảm bảo ID này không bị trùng trong ứng dụng của chúng ta, để làm vậy thì chúng ta nên đặt có đủ 3 thành tố sau: tên_file_xml + viết_tắt_của_widget + tên_widget. Theo đó thì tên file xml là activity_main, viết tắt của TextView là tv, và tên của TextView này thông báo không có ghi chú nên đặt là empty. Cộng lại chúng ta có ID như trên.

– layout_width – Được set là match_constraint, như mình có nói ở mục trên, tức là chiều rộng sẽ là maximum so với chiều rộng của view cha.

– layout_height – Được set là wrap_content, mình cũng đã nói rõ, tức chiều cao sẽ tùy theo số dòng của text.

Ngoài ID và kích thước ra, mình còn muốn các bạn chỉ định margin cho TextView này nữa. margin là gì thì mình có nói ở mục trên rồi nhé. Để chỉnh sửa các thuộc tính này, thì bạn vẫn cứ hãy ở tab Design, và đảm bảo TextView ở giữa màn hình được chọn, rồi thêm các chỉnh sửa như hình sau vào trong TextView.

Tạo giao diện người dùng - Margin cho TextView

Chỉ cần nhấn chọn vào hình tam giác bên phải mỗi số thôi là bạn có sẵn các chọn lựa cho các giá trị này. Bạn có thể tự đưa ra cho View một con số bất kỳ nào đó khác cũng được. Và sau khi bạn chỉ định một số, đơn vị của nó sẽ là dp. Bạn hãy so sánh giao diện trực quan giữa trước và sau khi set margin cho View nhé, bạn sẽ hiểu margin là gì thông qua việc so sánh này thôi.

Tạo giao diện người dùng - So sánh margin

Bước tiếp theo của bài thực hành này, mình muốn các bạn chỉnh sửa kích cỡ của text trong TextView. Để chỉnh sửa các thuộc tính này, thì bạn vẫn cứ hãy ở tab Design, và đảm bảo TextView ở giữa màn hình được chọn, rồi thêm kích cỡ text như hình sau vào trong TextView.

Tạo giao diện người dùng - Thay đổi text size

Bạn có thể thấy rằng, với kích thước text, bạn nên định nghĩa cho nó một độ lớn kèm theo giá trị là sp. Về cơ bản dp cũng như sp thôi, nhưng chúng có một chút khác biệt nhau. Việc bây giờ bạn cần biết rằng bạn nên sử dụng thông số sp cho kích cỡ text, hay còn gọi là font size, còn dp dành cho các kích cỡ về các kích thước của View. Tại sao có sự khác biệt này thì chúng ta sẽ tìm hiểu ở bài khác nhé.

Giờ thì mình muốn bạn chuyển cửa sổ thiết kế của activity_main.xml này sang tab Text, để xem các giá trị mới được thêm vào cho code XML của chúng ta như thế nào nhé. Mình liệt kê thay đổi đó bằng các dòng code được tô sáng sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <TextView
        android:id="@+id/activity_main_tv_empty"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginBottom="8dp"
        android:gravity="center"
        android:text="@string/empty_note"
        android:textSize="15sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
 
</android.support.constraint.ConstraintLayout>

Bạn thấy làm việc với giao diện của Android có dễ không nào. Tất cả chỉ là canh chỉnh các giá trị của chúng thôi. Tuy nhiên làm việc hoài với TextView cũng chán, phần sau đây mình mời các bạn cùng thử vọc với việc hiển thị ảnh Bitmap lên ứng dụng xem sao nhé.

Ảnh (Image) Sẽ Giúp Ứng Dụng Trông Pro Hơn

TextView không thôi vẫn chưa đủ mạnh (strong), người dùng sẽ bỏ qua câu thông báo của bạn ngay (ví dụ như trong trường hợp này của ứng dụng TourNote), bạn cần phải có một ảnh làm cho ứng dụng thêm đẹp hơn và người dùng chú ý nhiều hơn tới câu thông báo. Ngoài ra trong hầu hết các ứng dụng khác của Android sau này, bạn sẽ cần dùng đến rất nhiều ảnh, do đó mình xin nói đến cách sử dụng ảnh rất sớm từ bài này.

Có 2 widget giúp hiển thị ảnh trong Android, đó là ImageView và ImageButton. Bài hôm nay chỉ nói về ImageView thôi, trong khi ImageButton là con của ImageView nên cũng có các thuộc tính tương tự ImageView. Tất cả các widget đã được mình tổng hợp lại trong bài Tìm Hiểu Các Widget Cơ Bản, các bạn vào xem nhé.

Các Thuộc Tính Thường Dùng Của ImageView

Tương tự như TextViewImageView cũng có các thuộc tính quan trọng của riêng nó như sau.

– android:src – Hiển thị ảnh theo dạng content, ảnh này sẽ nằm đè lên trên ảnh background nói đến ở dưới đây.

– android:background – Thuộc tính này dùng để set màu nền hoặc ảnh nền cho view.

– android:scaleTyle – Cho biết tỉ lệ co dãn hoặc vị trí của ảnh so với khung của view, các giá trị của thuộc tính này sẽ được nói kỹ sau bài thực hành dưới đây.

– android:padding – Tương tự padding của TextView ở trên. Tuy nhiên padding sẽ làm ảnh hưởng đến không gian hiển thị của src, với background thì “miễn nhiễm” với thuộc tính này.

Thực Hành Thêm ImageView Cho TourNote

Trước hết bạn hãy nhấn vào đây để download resource.

Resource bạn vừa down về chứa đựng nhiều thư mục (drawable-mdpi/drawable-hdpi/drawable-xhdpi/drawable-xxhdpi/ và drawable-xxxhdpi/), bạn giải nén và copy các thư mục này vào thư mục res/ của project TourNote, trong các thư mục đều chứa một ảnh có tên giống nhau là empty_note.png. Bạn biết tại sao ảnh này lại để trong các thư mục này đúng không nào, đó là cách bạn xác định ảnh cho alternative resource (bạn có thể xem bài 8 nếu chưa biết về alternative resource), để khi load lên, độ phân giải của ảnh sẽ được hiển thị chính xác nhất theo tỉ lệ mình hàn hình trên thị trường.

Bạn phải đảm bảo rằng sau khi giải nén và copy các thư mục chứa ảnh vào trong thư mục res/ của project, thì chúng phải hiển thị như hình sau trong ứng dụng, nếu không bạn có thể xóa các thư mục vừa tạo để copy lại, hoặc download source code mẫu theo đường link cuối bài học để check xem thế nào nhé.

Tạo giao diện người dùng - Tổ chức ảnh bitmapSau khi đã copy các ảnh như trên rồi chúng ta tiến hành chỉnh sửa thêm cho giao diện activity_main.xml như sau. Bạn đảm bảo file activity_main.xml đang mở, và tab Design đang được chọn. Ở cửa sổ Palette phía trên bên trái editor này, bạn hãy tìm đến thành phần ImageView. Nắm và kéo thành phần này vào màn hình trực quan thiết kế, bạn hãy thả thành phần ImageView này ra khi thấy rằng nó nằm đâu đó ở dưới TextView như hình sau (chú ý là bạn cũng đừng quan trọng quá ImageView sẽ nằm dưới TextView bao xa, chúng ta chỉ cần đảm bảo nó nằm dưới, thế thôi, việc còn lại sẽ chỉnh sửa sau).

Tạo giao diện người dùng - Kéo thả ImageView

Sau khi thả chuột ra sau quá trình kéo thả, một cửa sổ mới mở lên, cửa sổ này buộc phải chọn ảnh để hiển thị vào trong ImageView này. Bạn hãy đảm bảo Drawable ở bên trái cửa sổ được chọn, tùy chọn này cho hệ thống biết sẽ tìm ảnh trong thư mục res/drawable. Sau đó nhấn chọn vào Project, tùy chọn này ý muốn chúng ta tìm đến ảnh trong project của chúng ta, và sau cùng là chọn vào empty_note, chính là ảnh bạn vừa để vào thư mục của project ở bước trên đây.

Tạo giao diện người dùng - Tìm đến drawable image

Sau khi chọn xong ảnh, thì bạn nhấn OK. Bạn sẽ thấy ảnh hiện ra sẽ đâu đó như thế này.

Tạo giao diện người dùng - Thêm vào một imageview

Nếu chú ý đến field srcCompat bạn sẽ thấy nội dung của nó là @drawable/empty_note. Giá trị này cũng giống với cấu trúc giá trị text của TextView đúng không nào. Đừng quá lo lắng nếu như ảnh không hiển thị canh giữa vào màn hình nhé. Chúng ta cùng đến bước tiếp theo để canh chỉnh ImageView này cho nó đẹp hơn.

Để có thể hiểu được sự canh chỉnh các giá trị bên trong một ConstraintLayout như thế nào thì mình mời các bạn hãy bỏ thời gian đọc qua loạt bài về ConstraintLayout của mình nhé. Với bài thực hành hôm nay, nếu bạn chưa có nhu cầu muốn hiểu tường tận về ConstraintLayout thì cũng có thể thử các bước sau.

Tạo giao diện người dùng - Canh chỉnh View

Mình giải nghĩa một chút các ý đồ của các bước trên đây của mình.

– Đảm bảo điểm neo bên trên của ImageView chính là TextView, điểm neo bên trái là biên trái màn hình, và điểm neo bên phải là biên phải màn hình. Vì ConstraintLayout buộc các View phải có từ 2 điểm neo trở lên, và việc neo các view với nhau này sẽ giúp ConstraintLayout xác định được các View sẽ được vẽ ở đâu với kích thước và khoảng cách như thế nào.

– layout_width và layout_height của ImageView đều mang giá trị 60dp.

– Đảm bảo các giá trị margin đều là 8dp.

– scaleType được set là fitCenter, chúng ta sẽ tìm hiểu các kiểu co dãn ảnh ở mục sau đây.

Chúng ta lại chuyển cửa sổ thiết kế của activity_main.xml này sang tab Text, để xem các giá trị mới được thêm vào cho code XML của chúng ta như thế nào. Mình liệt kê thay đổi đó bằng các dòng code được tô sáng sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <TextView
        android:id="@+id/activity_main_tv_empty"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginBottom="8dp"
        android:gravity="center"
        android:text="@string/empty_note"
        android:textSize="15sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
 
    <ImageView
        android:id="@+id/imageView"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:scaleType="fitCenter"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/activity_main_tv_empty"
        app:srcCompat="@drawable/empty_note" />
 
</android.support.constraint.ConstraintLayout>

Giờ bạn có thể thử run project lên để xem thành quả được rồi đấy.

Tạo giao diện người dùng - Kết quả thực thi

Các Dạng Scale Của Ảnh

Bước thực hành trên đây chúng ta vừa làm quen với một dạng scale, đó là android:scaletype=”fitCenter”, các dạng scale này chính là do widget ImageView này hỗ trợ, nó giúp bạn có được các tùy chỉnh thú vị hơn khi sử dụng đến ảnh.

Chúng ta cùng xem qua các dạng scale có thể có của một ImageView nhé.

Giả sử mình dùng ảnh có hình con robot như sau ( bạn chú ý khung màu xanh dương là khung giới hạn của ImageView để xem chúng “biến hóa” như thế nào trong khung giới hạn).

fitCenter

Nếu sử dụng giá trị này cho scaleType, ảnh sẽ tự động scale sao cho hiển thị gọn bên trong khung mà tỉ lệ của ảnh vẫn không thay đổi.

Tạo giao diện người dùng - Scale fit center

fitXY

Giá trị này làm ảnh bị scale sao cho hiển thị gọn bên trong khung, nhưng khác fitCenter ở chỗ fitXY làm cho ảnh bị biến dạng không còn tỉ lệ gốc nữa.

Tạo giao diện người dùng - Scale fit xy

fitStart

Cách scale này giống với fitCenter là sẽ đảm bảo ảnh nằm gọn trong khung mà tỉ lệ ảnh không đổi, nhưng thay vì ảnh nằm giữa khung thì nó bị neo góc trên bên trái khung.

Tạo giao diện người dùng - Scale fit start

fitEnd

Tương tự như fitStart, nhưng góc neo của ảnh lại là góc dưới bên phải khung.

Tạo giao diện người dùng - Scale fit end

centerCrop

Giá trị này làm ảnh sẽ bị scale đến khi một chiều ngắn nhất của ảnh nằm gọn trong khung, chiều còn lại bị cắt sao cho tỉ lệ của ảnh không bị đổi. Trường hợp dưới đây cho thấy chiều dọc của ảnh sẽ nhìn thấy gọn trong khung, trong khi chiều dài bị cắt.

Tạo giao diện người dùng - Scale center crop

center

Giá trị này không làm scale ảnh, nó chỉ có nhiệm vụ hiển thị ảnh đúng với kích cỡ của nó vào giữa khung mà thôi.

Tạo giao diện người dùng - Scale center

Nói như vậy có nghĩa là nếu như khung nhỏ hơn kích thước ảnh, ảnh sẽ bị cắt đi như hình sau.

Tạo giao diện người dùng - Scale center

centerInside

Cách scale này cũng giống như center, tức là nó cũng sẽ giữ kích thước gốc của ảnh, nhưng nếu khung vẫn còn nhỏ hơn kích thước ảnh, lúc này ảnh sẽ tự scale xuống mà vẫn giữ tỉ lệ của nó.

Tạo giao diện người dùng - Scale center indise

matrix

Dùng khi bạn muốn scale ảnh và xoay ảnh dạng Matrix, bạn có thể search thêm cách scale hay xoay ảnh dạng Matrix trên mạng, hoặc mình sẽ viết cách sử dụng Matrix ở một bài khác.

Bạn vừa cùng mình thực hành 2 cách sử dụng widget là TextView và ImageView, hãy đọc Tìm Hiểu Các Widget Cơ Bản để biết cách sử dụng các widget cơ bản còn lại bạn nhé

Đi Sâu Vào Layout

Mình nhắc lại một chút về kiến thức ở bài học cũ. Ở Bài 7, chúng ta đã nói đến khái niệm ViewGroup, hiển nhiên như tên gọi của nó, ViewGroup dùng để chứa đựng các View hay các ViewGroup khác. Hôm nay chúng ta cùng nhau nói về các thể hiện cụ thể của ViewGroup, đó chính là các Layout.

Mình cũng xin nhắc lại một tí, đó là:

– Từ View chúng ta xây dựng nên các Widget.
– Từ ViewGroup chúng ta xây dựng nên các Layout.

Có thể nói layout mạnh mẽ nhất, thông dụng và hiệu quả nhất bây giờ chỉ có thể là ConstraintLayout. Nhưng bài này mình sẽ không nói về ConstraintLayout. Vì kiến thức của nó khá nhiều và mình đã dành hẳn 2 bài để nói về nó, bạn có thể bắt đầu tìm hiểu ở link này. Bạn nên đọc qua ConstraintLayout sau khi đọc bài viết hôm nay. Ngoài ConstraintLayout ra thì mình cũng dành hẳn một bài viết khác để nói về các layout phổ biến còn lại như FrameLayout, TableLayout,… các bạn có thể xem tại đây.

Nhắc lại rằng, bài học này chúng ta cùng nhau tìm hiểu hai layout cơ bản, đó là LinearLayout và RelativeLayout. Tuy độ hot của 2 layout này không thể sánh với ConstraintLayout được. Nhưng LinearLayout và RelativeLayout được mình đánh giá là 2 layout cơ bản, chúng tạo nền tảng cho sự ra đời của ConstraintLayout. Bạn có thể xây dựng một giao diện khởi đầu với ConstraintLayout, nhưng rồi đâu đó, bạn cũng sẽ cần đến LinearLayout hoặc RelativeLayout hoặc sự kết hợp của cả hai, chúng lồng vào bên trong ConstraintLayout của bạn. Việc kết hợp này giúp bạn giải quyết các giao diện hóc búa. Hoặc nó đơn giản chỉ là bạn đang tận dụng lại giao diện của một ai đó mà họ không đang sử dụng ConstraintLayout như bạn.

Chúng ta bắt đầu kiến thức về Layout với các khái niệm “mở màn” như sau.

Nói Đến Layout Phải Nói Đến RTL Hay LTR

Bạn biết không, hầu hết các ngôn ngữ trên trái đất này đều có cách viết từ trái sang phải, chúng ta cũng vậy vì tiếng Việt cũng theo cách viết này. Cách viết từ trái sang phải như vậy được gọi là LTR (left-to-right).

Một số ít ngôn ngữ còn lại, như mình biết có ngôn ngữ Ả Rập (Arabic) có cách viết từ phải sang trái, cách viết này được gọi là RTL (right-to-left). Nhớ lại ngày xưa khi còn làm game J2ME, mình đã phải xây dựng thuật toán để hiển thị text (bằng image) cho các game với ngôn ngữ Ả Rập, tuy khó nhưng thú vị lắm các bạn. Bây giờ thì chúng ta đã được Android hỗ trợ rồi nên các bạn yên tâm sẽ không cần phải biết ngôn ngữ Ả Rập là gì đâu nhé.

Tại sao mục này lại nói đến các ngôn ngữ LTR hay RTL? Bạn cũng biết Layout là các ViewGroup dùng để sắp xếp các View hay ViewGroup khác vào bên trong nó, và vì vậy các Layout này cũng sẽ chịu ảnh hưởng nhiều từ các hệ ngôn ngữ LTR hay RTL. Mình ví dụ với LinearLayout bạn sắp làm quen, sẽ có cách sắp xếp các thành phần con của nó bắt đầu từ bên trái hay bên phải là tùy vào hệ ngôn ngữ này. Hoặc hầu hết các Layout đều có điểm neo mặc định ban đầu cho thành phần con là trên-trái (top-left) hay trên-phải (top-right) cũng là tùy vào hệ ngôn ngữ này.

Thêm một ý nữa, nếu như các bài trước bạn có làm quen với các thuộc tính android:paddingLeft hay android:paddingRight hay bất kỳ các thuộc tính nào có các từ “Left“ và “Right“ thì bạn có thể thay thế chúng bằng “Start“ và “End“ nếu bạn muốn hỗ trợ cho các ngôn ngữ LTR và RTL. Chẳng hạn android:paddingStart hay android:paddingEnd. Bạn có thể hiểu nếu Left và Right dùng để chỉ định cứng bên phía nào, thì Start và End sẽ động hơn. Cụ thể, với ngôn ngữ LTRStart sẽ chỉ định phía bắt đầu, tức là phía trái. Còn với ngôn ngữ RTLStart sẽ chỉ định là bên phải.

Nói Về Các Thuộc Tính Của Layout

Nếu như bạn đã đọc qua Bài 9 và đã biết về Khái Niệm Thuộc Tính là dùng để khai báo các biểu hiện của một widget. Và bạn cũng biết Một Số Thuộc Tính Quan Trọng bao gồm IDlayout_widthlayout_height. Thì với bài hôm nay khi làm quen với layout, các khái niệm và các thuộc tính quan trọng này vẫn được áp dụng một cách trọn vẹn, không có khác biệt gì cả.

Các mục phía dưới đây sẽ hướng dẫn bạn cách sử dụng thêm các thuộc tính chuyên biệt cho từng loại layout cụ thể. Chúng ta sẽ bắt đầu với LinearLayout.

LinearLayout – Sắp Xếp Các View Nằm Cạnh Nhau

LinearLayout thường được mình mô tả cho dễ hiểu là “xếp cá mòi vào hộp”, bạn sẽ không được phép xếp con này chồng lên con kia, mà chỉ được xếp con này cạnh con kia mà thôi. Bạn có thể chỉ định việc xếp các con cá nằm cạnh nhau theo một chiều ngang, hoặc một chiều dọc.

Các Thuộc Tính Thường Dùng Của LinearLayout

Ngoài các thuộc tính dùng như widget được liệt kê ở mục trên đây, thì LinearLayout còn có các thuộc tính riêng để chỉ định việc sắp xếp “cá mòi” vào trong nó như thế nào.

Vậy mình tổng hợp lại bạn cần quan tâm đến các thuộc tính sau:

– Hướng “xếp cá” (orientation).
– Trọng số (weight).
– Một thuộc tính dịch ra tiếng Việt hơi chuối đó là lực hấp dẫn (gravity).
– Ngoài ra còn có một điều không phải là thuộc tính nhưng cũng được liệt kê trong đây, đó là việc phân bổ không gian cho view con (fill).

Chúng ta cùng làm quen với từng thuộc tính quan trọng này sau đây.

Thuộc Tính Hướng (Orientation)

Như mình đã nói, LinearLayout chỉ xếp các thành phần con nằm cạnh nhau theo một trong hai hướng là ngang hay dọc. Thuộc tính XML android:orientation sẽ giúp bạn định nghĩa được điều đó, tham số của thuộc tính này sẽ là một trong hai giá trị.

– android:orientation=”horizontal” – Giúp xếp các thành phần con nằm cạnh nhau theo phương ngang.
– android:orientation=”vertical” – Giúp xếp vác thành phần con nằm cạnh nhau theo phương dọc.

Tương tự, nếu muốn dùng thuộc tính hướng cho Java code bạn có thể dùng hàm setOrientation() với tham số truyền vào là một hằng số LinearLayout.HORIZONTAL hoặc LinearLayout.VERTICAL.

Phân Bổ Không Gian Cho View Con (Fill)

Như được nói trước, đây không phải một thuộc tính, nó là một nguyên tắc của LinearLayout. Bạn hãy thử tưởng tượng như vầy, bạn có một LinearLayout mà bên trong nó có 3 view con (view ở đây nói chung là View hoặc ViewGroup) được xếp theo nguyên tắc horizontal, tức xếp lớp theo chiều ngang. Giả sử có một view con có thuộc tính android:layout_width=”match_parent”, tức là view con này có chiều rộng hết LinearLayout cha luôn rồi, thì các view còn lại sẽ hiển thị như thế nào?

Câu trả lời là LinearLayout sẽ dành không gian cho con kiểu “ưu tiên con cả trước”, tức là anh nào được đưa vào trước sẽ được ưu tiên không gian trước, anh nào vào sau sẽ… ra khỏi màn hình mà nằm nếu không đủ chỗ, tức là người dùng sẽ không thấy anh này đâu cả.

Bạn thấy như hình ví dụ sau, mình cố tình thiết lập cho cả 3 view con đều dành không gian rộng 200dp, như vậy ắt sẽ có view phải ra khỏi màn hình, đó là view có background màu xanh dương.

Layout - Phân bổ không gian trong LinearLayout

Thuộc Tính Trọng Số (Weight)

Sau khi xem ví dụ trên đây về việc phân bổ không gian cho view con, ắt hẳn bạn hơi bị thắc mắc, câu hỏi bạn đặt ra bấy giờ là liệu có cách nào đó, với một sự phân chia hợp lý hơn, sao cho tất cả các view con đều có thể hiển thị được trên màn hình?

Chà khó quá… nhưng thật dễ dàng đối với LinearLayout, Google đã tính trước câu hỏi của bạn nên mới ra khái niệm weight.

Nói một cách đầy đủ, weight sẽ giúp bạn thiết lập trọng số cho các view con để sử dụng phần không gian trống còn lại của một LinearLayout, với mỗi trọng số khác 0, phần không gian trống còn lại của LinearLayout sẽ được chia ra cho các view con theo trọng số đó. Để sử dụng trọng số weight, bạn không cần dùng đến layout_width hay layout_height cho view con tương ứng nữa, chúng ta xem qua cách sử dụng weight như sau trước khi xem ví dụ bên dưới.

– Với LinearLayout có orientation là horizontal, bạn nên set android:layout_width của view con về 0dp. Với LinearLayout có orientation là vertical, bạn set android:layout_height của view con về 0dp.
– Khai báo android:layout_weight cho view con đó và cho nó một con số (số nguyên hay số thực đều được, và không kèm theo dp hay dip vào giá trị này).

Bạn có thể xem ví dụ ở hình dưới, mình đã sửa lại bằng cách khai báo weight cho các view con lần lượt là 2-1-1. Khi đó view đầu tiên sẽ có chiều rộng sao cho bằng 2 lần các view còn lại.

Layout - Trọng số weight

Để bạn dễ hiểu hơn, mình tiếp tục thử nghiệm bằng cách đổi lại weight cho chúng lần lượt là 2-3-1. Bạn sẽ thấy view màu đỏ với weight là 2 sẽ chiếm không gian rộng gấp 2 lần so với view xanh dương với weight là 1. Tương tự, view màu xanh lá với weight là 3 sẽ rộng gấp 3 lần so với view xanh dương với weight là 1. Bạn hiểu độ tương quan của các trọng số weight chưa nào?

Layout - Trọng số weight

Với cách set trọng số weight như trên thì dù cho LinearLayout có không gian như thế nào thì view con cũng sẽ tìm cách lấp đầy không gian trống đó. Nhưng nếu bạn muốn chừa không gian trống cho mục đích nào đó, bạn có thể thiết lập một giá trị android:weightSum cho LinearLayout rồi set weight cho các view con của nó sao cho tổng giá trị weight nhỏ hơn weightSum như hình bên dưới đây.

Như hình thì bạn thấy mình set giá trị weightSum cho LinearLayout là 8, mà tổng weight của các view con chỉ là 6 thôi, như vậy sẽ còn dư 2 weight không có con nào được fill vào, nên nó mới trống như vậy.

Layout - Giá trị weightSum

Thuộc Tính Lực Hấp Dẫn (Gravity)

Chắc bạn còn nhớ ở bài trước, bài về widget, cũng có dùng đến thuộc tính android:gravity này. LinearLayout của bài hôm nay cũng sử dụng thuộc tính này giống như vậy để canh chỉnh cho các thành phần con của mình.

Mặc định thì các thành phần con bên trong LinearLayout sẽ được “hút” về start-top theo “lực hấp dẫn” mặc định (như đã nói ở trên, start sẽ là left hay right là tùy vào ngôn ngữ LTR hay RTL).

Ví dụ sau mình set android:gravity=”bottom” sẽ làm các view con bị hút (neo) hết xuống dưới so với layout cha.

Layout - Thuộc tính gravity

Bạn cứ từ từ làm quen hết tất cả các giá trị gravity này. Bạn cũng có thể kết hợp nhiều giá trị gravity vào cùng một khai báo thuộc tính bằng cách thêm vào ký tự “|” như này android:gravity=”right|bottom”.

Các Thuộc Tính Thêm Vào Cho View Con

Một điều thú vị là khi các View (hay ViewGroup) khác nằm bên trong một LinearLayout, chúng sẽ có thêm một vài thuộc tính, chủ yếu là canh chỉnh vị trí của bản thân nó so với không gian của layout cha.

Với LinearLayout thì chúng ta chỉ cần quan tâm đến 2 thuộc tính được thêm vào thành phần con, trong đó có thuộc tính weight đã được nói trước ở trên, thuộc tính còn lại như sau.

– android:layout_gravity – Không cần giải thích dài dòng, thuộc tính này dùng chính xác như android:gravity của LinearLayout được nói ngay trên đây. Nếu như android:gravity ở LinearLayout sẽ áp dụng lực hút (hay còn gọi là điểm neo) cho tất cả các con của nó, thì android:layout_gravity dùng cho mỗi thành phần con sẽ có tác dụng neo cho chính thành phần con này mà thôi. Bạn nhớ là tên thuộc tính hơi khác nhau chút, layout_gravity và gravity nhé.

Thực Hành Xây Dựng Màn Hình Empty Của TourNote Bằng LinearLayout

Sau khi thực hành qua các bài trước, giao diện đầu tiên khi mở TourNote ra sẽ trông như thế này.

Layout - Giao diện TourNote bài trước

Giờ thì bạn hãy quên đi layout nào đang chứa TextView và ImageView mà bạn đã thêm vào trước đó. Nhiệm vụ của bài thực hành này là sử dụng LinearLayout để bao lấy 2 widget này, sao cho chúng vẫn hiển thị đúng như thiết kế ban đầu.

Lưu ý là bài thực hành này chỉ là vọc chơi cho biết LinearLayout thôi nhé. Mong muốn của bài thực hành này là làm sao có thể sử dụng một layout khác, cụ thể là LinearLayout mà vẫn xây dựng được một giao diện như với ConstraintLayout. Kết thúc bài học hôm nay bạn nên trả lại giao diện của TourNote về lại ConstraintLayout như những gì bài học trước đạt được. Và cũng vì lý do vọc chơi thôi nên mình cũng không đẩy source code của bài hôm nay lên Github đâu nhé. Mặc dù chúng ta không công nhận những chỉnh sửa về mặt UI của bài hôm nay, nhưng mình vẫn mong muốn các bạn hãy cứ mở TourNote lên và thực hành đầy đủ. Các layout của bài hôm nay khá quan trọng. Trừ khi các bạn đã biết đến LinearLayout và RelativeLayout là gì rồi thì có thể bỏ qua.

Để bắt đầu, bạn hãy mở file activity_main.xml ra. Trước tiên chúng ta hãy chuyển đổi từ ConstraintLayout sang LinearLayout. Nhớ là chuyển đổi chứ không phải là thêm một LinearLayout vào dưới ConstraintLayout có sẵn đâu nhé. Nhiều bạn không lưu ý chỗ này khi báo cả 2 layout ngang cấp như vậy dẫn đến trình biên dịch báo lỗi đấy.

Để chuyển đổi các layout như thế này thì bạn có 2 cách. Nếu bạn đang ở tab Text của màn hình thiết kế, thì bạn chỉ việc thay đổi khai báo tên thẻ android.support.constraint.ConstraintLayout thành LinearLayout thôi. Đại loại như sau.

Layout - Chỉnh giao diện sang LinearLayouthttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2016/11/Hinh1.png?resize=286%2C300&ssl=1 286w" data-lazy-loaded="1" sizes="(max-width: 650px) 100vw, 650px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px auto; clear: both; text-align: center;">

Tất nhiên bước chuyển đổi tạm như vậy sẽ phát sinh lỗi. Bởi vì bạn nên biết rằng, mỗi một layout sẽ có một cách sắp xếp các thành phần con khác nhau. Và mỗi một view con nằm trong một layout cũng có cách tự canh chỉnh nó khác với khi nằm ở layout khác nữa. Nên việc bạn chuyển một layout này sang layout khác đã làm xáo trộn đi sự canh chỉnh này. Nhưng hãy tạm bỏ qua lỗi đi nhé, chúng ta sẽ canh chỉnh thuộc tính của chúng sau. Giờ chúng ta hãy xem cách chuyển đổi khác từ ConstraintLayout sang LinearLayout nào.

Cách chuyển đổi thứ hai là nếu bạn ở tab Design của màn hình thiết kế, thì bạn hãy tìm đến cửa sổ nhỏ có tên Component Tree. Như hình dưới đây.

https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2016/11/Hinh2-1.png?resize=300%2C209&ssl=1 300w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2016/11/Hinh2-1.png?resize=768%2C536&ssl=1 768w" data-lazy-loaded="1" sizes="(max-width: 993px) 100vw, 993px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px auto; clear: both; text-align: center;">

Nơi đây chính là nơi hiển thị cách tổ chức layout của bạn. Nó sẽ liệt kê theo dạng cây (tree) các widget hay layout của ứng dụng, theo đúng như những gì mà mình có mô tả về cây này ở mục này đấy nhé. Với cây này thì bạn có thể thấy layout gốc chính là ConstraintLayout. Để chuyển đổi layout gốc này về LinearLayout, bạn hãy click chuột phải vào nó và chọn chức năng Convert view… (như hình).

https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2016/11/Hinh3.png?resize=300%2C262&ssl=1 300w" data-lazy-loaded="1" sizes="(max-width: 454px) 100vw, 454px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px auto; clear: both; text-align: center;">

Sau bước chọn lựa trên bạn sẽ thấy một popup xuất hiện kế tiếp. Bạn sẽ dễ dàng nhìn thấy LinearLayout trong danh sách các layout bạn muốn chuyển đổi. Dĩ nhiên chúng ta chọn LinearLayout rồi, xong thì hãy nhấn Apply.


Sau thao tác chuyển đổi (bằng một trong hai cách trên), thì bạn thấy rằng các widget mà bạn đã sắp xếp từ bài hôm trước chúng bị thay đổi vị trí đến mức không còn có thể nhìn thấy gì cả. Bởi vì chúng khác layout như mình có giải thích ở trên.

Đặc thù của LinearLayout chính là một layout theo kiểu cũ. Tức layout này sinh ra từ khi các lập trình viên Android còn đang code giao diện bằng XML cơ, chứ không phải kéo thả như ConstraintLayout đâu. Nên để làm quen với layout này, bạn nên tập code một tí. Bạn hãy mở tab Text của cửa sổ thiết kế giao diện activity_main.xml lên. Đảm bảo cửa sổ Preview đang được chọn để bạn có thể xem được sự thay đổi đến tức thì khi bạn sửa code XML bên trái.


Mình sẽ liệt kê các chỉnh sửa sau, bạn có thể nhìn qua một lượt rồi tự code lại để so sánh kết quả nhé.

Với LinearLayout gốc.

– Mình sẽ thêm thuộc tính orientation và chỉ định giá trị của nó là vertical. Vì hai widget con của nó cần được sắp xếp theo chiều dọc mà.

– Thêm thuộc tính gravity và set giá trị là center. Vì mong muốn các phần tử con canh chỉnh vào giữa màn hình.

Với TextView.

– layout_width của TextView sẽ là match_parent. Nó không còn giá trị 0dp nữa, vì chỉ có với ContraintLayout thì giá trị này mới là là match_constraint (tức 0dp) thôi, bài trước mình có nói tới giá trị này rồi.

– Bỏ các thuộc tính layout_constraintBottom_toBottomOflayout_constraintLeft_toLeftOflayout_constraintRight_toRightOflayout_constraintTop_toTopOf. Vì với LinearLayout, các thuộc tính này vô nghĩa.

Với ImageView.

– Bỏ các thuộc tính layout_constraintEnd_toEndOflayout_constraintStart_toStartOflayout_constraintTop_toBottomOf. Cũng với ý nghĩa tương tự như TextView trên kia.

Code giao diện sẽ trông như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">
 
    <TextView
        android:id="@+id/activity_main_tv_empty"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginBottom="8dp"
        android:gravity="center"
        android:text="@string/empty_note"
        android:textSize="15sp" />
 
    <ImageView
        android:id="@+id/imageView"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:scaleType="fitCenter"
        app:srcCompat="@drawable/empty_note" />
 
</LinearLayout>
[/code]

Kết quả khi run ứng dụng sẽ như chúng ta mong muốn. Bạn có thấy giao diện giống với bài học trước không nào.

Layout - Giao diện TourNote bài trước

RelativeLayout – Sắp Xếp Các View Theo Mối Quan Hệ

Nghe đến mối quan hệ chắc bạn sẽ nghĩ đến họ hàng hay quen biết gì đó. Thực chất RelativeLayout sẽ sắp xếp các con của nó theo một trật tự… tự do, mỗi thành phần con sẽ tự quyết định cho mình một chỗ đứng. Cách thức chọn chỗ đứng của các thành phần con là nói cho RelativeLayout biết nó đứng như thế nào so với chính layout cha hay so với các thành phần kế cận mà thôi, đó cũng chính là khái niệm mối quan hệ trong RelativeLayout.

Các Thuộc Tính Thêm Vào Cho View Con

Về cơ bản thì RelativeLayout có tính “dân chủ” hơn LinearLayout, mọi quyết định về vị trí đều ưu tiên cho các thành phần con tự chọn. Chính vì vậy mà với RelativeLayout bạn sẽ hầu như không cần quan tâm đến thuộc tính của chính nó, mà quan tâm đến các thuộc tính thêm vào cho các thành phần con của nó. Và bởi vì có rất nhiều thuộc tính được thêm vào thành phần con, nên mình sẽ tách các thuộc tính này làm nhiều nhóm như sau.

Quyết Định Vị Trí So Với RelativeLayout Cha

Tất cả các thuộc tính dưới đây chỉ cần truyền vào true hay false thể hiện rằng bạn có đồng ý với việc canh chỉnh này hay không mà thôi.

– android:layout_alignParentTop – Chỉ định đỉnh của thành phần này sẽ được canh theo đỉnh của layout cha.
– android:layout_alignParentBottom – Ngược lại, chỉ định đáy của thành phần này sẽ được canh theo đáy của layout cha.
– android:layout_alignParentStart – Chỉ định cạnh start của thành phần này sẽ được canh theo cạnh start của layout cha, dùng cho việc phân biệt RTL hay LTR được nói ở đầu bài hôm nay.
– android:layout_alignParentEnd – Chỉ định cạnh end của thành phần này sẽ được canh theo cạnh end của layout cha, dùng cho việc phân biệt RTL hay LTR được nói ở đầu bài hôm nay.
– android:layout_alignParentLeft – Chỉ định cạnh trái của thành phần này sẽ được canh theo cạnh trái của layout cha, không phân biệt RTL hay LTR.
– android:layout_alignParentRight – Chỉ định cạnh phải của thành phần này sẽ được canh theo cạnh phải của layout cha, không phân biệt RTL hay LTR.
– android:layout_centerHorizontal – Chỉ định thành phần này được đặt ngay giữa theo chiều ngang so với layout cha.
– android:layout_centerVertical – Chỉ định thành phần này được đặt ngay giữa theo chiều dọc so với layout cha.
– android:layout_centerInParent – Chỉ định thành phần này được đặt ngay giữa theo cả hai chiều ngang và dọc so với layout cha.

Quyết Định Vị Trí So Với Các Widget Khác

Tất cả các thuộc tính dưới đây cần phải truyền vào một ID @id/ hay @+id/ (nếu không rõ phần sử dụng ID này, bạn có thể đọc ở mục này của Bài 9.

– android:layout_alignTop –  Chỉ định đỉnh của thành phần này sẽ được canh theo đỉnh của thành phần gọi đến bằng ID.
– android:layout_alignBottom – Chỉ định đáy của thành phần này sẽ được canh theo đáy của thành phần gọi đến bằng ID.
– android:layout_alignStart – Chỉ định cạnh start của thành phần này sẽ được canh theo cạnh start của thành phần gọi đến bằng ID.
– android:layout_alignEnd – Chỉ định cạnh end của thành phần này sẽ được canh theo cạnh end của thành phần gọi đến bằng ID.
– android:layout_alignBaseline – Chỉ định baseline của thành phần này sẽ được canh theo baseline của thành phần gọi đến bằng ID. Baseline này bạn không nhìn thấy được, dùng để canh chỉnh cho text hiển thị bên trong widget, do đó sẽ hữu dụng khi canh chỉnh các TextView với nhau).
– android:layout_above – Chỉ định thành phần này sẽ nằm ở trên so với thành phần gọi đến bằng ID.
– android:layout_below – Chỉ định thành phần này sẽ nằm dưới so với thành phần gọi đến bằng ID.
– android:layout_toStartOf – Chỉ định thành phần này sẽ nằm bên phía start so với thành phần gọi đến bằng ID.
– android:layout_toEndOf – Chỉ định thành phần này sẽ nằm bên phía end so với thành phần gọi đến bằng ID.
– android:layout_toLeftOf – Chỉ định thành phần này sẽ nằm bên phía trái so với thành phần gọi đến bằng ID.
– android:layout_toRightOf – Chỉ định thành phần này sẽ nằm bên phía phải so với thành phần gọi đến bằng ID.

Thực Hành Xây Dựng Màn Hình Empty Của TourNote Bằng RelativeLayout

Giờ chúng ta tiếp tục “đập” giao diện activity_main.xml một lần nữa, chúng ta hãy chuyển từ LinearLayout sang RelativeLayout.

Cách chuyển đổi thẻ XML thì bạn làm tương tự như khi chuyển từ ConstraintLayout sang LinearLayout trên kia nhé. Sau bước chuyển đổi từ LinearLayout sang RelativeLayout này, bạn lại chứng kiến các widget bên trong màn hình trực quan lại chạy loạn hết cả. Bạn đã hiểu mỗi layout có cách canh chỉnh của chúng như thế nào chưa nào. Mỗi lần bạn chuyển đổi layout, bạn phải canh chỉnh lại các thuộc tính của layout và của chính các widget bên trong layout đó nữa.

Cũng như cách trình bày ở bài thực hành trên. Mình sẽ liệt kê các chỉnh sửa sau cho layout mới, bạn có thể nhìn qua một lượt rồi tự code lại để so sánh kết quả.

Với RelativeLayout gốc.

– Bỏ các thuộc tính không cần thiết với RelativeLayout, như android:gravity và android:orientation.

Với TextView.

– Thêm thuộc tính layout_centerInParent và set giá trị true cho nó. Như mình có diễn đạt trên kia, điều này giúp TextView tự canh vào giữa layout cha.

Với ImageView.

– Thêm thuộc tính layout_centerHorizontal và set giá trị true cho nó. Điều này giúp canh ImageView vào giữa layout cha theo chiều ngang.

– Thêm thuộc tính layout_below và set giá trị trỏ đến ID của TextView. Điều này như mình có giải thích ở mục này của bài trước, nó sẽ giúp ImageView canh chỉnh sao cho nằm dưới view có ID được chỉ định.

Code giao diện sẽ trông như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <TextView
        android:id="@+id/activity_main_tv_empty"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginBottom="8dp"
        android:gravity="center"
        android:text="@string/empty_note"
        android:textSize="15sp" />
 
    <ImageView
        android:id="@+id/imageView"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_below="@id/activity_main_tv_empty"
        android:layout_centerHorizontal="true"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:scaleType="fitCenter"
        app:srcCompat="@drawable/empty_note" />
 
</RelativeLayout>

Kết quả run lên như thế nào tự bạn kiểm chứng nhé.

Bạn vừa xem qua cách sử dụng 2 layout phổ biến nhất hiện nay là LinearLayout và RelativeLayout. Tuy như mình có nói, chúng ta chưa vội sử dụng 2 layout này vào trong TourNote vội, nhưng bạn hãy cứ biết đến nó nhé. Và nếu có thời gian, mình có một bài viết riêng để nói đầy đủ tất cả các layout trong Android, các bạn có thể vào tham khảo bài Tìm hiểu các layout trong Android.

Cấu Hình Cho Gradle

Bạn biết không, các thông tin liên quan đến cấu hình cho Gradle (chính là file build.gradle) và cấu hình cho Manifest (chính là file AndroidManifest.xml) đã được mình nhắc đến khá sớm ở Bài 5, tuy nhiên lời hứa đó mình đã nợ các bạn mãi tận hôm nay – Bài học Android thứ 11 – Mới có dịp được nói cụ thể về nó.

Và khi mình quyết định nói về chúng nó (Gradle và Manifest), mình muốn nói đến cả hai trong cùng một bài viết, nhưng thấy có vẻ dài hơi quá nên mình sẽ ngắt chúng ra làm hai phần và nói ở hai bài khác nhau.

Đầu tiên cho mình có đôi lời về cả hai thằng này xíu (vì xém chút nữa chúng nằm chung một bài rồi). Thực ra thì hai thằng này chúng nó chẳng liên quan gì đến nhau hết á, chúng chẳng phải anh em họ hàng với nhau, thậm chí đến cả bạn bè cũng không. Nhưng khi nói đến Gradle thì không thể không nói đến Manifest. Bởi vì sao, chúng ta cùng quay về lịch sử một chút, bạn biết không, dân lập trình Android xưa kia cày cuốc trên nền Eclipse chỉ có biết đến cấu hình cho project thông qua Manifest, file này được mình liệt kê ở Bài 5 rồi nhé. Về sau khi công cụ cày cuốc được nâng cấp lên Android Studio, Manifest vẫn còn giữ nguyên vai trò của nó, tuy nhiên anh chàng Gradle được Android Studio giới thiệu sau này đã kiêm luôn một số việc của Manifest, khiến cho vai trò bị trộn lẫn. Mình nói vấn đề này để khi bạn đọc đâu đó thấy có nói phải khai báo ở Manifest, như khai báo version của app chẳng hạn, nhưng đọc chỗ khác lại thấy khai báo ở Gradle, thì đó là do ảnh hưởng của lịch sử bạn nhé.

Vai trò của bài hôm nay không phải làm bạn rối lên giữa Gradle và Manifest, cũng không phải một tiết học lịch sử. Mà bài học sẽ giúp bạn biết khi nào thì khai báo Gradle, khi nào thì khai báo Manifest một cách rõ ràng và chuyên nghiệp.

Uhm… Dù sao thì các bạn cũng cần biết đến khái niệm của 2 thằng này như nào phải không. Nhắc lại là chúng ta cùng nói đến Gradle trong bài hôm nay, và Manifest ở bài kế tiếp nhé.

Khái Niệm Gradle

Nói ngắn gọn thôi nhé, Gradle có thể hiểu như là một công cụ để build hệ thống, cũng giống như bạn nào đã từng sử dụng Ant như là một công cụ để build Android theo hướng command line vậy. Sau này khi Android Studio ra đời, Google đã tận dụng Gradle để phát triển Android plugin cho nó và thế là Android Studio có thể sử dụng được Gradle này để build các ứng dụng Android cho chúng ta.

Xong khái niệm rồi nhé, bạn nào chưa hiểu lắm muốn biết nhiều hơn thì xem thêm Gradle ở đây. Nhưng mình nghĩ không cần phải biết nhiều quá đâu. Gradle được tích hợp sẵn vào Android Studio, và được điều khiển một cách tự động thông qua Android Studio. Trước mắt bạn cần biết đủ để cấu hình cho Gradle thôi, cấu hình cho Gradle có nghĩa là bạn đang cấu hình cho project của mình, từ việc cấu hình version của công cụ build, cho đến cấu hình version của app, chỉ định hệ điều hành Android mà ứng dụng hỗ trợ, khai báo thư viện từ bên ngoài… Bạn sẽ có dịp thực hành nhiều với Gradle và như vậy bạn sẽ ngày càng hiểu nó hơn. Tin mình đi!

Trước khi đi sâu vào Gradle bên dưới, mình cũng trình bày thêm rằng bài học hôm nay tập trung vào cách thức sử dụng Gradle có trong Android Studio. Có nghĩa là bạn có thể sử dụng Gradle độc lập ở đâu đó mà không cần phải có Android Studio vẫn được, nhưng nó thuộc về kiến thức khác mà bài học hôm nay xin phép chưa nói đến. Mình và các bạn vẫn phải dùng Android Studio để thực hành và xây dựng TourNote như mọi khi.

Làm Quen Với Gradle Wrapper

Chúng ta bắt đầu “vọc” Gradle bằng việc xem qua file Gradle Wrapper. Trong Android Studio bạn sẽ thấy file này nằm ở thư mục gradle/wrapper/. Tên file là gradle-wrapper.properties. Bạn có thể thấy file này như hình sau, hình trên máy bạn sẽ giống với hình bên trái hay bên phải là tùy vào cách view của cửa sổ này.

Gradle - Nơi chứa Gradle Wrapper

Bạn đừng ngần ngại, hãy click đúp vào file này để mở nó lên. Bạn đã thấy nội dung của Gradle Wrapper rồi đúng không nào, chúng rất ngắn… nhưng không kém phần quan trọng.

Gradle - Nội dung của Gradle Wrapper

Sở dĩ mình nói đến Gradle Wrapper đầu tiên, là vì file này sẽ chứa đường dẫn quyết định đến việc download Gradle ở đâu, và cả version của Gradle nữa, đường dẫn mình vừa nhắc đến hiển thị ở dòng distributionUrl. Thường thì khi tạo mới một project, Android Studio đã để sẵn đường dẫn này cho bạn rồi nên bạn yên tâm không cần cấu hình gì cả. Có điều bạn nên biết đến file này, để sau này Android có nâng cấp version của Gradle, thường thì Android Studio sẽ giúp bạn thay đổi nội dung file luôn, nhưng biết đâu được, có khi chính bạn là người phải sửa đường dẫn lại theo hướng dẫn của Google không chừng. File Gradle ở đường dẫn này sẽ được down về và install rồi để ở thư mục .gradle/ (có dấu chấm ở trước nhé). Không tin à, bạn cứ thử mở thư mục này ra xem.

Gradle - Nơi chứa file Gradle

Làm Quen Với Các File Gradle

Yep đúng vậy, chúng ta chính thức làm quen với… các file Gradle… “các”? Thật không vui là có đến hai file Gradle mà bạn cần phải biết và chúng đều có tên là build.gradle. Một file dùng cho cấu hình ở cấp độ project, còn một file dùng cho cấu hình ở cấp độ module. Tại sao lại chia ra làm hai cấp độ file thì bạn hãy xem tiếp bên dưới nhé.

Gradle - Nơi chứa các file build.gradle

build.gradle Ở Cấp Độ Project

Theo đường dẫn ở hình trên, bạn click đúp để mở build.gradle ở cấp độ project lên xem trước. File này sẽ cấu hình cho tất cả các module bên trong project của bạn. Thông thường thì các project chỉ có một module mà thôi, vẫn có project có nhiều module nhưng ở giai đoạn này bạn không cần quan tâm lắm với việc này, chúng ta sẽ nói đến project nhiều module ở phần riêng.

Gradle - Nội dulng build.gradle cấp độ project

build.gradle ở cấp độ này cũng không có nhiều thứ để nói, tuy nhiên mình có thể điểm sơ qua cho các bạn nắm rõ cách thức cấu hình file này như sau.

– Mỗi một mục buildscriptallprojectstask clean trong file này được gọi là một closure, chúng ta tạm gọi là các khối.
– Khối buildscript diễn đạt… uhm… bạn cứ xem như là nó đưa ra một kịch bản cho biết cần phải download phiên bản gradle ở đâu về, cụ thể trong khối này nó định nghĩa repositories là ở jcenter() (hàm jcenter() này giúp định nghĩa thông tin về repository cho Maven Central, một nơi thu thập các dependencies mã nguồn mở). Trong khối buildscript này còn có khối con dependencies là nơi liệt kê các thư viện dùng chung cho project, như hiện tại bạn thấy nó khai báo sẽ dùng một thư viện gradle, với thông tin về đường dẫn và version của gradle cần down.
– Khối allprojects là nơi định nghĩa các thiết lập cho tất cả project. Trong trường hợp này nó đang định nghĩa jcenter() là repositories để tìm tất cả các thư việc cần thiết cho các module bên trong project này.

Phần thông tin còn lại nếu có trong file này, bạn cứ để như mặc định. Thường thì mình cũng ít đụng vào file này, chỉ nói sơ qua cho các bạn hiểu chút đỉnh thôi. File build.gradle kế tiếp này mới thực sự quan trọng nè.

build.gradle Ở Cấp Độ Module

Bạn hãy mở file build.gradle ở cấp độ module này lên nhé (theo đường dẫn đến thư mục như hình trên đây). Như tên gọi, thì file này sẽ cấu hình cho từng module trong project, nên nó sẽ được sử dụng nhiều hơn.

Gradle - build.gradle cấp độ module

Mình sẽ nói đến từng closure (khối) trong file này bằng những mục riêng bên dưới chứ không phải bằng các gạch đầu dòng, vì thông tin ở file này sẽ khá nhiều.

Khối android

Chà khối này định nghĩa rất nhiều cấu hình cho project của bạn (tuy build.gradle này dành để cấu hình cho module, nhưng vì project của chúng ta thường chỉ có một module, nên mình nói luôn là cấu hình này là cấu hình cho project cũng được bạn nhé).

– compiledSdkVersion: giá trị ở mục này chính là version của Android mà bạn muốn chỉ định để project của bạn build ra. Nghe có vẻ lạ phải không nào, mới đầu làm quen với Android mình cũng lùng bùng lỗ tai chỗ này, mất bao nhiêu lâu mới phân biệt rõ ràng. Để mình giúp bạn rõ. Chúng ta đã biết minSdkVersion là thông số version Android nhỏ nhất mà project của bạn hỗ trợ, thông số này bạn biết rồi nhé, lúc bạn tạo mới project í, nhớ không, bạn nào quên thì xem lại định nghĩa của khai báo Minimum API Level ở Bài 3 nhé. Rồi chúng ta lại có targetSdkVersion là version Android mới nhất mà project của bạn hỗ trợ, cái này mình nói sau. Nhưng bạn nên biết là cho dù project của bạn hỗ trợ Android trễ nhất và mới nhất như nào thì khi build, project của bạn vẫn phải dựa vào một version Android cụ thể để nó còn biết gắn các thư viện hỗ trợ vào, chính vì vậy mà compiledSdkVersion ra đời. Theo mặc định thì giá trị này sẽ được thiết lập là version Android lớn nhất (giống với version ở targetSdkVersion). Bạn hoàn toàn có thể chỉ định một version thấp hơn version của target, nhưng compile với một version lớn nhất sẽ cho phép ứng dụng của bạn có được các thư viện Android mới nhất, như giao diện của Android 7.0 chẳng hạn, giúp tạo trải nghiệm tốt nhất cho user.
– buildToolsVersion (các project mới hiện tại không còn thấy khai báo này nữa): giá trị ở mục này chỉ định version của công cụ build Android (Android SDK build tool). Giá trị này bạn không cần quan tâm lắm, mặc dù nó cũng khá quan trọng, nhưng thông thường bạn được Android Studio hỗ trợ chỉnh sửa tự động mỗi khi bạn update công cụ này.
– applicationId: thông số này nằm bên trong khối defaultConfig, cũng như bao thông số khác bên trong khối này, chúng giúp định nghĩa một số cấu hình default ban đầu, dĩ nhiên bạn có thể sửa được, và chắc chắn chúng ta sẽ thực hành sửa chữa mục này ở phần kế tiếp của bài học. Thông số applicationId này mặc định chính là package name. Chà chà bạn còn nhớ hay đã quên, vui lòng xem lại Bài 3 nếu bạn quên package name có ý nghĩa là gì nhé nhé.
– minSdkVersion: đã được nhắc ở mục compiledSdkVersion trên đây rồi nhé bạn.
– targetSdkVersion: cũng đã được nhắc đến ở trên rồi nhé, thông số này được tạo mặc định nên ban đầu bạn không cần chú ý lắm, về sau nếu cần hỗ trợ Android mới nhất thì nhớ nâng em này với em compiledSdkVersion lên. À có lẽ mình quên nói là các version của Android dùng cho compiledSdkVersionminSdkVersion và targetSdkVersion đều là các con số và chúng được tham chiếu dựa trên bảng này, các bạn vào tham khảo nhé.
– versionCode và versionName: dễ hiểu thôi, versionName là version của app sẽ được Google Play hiện ra khi user chuẩn bị download và install app, versionName còn được hiển thị trong Setting của app nữa, dù vậy versionName cũng chỉ hiển thị cho user biết thôi, nó không mang ý nghĩa so sánh gì cả. Trong khi đó versionCode thì dùng để hệ thống biết các phiên bản cập nhật của app, versionCode được set ban đầu là 1, nếu bạn xuất bản những bản cập nhật của app lên Google Play, bạn phải nâng versionCode, trong khi versionName như thế nào Google cũng không quan tâm.

Khối dependencies

Khối dependencies này cũng giống như khối dependencies dùng trong buildscript ở file build.gradle cấp độ project đã nói ở trên đây, là đều dùng để định nghĩa ra các thư viện. Chúng chỉ khác nhau ở chỗ một thằng định nghĩa thư viện dùng chung cho project, còn một thằng định nghĩa thư viện cho từng module.

Bạn nhìn xem có khá nhiều thư viện được định nghĩa sẵn trong đây, tùy vào thời điểm bạn tạo project mà hệ thống quyết định project của bạn nên có những thư viện nào.

Chúng ta sẽ cùng nhau thực hành trên TourNote về cách sử dụng dependencies này cũng như các thuộc tính khác trong Gradle ở phần dưới nhé.

Thực Hành Cấu Hình Gradle Cho TourNote

Bài thực hành này chúng ta sẽ tập cấu hình Gradle một cách đơn giản cho project TourNote. Bạn nên thực hành qua cho biết, nhưng nếu bạn đã biết Gradle là gì rồi thì có thể không cần phải thực hành làm gì cho mất thời gian, bạn có thể đến với bài học tiếp theo. Về sau, ở các bài thực hành cụ thể, nếu có bất cứ chỉnh sửa nào đối với Gradle này, chúng ta sẽ cùng nhau nhắc đến Gradle là gì và cấu hình chúng theo các yêu cầu cụ thể sau.

Thay Đổi Version Của Ứng Dụng

Chúng ta đã nói đến hai thuộc tính liên quan đến quản lý version bên trong ứng dụng, hai thuộc tính này là versionCode và versionName, chúng nằm ở file build.gradle của cấp độ module. Nếu bạn mở file này lên, bạn sẽ thấy hiện tại versionCode đang là 1 và versionName đang là “1.0”.

Với mỗi lần submit app lên Google Play, bạn nên nâng số versionCode lên một đơn vị (lưu ý giá trị này là số nguyên nhé), nếu bạn không nâng số này lên thì Google Play sẽ không cho bạn submit app đâu nhé. Nhưng với bài thực hành này chúng ta không publish app do đó chúng ta sẽ chỉ cần quan tâm đến versionName mà thôi.

Giả sử chúng ta cung cấp đến user một version có 3 phần “x.y.z”. Version đầu tiên của app sẽ là “1.0.0”. Nếu có thay đổi lớn trong app chúng ta sẽ nâng số ở phần x lên 1 đơn vị, tương tự với thay đổi vừa sẽ nâng số y lên 1 đơn vị, và thay đổi nhỏ sẽ nâng z. Nên nhớ là versionName được đặt thoải mái vì đây là kiểu String, nên bạn có thể đặt các ký tự vào đây, miễn sao user hay ai đó hiểu được ý nghĩa của version này.

Vậy bạn hãy mở file build.gradle cấp module và sửa version của app như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apply plugin: 'com.android.application'
 
android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.yellowcode.tournote"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}
 
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

Bạn thấy rằng bất kỳ chỉnh sửa gì trên file build.gradle, thì hệ thống đều xuất hiện thông báo yêu cầu bạn phải đồng bộ (sync) lại project.


Bạn chỉ cần nhấn vào Sync Now ở bên phải vạch vàng phía trên và đợi trong giây lát. Trong lúc đang đồng bộ hệ thống cũng có thông báo trạng thái.

https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2016/12/Hinh4.png?resize=300%2C241&ssl=1 300w" data-lazy-loaded="1" sizes="(max-width: 384px) 100vw, 384px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px auto; clear: both; text-align: center;">

Sau khi đồng bộ xong, bạn hãy run ứng dụng lên thiết bị. Rồi mở Settings, vào xem thông tin của ứng dụng TourNote, bạn sẽ thấy version mà chúng ta vừa thay đổi được hiển thị như sau.

device-2016-12-16-124255

Và nếu ứng dụng của bạn có được đưa lên Google Play, versionName sẽ xuất hiện như sau (cái này mình nhờ lấy một app khác show giúp, chứ ứng dụng của chúng ta còn chưa đâu vào đâu, làm sao mà đưa lên Google Play).

device-2016-12-16-125039

Khai Báo Manifest

Phải nói rằng cũng bẵng một thời gian mình mới có thể cho ra tiếp bài học hôm nay. Nếu vì thời gian chờ lâu các bạn quên bài thì mình nhắc lại một chút. Như bài hôm trước mình có nói, Gradle và Manifest luôn được nói chung với nhau dù cho chúng chẳng có bà con gì với nhau hết. Chúng chỉ tranh nhau vai trò cấu hình cho project của bạn. Tất nhiên chúng cũng có những vai trò tách biệt nhau như chỉ có ở file này mà không có ở file kia. Nhưng mình không muốn bạn phải đau đầu vì những thứ giống nhau và khác nhau đó. Mình đã nói hết những gì nên làm với Gradle ở bài trước, và những gì nên làm với Manifest ở bài này, không hề có những thông tin trùng lắp khiến bạn bối rối đâu. Tuy nhiên nếu bạn muốn tìm hiểu xem Gradle và Manifest giao thoa ở những chức năng nào thì vẫn có thể dễ dàng tìm thấy thông tin này ở nhiều tài liệu khác nhé.

Khái Niệm Manifest

Khác với Gradle xuất hiện khá muộn khi mà Android Studio ra đời, giúp hỗ trợ bạn quản lý project dễ dàng hơn trước kia. Thì Manifest lại xuất hiện với vai trò là một nền tảng của Android ngay từ ban đầu. Nếu bạn còn nhớ ở Bài 5 mình có nhắc đến Manifest như là một thành phần cơ bản nhất và không thể thiếu trong một ứng dụng Android, thì bạn sẽ thấy Manifest gắn bó với Android như thế nào.

Bạn cứ nhớ là Manifest dùng để định nghĩa những thứ bên trong ứng dụng của bạn. Tại sao phải định nghĩa những thứ này? Vì đây chính là một nơi giúp tóm tắt toàn bộ thông tin về ứng dụng, từ việc ứng dụng sẽ sử dụng internet, sẽ đọc thẻ nhớ ngoài, sẽ dùng GPS, đến việc ứng dụng có bao nhiêu màn hình, có các service nào, broadcast receiver nào, vân vân và mây mây… Dựa trên những định nghĩa này của bạn mà hệ thống sẽ cho phép ứng dụng của bạn được sử dụng các chức năng đặc biệt của hệ thống, đồng thời cảnh báo với user một số thông tin quan trọng trong ứng dụng của bạn trước khi họ quyết định cài đặt và sử dụng ứng dụng, hoặc khi họ vào Settings và xem lại các thông tin đó, hay khi ứng dụng đang chạy thì hệ thống vẫn có căn cứ vào các định nghĩa này mà hỏi quyền từ người dùng. Chà phức tạp nhĩ, tạm thời bỏ qua đi bạn. Để nhẹ đầu hơn thì bạn có thể nhìn vào một số ví dụ của các ứng dụng nổi tiếng bên dưới để xem những cảnh báo của hệ thống như thế nào nhé.

Manifest - Ví dụ sử dụng Manifest của các ứng dụng

Ảnh trên là tất cả các thông tin mà Manifest của 2 app nổi tiếng là Facebook và Grab đã khai báo. 2 hình đầu tiên bên trái là khi user nhấn chọn cài đặt, khi đó Google Play sẽ dựa vào khai báo đó mà hiển thị thông tin ra cho các bạn xem (hơi khó tìm một chút), những thông tin này chính là những “quyền” mà bạn cho phép các ứng dụng này được phép dùng trên máy của bạn hay không, như sử dụng camera, gửi tin nhắn SMS, xem lịch của ban, đọc danh bạ của bạn,…. Còn hình ngoài cùng bên phải chính là thông tin của ứng dụng Grab sau khi bạn đã cài đặt và vào Settings của hệ thống để xem lại.

Chúng ta sẽ biết nhiều hơn về Manifest ở các mục bên dưới. Và cũng như Gradle, bạn sẽ có cơ hội được đụng đến Manifest khá nhiều, đến nỗi bạn thân quen với 2 file này lúc nào không hay.

Làm Quen Với File Manifest

May mắn cho chúng ta là Manifest chỉ có duy nhất một file, ý mình là không có sự phân cấp giữa Manifest cho project và Manifest cho module như với Gradle. Nhưng Manifest vẫn được dùng riêng cho từng module, và vì ở giai đoạn này chúng ta chỉ làm quen với việc một project chỉ có một module, do đó hiện tại chúng ta xem như chỉ duy nhất một Manifest trong project TourNote mà thôi.

Bạn có thể tìm thấy file Manifest trong thư mục src/main/ của module. File này có tên đầy đủ là AndroidManifest.xml. Như hình sau (hình trái hay phải tùy vào cách view của cửa sổ này là Android hay Project).

group

Nếu đã tìm thấy file Manifest này rồi thì bạn hãy click đúp vào để mở nó lên.

Điều đầu tiên bạn nhận ra khi nhìn thấy cấu trúc của Manifest là gì? Vâng mình hiểu ý bạn, nhìn nó như đám rừng… Đùa thôi, bạn cũng nhận thấy là Manifest sử dụng cấu trúc XML giống như cấu trúc của layout mà bạn học ở các bài trước vậy. Điều này làm cho Manifest khác với Gradle vốn sử dụng một kiểu setting riêng dạng khối, phải mất một khoảng thời gian bạn mới quen với việc cấu hình Gradle. Còn với Manifest mình chắc chắn bạn sẽ cảm thấy thân thiện hơn với cấu trúc XML của nó.

Và mình liệt kê lại dưới đây nội dung Manifest của TourNote, nó được tạo mặc định khi bạn tạo mới một project.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
    package="com.yellowcode.tournote">
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
 
</manifest>

Thực ra mà nói thì file Manifest trên đây của TourNote chả nhằm nhò gì cả, nó còn thiếu nhiều lắm. Nhưng bạn đừng vội thêm vào, mình sẽ từ từ hướng dẫn bạn thêm vào các khai báo cho Manifest ở các bước thực hành ở bài này và các bài tiếp theo. Còn bây giờ mình xin phép được liệt kê đầy đủ hơn các khai báo thường dùng của Manifest để bạn làm quen trước. Bạn hãy xem liệt kê dưới đây.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?xml version="1.0" encoding="utf-8"?>
 
<manifest>
 
    <uses-permission />
    <permission />
    <permission-tree />
    <permission-group />
    <supports-screens /> 
 
    <application>
 
        <activity>
            <intent-filter>
                <action />
                <category />
                <data />
            </intent-filter>
            <meta-data />
        </activity>
 
        <service>
            <intent-filter> . . . </intent-filter>
            <meta-data/>
        </service>
 
        <receiver>
            <intent-filter> . . . </intent-filter>
            <meta-data />
        </receiver>
 
        <provider>
            <grant-uri-permission />
            <meta-data />
            <path-permission />
        </provider>
 
    </application>
 
</manifest>

Bùm! Bạn đã thấy sợ chưa. Đó mới chỉ là cái “sườn” của Manifest thôi đấy, tức là dựa trên đó chúng ta phải thêm vào, thêm vào nhiều nữa.

Nào đừng nản nhé, cái gì cũng có nguyên tắc của nó, và khi hiểu rõ nguyên tắc chúng ta sẽ thấy dễ dàng hơn đấy. Mình sẽ nói qua một số thẻ có trong liệt kê trên đây cho các bạn làm quen trước.

Thẻ manifest

Bao bọc file Manifest này chính là thẻ có tên manifest, thẻ này là thẻ gốc cho các thẻ con bên trong nó.

Thẻ này ngày xưa chứa vài thông tin cấu hình cho ứng dụng, nhưng do sự ra đời của Gradle nên một vài thông bị trùng lắp giữa 2 file, các thông tin trùng lắp này dù vẫn còn nhưng không được dùng đến ở Manifest nữa. Do đó chúng ta sẽ không nói đến các thông tin bị trùng lắp kia nữa, mà chỉ nói qua thông tin được dùng đến ở thẻ này của chỉ riêng Manifest mà thôi.

– xmlns:android. Đây đơn giản là dòng khai báo namspace android mà thôi. Sau dòng khai báo này thì các thẻ khác bên trong thẻ manifest này mới có thể sử dụng được từ khóa android.
– package: chính là package name của project. Mặc định package này sẽ là giá trị giống như thuộc tính applicationId trong file build.gradle ở cấp độ module mà bạn đã làm quen ở bài trước.

Hầu như chúng ta sẽ chẳng cần đụng đến thẻ này, bạn chỉ cần biết thôi và để nó như mặc định.

Thẻ uses-permission

Như bạn thấy ở hình chụp màn hình 2 ứng dụng Facebook và Grab ở trên kia, những quyền mà hệ thống hiển thị ra đó đều được lấy ra từ các thẻ uses-permission này.

Lưu ý là với ứng dụng hỗ trợ Android 6.0 hoặc mới hơn, một số quyền còn yêu cầu ứng dụng phải hiển thị popup xin phép người dùng khi lần đầu tiên họ sử dụng đến chức năng có liên quan đến quyền này trong ứng dụng. Chẳng hạn khi lần đầu tiên người dùng nhấn vào nút chụp hình trên TourNote để lưu lại một ghi chú về món ăn, lập tức TourNote sẽ phải hỏi người dùng quyền được sử dụng camera, nếu user cho phép thì TourNote sẽ chạy như bình thường, nếu user không cho phép thì TourNote mãi không sử dụng được camera của hệ thống và sẽ không chụp ảnh được. Một ảnh minh họa cho việc hỏi quyền trong khi sử dụng app, mà mình lấy từ trang developer.android.com về, ứng dụng này đang hỏi quyền truy cập vào danh bạ của user.

Manifest - Ví dụ cho việc hỏi người dùng

Còn đây là cách mà một ứng dụng xin quyền dựa vào thẻ uses-permission từ file Manifest, mình đã “chôm” được khai báo này từ một ứng dụng hoàn chỉnh trên Google Play.

Manifest - Ví dụ sử dụng thẻ uses-permission

Xin quyền thì có rất nhiều chuyện để nói. Có thể mình sẽ có một bài viết riêng về nó. Hoặc nếu bạn có thắc mắc gì ở bước này thì cứ để lại bình luận ở cuối bài học nhé, mình sẵn lòng giải đáp cho các bạn.

Thẻ permission

Thẻ này mình xin được nói sơ thôi vì tuy trông giống với uses-permission ở trên đây nhưng permission có công dụng hoàn toàn khác. Thẻ permission này có thể dùng để định nghĩa ra các quyền cho riêng ứng dụng của bạn. Vì chúng khá ít xài nên các thẻ permissionpermission-treepermission-group sẽ được mình nói đến cụ thể sau nhé.

Một ví dụ cho thẻ permission mà mình chôm được của một app.

Manifest - Ví dụ về sử dụng thẻ permission

Thẻ supports-screens

Thẻ này cũng kha khá quan trọng, nó được dùng để định nghĩa các thể loại màn hình mà ứng dụng của bạn hỗ trợ. Thông thường thì các ứng dụng hay loại bỏ việc hỗ trợ màn hình nhỏ, là các thiết bị có màn hình dưới 3 inch. Chúng ta sẽ sử dụng thẻ này ở bài thực hành phía dưới.

Và đây là một ví dụ cho thẻ này, tất nhiên cũng là do mình chôm luôn.

Manifest - Ví dụ cho thẻ supports-screens

Thẻ application

Thẻ này quan trọng lắm đây, thẻ này chứa một số thuộc tính liên quan đến cấu hình của ứng dụng (đúng như tên gọi của nó), mà mình sẽ liệt kê như bên dưới đây.

1
2
3
4
5
6
7
<application android:allowBackup=["true" | "false"]
             android:icon="drawable resource"
             android:label="string resource"
             android:supportsRtl=["true" | "false"]
             android:theme="resource or theme" >
    . . .
</application>

Chúng ta sẽ nói sơ vài thuộc tính đã được liệt kê.

– android:allowBackup – Cho phép ứng dụng của bạn nằm trong chương trình tự động backup của hệ thống Android hay không. Thực sự mình cũng chưa kiểm chứng tính hiệu quả của chế độ backup này, nghe nói hệ thống sẽ giới hạn dữ liệu backup của ứng dụng với mỗi người dùng là 25MB, kèm theo việc giới hạn các loại dữ liệu có thể backup. Dù sao thì tính năng này cũng rất tốt nên bạn cứ để giá trị của thuộc tính này là true như mặc định nhé.
– android:icon – Thuộc tính này là nơi bạn thiết lập icon cho ứng dụng. Icon này sẽ xuất hiện trên màn hình chính của thiết bị. Hiện tại thì icon này đang được set mặc định là file ic_launcher.png để trong thư mục res/mipmap/. Cách sử dụng icon cho ứng dụng cũng giống như cách bạn hiển thị một ảnh lên ImageView ở bài trước vậy. Chúng ta sẽ thực hành thay đổi icon cho TourNote ở bài học sau nhé.
– android:label – Thuộc tính này trỏ đến một giá trị string trong resource, hoàn toàn tương tự như cách bạn khai báo text cho TextView ở bài trước vậy. String này chính là tên ứng dụng của chúng ta, hiện tại đang là “TourNote”. Chúng ta cũng sẽ thực hành thay đổi tên cho TourNote ở bài học sau.
– android:supportsRtl – Nếu bạn còn nhớ, từ RTL viết tắt của right-to-left, thuộc tính này cho phép ứng dụng của bạn có hỗ trợ cho các hệ ngôn ngữ viết từ phải-sang-trái hay không. Việc hỗ trợ các ngôn ngữ RTL đã được mình nhắc đến cụ thể ở Bài 10 rồi nhé.
– android:theme – Thuộc tính này khá hay, nó giúp chúng ta định nghĩa “giao diện chủ đề” cho ứng dụng. Và vì nó hay quá nên mình sẽ không nói ở đây, mình dành hẳn một bài để nói về chủ đề này để cho các bạn hiểu rõ nhất về nó.

Tất nhiên thẻ application còn nhiều thuộc tính lắm, bạn có thể vào link này để xem thông tin đầy đủ về nó. Mình chỉ nói qua những thuộc tính dùng nhiều nhất, còn lại thì khi nào phát sinh thuộc tính mới mình sẽ nói thêm với các bạn.

Ngoài ra thì trong thẻ application này có các thẻ con quan trọng nữa, chúng ta lại tiếp tục nói đến các thẻ con của application ở bên dưới đây.

Thẻ activity

Thẻ này giúp bạn khai báo các Activity có trong ứng dụng. Activity là gì? Chúng được xem như các màn hình trong ứng dụng của chúng ta. Hiện tại khi mở ứng dụng TourNote lên, bạn sẽ thấy có một màn hình duy nhất mà chúng ta hì hụi xây dựng giao diện cho nó mấy hôm nay, chính vì vậy mà khi nhìn vào file Manifest hiện tại bạn chỉ thấy một thẻ activity mà thôi. Tất nhiên về sau chúng ta sẽ xây dựng nhiều màn hình khác, và khi đó thẻ activity sẽ nhiều lên, vì mỗi thẻ activity như vậy giúp khai báo cho một màn hình duy nhất. Chúng ta dành “giấy mực” cho thẻ này khi học về Activity sau nhé.

Thẻ service

Tương tự như thẻ activity trên đây. Mỗi thẻ service dùng để định nghĩa cho một loại Service. Service là gì á? Bạn có thể xem lại một tí ở Bài 5, nhưng chúng ta sẽ nói cụ thể về Service ở bài học sau.

Thẻ receiver

Cũng như hai thẻ activity và service trên đây. Mỗi thẻ receiver dùng để định nghĩa cho một loại Broadcast Receiver. Broadcast Receiver cũng được nhắc đến ở Bài 5, và sẽ được nói cụ thể sau.

Thẻ provider

Cũng như các thẻ activityservice, và receiver trên đây. Mỗi thẻ provider dùng để định nghĩa cho một loại Content Provider. Content Provider cũng được nhắc đến ở Bài 5và sẽ được nói cụ thể sau.

Thực Hành Thay Đổi Manifest Cho Ứng Dụng TourNote

Bài học đã khá dài rồi, do đó phần thực hành này chúng ta làm nhanh gọn lẹ thôi. Những bổ sung cho Manifest của TourNote sẽ được nói đến ở các bài thực hành sau bạn nhé.

Như đã nói ở trên, chúng ta sẽ thiết lập cho Manifest của TourNote sao cho không hỗ trợ màn hình nhỏ. Và cũng từ kiến thức trên đây, bạn đã biết nên thêm vào Manifest thẻ supports-screens, thẻ này là thẻ con của manifest. Vậy bạn hãy mở file AndroidManifest.xml của TourNote lên và thêm vào các dòng như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="utf-8"?>
    package="com.yellowcode.tournote">
 
    <supports-screens
        android:smallScreens="false"
        android:normalScreens="true"
        android:largeScreens="true"
        android:xlargeScreens="true"/>
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
 
</manifest>

Bạn có thể thấy việc khai báo android:smallScreen=”false” sẽ giúp Google Play kiểm tra màn hình của người dùng trước khi cho họ download ứng dụng của bạn. Nếu màn hình của người dùng rơi vào giá trị smallScreen, như đã khai báo, người dùng sẽ không được phép tải và cài đặt TourNote. Các loại màn hình còn lại đều mang giá trị true nên sẽ được phép tải và cài đặt TourNote một cách bình thường. Bạn đã hiểu chưa nào.

Sử Dụng Style

Như đã hứa ở cuối bài trước, là từ bài học hôm nay chúng ta sẽ lần lượt nói về từng resource chuyên sâu của Android. Mình sẽ ưu ái nói đến resource Style trước, bởi vì nó là một resource đặc biệt, nó quyết định đến SỰ NHẤT QUÁN trong giao diện của ứng dụng của bạn.

Mình đã từng góp ý cho nhiều project của các bạn mới tham gia vào lập trình ứng dụng, đa số các bạn đều mắc một lỗi, đó là sự nhất quán. Bạn thử nghĩ xem ở màn hình thứ nhất, các Button của các bạn thiết kế với phong cách vuông, background màu xám, nhưng qua màn hình thứ hai thì phong cách bị đảo lộn với kiểu Button được bo tròn, background màu xanh. Chưa kể đến font size và chiều cao của các Button này không đồng nhất nữa. Đó là một ví dụ nhỏ về Button, các widget khác trong các ứng dụng này đều chịu một “số phận” tương tự, đó là việc thiếu nhất quán trong phong cách. Việc thiếu nhất quán này làm cho người dùng cảm thấy bối rối khi ở mỗi màn hình mỗi khác, họ sẽ đánh giá rằng ứng dụng của bạn thiếu sự đầu tư, thiếu chuyên nghiệp.

Vậy làm cách nào để cải thiện? Mời các bạn cùng xem qua bài học về Style nhé.

Khái Niệm Style

Style – Phong cách. Như mình có trình bày ở trên, Phong Cách ở đây là muốn nói về cách mà bạn tạo ra một sự đồng nhất về giao diện cho ứng dụng. Như ví dụ về Button trên đây, bạn muốn tạo ra các Button giống nhau ở các màn hình, để cho nó có sự đồng nhất. Khi đó việc thiết kế cho Button sao cho chúng giống nhau như vậy khiến bạn cảm thấy nhàm chán vì mất khá nhiều thời gian. Lúc đó bạn sẽ cần đến một nơi định nghĩa hết tất tần tật những thuộc tính của Button trên, rồi đến với từng Button bạn chỉ việc mang định nghĩa đó ra dùng. Thì khi đó Style chính là kiểu resource mà bạn đang tìm.

Nói Đến Style Phải Nói Đến Theme

Đã nói về Style thì mình cũng muốn các bạn làm quen với khái niệm Theme luôn. Vì về cơ bản, trong Android Theme không khác gì Style. Chúng đều giúp tạo ra sự đồng nhất về giao diện. Nhưng thay vì Style giúp áp dụng sự đồng nhất cho các view sử dụng nó, thì Theme lại giúp áp dụng sự đồng nhất cho toàn bộ Activity (toàn bộ màn hình) hoặc toàn bộ ứng dụng. Ví dụ như bạn khai báo font cho Theme, rồi áp dụng Theme đó cho toàn bộ ứng dụng, thì tất cả các view trong màn hình đó đều được thay đổi font.

Nếu đến đây bạn vẫn còn mơi hồ về hai khái niệm Style và Theme, thì bạn cứ tưởng tượng chúng giống như khái niệm CSS (Cascading Style Sheets) trong lập trình Web vậy. CSS cũng giúp định hình các phong cách, để tạo nên sự đồng nhất cho các thẻ HTML bên trong trang Web đó.

Có một điều bạn cần phải nhớ là mặc dù Theme là một khái niệm để mang ra nói chung với Style, nhưng bạn không cần phải định nghĩa ra bất kỳ Theme nào cả. Bạn chỉ cần chọn một trong các Theme được xây dựng sẵn bởi hệ thống. Và vì kiến thức về Theme cũng kha khá nhiều và quan trọng, nên mình sẽ nói tiếp ở bài sau nhé, bài này sẽ tập trung cho Style.

Ví Dụ Về Sử Dụng Style

Nào, chưa cần nói gì về cách sử dụng Style nhé. Phần này đưa ra một ví dụ mà không nói quá chi tiết, để bạn có một cách hiểu tổng quát về Style trước, rồi chúng ta mới đi từ từ vào các cách thức cụ thể.

Giả sử mình có một TextView như sau.

1
2
3
4
5
6
<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textColor="#00FF00"
    android:typeface="monospace"
    android:text="@string/hello" />

Mình muốn TextView này là một TextView chuẩn, để ở các màn hình khác mình cũng sẽ định nghĩa các TextView giống vậy. Và thay vì mình phải khai báo lại các thuộc tính như trên cho tất cả các TextView ở các màn hình khác, thì mình chỉ cần tạo ra một Style có tên là CodeFont, rồi sau đó áp dụng Style này cho TextView này và các TextView về sau như sau.

1
2
3
<TextView
    style="@style/CodeFont"
    android:text="@string/hello" />

Vậy đó. Cách để áp dụng Style cho TextView đơn giản vậy thôi. Cách thức khởi tạo chi tiết hơn một Style được mô tả tiếp ở bước kế tiếp.

Chi Tiết Sử Dụng Style

Như mình có nói thì Style là một dạng resource của Android, vậy chắc chắn file Style phải ở đâu đó trong thư mục res/ rồi, và chính xác của thư mục chứa file Style này là res/values/. Bạn hãy mở thư mục này theo đường dẫn như hình bên dưới. Khi bạn tạo mới một project thì hệ thống cũng tạo sẵn cho chúng ta một file Style và để ở thư mục này, tên của file Style này là styles.xml.

Style - Nơi chứa file Stylehttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2017/01/Hinh1-1.png?resize=161%2C300&ssl=1 161w" data-lazy-loaded="1" sizes="(max-width: 361px) 100vw, 361px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px auto; clear: both; text-align: center;">

Và dĩ nhiên vì là một loại resource của Android, nên file Style cũng theo quy luật Alternative Resource và Default Resource. File styles.xml các bạn đang thấy trên hình thuộc về Default Resource, nhưng có thể project ở máy của bạn có chứa đựng một file styles.xml khác ở thư mục values-xxx/ nào khác thì có nghĩa file Style đó thuộc về Alternative Resource.

Cấu Trúc File styles.xml

Giờ thì chúng ta cùng mở file styles.xml ở thư mục Default Resource này lên nhé.

Style - Cấu trúc file Stylehttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2017/01/Hinh2-2.png?resize=300%2C118&ssl=1 300w" data-lazy-loaded="1" sizes="(max-width: 701px) 100vw, 701px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px auto; clear: both; text-align: center;">

Bạn có thể thấy rằng file styles.xml cũng giống như file strings.xml mà bạn đã làm quen hay tất cả các file .xml khác trong thư mục values/ mà bạn sẽ làm quen sau này cũng đều được để trong một thẻ gốc có tên resources.

Hình trên đây đang có một Style, mỗi Style mà bạn tạo ra sẽ được nằm trong cặp đóng mở thẻ có tên style. Và thẻ style duy nhất bạn thấy trong hình lại dùng như một Theme của ứng dụng, mà chúng ta sẽ nói về Theme này ở bài sau.

Còn bây giờ giả sử chúng ta cần tạo một Style có tên là CodeFont để dùng cho ví dụ về TextView ở trên, vậy mình sẽ thêm các dòng sau vào file styles.xml.

1
2
3
4
5
6
<style name="CodeFont" parent="@android:style/TextAppearance.Medium">
    <item name="android:layout_width">match_parent</item>
    <item name="android:layout_height">wrap_content</item>
    <item name="android:textColor">#00FF00</item>
    <item name="android:typeface">monospace</item>
</style>

Khi đó ở mọi nơi mà mình muốn dùng đến Style này, mình sẽ gọi đến bằng thuộc tính style như sau.

1
2
3
<TextView
    style="@style/CodeFont"
    android:text="@string/hello" />

Giờ thì chúng ta hãy làm quen với những gì có thể có bên trong thẻ style nhé, rồi sau đó cùng nhau thực hành tạo style cho TourNote ở bên dưới bài học này.

Thuộc Tính name

Đây là tên của Style, như bạn thấy ở ví dụ trên tên này là CodeFont, bạn phải đặt tên cho bất kỳ Style nào mà bạn đặt ra để có thể dùng đến sau này. Cách đặt tên cho Style cũng như cách đặt tên cho resource dạng String mà bạn đã thực hành ở Bài 8.

Các Thẻ item

Chà, như Style ví dụ trên đây thì sau thuộc tính name còn có thuộc tính parent. Nhưng mình xin phép được nói đến các thẻ item trước vì nó quan trọng hơn.

Mỗi thẻ item như vậy định nghĩa ra một loại phong cách nào đó, trong đó tên của loại phong cách đó được miêu tả trong thuộc tính name của thẻ này, như bạn đã thấy trong ví dụ, các tên này như là “android:layout_width”“android:layout_height”“android:textColor”“android:typeface” và còn nhiều tên nữa… Bạn có thể thấy các giá trị cho thuộc tính name này đều là các thuộc tính của các widget hay của layout chứa widget đó mà bạn đã làm quen trong việc thiết kế giao diện trước đây. Như vậy Style cũng chính là nơi bạn lấy các thuộc tính có sẵn ra, rồi định nghĩa cho chúng một giá trị nào đó, rồi áp dụng lại cho các widget, đó chính là khái niệm Phong Cách trong bài học hôm nay.

Theo sau các khai báo name trong thẻ này là các giá trị của nó, đó có thể là một kiểu String, một giá trị màu sắc, hay một độ lớn dpsp,… tùy thuộc vào từng loại phong cách.

Thuộc Tính parent

Thuộc tính này không bắt buộc. Nhưng nếu bạn muốn Style của mình được kế thừa từ một Style có sẵn (do bạn tạo ra trước đó, hay từ Style của một thư viện nào đó, hoặc Style của hệ thống), việc kế thừa này giúp bạn tận dụng lại những định nghĩa từ Style gốc, và tạo ra các định nghĩa mới bổ sung cho Style gốc còn thiếu.

Như ví dụ từ định nghĩa trên đây, bạn sẽ thấy Style dành cho TextView có kế thừa từ một Style của TextView có sẵn của hệ thống, đó là @android:style/TextAppearance.Medium. Ngoài việc dùng lại tất cả những thuộc tính được khai báo sẵn của hệ thống, thì Style CodeFont trên lại định nghĩa mới các thuộc tính về layout_widthlayout_heighttextColor, và typeface cho riêng mình.

Còn nếu bạn muốn biết nhiều hơn về ví dụ cách kế thừa từ một Style do bạn tạo ra, thì mời bạn xem đoạn định nghĩa các Style bên dưới.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<style name="BaseTextViewStyle">
    <item name="android:layout_width">wrap_content</item>
    <item name="android:layout_height">wrap_content</item>
    <item name="android:textColor">@color/cl_default</item>
    <item name="android:textSize">@dimen/text_size_normal</item>
</style>
 
<style name="LargeTextViewStyle" parent="BaseTextStyle">
    <item name="android:textSize">@dimen/text_size_large</item>
</style>
 
<style name="SmallTextViewStyle" parent="BaseTextStyle">
    <item name="android:textSize">@dimen/text_size_small</item>
</style>

Với ví dụ này thì mình định nghĩa ra một Style chung cho nhiều TextView có tên là BaseTextViewStyle. Sau đó mình có thêm hai Style nữa là LargeTextViewStyle và SmallTextViewStyle đều có thuộc tính parent chỉ đến BaseTextViewStyle, nhưng hai Style con lại có cách định nghĩa riêng giá trị textSize cho chúng.

Lưu ý là với việc kế thừa từ Style của bạn ở cùng một file, thì bạn có thể không cần đến khai báo parent nữa mà dùng dấu chấm như thế này cũng được.

1
2
3
4
5
6
7
<style name="BaseTextStyle.LargeTextViewStyle">
    <item name="android:textSize">@dimen/text_size_large</item>
</style>
 
<style name="BaseTextStyle.SmallTextViewStyle">
    <item name="android:textSize">@dimen/text_size_small</item>
</style>

Thực Hành Tạo Style Cho TourNote

Chà lý thuyết nhiều quá, hi vọng các bạn đã nắm tốt kiến thức về Style.

Quay trở lại với bài thực hành, ở các bài thực hành trước, chúng ta đã tạo ra một TextView ở giữa màn hình để hiển thị thông báo cho người dùng biết rằng chưa có ghi chú nào được tạo và người dùng phải tạo ra ghi chú mới. Nhưng bạn cũng nên biết trong ứng dụng cũng sẽ có nhiều các câu thông báo như vậy. Vậy thì để tiết kiệm thời gian sau này mỗi khi bạn cần hiển thị một thông báo ở đâu đó, thay vì thiết kế lại các thuộc tính cho các câu thông báo, thì chúng ta sẽ tạo một Style đầu tiên dành cho các câu thông báo như thế này ở mọi nơi trong ứng dụng TourNote của chúng ta.

Tạo Style

Để bắt đầu, bạn hãy mở file styles.xml của TourNote lên nhé. Bạn chớ có xóa đi Style hiện có trong file này, mà hãy thêm một Style mới bên dưới Syle đã có như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<resources>
 
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
 
    <style name="InformationTextView">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_margin">8dp</item>
        <item name="android:gravity">center</item>
        <item name="android:textSize">15sp</item>
    </style>
 
</resources>

Bạn thấy rằng Style được tạo ra này có tên là InformationTextView. Trong đó có các thuộc tính được định nghĩa là phong cách cho loại TextView này, bao gồm layout_widthlayout_heightpaddinggravity và textSize.

Tại sao những thuộc tính khác của TextView này không được sử dụng làm Style? Bởi vì hoặc là chúng không mang tính đặc trưng sẽ được dùng đi dùng lại nhiều lần, như thuộc tính text chẳng hạn. Hay chúng là các thuộc tính của layout cha, mà nếu như vậy thì ai mà biết được TextView sẽ thuộc về ConstraintLayout hay LinearLayout hay RelativeLayout trong tương lai.

Sử Dụng Style

Với việc tạo ra một Style như trên đây, thì bước này chúng ta áp dụng Style đó vào TextView cần thiết. Bạn hãy mở file activity_main.xml lên nhé.

Nếu bạn đang ở tab Design của cửa sổ này, thì hãy đảm bảo TextView ở giữa màn hình đang được chọn. Sau đó hãy tìm trong hộp thoại Attributes bên phải màn hình thuộc tính style. Có thể bạn phải nhấn vào View all attributes mới có thể tìm thấy field này.

Style - Thêm Style cho TextView bằng Designhttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2017/01/Hinh3.png?resize=300%2C163&ssl=1 300w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2017/01/Hinh3.png?resize=768%2C418&ssl=1 768w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2017/01/Hinh3.png?resize=1024%2C557&ssl=1 1024w" data-lazy-loaded="1" sizes="(max-width: 1000px) 100vw, 1000px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px auto; clear: both; text-align: center;">

Khi thấy field style, hãy đưa trỏ chuột vào field này bạn sẽ thấy bên phải có dấu ba chấm (). Nhấn vào đó sẽ dẫn bạn đến một dialog chọn Style đã được định nghĩa sẵn. Và vì có quá nhiều Style trong đây nên bạn hãy tìm bằng cách gõ từ khóa vào field tìm kiếm như sau.

Style - Tìm Style trong cửa sổ quản lýhttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2017/01/Hinh5.png?resize=300%2C264&ssl=1 300w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2017/01/Hinh5.png?resize=768%2C677&ssl=1 768w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2017/01/Hinh5.png?resize=1024%2C903&ssl=1 1024w" data-lazy-loaded="1" sizes="(max-width: 1000px) 100vw, 1000px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px auto; clear: both; text-align: center;">

Sau khi tìm thấy Style InformationTextView rồi thì bạn hãy nhấn chọn OK. Kết quả nhận được ở màn hình trực quan sau đó chẳng có gì khác lạ cả. Một phần vì Style mà bạn định nghĩa nó dựa trên các thuộc tính có sẵn của TextView này rồi, nên việc áp dụng nó vào đây cũng chẳng giúp gì. Nó sẽ hữu dụng khi bạn xây dựng TextView nào đó ở những chỗ khác, mà áp dụng Style InformationTextView này, thì sẽ giúp tiết kiệm khá nhiều công sức định nghĩa lại các thuộc tính giống với TextView này.

Tuy nhiên sự khác lạ sẽ ở code XML, bạn hãy chuyển qua tab Text ở cửa sổ thiết kế cho giao diện activity_main.xml này. Bạn sẽ thấy TextView hôm trước nay đã có thêm thuộc tính style.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<TextView
    android:id="@+id/activity_main_tv_empty"
    style="@style/InformationTextView"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginLeft="8dp"
    android:layout_marginTop="8dp"
    android:layout_marginRight="8dp"
    android:layout_marginBottom="8dp"
    android:gravity="center"
    android:text="@string/empty_note"
    android:textSize="15sp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

Bạn thấy rằng, mặc dù chúng ta không cần chỉnh sửa gì cả, vì giao diện của việc thêm style vào cho TextView có khác gì với không thêm đâu. Nhưng chúng ta cũng phải thực hành tiếp bước sau đây. Vì chắc bạn đã hiểu, Style giúp định nghĩa ra các thuộc tính chung nhất cho các view trong Android, nên khi áp dụng Style vào view rồi, chúng ta cũng nên bỏ đi khác khai báo trùng lắp giữa Style và view được áp dụng Style đó.

Vì vậy hãy quay trở lại activity_main.xml. Bạn vẫn để chế độ thiết kế ở tab Text để chỉnh sửa cho dễ. Chúng ta sẽ bỏ đi các thuộc tính dư thừa, và TextView lúc bấy giờ chỉ còn như sau.

1
2
3
4
5
6
7
8
<TextView
    android:id="@+id/activity_main_tv_empty"
    style="@style/InformationTextView"
    android:text="@string/empty_note"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
 

Bạn có thể thấy rằng với Style thì việc định nghĩa ra các widget tương tự nhau là dễ dàng, chẳng những vậy chúng còn giúp rút ngắn khai báo các widget của bạn nữa. Thật là tiện lợi.

Và đây là tổng thể code của màn hình activity_main.xml cho đến lúc này.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <TextView
        android:id="@+id/activity_main_tv_empty"
        style="@style/InformationTextView"
        android:text="@string/empty_note"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
 
    <ImageView
        android:id="@+id/imageView"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:scaleType="fitCenter"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/activity_main_tv_empty"
        app:srcCompat="@drawable/empty_note" />
 
</android.support.constraint.ConstraintLayout>

Sử Dụng Theme

Với bài học hôm trước khi đã nói về cách sử dụng Style, mình hi vọng các bạn đã nắm vững cách dùng resource đặc biệt này để làm cho giao diện của ứng dụng trông nhất quán hơn. Bên cạnh đó, ngoài mục tiêu nhất quán mà Style mang đến, bạn nên biết rằng chúng còn giúp bạn xây dựng giao diện được gọn gàng và nhanh hơn nữa. Nếu bạn có bất kỳ ứng dụng nào sắp được viết, hay đang viết dở dang, thì bạn nên kiểm chứng lại xem bạn đã sử dụng Style chưa, nếu chưa thì bạn nên áp dụng từ bây giờ. Không phải widget nào cũng bắt buộc bạn phải tạo Style cho nó, nhưng một ứng dụng sẽ luôn có hầu hết các Style cho các loại widget đấy nhé, càng nhiều Style thì tính chuyên nghiệp càng cao.

Và bài học hôm nay mình sẽ nói đến kiến thức còn lại có liên quan đến Style, đó là kiến thức về Theme.

Định Nghĩa Theme

Nếu bạn đã nắm rõ Style rồi, thì với Theme sẽ thực sự dễ dàng. Vì Theme chỉ khác Style ở ba chỗ.

  • Khác nhau đầu tiên là Theme áp dụng Phong Cách cho toàn bộ Activity (toàn bộ màn hình) hoặc toàn bộ ứng dụng, còn Style thì áp dụng Phong Cách cho các widget.
  • Khác nhau thứ hai là với Theme bạn không phải định nghĩa ra bất kỳ Theme mới nào mà chỉ kế thừa từ các Theme được xây dựng sẵn của hệ thống, nói như vậy có nghĩa là hệ thống đã định nghĩa hết các Phong Cách cho ứng dụng của bạn rồi, bạn có thể thấy là nếu bạn có tạo mấy chục project mới hiện tại bây giờ thì nó cũng sẽ có giao diện và màu sắc ban đầu giống như nhau, đó là vì Theme, trừ khi bạn thay đổi các giá trị ban đầu đó như bài học và thực hành hôm nay.
  • Và khác nhau cuối cùng là ở cách áp dụng Theme vào ứng dụng, lát nữa chúng ta sẽ cùng nhau xem qua.

Còn lại thì cách thức khai báo và cấu trúc của Theme ở XML resource không khác gì Style cả.

Có Bao Nhiêu Theme Trong Android?

Chúng ta đang nói đến làm sao biết được hệ thống có bao nhiêu Theme mà kế thừa? Thực ra thì bạn chỉ cần nhớ đến bộ ba các Theme này mà thôi.

  • Theme
  • Theme.Holo
  • Theme.Material

Trong đó chắc bạn cũng biết giao diện của project mà bạn tạo ra ở giai đoạn này là giao diện theo Theme Material, nếu bạn muốn biết thêm kiến thức về phong cách Material thì có thể xem thêm ở đây. Trước khi đi vào chi tiết cách làm việc với Theme Material thì mình mời các bạn cùng xem sơ qua từng Theme một chút.

Theme Đầu Tiên Chỉ Có Tên Là “Theme”

Từ giai đoạn đầu của Android, Android 1.0, hệ điều hành này đã định nghĩa ra một Theme có cái tên đơn giản là “Theme”. Khi đó nếu bạn tạo mới một project, thì giao diện của các thành phần trong app sẽ trông như thế này. Vì rất khó để quay trở lại ngày xưa ấy chụp lại hình, nên mình “chôm” trên mạng một màn hình mang Theme có tên là “Theme” cho các bạn xem.

Ứng dụng được xây dựng với Theme là Theme
Ứng dụng được xây dựng với Theme là Theme

Bạn có thể thấy rằng tên của ứng dụng (ứng dụng này tên là RelativeLayoutDemo) được hiển thị ở một thanh mỏng màu xám phía trên, thanh này được gọi là Title BarEditText lúc này là một hình chữ nhật bo tròn góc, khi nhấn vào thì có viền cam xung quanh. Còn các Button thì màu xám và có hiệu ứng hơi nổi lên. Lúc này giao diện của ứng dụng chưa mang phong cách phẳng như bây giờ.

Theme Được Nâng Cấp Sau Đó Có Tên “Theme.Holo”

Theme Holo được trình làng cùng với sự ra đời của Android 3.0. Và mình cũng “chôm” hình ảnh này cho bạn xem. Với Theme Holo thì khi bạn tạo mới một project thì màn hình với các widget được tạo ra sẽ trông như thế này đây.

Ứng dụng được xây dựng với Theme là Theme.Holo
Ứng dụng được xây dựng với Theme là Theme.Holo

Bạn hãy so sánh sự nâng cấp này. Với Theme Holo thì Title Bar và phần thân giao diện có cùng màu, và được ngăn cách nhau bởi một vạch mỏng, ngoài ra thì Title Bar còn được miễn phí hiển thị thêm icon của app nữa. Uhm… thực ra thì Title Bar ở giai đoạn này đã được gọi là Action Bar, lý do vì sao thì qua bài học về Menu chúng ta sẽ nói rõ hơn. EditText thì không còn hình chữ nhật to nữa mà là một thanh mỏng, khi nhấn vào thì thanh này chuyển thành màu xanh, bình thường thì màu xám. Các Button thì trông phẳng hơn chứ không nổi nữa.

Và Bây Giờ Theme Mới Nhất Có Tên “Theme.Material”

Từ Android 5.0, Android đã khoác lên mình giao diện mới, và khi đó các ứng dụng có thêm một Theme mới đó là Material mà bạn đang làm quen ở các bài học của Yellow Code Books này.

Ứng dụng được xây dựng với Theme là Theme.Material
Ứng dụng được xây dựng với Theme là Theme.Material

Với Material thì Action Bar không còn icon app nữa, màu sắc của Action Bar so với phần thân cũng khác nhau. EditText được hiển thị bằng một thanh thẳng, không còn móc vuông hai bên, thanh này cũng có màu xám khi chưa nhấn vào, và hiển thị theo màu khác nếu bạn nhấn vào nó. Button thì vẫn phẳng, nhưng text được tự động in hoa, với lại font chữ cũng nhỏ hơn chút đỉnh so với Button ở Theme Holo.

Chuyện Tương Thích Ngược

Thực tế thì từ nào giờ Android chỉ mới có ba Theme ở ba giai đoạn khác nhau như mình đã trình bày ở trên. Nhưng đau đầu ở chỗ, như bạn biết Theme Material chỉ xuất hiện từ Android 5.0 trở lên, sẽ như thế nào nếu bạn muốn các thiết bị có cài Android trước 5.0 vẫn mang giao diện tươi đẹp của Material? Câu hỏi này đã được Google dự liệu trước, và kết quả là ngoài ba Theme bạn đã biết, nay Google ra thêm vài Theme nữa để có thể hỗ trợ ngược lại các đời hệ điều hành cũ.

Nhưng bạn có thể tin chắc rằng dù cho có bao nhiêu Theme được hệ thống tạo ra sẵn, thì chung quy lại chúng chỉ gói gọn phong cách vào ba Theme chủ lực như mình đã liệt kê trên đây, các Theme còn lại nằm ngoài ba loại trên đều là các Theme giúp tương thích ngược cả.

Vậy Có Thể Làm Gì Được Với Theme?

Như bạn có thể thấy, với mỗi thời kỳ của Android, hệ thống sẽ tạo ra một Theme và buộc ứng dụng của bạn phải theo. Điều này gây chút bối rối với một số bạn đã từng lập trình game nay muốn làm quen với lập trình ứng dụng, vì với game các bạn được phép tự do thoải mái thiết kế phong cách của các thành phần giao diện theo design có sẵn, nhưng với ứng dụng, mọi thứ bị giới hạn bởi Theme.

Nhưng như vậy không có nghĩa là Theme sẽ cứng nhắc và bắt bạn phải phục tùng theo. Theme được hiểu đúng đắn là “mọi thứ có sẵn ban đầu nhưng bạn vẫn có thể làm nó khác đi”. Có sẵn là để bạn làm nhanh các ứng dụng, mà vẫn có được phong cách giao diện hiện đại. Làm cho khác đi là làm cho ứng dụng của bạn có những đặc trưng riêng, vừa mang phong cách hòa hợp với Theme của hệ thống, vừa mang phong cách riêng của bạn.

Và bài học hôm nay tập trung vào làm thế nào để tạo ra phong cách riêng đó. Nói dông dài từ đầu giờ chỉ với mục đích này thôi đó bạn, khổ ghê, hic!

Cách Sử Dụng Theme

Tạo Theme

Cách tạo ra một Theme giống với cách tạo một Style ở bài trước, mình xin tóm tắt lại như sau.

  • Theme cũng như Style được để ở thư mục res/values/ với tên file là styles.xml.
Nơi lưu trữ resource Theme
Nơi lưu trữ resource Theme
  • Mỗi Theme cũng được chứa trong thẻ style y như Style vậy.
Các thẻ liên quan đến Theme và Style trong ứng dụng
Các thẻ liên quan đến Theme và Style trong ứng dụng
  • Bạn phải đặt tên cho Theme ở thuộc tính name.
  • Bạn có thể thiết lập các phong cách riêng cho Theme thông qua các thẻ item bên trong Theme này.
  • Bạn phải chỉ định parent là Theme nào của hệ thống, hoặc nếu không phải Theme của hệ thống thì là một Theme mà có parent của nó là Theme hệ thống.

Sử Dụng Theme

Nếu như khai báo Theme giống hoàn toàn với khai báo Style, thì sử dụng Theme lại khác Style hoàn toàn.

Khác nhau thứ nhất chắc bạn cũng biết rồi, đó là Style được dùng trong các widget, còn Theme chỉ được chỉ định cho từng Activity (từng màn hình) hoặc cho cả ứng dụng mà thôi. Cách thức áp dụng Theme cho màn hình hay cho ứng dụng sẽ được mình nói chung ở phần thực hành bên dưới luôn nhé.

Khác nhau thứ hai là nếu như để sử dụng Style, ở widget bạn cần chỉ định ở thuộc tính style như này.

Xem lại nơi định nghĩa Style ở Widget
Xem lại nơi định nghĩa Style ở Widget

Thì với Theme bạn phải dùng thuộc tính android:theme, mời bạn cùng làm quen ở bài thực hành bên dưới luôn nhé.

Thực Hành Với Theme Của TourNote

Bài hôm nay chúng ta hãy cùng xem qua các cách sử dụng Theme, và rồi cuối cùng sẽ cùng thực hành việc chỉnh sửa Theme cho TourNote sao cho đúng với thiết kế ban đầu của ứng dụng nhé bạn.

Trước hết bạn hãy mở file styles.xml ở thư mục res/values/ lên trước. Với việc thực hành ở bài trước, bạn đã tạo ra một Style có tên là InformationTextView. Bỏ qua Style InformationTextView này ra thì bạn có thể thấy một Style khác có tên là AppTheme, đây chính là Theme được tạo sẵn, Theme này kế thừa từ Theme cha có tên là Theme.AppCompat.Light.DarkActionBar, như bạn có thể đoán được thì Theme này là Theme Material nhưng giúp tương thích ngược lại với các máy Android trước 5.0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<resources>
 
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
 
    <style name="InformationTextView">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_margin">8dp</item>
        <item name="android:gravity">center</item>
        <item name="android:textSize">15sp</item>
    </style>
 
</resources>

Tạm thời chúng ta không thay đổi các thuộc tính name hay parent của Theme này làm gì, mà hãy xem qua cách sử dụng Theme như thế nào trong ứng dụng TourNote.

Sử Dụng Theme

Bạn cũng đã biết Theme giúp tạo phong cách cho màn hình hay toàn ứng dụng. Vậy cách tốt nhất để ứng dụng Theme là ở đâu bạn có thể đoán ra luôn không? Đó chính là file AndroidManifest.xml. Bạn hãy mở file này lên để kiểm chứng nhé.

Áp Dụng Theme Cho Toàn Bộ Ứng Dụng

Nếu muốn áp dụng Theme cho toàn bộ ứng dụng, thì bạn nên gọi đến Theme thông qua thuộc tính android:theme của thẻ application trong file Manifest. Điều này mình cũng có nói sơ ở bài học về Manifest rồi nhé.

Áp dụng Theme cho toàn bộ ứng dụng
Áp dụng Theme cho toàn bộ ứng dụng

Áp Dụng Theme Cho Từng Activity

Nếu bạn có những Theme cho riêng từng Activity, thì bạn nên tìm tới thẻ activity cần áp dụng Theme và cũng khai báo thông qua thuộc tính android:theme như với thẻ application vậy.

Màn hình dưới đây mình ví dụ một Theme cho Activity duy nhất của TourNote, bạn chỉ cần nhớ thôi chứ đừng chỉnh sửa file Manifest của TourNote như này nhé.

Áp dụng Theme cho MainActivity
Áp dụng Theme cho MainActivity

Chỉnh Sửa Theme Cho TourNote

Giờ thì chúng ta mới vào phần thực hành chính. Bạn hãy đảm bảo là đang mở file styles.xml nhé. Chúng ta cùng nhìn lại “dung nhan” của TourNote trước khi làm cho nó đẹp hơn nào.

Giao diện TourNote khi chưa chỉnh sửa Theme
Giao diện TourNote khi chưa chỉnh sửa Theme

Bắt đầu, bạn hãy thử thay giá trị ở parent của Theme mặc định thành Theme.AppCompat.Light.NoActionBar. Có nghĩa chúng ta thay đổi DarkActionBar thành NoActionBar, bạn cũng có thể tưởng tượng chuyện gì xảy ra với TourNote rồi đúng không, file styles.xml của chúng ta trông như sau.

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<resources>
 
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
 
    <style name="InformationTextView">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_margin">8dp</item>
        <item name="android:gravity">center</item>
        <item name="android:textSize">15sp</item>
    </style>
 
</resources>

Lúc này khi run ứng dụng lên. Opps… mất tiêu Action Bar rồi!!!

Giao diện TourNote khi chọn Theme NoActionBar
Giao diện TourNote khi chọn Theme NoActionBar

Không được rồi, nhất định là không được! Action Bar khá là quan trọng trong ứng dụng TourNote của chúng ta. Mình chỉ xúi dại bạn vọc chơi cho vui thôi. Giờ thì chúng ta Ctrl-Z (Mac là Command-Z) để styles.xml về lại hiện trạng ban đầu nhé.

Chúng ta tiếp tục thực hành. Có thể là bạn quên, mình xin nhắc lại là design của TourNote ban đầu có một bộ màu sắc khác với màu mặc định mà bạn thực hành bấy lâu nay, mời bạn xem lại.

Thiết kế ban đầu của TourNote
Thiết kế ban đầu của TourNote

Vậy phải làm sao? Bạn có thể xem thêm ở link này sẽ có được thông tin chi tiết hơn. Nhưng cơ bản với Theme Material, Android cung cấp cho chúng ta một thông tin về các thuộc tính mà chúng ta có thể chỉnh sửa như hình sau.

Thông tin về Theme của ứng dụng mà chúng ta có thể chỉnh sửa
Thông tin về Theme của ứng dụng mà chúng ta có thể chỉnh sửa

Bạn có thể thấy các text màu trắng cho bạn biết các name của các item, đây là các item về màu sắc, mà bạn có thể override từ Theme cha. Override ở đây có nghĩa là bạn định nghĩa lại giá trị mới cho nó, dù là ở Theme cha đã định nghĩa rồi.

Để bắt đầu override thì bạn hãy quay trở lại với Theme AppTheme và tiến hành chỉnh sửa như sau.

  • Với item có name là colorPrimary, bạn đổi giá trị của nó từ @color/colorPrimary sang #00BCD4, đây chính là mã màu theo mã Hex, chúng ta sẽ nói đến cách sử dụng màu và resource liên quan đến màu ở bài sau. Như vậy là bạn vừa chỉnh sửa xong màu cho Action Bar.
  • Với item có name là colorPrimaryDark, bạn đổi giá trị của nó từ @color/colorPrimaryDark sang #0097A7. Như vậy là bạn vừa chỉnh sửa xong màu cho Status Bar, đây là thanh trạng thái của ứng dụng, thông thường thì màu Status Bar sẽ cùng tông màu với Action Bar nhưng hơi tối hơn một xíu.
  • Với item có name là colorAccent, bạn đổi giá trị của nó từ @color/colorAccent sang #FFEB3B. Như vậy là bạn vừa chỉnh sửa xong màu cho các điểm nhấn trong app, các điểm nhấn này như là màu của EditText khi user nhấn vào, màu của CheckBoxRadioButton, hay của Tab bar đều chịu ảnh hưởng bởi màu này.
  • Ngoài ra thì bạn hoàn toàn có thể thêm vào các item với các name còn lại theo hình trên đây, như windowBackground hay navigationBarColor rồi cho chúng nó một màu sắc nào đó để thử nghiệm chơi, nhưng với TourNote thì mình chỉ muốn bạn chỉnh ba màu trên đây cho Theme là đủ.

Với mỗi màu bà bạn chỉnh sửa ở bước này, Android Studio đều hiển thị nó ra ở thanh bên trái của editor, rất trực quan đúng không.

File Theme của TourNote sau khi chỉnh sửahttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2019/06/Hinh13.png?resize=300%2C173&ssl=1 300w" data-lazy-loaded="1" sizes="(max-width: 667px) 100vw, 667px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px; vertical-align: bottom;">
File Theme của TourNote sau khi chỉnh sửa

Giờ thì chúng ta run app lên để xem kết quả nhé.

Ứng dụng TourNote sau khi làm mới Theme
Ứng dụng TourNote sau khi làm mới Theme

Sử Dụng String

Chúng ta đã cùng nhau xem qua cụ thể hai (thực ra là một) resource của Android rồi. Đó là:

  • Style
  • Và Theme

Hôm nay chúng ta lại xem đến cách sử dụng một dạng resource nữa. Đó là resource string.

Thoạt nghe qua các bạn có thể nghĩ là rất dễ, vì dù sao khái niệm string cũng không cao siêu gì. Nhưng thực ra nếu hiểu đầy đủ về string thì mình nghĩ các bạn có thể làm ra được nhiều điều hay ho cho ứng dụng của các bạn nữa đấy. Hãy cùng nhau khám phá nào.

Giới Thiệu Về String Resource

String là một dạng resource khá cơ bản trong Android, chúng đơn giản chỉ là một chuỗi hoặc một danh sách nhiều chuỗi. Nhiệm vụ cơ bản của string là cung cấp text dựng sẵn cho các view, đó có thể là một nhãn của TextView, nhãn của Buttonhint cho EditText,… Sở dĩ mình dùng từ text dựng sẵn, là vì đây là các text được định nghĩa trước và gán vào các view khi thiết kế, để khi người dùng mở app lên thì tất cả các text này đã được các view sử dụng đến rồi. Nhưng nếu trong quá trình sử dụng ứng dụng, người dùng thêm vào các content, như là các ghi chú, hay các nhãn cho category trong TourNote, thì đó lại là các text khác không thuộc phạm vi quản lý của resource string nhé.

Như bạn đã thực hành ở Bài 8, resource string được để trong thư mục res/values/ (hoặc res/values-xxx/ cho alternative resource), và nằm trong file strings.xml.

Nơi chứa đựng resource String
Nơi chứa đựng resource String

Nếu click đúp vào file strings.xml này ra bạn sẽ thấy thẻ gốc cho file này (hay cho tất cả các file nào khác bên trong thư mục values/) là thẻ resources. Bên trong thẻ resources này là các thẻ string. Cụ thể các thẻ string này sẽ được định nghĩa và sử dụng như thế nào thì chúng ta cùng qua các phần ở bên dưới.

Các thẻ là những nơi được tô sáng bên trong resource
Các thẻ là những nơi được tô sáng bên trong resource

Sử Dụng Plain String

Plain String, mình không biết dịch ra tiếng Việt như thế nào, nên nói tạm rằng đây là “string không có định dạng, hay không có phong cách”. Nó đơn giản là một dạng cơ bản nhất của string, các text trong plain string chỉ đơn giản là các ký tự bình thường, hoặc ký tự “\n” để tách xuống một dòng mới. Bạn sẽ làm quen với các dạng nâng cao khác của string ở các mục tiếp theo bên dưới bài học.

Khai Báo Plain String

Mỗi một plain string được khai báo trong một thẻ string (1), và được đặt cho một cái tên theo sau thuộc tính name (2). Sau cùng là nội dung của plain string đó (3).

Cách khai báo một plain string
Cách khai báo một plain string

Truy Xuất Đến Plain String

Từ bất kỳ file XML nào, hay từ file AndroidManifest.XML, bạn có thể truy xuất đến plain string này bằng cách gọi @string/tên_string (như những gì bạn đã làm quen ở Bài 8).

Truy xuất đến plain string
Truy xuất đến plain string

Còn nếu từ Java source code, bạn có thể chỉ định đến tên_string này bằng cách gọi R.string.tên_string. Chẳng hạn hàm sau sẽ set text về cho một TextView ở Java code.

1
2
3
4
5
// Khai báo TextView chính là TextView từ XML resource, với id đã khai báo là activity_main_tv_empty
TextView textView = (TextView) findViewById(R.id.activity_main_tv_empty);
         
// Set plain text cho TextView
textView.setText(R.string.empty_note);

Sử Dụng Styled String

Ngược lại với plain string chỉ giúp hiển thị các ký tự rất đỗi bình thường. Thì styled string là một loại “string có phong cách”, nó giúp bạn tạo một string trông có phần điệu nghệ hơn, như in nghiêng, in đậm, hay có thể hiển thị các ký tự đặc biệt,…

Dưới đây là các cách sử dụng từng loại styled string mà bạn nên biết.

Hiển Thị Dấu (‘), Dấu (\) Và Dấu (“)

Điều đầu tiên với plain string, là bạn không thể khai báo ba ký tự đặc biệt này được. Ví dụ khai báo string như sau sẽ bị hệ thống báo lỗi. Hoặc nếu không báo lỗi như trường hợp sử dụng dấu nháy kép ở dòng thứ ba, thì cũng sẽ không thể hiển thị dấu nháy này lên màn hình được.

Hệ thống báo lỗi khi sử dụng các ký tực đặc biệt
Hệ thống báo lỗi khi sử dụng các ký tực đặc biệt

Khai Báo Các Dấu Đặc Biệt Này

Vậy khi bạn gặp ba ký tự đặc biệt trên, với cách giải quyết rất đơn giản, bạn hãy đặt thêm một ký tự gạch chéo ngược (\) ở trước mỗi ký tự đặc biệt này.

Cách khai báo các ký tự đặc biệt cho string
Cách khai báo các ký tự đặc biệt cho string

Một ghi nhớ nhỏ, là với string chỉ có chứa một loại ký tự (‘), thì bạn còn có một cách khác là sử dụng dấu nháy kép bao hết string này, khi đó bạn không cần dùng đến ký tự (\) nữa.

Mẹo dùng dấu nháy kép trong string
Mẹo dùng dấu nháy kép trong string

Truy Xuất Đến Các String Đặc Biệt Này

Chúng ta vẫn truy xuất đến các string này thông qua XML hay Java code giống như với plain string vậy.

Hiển Thị Các Ký Tự (&), (<), (>), (…)

Ngoài ba dấu đặc biệt đã nói ở trên, string XML của Android còn có các ký tự đặc biệt nữa mà bạn không thể dùng trực tiếp chúng được, bạn phải dùng code để hiển thị. Bạn xem ví dụ lỗi.

Hệ thống báo lỗi khi sử dụng các ký tực đặc biệt
Hệ thống báo lỗi khi sử dụng các ký tực đặc biệt

Trường hợp dòng thứ ba, với ký tự (…) thì hệ thống không báo lỗi gì đâu, và khi run ứng dụng thì ký tự này cũng hiển thị tốt, không như trường hợp của dấu (“) trên kia. Nhưng vì hệ thống có gợi ý không nên dùng thẳng ra như vậy, nên mình liệt kê nó vào danh sách phải thay thế này.

Khai Báo Các Ký Tự Đặc Biệt Này

Vậy tóm lại là bạn phải thay chúng bằng các mã như sau (không có khoảng trắng đằng sau các ký tự & nhé).

– & thay bằng & amp;
– < thay bằng & lt;
– > thay bằng & gt;
–  thay bằng & #8230;

Cách khai báo các ký tự đặc biệt cho string
Cách khai báo các ký tự đặc biệt cho string

Truy Xuất Đến Các String Đặc Biệt Này

Cũng tương tự như plain string.

Tạo Phong Cách Bằng Thẻ HTML

Sẽ có lúc bạn rất cần đến các tùy chỉnh in nghiêng, in đậm, hay gạch chân một vài ký tự nào đó trong một item string. Vậy lúc nào cần thì bạn hãy nhớ bài học hôm nay.

Cách Tạo Phong Cách Cho String Bằng Thẻ HTML

Với sự hỗ trợ của thẻ HTML, bạn chỉ có thể định dạng được ba phong cách cho string như sau.

  • in đậm text
  • in nghiêng text
  • gạch chân text

Bạn có thể thử nghiệm với string empty_note của TourNote như sau.

Cách tạo phong cách in đậm, nghiêng, gạch chân cho string
Cách tạo phong cách in đậm, nghiêng, gạch chân cho string

Truy Xuất Đến Các String Dùng Thẻ HTML Này

Nếu truy xuất đến string có dùng thẻ HTML này từ các file resource XML khác, thì bạn cứ dùng như plain string bằng cách gọi @string/tên_string.

Còn khi truy xuất từ file Java. Nếu bạn dùng hàm set text như ví dụ với plain string trên kia thì các thẻ HTML sẽ hoạt động bình thường. Nhưng nếu bạn dùng hàm này thì định dạng HTML sẽ không thấy hiển thị đâu nhé setText(getString(R.string.empty_note));, vì hàm getString() sẽ loại bỏ tất cả các thông tin liên quan đến thẻ HTML ra khỏi chuỗi mất rồi.

Bạn hãy tự thử nghiệm, thì màn hình TourNote sẽ trông như sau.

Kết quả TextView khi có gắn các text in đậm, nghiêng, gạch chân
Kết quả TextView khi có gắn các text in đậm, nghiêng, gạch chân

Tạo Phong Cách Bằng CDATA

Với việc dùng các thẻ HTML như trên để tạo phong cách cho string, bạn chỉ có thể làm cho vài ký tự nào đó in đậm, in nghiêng, hay gạch chân. Vậy câu hỏi là với các định dạng HTML khác thì sao, khi đó CDATA là một trong những câu trả lời cho câu hỏi này.

Tạo Phong Cách Cho String Bằng CDATA

Bạn vẫn khai báo như một plain string bình thường, chỉ có khác là ở nội dung của string, bạn phải bao bọc lấy string bằng một khai báo CDATA (1), bên trong CDATA là các định dạng theo HTML mà bạn biết (2).

Cách tạo một string CDATA
Cách tạo một string CDATA

Truy Xuất Đến String Dùng CDATA

Bạn không thể truy xuất đến string dạng này thông qua XML. Mà phải gọi thông qua hàm Html.fromHtml(getString(R.string.example_for_cdata));. Bạn nhìn code ví dụ sau.

1
2
3
4
5
// Khai báo TextView chính là TextView từ XML resource, với id đã khai báo là activity_main_tv_empty
TextView textView = (TextView) findViewById(R.id.activity_main_tv_empty);
 
// Set text có style là CDATA cho TextView
textView.setText(Html.fromHtml(getString(R.string.example_for_cdata)));

Khi bạn chạy lại TourNote thì có thể thấy nội dung text được định dạng khá đẹp như này.

Kết quả TextView khi có gắn các text theo định dạng CDATA
Kết quả TextView khi có gắn các text theo định dạng CDATA

Tạo Phong Cách Bằng SpannableString

Uhm, có thể có bạn không biết cách dùng thẻ HTML. Hoặc có thể bạn không thích định dạng string thông qua các thẻ HTML này. Vậy thì có một cách khác thay thế HTML hoàn hảo nhưng cũng rất mạnh mẽ, đó là SpannableString.

Cách Tạo Phong Cách Cho String Bằng SpannableString

Như bạn thấy, SpannableString là một Java class, vậy chắc chắn chúng ta phải tương tác với Java code rồi. Nhưng trước tiên bạn có thể định nghĩa một string trong XML trước cũng được. Như ví dụ mình khai báo một string sau.

Bước đầu tiên là khai báo string bình thường
Bước đầu tiên là khai báo string bình thường

Bạn nhớ rằng khi khai báo một string dài trong XML, việc bạn tự nhấn enter xuống hàng chỉ là để bạn quản lý nội dung string rõ ràng hơn, khi run ứng dụng, thì các enter đó sẽ không được thể hiện thành các xuống hàng thực sự, như những xuống hàng ở hình trên đây. Nếu bạn muốn xuống hàng thực sự khi run ứng dụng, thì hãy dùng ký tự “\n”, ký tự này cũng xuất hiện ở hình trên đấy nhé.

Và với SpannableString thì mình muốn đoạn XML như trên sẽ được chỉnh sửa như sau.

  • String (1) sẽ có màu đỏ.
  • String (2) sẽ có font lớn gấp 2 lần các string xung quanh.
  • String (3) khi click vào sẽ hiển thị một thông báo dạng Toast.
  • String (4) khi click vào sẽ dẫn đến trang chủ của Yellow Code Books.
String sẽ được định dạng như hình này
String sẽ được định dạng như hình này

Bạn tham khảo code sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Khai báo TextView chính là TextView từ XML resource, với id đã khai báo là activity_main_tv_empty
TextView textView = (TextView) findViewById(R.id.activity_main_tv_empty);
 
// Khai báo SpannableString với text lấy từ string resource
CharSequence exampleForSpannable = getText(R.string.example_for_spannable);
SpannableString spannableString = new SpannableString(exampleForSpannable);
 
// Set màu đỏ cho string (1). Vị trí từ 0 đến 30
spannableString.setSpan(new ForegroundColorSpan(Color.RED), 0, 30, 0);
 
// Làm cho string (2) bự gấp 2 lần. Vị trí là ký tự 84
spannableString.setSpan(new RelativeSizeSpan(2), 84, 85, 0);
 
// Làm cho string (3) có thể click được, đồng thời có màu xanh, khi click vào hiển thị Toast. Vị trí từ 128 đến 150
ClickableSpan clickableSpan = new ClickableSpan() {
    @Override
    public void onClick(View widget) {
        Toast.makeText(MainActivity.this, "Will be implemented later...", Toast.LENGTH_SHORT).show();
    }
};
spannableString.setSpan(clickableSpan, 128, 150, 0);
spannableString.setSpan(new ForegroundColorSpan(Color.BLUE), 128, 150, 0);
 
// Làm chi string (4) có thể click được, đồng thời có màu đen, bự gấp rưỡi, khi click vào thì đến link. Vị trí từ 178 đến hết string
spannableString.setSpan(new URLSpan("https://yellowcodebooks.com/"), 178, exampleForSpannable.length(), 0);
spannableString.setSpan(new ForegroundColorSpan(Color.BLACK), 178, exampleForSpannable.length(), 0);
spannableString.setSpan(new RelativeSizeSpan(1.5f), 178, exampleForSpannable.length(), 0);
 
// Đoạn này làm cho ClickableSpans và URLSpans có thể hoạt động
textView.setMovementMethod(LinkMovementMethod.getInstance());
 
// Set spannable text vào cho TextView
textView.setText(spannableString);

Để cho string hiển thị thêm đẹp hơn, thì bạn có thể kết hợp cả SpannableString lẫn các thẻ HTML như đã nói ở trên vào khai báo string nhé. Bạn hãy tự thử nghiệm xem.

Xong, chúng ta vừa kết thúc làm quen với resource kế tiếp của Android, resource kiểu string. Tuy nhiên các cách sử dụng string trên đây vẫn chưa phải là tất cả những thú vị mà Android mang lại, chúng ta vẫn còn một số cách hiển thị string nữa, như string có định dạng, string array, hay string theo số lượng sẽ được mình nói đến ở bài kế tiếp nhé.

Tiếp Tục Nói Về Sử Dụng String

Các bạn có thể thấy rằng, với một chủ đề đơn giản là tập trung vào một loại resource bình thường thôi, vậy mà chúng ta cần đến hai bài học mới có thể nói đủ về nó. Thật ra thì khi nhìn vào resource này, chúng ta ngỡ là nó đơn giản. Nhưng bạn nên nhớ rằng, string cũng là một dạng resource rất quan trọng, nó vừa giúp ứng dụng chuyền tải được nội dung của từng view đến với người dùng, hướng dẫn người dùng cách sử dụng ứng dụng, nó còn có thể giúp làm ứng dụng trông đẹp hơn nữa.

Và bài học hôm nay chúng ta cùng đi đến các loại string còn lại mà bài hôm trước còn đang nói dở dang.

Sử Dụng Formatting String

Nếu như bài hôm trước các bạn đã làm quen đến styled string, đó là các cách giúp bạn hiển thị string trông đẹp hơn nhờ có các phong cách kèm theo, như màu sắc, độ đậm, độ lớn,… Thì hôm nay chúng ta làm quen với một dạng string khác, mình tạm gọi rằng đây là một “string được định dạng”.

Nghe qua thì mông lung tí, thực chất loại string này giúp bạn tạo ra các định dạng bằng các text giữ chỗ. Mục đích của các text này là giúp cho bạn linh động hơn trong việc thiết kế một string. Ví dụ như, khi bạn không biết phải hiển thị gì khi khai báo string này ở XML, vì bạn cần phải để chương trình chạy lên thì mới biết thông tin của string cần hiển thị, thì một text giữ chỗ như vậy là khá quan trọng.

Khai Báo Các Formatting String Này

Việc khai báo formatting string rất giống với plain string hay styled string. Nhưng nội dung của formatting string thì hơi khác một chút, nội dung được hỗ trợ thêm các định dạng trong đó, bạn hãy xem những ví dụ về định dạng được mình tô sáng như sau.

Ví dụ về các formatting string
Ví dụ về các formatting string

Những định dạng trên được tuân thủ theo cú pháp.

1
%[thứ_tự_xuất_hiện$]kiểu_định_dạng

Trong đó:

  • % –  Biểu thị rằng bạn đang định dạng một string. Nếu chỉ có % thôi mà không có các thành phần kèm theo như cú pháp trên, thì % này vẫn sẽ hiển thị bình thường là một ký tự.
  • thứ_tự_xuất_hiện – Thứ tự này sẽ làm việc chặt chẽ với Java code mà bạn sẽ làm quen ngay sau đây.
  • $ – Nếu có khai báo thứ_tự_xuất_hiện, thì ngay sau nó phải là ký hiệu $ này. Bạn nên nhớ giữa chúng không có khoảng trắng nào cả đâu nhé. Và vì chúng đang nằm trong cặp ngoặc vuông, có nghĩa là chúng có thể không cần hiển thị nếu bạn chỉ có một định dạng trong string mà thôi.
  • kiểu_định_dạng – Tùy vào bạn muốn Java code sẽ điền vào đây kiểu dữ liệu nào. Chúng ta có các kiểu định dạng sau.
    • s – Dùng để thay thế bằng một chuỗi
    • d – Dùng để thay thế bằng một số nguyên
    • f – Dùng để thay thế bằng một số thực

Ví Dụ Khai Báo Formatting String

Ví dụ này chính là hình mình đã chụp ở trên kia nhé. Trong hình đó, các phần tô sáng %1$s%2$d và %3$f chính là các định dạng. Mình cá là các bạn đã hiểu rõ gần hết, ngoại trừ các thứ_tự_xuất_hiện 123 là gì đúng không nào. Vậy chúng ta cùng xem qua cách sử dụng sau.

Truy Xuất Đến Formatting String

Như bạn biết, mục đích của việc sử dụng định dạng cho string, là vì chúng ta muốn string linh động trong quá trình thực thi ứng dụng. Do đó sự linh động này chỉ có thể được sử dụng thông qua Java code mà thôi.

Bạn hãy xem cách sử dụng string đã được định dạng, và chú ý đặc biệt vào hàm getString() được mình tô sáng ở code dưới đây.

1
2
3
4
5
6
7
8
9
10
// Khai báo TextView chính là TextView từ XML resource, với id đã khai báo là activity_main_tv_empty
TextView textView = (TextView) findViewById(R.id.activity_main_tv_empty);
 
// Lấy ngày tháng năm hiện tại từ hệ thống, hiển thị theo dạng dd/mmm/yyyy
Calendar c = Calendar.getInstance();
SimpleDateFormat df = new SimpleDateFormat("dd/MMM/yyyy");
String currentDate = df.format(c.getTime());
 
// Set text có các tham số truyền vào tương đương các định dạng %1$s, %2$d, %3$f trong XML
textView.setText(getString(R.string.example_for_formatting_string, currentDate, 0, 3.5f));

Bạn thấy rằng nếu bỏ qua tham số thứ nhất của hàm là R.string.example_for_formatting_string, tham số này giúp load string từ XML resource lên. Thì ba tham số còn lại là currentDate0, và 3.5f chúng sẽ khớp với định dạng %1$s%2$d và %3$f trong XML trước đó, cả về thứ tự lẫn kiểu dữ liệu của định dạng. Bạn đã hiểu rồi đúng không nào. Và do đó bạn hoàn toàn có thể định nghĩa thêm %4$x%5$x%6$x,… trong XML, rồi tăng thêm tham số truyền vào tương ứng cho hàm getString(), vậy là xong!

Như mình cũng đã nói, là nếu bạn chỉ có duy nhất một định dạng cho string, thì bạn không cần đến thứ_tự_xuất_hiện của nó, khi đó chỉ cần %s, hoặc %d, hoặc %f là được.

Sử Dụng String Theo Số Lượng

Đây là một dạng hỗ trợ đặc biệt của string. Bạn có nhớ rằng với ngôn ngữ tiếng Anh, sẽ có sự khác biệt khi hiển thị một từ theo sau số lượng? Chẳng hạn, với ví dụ trên đây, nếu bạn định nghĩa string trong XML là “You have %d note now”, thì chắc chắn sẽ có trường hợp ngữ pháp tiếng Anh bị sai, nếu như tham số truyền vào cho %d là một số lớn hơn 1, khi đó string sẽ hiển thị chẳng hạn như “4 note”. Sai rồi, phải là “4 notes.

Trước đây mình không áp dụng string theo số lượng này, mình định nghĩa hai string khác biệt trong XML, rồi if ở Java code để biết khi nào số ít thì load string số ít, khi nào số nhiều thì load string số nhiều. Đó cũng là một cách, nhưng với string theo số lượng của bài học hôm nay, thì Android hỗ trợ chúng ta một cách khác hiệu quả hơn, có thể phục vụ cho nhiều ngôn ngữ khác nhau.

Khai Báo String Theo Số Lượng

Có một chút khác biệt so với các string mà chúng ta đã làm quen trước đó, bạn hãy nhìn hình bên dưới đây, string theo số lượng lần này được định nghĩa bằng thẻ plurals (1) thay vì thẻ string. Tên cho thẻ plurals này vẫn được để trong thuộc tính name (2). Sau cùng, “nội dung” của plurals là các thẻ item (3) khác, mỗi thẻ item như vậy được định nghĩa một quantity (4) cho biết khi nào nên load nội dung (5) của item đó.

Chi tiết tất cả các quantity có thể có, các bạn hãy tham khảo link này. Vì thực sự mình chưa hiểu hết ý nghĩa của tất cả các quantity, nên chưa dám chém trên đây, mong các bạn thông cảm. 🙂

Như ví dụ khai báo của hình dưới đây chúng ta có hai quantity là “one” và “other”. Hai quantity này có nghĩa là, nếu hệ thống biết string này biểu diễn cho số ít, thì quantity “one” được load, ngược lại nếu string này biểu diễn cho số nhiều, thì quantity “other” được load.

Phân chia trường hợp số ít, số nhiều trong tiếng Anh
Phân chia trường hợp số ít, số nhiều trong tiếng Anh

Còn với resource string tiếng Việt, mình cũng phải khai báo thẻ plurals với name như bên resource tiếng Anh này. Nhưng vì tiếng Việt không phân biệt số ít số nhiều, nên chúng ta chỉ cần một quantity “other” là đủ.

Không phân chia số ít, số nhiều trong tiếng Việt
Không phân chia số ít, số nhiều trong tiếng Việt

Truy Xuất Đến String Theo Số Lượng

Cũng giống formatting string, string theo số lượng này chỉ dùng ở Java code. Khi đó chúng ta phải nhờ đến hàm getResources().getQuantityString(). Bạn xem code sau.

1
2
3
4
5
6
7
8
// Khai báo TextView chính là TextView từ XML resource, với id đã khai báo là activity_main_tv_empty
TextView textView = (TextView) findViewById(R.id.activity_main_tv_empty);
 
// Giả lập số lượng note, bạn thử thay đổi để xem từng trường hợp nhé
int numOfNotes = 2;
 
// Set text cho TextView, với string truyền vào là R.plurals.example_for_quantity_string
textView.setText(getResources().getQuantityString(R.plurals.example_for_quantity_string, numOfNotes, numOfNotes));

Giờ mình sẽ tập trung giải thích các tham số truyền vào hàm getQuantityString().

  • R.plurals.example_for_quantity_string – Bạn thấy tham số này vẫn bắt đầu bằng R như những tham số cho các hàm liên quan đến string, nhưng thay vì R.string thì lần này lại là R.plurals, vì bạn thấy chúng tương tự như các định nghĩa ở thẻ gốc trong XML đúng không nào.
  • numOfNotes thứ nhất – Tham số này sẽ giúp hệ thống biết string bạn đang ở số ít hay số nhiều.
  • numOfNotes thứ hai – Tham số này dùng cho định dạng %d bên trong mỗi item thôi. Nếu bạn không dùng đến formatting string trong item thì không cần tham số này. Hoặc nếu bạn định nghĩa nhiều formatting như %1$x%2$x thì bạn cứ tăng tham số truyền vào như là cách dùng formatting bên trên vậy.

Mình làm vài thử nghiệm thay thế các giá trị cho numOfNotes, bạn xem mình chạy lên thiết bị và chụp màn hình ở cả hai ngôn ngữ Anh và Việt như sau.

Sử Dụng String Array

Như tên gọi của nó cũng đủ nói lên tất cả. String array giúp bạn khai báo mảng các string ngay trong XML, và rồi sẽ được tự động load lên ở dạng mảng các chuỗi khi bạn thao tác với Java code.

Khai Báo String Array

String Array không dùng thẻ string, không dùng thẻ plurals như các bạn đã biết, mà dùng thẻ string-array (1). Tên cho thẻ này vẫn được để trong thuộc tính name (2). Từng phần tử mảng sẽ được định nghĩa ở mỗi thẻ item (3), mỗi thẻ item như vậy chứa một nội dung (4) của phần tử mảng đó.

Khai báo string array
Khai báo string array

Truy Xuất Đến String Array

Lại một lần nữa, chúng ta chỉ có thể gọi đến string array qua Java code, với hàm getResources().getStringArray(), hàm này sẽ trả về kết quả là mảng String[]. Tham số truyền vào cho getStringArray() là R.array.tên_string_array. Bạn xem ví dụ.

1
2
3
4
5
// Khai báo TextView chính là TextView từ XML resource, với id đã khai báo là activity_main_tv_empty
TextView textView = (TextView) findViewById(R.id.activity_main_tv_empty);
 
// Mảng String[] sẽ chứa danh sách các string được load lên từ string-array
String[] stringArray = getResources().getStringArray(R.array.example_for_string_array);

Thực Hành Hoàn Thiện Resource String Cho TourNote

Bài thực hành hôm nay hoàn toàn mở. Đó là mình sẽ để cho bạn tự tạo toàn bộ resource string cho ứng dụng TourNote. Tất nhiên mình cũng sẽ xây dựng bộ resource cùng với bạn và để trên GitHub để bạn tham khảo. Và mình cũng sẽ đưa toàn bộ screenshot về string resource của TourNote ra đây để bạn quyết định những nội dung, style, format có thể có cho resource của bạn. Nhớ là phải có đầy đủ ngôn ngữ cho cả tiếng Anh và Việt nhé.

Như vậy là từ đây về sau mình sẽ sử dụng các string resource đã được định nghĩa ở bài hôm nay ra dùng. Nếu có sửa đổi hoặc thêm mới resource string thì mình sẽ thông báo.

Đây là tất cả những thiết kế cho TourNote đến giai đoạn này.

Toàn bộ thiết kế của TourNote
Toàn bộ thiết kế của TourNote

Với các màn hình như trên thì mình tạo ra bộ resource cho tiếng Anh và tiếng Việt như hình chụp sau. Chú ý là mình có thay đổi tên string empty_note thành mainscreen_empty_note cho nó đồng nhất. Tất cả code từ hình ảnh này đều đã có trên GitHub cả rồi nhé.

Sử Dụng Color

Bài học hôm nay chúng ta sẽ thử tài vừa là một lập trình viên vừa là một nhà thiết kế, cùng nhau thảo luận về việc sử dụng và phối hợp các màu sắc, sao cho khéo léo nhất có thể, để tạo ra một ứng dụng đẹp lung linh mà không chói lóa.

Bạn cũng nên biết rằng, việc khó khăn nhất khi phát triển một ứng dụng, không phải là viết code đâu, mà chính là định nghĩa ra UI/UX của sản phẩm, hay nói cách khác là thiết kế sản phẩm, trong đó việc chọn lựa màu sắc là cực kỳ quan trọng. Bạn nên chọn lựa màu nào là màu chủ đạo, màu nào làm điểm nhấn, màu nào làm nền,… sao cho chúng hòa hợp, không bị quá chói, cũng không bị quá chìm. Nếu may mắn bạn có những người bạn biết thiết kế, hoặc bạn làm trong một công ty chuyên nghiệp có đội ngũ thiết kế UI/UX riêng, thì mọi thứ lại trở nên đơn giản. Nhưng nếu bạn là một nhà lập trình tự do, mọi sản phẩm đều do chính đôi tay của bạn làm ra từ a đến z, hoặc bạn là một freelancer làm cùng với đội ngũ thiết kế không được đông đảo và mạnh mẽ lắm. Thì bài học hôm nay sẽ giúp ích cho các bạn rất nhiều, chí ít là về phần màu sắc của sản phẩm.

Làm Quen Với Việc Hiển Thị Màu Sắc Trên Thiết Bị

Mục này mình dành để nói cho những bạn còn mù màu… à không, ý mình là các bạn mới làm quen với việc tổ chức màu sắc trong các thiết bị hiển thị hình ảnh ngày nay, và cách mà ngôn ngữ Android ứng dụng nó vào trong việc định nghĩa ra mã màu cho chúng ta sử dụng. Bạn nào hiểu rõ rồi thì cho qua nhé.

Làm Quen Hệ Màu RGB

Nếu như ngoài thực tế, khi bạn tham gia vào lớp học vẽ, bạn sẽ được làm quen với rất nhiều màu chuẩn khác nhau, pha trộn chúng lại theo một tỉ lệ nào đó bạn sẽ có được một số màu sắc mới, và nếu trộn tất cả các màu đó lại (trừ màu trắng) bạn sẽ có được một màu đen (hay đại loại vậy).

Còn trong các thiết bị hiển thị hình ảnh bây giờ, do đặc điểm là dùng ánh sáng để làm nổi bật các điểm ảnh. Mỗi điểm ảnh chứa đựng ba màu đặc trưng, đó là R (Red, Đỏ)G (Green, Xanh lá) và B (Blue, Xanh dương). Ba màu đặc trưng này được hệ thống máy tính điều khiển, từ việc tắt/sáng cho đến các cường độ sáng khác nhau. Cuối cùng thì tập hợp ánh sáng từ ba màu đặc trưng của điểm ảnh này lại sẽ cho ra các màu sắc mà chúng ta mong muốn ở các điểm ảnh đó.

Tuy nhiên bạn cũng không cần thiết phải hiểu nhiều về cấu tạo và cách thức hoạt động này của các điểm ảnh. Bạn chỉ cần biết rằng, dựa trên cấu trúc vật lý đó, mà chúng ta cũng vẫn sẽ điều khiển ba màu đặc trưng của điểm ảnh đã nói ở trên. Và vì vậy người ta còn gọi cách hiển thị màu theo mô hình này là kiểu màu RGB.

rgb

Và bởi cấu tạo của màu là các điểm sáng, nên khi trộn chúng vào nhau bạn sẽ có một quy tắc pha màu hết sức ngược với ngày xưa bạn từng học. Chẳng hạn như, Đỏ với Xanh lá lại cho ra màu Vàng. Còn nữa, nếu trộn hết cả ba màu đặc trưng của điểm ảnh này lại với một cường độ sáng lớn nhất, chúng ta sẽ thu được một màu trắng (bạn xem màu ở giao của cả ba hình tròn như sau).

rgb_mixed

Cuối cùng thì ngoài việc pha trộn ba màu chuẩn trên đây, chúng ta còn có thể tăng giảm cường độ sáng của từng màu, để rồi tạo ra rất rất nhiều màu sắc khác nhau cho ứng dụng.

Làm Quen Hệ Màu ARGB

Đây không phải là một hệ màu mới, mà chỉ là một dạng mở rộng hơn của RGB, nhưng khi này hệ thống cho phép chúng ta điều khiển được Độ trong suốt của RGB thông qua giá trị A, viết tắt cho chữ Alpha. Lát nữa đi vào chi tiết chúng ta sẽ xem xét cách định nghĩa một màu RGB hoặc ARGB này.

Biểu Diễn Màu Sắc Trong Android

Chúng ta khoan hãy nói đến cách khai báo và sử dụng màu sắc trong file resouce, mà hãy nói đến cách để biểu diễn màu sắc này trước nhé. Vì mình biết nhiều bạn còn bỡ ngỡ với cách sử dụng màu sắc lắm. Chúng ta cùng điểm qua các ý sau đây.

  • Đầu tiên, để hệ thống biết bạn đang biểu diễn màu sắc, thì bạn phải gõ ký tự # vào trước.
  • Sau đó, với kiểu màu là RGB hay ARGB thì số lượng ký tự đằng sau # sẽ tương ứng. Cụ thể, với mỗi loại A, R, G, hay B bạn dùng một giá trị Hexa để biểu diễn độ lớn, giá trị này được biểu diễn bằng hai ký tự, từ 00 đến FF, độ lớn này là cường độ sáng của mỗi màu đặc trưng (hoặc độ lớn của giá trị Alpha) trong một điểm ảnh. Trong đó 00 là tối nhất (nhỏ nhất) và FF là sáng nhất (lớn nhất). Như vậy theo sau ký tự # bạn sẽ phải biểu diễn bởi 6 hay 8 ký tự, tùy theo đó là RGB (sẽ được biểu diễn là #RRGGBB) hay ARGB (sẽ được biểu diễn là #AARRGGBB). Màu sắc cuối cùng của điểm ảnh sẽ là sự pha trộn của các thành phần A R G B này. Bạn hiểu rồi đúng không nào. Mình ví dụ một số mã màu sau.
Ví dụ về một số cách biểu diễn mã màu trong Android
Ví dụ về một số cách biểu diễn mã màu trong Android

Ngoài các ví dụ trên thì như mình nói, nếu ba màu R G B được tăng sáng hết cỡ trong một điểm ảnh sẽ tạo ra màu trắng, vậy thì nó là mã #FFFFFF. Ngược lại, nếu ba màu R G B được giảm tối hết cỡ trong một điểm ảnh sẽ tạo ra màu đen, đó là #000000.

Thêm: ngoài các cách biểu diễn 6 hay 8 ký tự ra, thì bạn có thể biểu diễn thành dạng rút ngắn với 3 hay 4 ký tự. Điều này xảy ra nếu như biểu diễn Hexa của tất cả các A, R, G, B đều là các cặp ký tự. Mình ví dụ với ba mã màu đầu tiên trên kia, bạn hoàn toàn có thể viết #F00#0F0 hay #00F đều được nhé. Hoặc nếu với mã màu này #DD880055 có thể biểu diễn bằng #D805 được. Nhưng mã #DD881255 không thể biểu diễn bằng #D8125 được đâu nhé.

Giới Thiệu Color Resource

Chúng ta bắt đầu bước vào phần chính của bài học hôm nay. Chắc chắn bạn đã biết vai trò to lớn của loại resource này rồi, mình không nói chi cho dài dòng. Điều bạn quan tâm, đó là resource color được để trong thư mục res/values/ (hoặc res/values-xxx/ cho alternative resource), giống như việc tổ chức thư mục với resource string vậy. Nhưng file chứa đựng các mã màu trong Android lại chính là file colors.xml.

Đường dẫn chứa đựng file màu sắc trong Android
Đường dẫn chứa đựng file màu sắc trong Android

Nếu click đúp vào file colors.xml này để mở nó lên, bạn sẽ thấy thẻ gốc cho file (cũng như cho tất cả các file nào khác bên trong thư mục values/ mà bạn biết) là thẻ resources. Bên trong thẻ resources này là các thẻ color.

Cấu trúc các định nghĩa màu sắc
Cấu trúc các định nghĩa màu sắc

Sử Dụng Color Resource

Không giống như với resource string định nghĩa ra khá nhiều cách sử dụng khác nhau, nào là plain string, styled string, formatting string,… thì với resource dạng màu sắc này, bạn chỉ có một cách sử dụng mà thôi. Cách dùng mở rộng color-state-list chỉ là cách kết hợp nhiều màu sắc vào một hiệu ứng nào đó sẽ được mình nói riêng ở bài kế tiếp.

Khai Báo Màu Sắc

Mỗi một màu sắc được khai báo trong một thẻ color (1), và được đặt một cái tên theo sau thuộc tính name (2)Mã màu (3) được thiết lập sau ký tự # (một lần nữa mình nhắc là bạn nên nhớ phải có ký tự này nhé), cách thiết lập mã màu đã được mình trình bày ở mục trên rồi. Ngoài ra thì editor của Android Studio còn có chức năng hiển thị màu thật (4), giúp chúng ta kiểm chứng xem đã khai báo đúng màu, hay đúng mã màu mong muốn hay chưa, bạn thử click vào một trong các màu thật này xem, sẽ có một chút bất ngờ mà Android Studio mang lại. Tuyệt vời đúng không bạn.

Cấu trúc các định nghĩa màu sắc
Cấu trúc các định nghĩa màu sắc

Truy Xuất Đến Màu Sắc

Cũng như với plain string, bạn có hai cách truy xuất đến các màu sắc đã định nghĩa trong file colors.xml, đó là truy xuất từ file XML và từ Java code.

Truy Xuất Từ file XML

Từ bất kỳ file XML nào, nếu bạn muốn dùng đến resource color này, chẳng hạn như set màu chữ cho TextView, set màu nền cho Button,… thì bạn hãy gọi đến color đã định nghĩa thông qua @color/tên_color.

Chúng ta hãy làm bài thực hành sau về việc khởi tạo và truy xuất đến màu sắc từ XML, đó là file Theme. Sau này sẽ có rất nhiều trường hợp khác phải sử dụng đến màu sắc, khi đó bạn cứ tham khảo bài học hôm nay nhé.

Thực Hành Truy Xuất Đến Màu Sắc Từ File XML

Tốt rồi. Giờ chúng ta chính thức đến với bài thực hành. Đầu tiên bạn hãy mở file styles.xml mà chúng ta từng thực hành ở bài 14 lên. Khi này bạn đang định nghĩa các màu sắc sai chỗ. Bạn xem.

Các màu sắc mà bạn đã định nghĩa từ bài 14
Các màu sắc mà bạn đã định nghĩa từ bài 14

Resource string phải để trong strings.xml, thì resource color bạn cũng phải để trong colors.xml. Do đó nên một styles.xml có định nghĩa màu sắc như vậy là chưa đúng nhé. Vậy chúng ta sẽ chỉnh lại cho TourNote.

Giờ thì bạn hãy mở file colors.xml, và sửa lại các màu bên trong file này cho đúng (hiện tại các màu sắc ở đây được định nghĩa tự động ban đầu theo theme Material, và ở bài thực hành ở bài học số 14 chúng ta đã định nghĩa lại bộ màu này ở Theme như hình trên, bây giờ chúng ta chỉ cần thay đổi lại cho colors.xml cho đúng mà thôi, mình nói dài dòng tí để đảm bảo chúng ta hiểu ý nhau).

Và file colors.xml sau khi chỉnh sửa sẽ như sau.

Định nghĩa lại màu cho TourNote trong file colors.xml
Định nghĩa lại màu cho TourNote trong file colors.xml

Cuối cùng, chúng ta quay lại styles.xml để truy xuất đến các màu sắc này. Và file styles.xml sẽ như sau.

Định nghĩa lại các màu sắc đã khai báo
Định nghĩa lại các màu sắc đã khai báo

Mọi thứ ở bài thực hành hôm nay sẽ không làm cho TourNote khác đi chút giao diện nào. Chỉ là giúp cho code được chuyên nghiệp hơn thôi.

Truy Xuất Từ Java Code

Với Java Code, cũng có rất nhiều trường hợp sử dụng đến màu sắc, như set màu chữ, màu nền các kiểu. Nhưng dù cho bất kỳ lúc nào bạn cần đến việc sử dụng màu sắc này, bạn phải gọi hàm getColor(), bạn truyền vào hàm này màu đã định nghĩa R.color.tên_color, sau đó hàm sẽ trả về một color kiểu int, color kiểu int này sẽ được dùng trong một số hàm cần đến màu sắc, chẳng hạn như setTextColor()setBackgroundColor(),…

1
2
3
4
5
Resources res = getResources();
int color = res.getColor(R.color.colorPrimary);
 
TextView textView = findViewById(R.id.activity_main_tv_empty);
textView.setTextColor(color);

Chọn Lựa Màu Sắc Trong Android

Mục này mình mạn phép đi ra khỏi khuôn khổ của một lập trình viên Andoid, bởi vì nó không còn là kiến thức lập trình nữa, nó nói về cách thức phối màu để tạo nên một giao diện hợp lý cho ứng dụng. Như mình đã nói, nếu bạn đã có người lo phần phối màu cho ứng dụng thì mọi thứ đều ok, nhưng nếu bạn bị rơi vào tình huống buộc phải làm một ứng dụng từ a đến z, từ khâu thiết kế đến khi xuất xưởng sản phẩm, thì khổ đời bạn rồi. Nếu lựa chọn màu sắc không khéo, ứng dụng của bạn trông như một rạp xiếc với vô vàn màu sắc, hoặc nếu không thì có thể sẽ rơi vào tình trạng buồn tẻ với các màu nhạt. Như với hai em này.

Bạn cũng đã nhìn thấy bộ giao diện TourNote ở bài trước rồi đó. Để chọn lựa được một bộ màu sắc như vậy, mình cũng phải mất khá nhiều thời gian với nó. Và sau đây là một vài kinh nghiệm của mình trong việc chọn lựa màu sắc, có thể kinh nghiệm này chưa thật sự xuất sắc vì mình cũng không phải là một designer, tuy nhiên mình tự tin rằng nó đủ tốt để có thể giúp bạn thiết kế ra các sản phẩm cho riêng bạn, hoặc cho nhóm khởi nghiệp của bạn.

Đầu tiên bạn có thể xem qua tất cả các gợi ý về phối màu, cũng như có một bảng với 500 màu đã được Google nghiên cứu và tổng hợp lại để bạn có thể tham khảo những khi cần thiết. Ở đây.

Cũng từ các gợi ý ở trang web trên, mình tóm tắt một số ý chính khi bạn bắt tay vào thiết kế ứng dụng. Trước hết, bạn phải chọn một màu sắc chủ đạo cho ứng dụng, màu này có thể là màu của thương hiệu sản phẩm, hoặc không thì là màu mà bạn thích nằm trong bảng màu ở link trên. Từ một màu chủ đạo, bạn có thể tham khảo bảng màu để tìm ra các màu nhạt hơn và đậm hơn (cũng thuộc tông màu chủ đạo đó) để phối cho nhiều thành phần khác nhau trong ứng dụng. Như các màu xanh lá ở ví dụ sau.

Ví dụ cho ứng dụng dùng màu chủ đạo là xanh lá
Ví dụ cho ứng dụng dùng màu chủ đạo là xanh lá

Nếu bạn thích sự đơn giản, thì màu chủ đạo cũng đủ để xây dựng một sản phẩm. Tuy nhiên mình thích chọn thêm một màu nữa, người ta gọi là màu thứ cấp. Màu này thường không xuất hiện nhiều trong ứng dụng, vì nó là thứ cấp mà, nó có vai trò làm điểm nhấn giúp giao diện đỡ buồn tẻ hơn. Màu thứ cấp thường dùng cho các ButtonEditText, con nháy nhập liệu, Progress bar, các đường link, hay các dòng text cần sự nổi bật. Do có đặc điểm trên nên màu thứ cấp thường sẽ là màu chõi lại với màu chủ đạo. Nếu như màu chủ đạo là tông màu lạnh, thì màu thứ cấp sẽ là tông màu nóng, và ngược lại. Kiểu như, trong âm có dương, trong dương có âm vậy. Bạn có thể xem ví dụ sau, màu tím là màu chủ đạo, trong khi màu xanh lá là màu thứ cấp.

Ví dụ một ứng dụng phối hai màu chủ đạo và thứ cấp với nhau
Ví dụ một ứng dụng phối hai màu chủ đạo và thứ cấp với nhau

Ngoài hai màu trên, bạn còn phải chọn ra các màu xám dùng cho các mục đích trung tính, như màu nền của một số cửa sổ. Rồi bạn lại phải chọn ra màu cho text nữa, thông thường là màu xám đậm. Ở link mà mình gửi bạn bên trên có nói rõ cách chọn màu cho text, nó có gợi ý rằng là nếu màu nền tối thì màu text nên là xám sáng, còn màu nền sáng thì màu text nên là xám tối. Bạn có thể điều chỉnh giá trị Alpha cho mỗi màu đã chọn, để có được một hiệu ứng bóng bẩy hơn cho người dùng.

Ý cuối cùng trong việc thiết kế. Đó là, khi bạn đã chọn ra các màu sắc ưng ý, bạn có thể dùng công cụ ở link này để kiểm tra lại một cách trực quan hơn, hoặc để có những tinh chỉnh cần thiết trước khi bắt tay vào code.

Chúng ta vừa xem qua cách sử dụng color trong Android. Và bài học hôm nay ngoài việc giúp bạn biết cách sử dụng loại resource thú vị này, nó còn giúp bạn biết cách thiết kế ra một ứng dụng với màu sắc hòa hợp hơn. Tuy nhiên, resource color vẫn còn một ứng dụng khá hay nữa và mình sẽ dành bài học hôm sau để nói về nó.

Sử Dụng Dimen

Hôm nay chúng ta tiếp tục nói về cách sử dụng một loại resource nữa của Android. Resource có cái tên hơi lạ – Dimen. Muốn biết nó thực chất là loại resource gì thì mời bạn cùng đọc qua bài viết hôm nay. Tuy nhiên mình xin tổng hợp lại, cho đến giờ phút này chúng ta đã nói qua cách sử dụng các resource sau đây.

  • Style
  • Theme
  • String
  • Và Color

Mình bật mí một chút về resource dimen này. Resource này liên quan đến các “số đo” bên trong ứng dụng, hay còn gọi là kích thước. Tuy dễ nhưng quan trọng, vì chúng có liên quan đến việc tạo ra một giao diện “động” (về kích thước) cho vô vàn các màn hình đang có mặt ngoài thị trường ngày nay.

Giới Thiệu Dimen Resource

Chúng ta cùng làm quen đến nơi “cất giấu” và cách khai báo loại resource này trước. Còn cách sử dụng cho từng dimen sẽ được mình giải thích cụ thể ở các phần sau của bài học.

Dimen, viết tắt của chữ Dimension, dịch ra là Kích thước. Vâng, resource này giúp bạn định nghĩa tất cả các kích thước của các view hoặc các thành phần khác trong ứng dụng, bao gồm cả font size, vào cùng một file resource để dễ quản lý. Đầu tiên bạn nên biết, đó là resource này được để trong thư mục res/values/ (hoặc res/values-xxx/ nếu dùng theo kiểu alternative resource), cùng thư mục với stylethemestring, và color. File chứa đựng các dimen này chính là file dimens.xml.

Nơi chứa đựng file resource dimen
Nơi chứa đựng file resource dimen

Khi bạn tạo mới project, thì hệ thống cũng đã để mặc định một vài thông số dimen trong file này rồi, bạn thử click đúp để mở file lên xem. Như bạn cũng biết, bất cứ file XML nào bên trong thư mục values/ cũng đều có thẻ gốc là resources. Bên trong thẻ resources này là các thẻ con dimen.

Cấu trúc file dimen
Cấu trúc file dimen

Sử Dụng Dimen Resource

Dimen cũng giống color, chỉ có một cách dùng cho từng trường hợp XML hay Java code. Tuy nhiên bạn nên chú ý một chút đến đơn vị đo lường, mình sẽ nói rõ từng đơn vị mà Android hỗ trợ, bên dưới mục khai báo dưới đây.

Khai Báo Dimen

Mỗi loại kích thước được khai báo trong một thẻ dimen (1), và được đặt một cái tên theo sau thuộc tính name (2)Độ lớn của kích thước được chỉ định ở (3), kích thước này có thể là số nguyên, cũng có thể là số thực. Nhưng bạn phải luôn nhớ đơn vị kèm theo kích thước, như trong hình bạn thấy đơn vị của các dimen đều là dp, bạn chỉ định đơn vị nào trong các đơn vị mà mình liệt kê dưới đây cũng được, tuy nhiên nếu không có chỉ định đơn vị cho dimen thì khi build, hệ thống sẽ báo lỗi đấy nhé.

Cấu trúc một thẻ dimen
Cấu trúc một thẻ dimen

Các Đơn Vị Mà Android Hỗ Trợ

Bạn vừa làm quen với việc khai báo một dimen, vậy thì các đơn vị mà Android hỗ trợ là những đơn vị nào.

  • dp – Density-independent Pixel – Mình đề nghị các bạn nên dùng đơn vị này cho tất cả các kích thước bên trong ứng dụng, kể cả cho padding và margin. Lý do cụ thể vì sao thì mình sẽ giải thích ở mục tiếp theo bên dưới bài học hôm nay.
  • sp – Scale-independent Pixel –  Thông số này tương tự như dp, nhưng dùng khi định nghĩa font size. Cụ thể của sp sẽ được mình nói rõ ở bài học về sử dụng font.
  • pt – Point – Một point được tính bằng 1/72 inch dựa trên loại màn hình 72 dpi. Mình chưa từng sử dụng đơn vị point này, chắc là nó sẽ giúp ích cho một số ứng dụng có làm việc với máy in, để đảm bảo kích thước thật trong qua trình thiết kế.
  • px – Pixel – Dựa trên đơn vị pixel vật lý của thiết bị. Như mình có nói rõ ở mục dưới đây, pixel không giúp bạn tạo được một giao diện nhất quán cho các tỉ lệ màn hình khác nhau ngoài thị trường. Do đó tốt nhất bạn nên chọn dp hoặc sp để thay thế.
  • mm – Milimet – Dựa trên đơn vị milimet thực tế. Mình cũng chưa từng sử dụng đơn vị này, có lẽ ứng dụng của nó cũng giống như với đơn vị pt.
  • in – Inch – Dựa trên đơn vị inch thực tế. Và mình cũng chưa từng sử dụng nốt.

Sử Dụng Dimen

Chúng ta cùng nhau điểm qua hai cách sử dụng đến dimen đã khai báo trên kia. Đó là, sử dụng ở XML và ở Java code.

Sử Dụng Ở File XML

Từ bất kỳ file XML nào, nếu bạn muốn dùng đến resource dimen này, nếu bạn đang ở tab Design, thì ở các field liên quan đến kích thước, bạn có thể nhấn vào nơi được khoanh tròn trong hình dưới để mở ra một cửa sổ liệt kê tất cả các dimen, bạn chỉ việc chọn lựa dimen đã khai báo sẵn mà thôi.

Nhấn vào những nơi như chỗ khoanh tròn, sẽ hiện ra cửa sổ với các dimen có sẵn
Nhấn vào những nơi như chỗ khoanh tròn, sẽ hiện ra cửa sổ với các dimen có sẵn

Còn nếu bạn đang ở tab Text, thì hãy dùng đến các dimen thông qua cú pháp @dimen/tên_dimen. Một lát nữa đến bài thực hành bạn sẽ hiểu rõ hơn cách sử dụng này.

Dử Dụng Ở Java Code

Nếu bạn muốn sử dụng đến các dimen đã khai báo ở Java code, thì hãy truyền R.dimen.tên_dimen vào hàm cần thiết.

Như ví dụ sau, mình sẽ thử set lại chiều dài và rộng của ảnh empty_note bằng giá trị mới activity_horizontal_margin đã khai báo trong dimen. Như vậy trong XML khai đã báo kích thước này là 60dp, nhưng khi ứng dụng thực thi, Java code set lại giá trị này thành 16dp.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        // R.id.imageView chính là id của ImageView bên file XML
        ImageView imageView = (ImageView) findViewById(R.id.imageView);
 
        // Set lại kích cỡ cho imageView
        imageView.getLayoutParams().width = getResources().getDimensionPixelSize(R.dimen.activity_horizontal_margin);
        imageView.getLayoutParams().height = getResources().getDimensionPixelSize(R.dimen.activity_horizontal_margin);
    }
}

Và kết quả thực thi lại chương trình.

Thực thi thử các dòng code sử dụng dimen ở Java code
Thực thi thử các dòng code sử dụng dimen ở Java code

Dùng pixel Hay dp?

Như trên đây, mình có nói qua rằng bạn đừng nên sử dụng đơn vị pixel để chỉ định các kích thước trong giao diện, mà hãy sử dụng dp hay sp. Kiến thức của phần này chẳng ở đâu xa, bạn vào trang developer của Android này sẽ có đầy đủ, mình chỉ tổng hợp lại vài ý quan trọng.

Thử Biểu Diễn Ảnh Bằng pixel

Mình tin chắc rằng hầu hết các bạn đều biết đến khái niệm pixel, đó là một điểm ảnh, một đơn vị ảnh vật lý được phát sáng bằng sự tập hợp của ba màu đặc trưng R-G-B mà mình có nhắc đến ở bài hôm trước.

Không thể phủ nhận rằng pixel là một đơn vị đo lường hữu dụng, không những trong thiết kế, mà với một số ngôn ngữ lập trình, tiêu chí sử dụng pixel để hiện thực giao diện cũng khá phổ biến. Thế nhưng, tại vì sao mà Android không khuyến khích bạn dùng pixel nữa? Chúng ta cùng xem ví dụ sau, ví dụ này mình hiển thị một ảnh có kích thước 160×160 pixel, nhưng khi thực thi ví dụ này lên ba thể loại màn hình sau, kích cỡ và tỉ lệ của chúng không đồng nhất.

Cùng một kích cỡ pixel, nhưng khi hiển thị ở 3 màn hình khác nhau sẽ cảm giác như chúng khác kích cỡ nhau
Cùng một kích cỡ pixel, nhưng khi hiển thị ở 3 màn hình khác nhau sẽ cảm giác như chúng khác kích cỡ nhau

Với cái cách mà một ảnh hiển thị trông khác nhau về tỉ lệ giữa các màn hình như thế này, sẽ làm bạn vô cùng khó khăn trong việc thiết kế các giao diện phức tạp khác đấy nhé.

Vậy chúng ta cùng xem qua tại sao có sự khác nhau về tỉ lệ của ảnh có cùng một kích cỡ như trên. Bạn biết không, ba màn hình mình cố tình dùng đến có “mật độ điểm ảnh” khác nhau, mật độ điểm ảnh này chính là số lượng pixel trên một inch màn hình, người ta gọi thông số này là PPI (Pixels Per Inch).

  • Với hình đầu tiên, màn hình Medium density này có 240 ppi. Như vậy một inch của nó chứa 240 pixel.
  • Màn hình thứ hai, High density, có 320 ppi. Một inch trên màn hình này chứa 320 pixel, vậy là nếu như hai loại màn hình đầu tiên này có thông số inch giống nhau, ví dụ 5.5 inch, thì màn hình High density sẽ có độ phân giải cao hơn và cho chất lượng ảnh “mịn” hơn Medium density.
  • Tương tự, Extra high density có 480 ppi. Nên sẽ có độ phân giải và khả năng hiển thị ảnh mịn hơn hai loại trên kia nếu cùng thông số inch.

Dựa vào số lượng pixel trên một inch, mời bạn tiếp tục nhìn lại cả ba màn hình, với ô vuông màu đỏ của mình biểu thị cho một inch. Medium density có 240 pixel trong một inch, nên ảnh 160 pixel sẽ như chiếm 2/3 inch. High density có 320 pixel trong một inch, do đó ảnh 160 pixel sẽ chiếm 1/2 inch. Cuối cùng, Extra high density có 480 pixel trong một inch, thì ảnh 160 pixel sẽ chiếm 1/3 inch.

Ô vuông đỏ chính là 1 inch vật lý
Ô vuông đỏ chính là 1 inch vật lý

Chuyển Sang Biểu Diễn Ảnh Bằng dp

Như vậy bạn đã biết rằng dùng pixel trong Android “thất bại” như thế nào thông qua ví dụ trên. Chúng ta hãy xem việc chuyển ảnh trên từ 160×160 pixel sang 160×160 dp xem như thế nào nhé. Kết quả như sau.

Biểu diễn lại kích cỡ của ảnh bằng dp
Biểu diễn lại kích cỡ của ảnh bằng dp

Bạn có thể thấy rằng, có thể tỉ lệ giữa ảnh và các khoảng trống còn lại ở từng màn hình vẫn chưa hoàn hảo, nhưng biểu diễn theo dp như thế này tốt hơn nhiều so với pixel rồi đúng không nào. Tại sao lại như vậy, chúng ta cùng xem cách mà Android hoạt động với dp nhé.

Đầu tiên, Android lấy một loại màn hình làm chuẩn, đó là màn hình 160 dpi. Với thể loại màn hình này thì 1 pixel tương đương với 1 dp. Vậy với các màn hình còn lại? Khi đó Android đưa ra công thức.

px = dp * (dpi / 160)

Nếu chúng ta bỏ qua sự khác biệt giữa ppi và dpi. Thì với màn hình Medium density ở ví dụ trên có 240 ppi, chúng ta khai báo ảnh 160 dp, vậy hệ thống sẽ áp dụng công thức và sẽ vẽ ảnh với 160 * (240 / 160) = 240 pixel. Tương tự như vậy với High density có 320 ppi, hệ thống sẽ vẽ ảnh 320×320 pixel. Và với Extra high density thì ảnh này được vẽ với kích thước 480×480 pixel. Sự linh động về pixel như vậy giúp giao diện chúng ta có được các tỉ lệ tương đương cho tất cả các màn hình ngoài thị trường. Giờ thì bạn đã hiểu rồi đúng không nào.

Thực Hành Truy Xuất Đến File Dimen

Và cuối cùng, chúng ta cũng nên vọc vọc cho TourNote một tí cho nhớ bài. Nếu bạn có thử code gì cho MainActivity.java như ví dụ thay đổi kích thước ảnh trên đây thì xóa đi nhé.

Mình nhắc lại tí, là bạn đã từng mang resource string về để trong strings.xml, mang resource color về colors.xml. Vậy thì resource dimen cũng phải được mang về dimens.xml. Việc file activity_main.xml có định nghĩa cứng kích thước và các margin ở nội dung của nó là không được, sau này khi cần chỉnh sửa chúng ta sẽ rất mệt mỏi khi phải tìm vào từng layout.

Vậy các bạn hãy mở file dimens.xml lên và thêm vào vài thông tin sau.

1
2
3
4
5
6
7
8
9
<resources>
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>
 
    <!-- Main Activity components -->
    <dimen name="empty_icon_width">60dp</dimen>
    <dimen name="empty_icon_height">60dp</dimen>
</resources>

Bây giờ bạn hãy vào lại activity_main.xml để điều chỉnh các thông số cho ImageView như sau. Bạn hãy đảm bảo đang ở tab Design. Hãy nhấn chọn vào ImageView ở giữa màn hình. Ở cửa sổ Attributes bên phải, tìm đến các field layout_width và layout_height. Bạn lần lượt nhấn vào nơi để thay đổi kích thước này như hình ảnh ở mục trên kia mình có nói đến.

Vùng khoanh đỏ cho bạn tùy chọn đến các dimen đã định nghĩa sẵn
Vùng khoanh đỏ cho bạn tùy chọn đến các dimen đã định nghĩa sẵn

Poup sau xuất hiện sẽ liệt kê các dimen có thể có, kể cả 2 dimen mà bạn vừa khai báo xong trên kia luôn nhé, hãy lần lượt chọn empty_icon_width cho layout_width, và chọn empty_icon_height cho layout_height của ImageView.

Cửa sổ này liệt kê các dimen mà bạn cầnhttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2019/07/Screen-Shot-2019-07-12-at-1.31.02-PM.png?resize=300%2C264&ssl=1 300w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2019/07/Screen-Shot-2019-07-12-at-1.31.02-PM.png?resize=768%2C677&ssl=1 768w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2019/07/Screen-Shot-2019-07-12-at-1.31.02-PM.png?resize=1024%2C903&ssl=1 1024w" data-lazy-loaded="1" sizes="(max-width: 1000px) 100vw, 1000px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px; vertical-align: bottom;">
Cửa sổ này liệt kê các dimen mà bạn cần

Sau đây là kết quả ở cửa số Attribute mà bạn đã thiết lập.

Kết quả sau khi set dimen cho ImageView
Kết quả sau khi set dimen cho ImageView

Bạn có thể chuyển qua tab Text để xem code XML thay đổi như thế nào nhé.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <TextView
        android:id="@+id/activity_main_tv_empty"
        style="@style/InformationTextView"
        android:text="@string/mainscreen_empty_note"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
 
    <ImageView
        android:id="@+id/imageView"
        android:layout_width="@dimen/empty_icon_width"
        android:layout_height="@dimen/empty_icon_height"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:scaleType="fitCenter"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/activity_main_tv_empty"
        app:srcCompat="@drawable/empty_note" />
 
</android.support.constraint.ConstraintLayout>

Xong. Bạn có thể chạy lại ứng dụng để kiểm chứng. Tuy nhiên mọi thứ ở bài thực hành hôm nay sẽ không làm cho TourNote khác đi chút giao diện nào. Chỉ là giúp cho code được chuyên nghiệp hơn thôi.

Sử Dụng Các Resource Values Khác

Như vậy là sau một vài cân nhắc, mình quyết định bài học Android hôm nay sẽ dành để nói đến các dạng resource còn lại được hỗ trợ trong thư mục res/values/. Bạn nhớ là trong res/values/ thôi nhé, có nghĩa là phía trước còn rất nhiều resource thú vị khác, nhưng chúng không được tổ chức bên trong thư mục này.

Bạn cũng biết rằng Android hỗ trợ rất nhiều dạng resource khác nhau cho chúng ta vận dụng vào việc tạo một project hoàn chỉnh. Tuy nhiên như ở bài học số 8 mình có nói là mỗi một thư mục được dựng sẵn bên trong res/ của Android sẽ có tác dụng hỗ trợ một loại resource nhất định. Và resource ở các bài học gần đây mà chúng ta từng làm quen vẫn chưa ra khỏi một thư mục – res/values/.

Điểm Lại Thư Mục res/values/

Trong thư mục res/values/ mà chúng ta đã trải qua, có các kiểu resource, mà như bạn đã làm quen, mỗi kiểu như vậy được đặt trong một file .xml nhất định, mình sẽ giúp bạn ôn lại bằng danh sách sau.

– Resource Style – Để trong res/values/styles.xml
– Resource Theme (thực chất cũng là Style) – Để trong res/values/styles.xml
– Resource String – Để trong res/values/strings.xml
– Resource Color – Để trong res/values/colors.xml
– Resource Dimen – Để trong res/values/dimens.xml
– Resource Bool – Bài hôm nay sẽ nói đến
– Resource ID – Bài hôm nay sẽ nói đến
– Resource Integer – Bài hôm nay sẽ nói đến
– Resource Integer Array – Bài hôm nay sẽ nói đến
– Resource Typed Array – Bài hôm nay sẽ nói đến

Bạn cũng nên biết là các resource trong res/values/ mà mình đã nói ở các bài trước khá là quan trọng, bởi vì tần suất sử dụng của chúng trong project của bạn là khá cao, thậm chí một loại resource nào đó, như một color chẳng hạn, sẽ được rất nhiều nguồn khác truy cập đến, nên việc bạn tổ chức như những gì mình thực hiện là khá hữu ích, trong trường hợp nào đó bạn chỉ cần vào resource loại này và thay đổi chúng, lập tức cả project sẽ thay đổi theo, không mất nhiều thời gian đúng không nào.

Tuy nhiên các resource còn lại của res/values/ mà chúng ta làm quen hôm nay, thì lại ít quan trọng hơn, bạn có thể không cần định nghĩa chúng cũng được, vì chúng ngược lại với những ý mình nói ở ngay trên đây.

Nói Thêm: Tên File Của Các Resource Bên Trong res/values/ Có Quan Trọng Không?

Có một điều mình chưa nói ở các bài trước. Rằng với các resource trong thư mục res/values/ này, liệu chúng có cần phải nằm trong đúng tên file mà chúng ta từng điểm qua hay không? Chẳng hạn như String có phải bắt buộc nằm trong strings.xml? Hay Color có bắt buộc nằm trong colors.xmlString để vào file colors.xml được không? Hoặc String để trong file với tên tùy ý, như string_cua_tao.xml có được không?

Câu trả lời là: riêng các resource bên trong thư mục res/values/ này thôi nhé, khi truy xuất đến chúng, chúng ta không cần quan tâm đến tên file chứa chúng, mà chỉ cần quan tên đến loại thẻ và tên của thuộc tính name của thẻ đó. Chẳng hạn với resource dimen, bạn chỉ cần gọi @dimen/tên_dimen hay R.dimen.tên_dimen. Và bởi vì không quan tân đến tên file chứa chúng, nên bạn có thể đặt tùm lum tá lả các resource res/values/ này ở bất kỳ file nào đều được cả.

Ví dụ sau mình làm chơi với các resource của TourNote cho vui thôi, mình trộn resource color và string lại rồi để trong một file strings_colors.xml. Tuy là làm được, nhưng mình khuyến khích các bạn đừng nên thử ở nhà nhé 😀

Screen Shot 2017-05-24 at 18.46.08

Nội dung của strings_colors.xml lai căng hỗn tạp đây.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<resources>
 
    <!-- Color resources -->
    <color name="colorPrimary">#00BCD4</color>
    <color name="colorPrimaryDark">#0097A7</color>
    <color name="colorAccent">#FFEB3B</color>
 
    <!-- String resources -->
    <string name="app_name">TourNote</string>
    <string name="common_action_close">Close</string>
    <string name="common_action_save">Save</string>
    <string name="menu_item_about_app">About App</string>
    <string name="menu_item_help">Help</string>
    <string name="menu_item_version_app">Version %s</string>
    <string name="mainscreen_add_catergory">Add Category</string>
    <string name="mainscreen_add_note">Add Note</string>
    <string name="mainscreen_edit_note">Edit Note</string>
    <string name="mainscreen_empty_note">Your note list is empty, let start to create your first note now</string>
    <string name="note_detail_share">Share</string>
    <string name="note_detail_delete">Delete</string>
    <string name="note_detail_category">Category</string>
    <string name="note_detail_note_detail">Detail</string>
    <string name="note_detail_other_information">Other Information</string>
    <string name="note_detail_map_location">Map Location</string>
    <string name="note_detail_note_name_hint">Name</string>
    <string name="note_detail_note_detail_hint">Description</string>
    <string name="note_detail_add_note_hint">Your Note (Can Add Later)</string>
    <string name="note_detail_address_hint">Address</string>
</resources>

Sử Dụng Bool

Chúng ta bắt đầu với loại resource thứ nhất trong bài hôm nay. Resource bool này giúp bạn định nghĩa ra các giá trị boolean để dùng trong project.

Như đã nói ở trên, bạn có thể tạo ra một file .xml bất kỳ trong thư mục res/values/ để chứa các giá trị của loại resource này. Hoặc bạn có thể để chung chúng với các file .xml có sẵn khác bên trong res/values/.

Khai Báo Bool

Resource bool lần này nằm trong thẻ bool, mỗi thẻ bool sẽ có một tên được để trong thuộc tính name, và giá trị của thẻ này chỉ có thể là true hoặc false mà thôi.

Mình ví dụ khai báo một cờ để mà ẩn/hiện TextView ở màn hình chính. Ví dụ này có tính ứng dụng thực tế đấy nhé, bạn có thể chỉ định một cờ như thế này trong resource bool, để rồi tùy thời điểm bạn sẽ quyết định đến logic của ứng dụng thông qua việc điều chỉnh các cờ này.

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <bool name="enable_empty_text_display">true</bool>
</resources>

Truy Xuất Đến Bool

Nếu từ bất kỳ file XML nào, bạn có thể truy xuất đến các giá trị bool bằng cách gọi @bool/tên_bool.

Còn từ Java code, bạn có thể gọi đến thông qua R.bool.tên_bool. Như ví dụ sau. Trong ví dụ thì hàm setVisibility() của một view sẽ giúp xác định trạng thái hiển thị của view đó, chẳng hạn với tham số truyền vào hàm này là view.VISIBLE sẽ hiển thị view lên màn hình, còn view.GONE sẽ làm mất view đó đi.

1
2
3
4
5
6
7
8
TextView tvEmpty = (TextView) findViewById(R.id.activity_main_tv_empty);
 
boolean enableDisplay = getResources().getBoolean(R.bool.enable_empty_text_display);
if (enableDisplay) {
    tvEmpty.setVisibility(View.VISIBLE);
} else {
    tvEmpty.setVisibility(View.GONE);
}

Sử Dụng ID

Mình thấy ít ai sử dụng loại resource này. Nó giúp bạn tạo trước các ID bên trong resource XML, rồi ở nơi nào đó sẽ dùng đến các ID này. Mình thì mình không thích cách sử dụng ID kiểu này lắm, cần thì khai báo, chứ không khai báo trước ở chỗ nào đó. Nhưng vẫn viết ra cho các bạn tham khảo, ai có kinh nghiệm dùng resource ID kiểu này thì để lại comment cho mình biết nha.

Và cũng như các resource khác bên trong res/values/, bạn có thể tạo resource dạng này ở một file .xml riêng biệt hoặc để cùng với các .xml khác cũng được.

Khai Báo ID

Để tạo ra các ID bên trong file resource, bạn khai báo thẻ item. Mỗi thẻ item bạn phải chỉ định hai thuộc tính, thứ nhất type=”id” là bắt buộc, sau đó tên của ID sẽ để trong thẻ name. Thẻ item cho ID này không chứa giá trị.

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item type="id" name="examle_id"/>
</resources>

Sử Dụng Đến ID

Nếu bạn dùng đến resource ID này để dùng vào việc khai báo một ID cho một view nào đó, bạn dùng như sau. Bạn có thể thấy rằng bởi vì ID đã được tạo trước rồi, nên thay vì khai báo ID cho TextView bằng @+id/tên_ID_mới như các bài học trước bạn đã làm quen, thì lần này bạn chỉ cần chỉ định đến ID đã khai báo mà không có dấu cộng như sau @id/tên_ID_đã_khai_báo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.yellowcode.tournote.MainActivity">
 
    <TextView
        android:id="@id/examle_id"
        style="@style/InformationTextView"
        android:layout_centerInParent="true"
        android:text="@string/mainscreen_empty_note" />
 
    <ImageView
        android:layout_width="@dimen/empty_icon_width"
        android:layout_height="@dimen/empty_icon_height"
        android:layout_below="@id/examle_id"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="@dimen/padding_medium"
        android:scaleType="fitCenter"
        android:src="@drawable/empty_note" />
 
</RelativeLayout>

Bạn có thể thấy ở ví dụ trên, cách khai báo ID mới cho view bằng việc chỉ định lại một ID đã được khai báo làm mình không thích, trông nó chẳng rõ ràng gì cả.

Sử Dụng Integer

Sử dụng resource dạng này nếu bạn muốn định nghĩa ra các giá trị int vào cùng một file resource. Nếu như cách sử dụng dimen hôm trước bạn buộc phải định nghĩa đơn vị kèm theo các con số, thì với integer hôm nay bạn chỉ cần định nghĩa một con số nguyên mà thôi.

Và cũng như các resource khác bên trong res/values/, bạn có thể tạo resource dạng này ở một file .xml riêng biệt hoặc để cùng với các .xml khác cũng được.

Khai Báo Integer

Resource integer này nằm trong thẻ integer, mỗi thẻ integer sẽ cần một tên trong name để bạn truy xuất đến, và giá trị của thẻ này chỉ có thể là một số nguyên.

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="maxline_textview_info">5</integer>
</resources>

Sử Dụng Đến Integer

Truy xuất từ XML thì gọi @integer/tên_integer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.yellowcode.tournote.MainActivity">
 
    <TextView
        android:id="@+id/activity_main_tv_empty"
        style="@style/InformationTextView"
        android:layout_centerInParent="true"
        android:text="@string/mainscreen_empty_note"
        android:maxLines="@integer/maxline_textview_info"/>
 
    <ImageView
        android:layout_width="@dimen/empty_icon_width"
        android:layout_height="@dimen/empty_icon_height"
        android:layout_below="@id/activity_main_tv_empty"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="@dimen/padding_medium"
        android:scaleType="fitCenter"
        android:src="@drawable/empty_note" />
 
</RelativeLayout>

Truy xuất từ Java code thì gọi R.integer.tên_integer.

1
2
3
4
TextView tvEmpty = (TextView) findViewById(R.id.activity_main_tv_empty);
 
int maxLine = getResources().getInteger(R.integer.maxline_textview_info);
tvEmpty.setMaxLines(maxLine);

Sử Dụng Integer Array

Resource dạng này cho phép bạn khai báo một mảng các int. Cách khai báo và sử dụng của integer array cũng khá giống với string array vậy.

Và cũng như các resource khác bên trong res/values/, bạn có thể tạo resource dạng này ở một file .xml riêng biệt hoặc để cùng với các .xml khác cũng được.

Khai Báo Integer Array

Gần gần giống với string array, integer array dùng thẻ integerarray. Tên cho thẻ này vẫn được để trong thuộc tính name. Từng phần tử mảng sẽ được định nghĩa ở mỗi thẻ item, mỗi thẻ item như vậy chứa một con số nguyên.

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer-array name="integer_array_sample">
        <item>1</item>
        <item>2</item>
        <item>3</item>
        <item>4</item>
    </integer-array>
</resources>

Sử Dụng Đến Integer Array

Và cũng như string array, integer array chỉ được dùng đến Java code, khi đó bạn gọi đến thông qua R.array.tên_integer_array.

1
int[] intArrayDemo = getResources().getIntArray(R.array.integer_array_sample);

Sử Dụng Typed Array

Nên hiểu đây là resource mà bạn có thể định nghĩa mảng các kiểu dữ liệu gì cũng được. Đó có thể là mảng các string, mảng các integer, hay mảng các color, mảng các drawable,… Vấn đề còn lại của bạn là khi sử dụng chúng (mảng các xx) ở Java code, bạn phải biết chúng là kiểu dữ liệu nào để gọi cho đúng hàm mà thôi.

Và cũng như các resource khác bên trong res/values/, bạn có thể tạo resource dạng này ở một file .xml riêng biệt hoặc để cùng với các .xml khác cũng được.

Khai Báo Typed Array

Typed array không dùng thẻ string-array hay integer-array để định nghĩa, mà chỉ dùng thẻ array. Mỗi array sẽ vẫn có một tên ở thuộc tính name để gọi đến. Từng phần tử mảng sẽ được định nghĩa ở mỗi thẻ item, mỗi thẻ item như vậy chứa một loại resource tùy ý.

Như bạn thấy ví dụ sau mình định nghĩa ba loại mảng khác nhau cho ba loại resource.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<resources>
 
    <array name="array_of_string_sample">
        <item>Hello</item>
        <item>Yellow</item>
        <item>Code</item>
        <item>Books</item>
    </array>
 
    <array name="array_of_int_sample">
        <item>1</item>
        <item>2</item>
        <item>3</item>
        <item>4</item>
    </array>
 
    <array name="array_of_color_sample">
        <item>#FF0000</item>
        <item>#00FF00</item>
        <item>#0000FF</item>
    </array>
 
</resources>

Sử Dụng Đến Types Array

Cũng như bao array khác, chỉ được dùng đến ở Java code. Khi đó tùy vào mảng có kiểu resource gì mà bạn hãy gọi đến hàm tương ứng, như ví dụ sau.

1
2
3
4
5
6
7
// Array of strings, sử dụng như resource string array
String[] strings = getResources().getStringArray(R.array.array_of_string_sample);
// Array of integers. sử dụng như resource integer array
int[] ints = getResources().getIntArray(R.array.array_of_int_sample);
// Array of color, cần đến TypedArray để lấy ra resource tương ứng
TypedArray colors = getResources().obtainTypedArray(R.array.array_of_color_sample);
int color = colors.getColor(0, 0);

Trên đây là tất cả các loại resource được tổ chức bên trong thư mục res/values/ của Android. Có một điểm chung nhất là, các resource bên trong thư mục này đều có thẻ gốc là thẻ resources. Chính điểm chung này giúp cho các loại resource này được để ở bất kỳ file .xml nào bên trong res/values/ đều được như mình có nói nhiều ở trên, miễn là thẻ gốc của tất cả các file đều là thẻ resources.

Sử Dụng Drawable – Ảnh Bitmap

Bài học hôm nay chúng ta sẽ bắt đầu nói về cách thức sử dụng một dạng resource có tên drawable. Drawable là một khái niệm mà Android dùng đến để nói về các resource liên quan đến ảnh, bao gồm cả các ảnh bitmap như PNG/JPG, các ảnh vector, hay các ảnh được dựng bằng XML,… Và do có nhiều dạng drawable cần nói đến, nên mình tách chúng riêng ra từng phần, bài hôm nay mình sẽ nói về ảnh bitmap trước, với mình thì nó khá thú vị, hi vọng bạn cũng thích bài học hôm nay.

Resource Ảnh Và Widget Hiển Thị Ảnh

Đầu tiên mình xin nói đến sự khác biệt giữa Resource là Ảnh của bài hôm nay và Các widget giúp hiển thị Ảnh. Ồ nếu bạn phân biệt được rồi thì tốt, nhưng mình nghĩ sẽ có nhiều bạn hơi hơi nhập nhằng phần này tí.

Bạn nên biết là các bài học như bài số 9, hay bài về các widget cơ bản, đều nói đến các widget chuyên về hiển thị ảnh, như ImageView, hay ImageButton. Ngoài ra thì các widget còn lại bên trong Android hay các layout cũng đều có khả năng hiển thị ảnh, chỉ có điều không chuyên bằng mấy thằng ImageView và ImageButton thôi. Thì đây chỉ là những nơi giúp bạn hiển thị ảnh ra ngoài màn hình. Còn ảnh được tổ chức như thế nào, được gọi đến ra sao, thì đó là trách nhiệm của resource về ảnh mà bài hôm nay sẽ nói đến. Bạn phân biệt được chưa nào. Nào chúng ta cùng bắt đầu.

Ảnh Bitmap

Chúng ta cùng đến với một dạng drawable đầu tiên, đó là bitmap drawable.

Bitmap, chắc bạn cũng biết rồi, là các dạng ảnh được tổ chức theo ma trận các điểm ảnh, như các ảnh PNG, JPEG, JPG, TIFF, PSD,… mà bạn biết.

Có nhiều các thể loại bitmap drawable là vậy nhưng Android chỉ hỗ trợ chúng ta sử dụng ba định dạng sau thôi, đó là PNG, JPG và GIF. Trong đó PNG được khuyến khích sử dụng, JPG thì chấp nhận được, và GIF thì bạn nên hạn chế. Ba định dạng ảnh này là gì thì chúng quá phổ biến rồi, bạn nên tự tìm hiểu bản chất của chúng nhé.

Nếu bạn nào thích thiết kế vẽ vời, bạn có thể xem hướng dẫn chi tiết của Google ở link này để có thể tự mình thiết kế các ảnh cho các icon trong app, bảo đảm đúng chuẩn material. Mình cũng thích thiết kế, cơ mà Google viết dài quá nên hơi lười đọc. Nhưng nếu bạn vừa lười đọc vừa không thích thiết kế, vậy thì hãy đọc các công cụ có sẵn của Google mà mình sắp trình bày ở bên dưới đây, để xem Google cung cấp cho chúng ta những phương tiện gì để phát triển một sản phẩm mà không cần designer nhé.

Tổ Chức Ảnh Bitmap Trong res/

 Có hai loại thư mục con của res/ mà bạn có thể dùng để tổ chức các ảnh bitmap này, đó chính là drawable-xxx/ và mipmap-xxx/. Chính là các thư mục được mình tô sáng như dưới đây.

Nơi chứa ảnh bitmap
Nơi chứa ảnh bitmap

Việc để các ảnh vào trong các cấu trúc alternaive resource thì bạn đã biết rồi, nhưng cớ vì sao mà lại có tới hai loại thư mục drawable và mipmap thì đó mới là chuyện đáng nói. Trước kia Android chỉ cung cấp cho chúng ta drawable là thư mục duy nhất để bạn có thể triển khai các ảnh bitmap vào đây. Nhưng một bữa nọ, khi tạo mới project, bạn có thêm một thư mục nữa đó chính là mipmap, thư mục này chủ yếu chỉ chứa các ảnh dùng làm icon cho ứng dụng. Lát nữa chúng ta sẽ cùng thực hành tạo icon chính thức cho TourNote, và icon đó (cũng là một ảnh bitmap) sẽ được để trong thư mục mipmap này.

Sử Dụng Ảnh Bitmap

Cũng giống như các resource mà chúng ta đã từng làm quen, có hai hướng để các bạn truy xuất đến một ảnh bitmap, đó là từ bất kỳ file XML nào, hoặc là từ Java code, mình sẽ nói rất cụ thể từng hướng.

Sử Dụng Ảnh Bitmap Từ File XML

Từ bất kỳ file XML nào, như là các file layout hay file Manifest, nếu bạn đang ở tab Design, thì ở các field liên quan đến hình ảnh, như field background ở hình dưới, bạn có thể nhấn vào nơi được khoanh tròn trong hình để mở ra một cửa sổ liệt kê tất cả các hình ảnh, bạn chỉ việc chọn lựa hình ảnh đã khai báo sẵn mà thôi.

Nhấn vào những nơi như chỗ khoanh tròn, sẽ hiện ra cửa sổ với các hình ảnh có sẵn
Nhấn vào những nơi như chỗ khoanh tròn, sẽ hiện ra cửa sổ với các hình ảnh có sẵn

Còn nếu bạn đang ở tab Text, bạn truy cập đến ảnh bitmap bằng cách gọi @drawable/tên_file hoặc @mipmap/tên_file tùy theo ảnh đó được để trong drawable hay mipmap. Bạn có thể xem lại cách ImageView trong activity_main.xml của TourNote đã sử dụng đến ảnh bitmap mà chúng ta đã code ở những bài học trước.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<ImageView
    android:id="@+id/imageView"
    android:layout_width="@dimen/empty_icon_width"
    android:layout_height="@dimen/empty_icon_height"
    android:layout_marginStart="8dp"
    android:layout_marginLeft="8dp"
    android:layout_marginTop="8dp"
    android:layout_marginEnd="8dp"
    android:layout_marginRight="8dp"
    android:scaleType="fitCenter"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/activity_main_tv_empty"
    app:srcCompat="@drawable/empty_note" />

Với code trên, bạn có thể thấy rằng, khi truy xuất đến ảnh bitmap, sẽ không có thông tin về định dạng ảnh, như .png hay .jpg hay .gif gì cả. Tức là không có chuyện bạn khai báo như này @drawable/empty_note.png, bạn chỉ cần truy xuất đến tên của file mà thôi, kiểu viết đúng luôn luôn là @drawable/empty_note. Và như vậy thì có nghĩa là, bạn sẽ không thể để hai ảnh có cùng tên nhưng khác định dạng, như empty_note.png và empty_note.jpg, vào trong cùng một project, vì khi gọi đến chúng ở file XML, hệ thống sẽ không biết bạn đang ám chỉ loại ảnh nào.

Sử Dụng Ảnh Bitmap Từ Java Code

Chắc bạn cũng có thể đoán được, là từ Java code, để gọi đến ảnh bitmap này, bạn có thể truyền vào tham số R.drawable.tên_file hoặc R.mipmap.tên_file – Tùy theo ảnh đó được để trong drawable hay mipmap – Vào trong các hàm cần một resource ID.

Như ví dụ sau mình có set lại ảnh cho ImageView ở màn hình chính của TourNote, mình set lại cho nó là ảnh ic_launcher trong thư mục mipmap. Bạn nên nhớ hàm set ảnh tương tự thuộc tính android:src bên XML là hàm setImageResource() bên Java code (như ví dụ dưới đây). Còn hàm tương tự thuộc tính android:background bên XML là hàm setBackgroundResource() bên Java code nhé. Bạn nên thử cả hai hàm xem chuyện gì xảy ra.

1
2
ImageView imageView = (ImageView) findViewById(R.id.activity_main_imv_empty);
imageView.setImageResource(R.mipmap.ic_launcher);

Công Cụ Image Asset

Chúng ta vẫn còn đang nói đến các ảnh bitmap, và mục này mình xin nói đến một công cụ đắc lực của Google, công cụ được tích hợp sẵn vào Android Studio, giúp bạn dễ dàng import một icon (được Google tạo sẵn theo đúng tiêu chí mà mình có để link cho các bạn tham khảo trên kia, hoặc các ảnh mà bạn tự thiết kế nhưng không biết kích cỡ sao cho phù hợp), vào trong ứng dụng của bạn, để làm cho ứng dụng được chuyên nghiệp hơn.

Thực tế thì các icon bên trong ứng dụng TourNote mà mình cho các bạn xem, đều được lấy ra từ thư viện icon có sẵn của Google cả, bằng cách này hay cách khác. Với mục này thì mình hướng dẫn các bạn dùng cách sử dụng công cụ Image Asset.

Để mở công cụ Image Asset thì từ Android Studio, bạn click chuột phải vào res/ rồi chọn New > Image Asset. Như hình.

Đường dẫn đến công cụ Image Assethttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2019/07/Screen-Shot-2019-07-23-at-2.46.49-PM.png?resize=300%2C241&ssl=1 300w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2019/07/Screen-Shot-2019-07-23-at-2.46.49-PM.png?resize=768%2C616&ssl=1 768w" data-lazy-loaded="1" sizes="(max-width: 942px) 100vw, 942px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px; vertical-align: bottom;">
Đường dẫn đến công cụ Image Asset

Popup xuất hiện sau đó sẽ trông như thế này.

Popup để cấu hình Image Asset
Popup để cấu hình Image Asset

Giao diện của popup khá là dễ hiểu. Nhưng mình cũng nói sơ qua một chút các thành phần.

  • Icon Type – Giúp bạn chọn lựa các bộ icon theo các chủ đề, các chủ đề này bao gồm.
    • Launcher Icons, với tùy chọn này bạn sẽ tạo ra được bộ các icon cho app, với các kích cỡ icon ưng ý, và để sẵn vào trong thư mục res/mipmap-xxx/ cho chúng ta luôn. Từ khi Android O ra đời (hay còn được gọi là Android 8.0), thì Launcher Icons được chia ra làm 2 tùy chọn nhỏ hơn. Tùy chọn thứ nhất Launcher Icons (Adaptive and Legacy) hỗ trợ cả Adaptive icon và icon truyền thống ngày xưa trước Android 0. Tùy chọn thứ hai Launcher Icons (Legacy only) thì chỉ hỗ trợ bộ icon trước Android O mà thôi. Tất nhiên là chúng ta sẽ chọn Launcher Icons (Adaptive and Legacy) rồi. Còn khái niệm Adaptive icon là gì thì bạn có thể xem thêm link này nhé.
    • Action Bar and Tab Icons, tùy chọn này giúp bạn tạo một bộ icon dùng cho action bar và tab, dĩ nhiên bạn cũng có thể tận dụng tùy chọn này để tạo các icon cho các thành phần khác, kết quả của tùy chọn này chính là bộ ảnh được sắp xếp “ưng ý” trong các thư mục res/drawable-xxx/ tương ứng.
    • Notification Icons, tùy chọn này giúp bạn tạo một bộ icon dùng cho notification, với bạn nào mới làm quen Android thì quả thật các quy luật về hiển thị notification icon với từng hệ điều hành Android khác nhau là một cơn ác mộng, mình đã từng mất khá nhiều thời gian để giải thích cho các bạn designer khi thiết kế các bộ icon này, “hiểu” được điều đó, công cụ Image Asset với tùy chọn Notification Icons này giúp bạn tạo các bộ icon cho từng đời Android đó.
  • Name – Chỉ là đặt tên cho icon thôi.
  • Nếu ở Icon Type mà bạn chọn Launcher Icons (Adaptive and Legacy) thì dưới mục Name sẽ có 3 tab tương ứng với 3 cài đặt cho các lớp của Adaptive icon.
    • Tab Foregaround Layer cho phép bạn chọn ảnh làm foreground. Đó có thể là một ảnh bitmap của bạn, hay ảnh từ thư viện Clip Art của Google, thậm chí là text.
    • Tab Background Layer cho phép bạn chọn ảnh làm background. Với 2 tùy chọn là màu sắc hoặc ảnh.
    • Tab Legacy cho phép bạn thiết lập các tùy chọn icon cho các hệ điều hàn Android khác nhau, thường thì mình để mặc định thông tin ở tab này.

Công Cụ Android Asset Studio

Nếu như bạn không thích công cụ có sẵn Image Asset trên đây, bạn có thể tìm đến một công cụ online thú vị hơn nhiều, đó là Android Asset Studio.

Hình ảnh công cụ Android Asset Studio
Hình ảnh công cụ Android Asset Studio

Nơi đây cung cấp khá nhiều tùy chọn cho bạn tạo các icon theo các mục đích khác nhau cho ứng dụng. Do là công cụ online và có nhiều tùy chọn quá nên mình không nói đến chi tiết nhé.

Tuy công cụ này khá tốt, nhưng nó cũng có một số khuyết điểm hơi bị ghét, chẳng hạn như các icon của nó không được nhóm lại theo từng chủ đề, bạn sẽ mất nhiều thời gian hơn mỗi lần tìm icon Clipart có sẵn. Hay sau khi chọn được icon ưng ý, bạn phải down về gói zip, tuy gói zip này có để sẵn các icon vào từng thư mục, nhưng cũng khiến bạn mất một ích thời gian để copy/paste chúng vào project, không được tự động như Image Asset trên kia.

Thực Hành Thay Đổi Icon Launcher Cho TourNote

Bài thực hành hôm nay mình sẽ sử dụng công cụ Image Asset để chính thức tạo một icon đẹp lung linh và tương thích với mọi phiên bản hệ điều hành Android cho TourNote. Trước hết bạn hãy click vào link này đây để download ảnh chất lượng cao của mình về trước nhé.

Dùng Công Cụ Image Asset Để Tạo Bộ Icon Cho App

Đầu tiên, như hướng dẫn trên kia, bạn hãy mở cửa sổ Configure Image Asset rồi làm theo các thiết lập như dưới đây của mình.

Ở mục Icon Type, hãy đảm bảo chọn Launcher Icons (Adaptive and Legacy).

Ở mục Name, hãy để như mặc định.

Ở tab Foreground Layer. Bạn để mặc định mục Layer Name. Mục Asset Type thì check chọn Image. Sau đó trỏ Path đến nơi bạn vừa download ảnh chất lượng cao trên kia về. Rồi điều chỉnh lại mục Resize sao cho ảnh nằm cân đối bên trong các khung bao. Tất cả các thiết lập đến bước này sẽ trông như sau.

Qua tab Background Layer. Cũng để mặc định mục Layer Name. Mục Asset Type thì check chọn Color. Click vào mục Color sẽ xuất hiện một popup nữa có tên Select Color. Khi này bạn hãy điền vào mã màu #F2CE3C, đây là màu vàng chuẩn của Yellow Code Books, bạn có thể thay đổi màu tùy thích.

Đến bước này thì công cuộc thiết lập sẽ trông như sau.

Bạn có thể thấy rất nhiều định dạng icon mẫu trưng ra cho bạn xem ở poup trên đúng không. Khi thấy ưng ý rồi bạn hãy nhấn Next. Bước sau cho bạn thấy các icon nào sẽ được thêm vào project để tạo thành bộ icon hoàn chỉnh cho ứng dụng của bạn.

Quan sát lần cuối trước khi nhấn Finish
Quan sát lần cuối trước khi nhấn Finish

Thực ra bước cuối cùng này cũng chẳng có gì quan trọng lắm, bạn hãy nhấn Finish để xem thành quả cuối cùng nhé.

Kiểm Tra Lại Manifest

Chắc chắn chúng ta không cần làm gì cả ở bước này, bởi vì bạn chỉ vừa thay đổi icon thôi, không đổi tên chúng nó, project sẽ tự cập nhật lại icon mới. Nhưng bạn hãy cứ mở file AndroidManifest.xml lên và xem cách khai báo và sử dụng icon (cũng chính là ảnh bitmap) cho ứng dụng. Còn nếu bạn không biết file AndroidManifest nằm ở đâu thì có thể xem lại bài 12 nhé.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="utf-8"?>
    package="com.yellowcode.tournote">
 
    <supports-screens
        android:smallScreens="false"
        android:normalScreens="true"
        android:largeScreens="true"
        android:xlargeScreens="true"/>
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
 
</manifest>

Giờ đây nếu bạn run ứng dụng lên, rồi vào lại danh sách các ứng dụng trong hệ thống, để xem icon của bạn là tròn hay vuông nhé. Với mình thì trông như thế này đây.

Kết quả sau khi thực thi ứng dụng
Kết quả sau khi thực thi ứng dụng

Sử Dụng Drawable – Ảnh 9-Patch & Ảnh Vector

ới bài học hôm trước, bạn đã làm quen với cách sử dụng resource drawable (resource ảnh) đầu tiên của Android, đó là dùng đến ảnh bitmap. Có đến hơn 90% trường hợp bạn sẽ dùng đến ảnh bitmap này trong xây dựng ứng dụng Android (theo thống kê không rõ nguồn gốc của mình). Tuy nhiên có đôi lúc bạn bị “bất lực” trong một số tình huống sử dụng ảnh bitmap, hoặc bạn muốn tìm kiếm một định dạng ảnh nào đó linh động hơn. Thì bài học hôm nay sẽ giúp bạn trả lời hai câu hỏi đó, bạn sẽ làm quen với hai loại ảnh khá “chất”, nhưng hơi khó sử dụng với các bạn mới làm quen với Android xíu, đó là ảnh 9-Patch và ảnh vector.

Chú ý là các bài ví dụ hôm nay mình đều lấy source code hiện tại của TourNote ra để làm mẫu. Nếu bạn nào chưa có thực hành TourNote ngày nào thì có thể down source code về từ link này, hoặc bạn có thể tự tạo project mới hoàn toàn.

Ảnh 9-Patch

Dạng ảnh linh động hơn bitmap đầu tiên mà mình muốn chia sẻ có cái tên là 9-Patch, hay một số nơi viết là Nine-Patch. Còn đọc lên thì bạn đừng đọc là “chín pát-ch” mà hãy đọc luôn là “nai-n pat-ch”. Đúng như tên gọi, ảnh này được chia nhỏ làm “9 miếng”. Thực chất thì bản thân ảnh này cũng là một ảnh PNG, nhưng bạn cần phải chỉ định miếng nào trong ảnh được phép co giãn, miếng nào phải giữ nguyên kích thước so với thiết kế ban đầu. Rồi sau đó, khi thực thi ở môi trường thực tế, hệ thống sẽ dựa vào các thiết lập của các miếng đó mà sẽ mang đến cho ứng dụng của chúng ta một cách thức sử dụng PNG khác hoàn toàn so với cách cũ.

Cấu Tạo Của Ảnh 9-Patch

Đầu tiên, một ảnh được gọi là ảnh 9-Patch nếu chúng được lưu trữ với đuôi là .9.png. Bạn xem, ảnh này đúng là ảnh PNG, nhưng có thêm số 9 ở trước, nhìn vào phần đuôi này mà hệ thống Android sẽ hiểu bạn đang sử dụng ảnh PNG dạng 9-Patch chứ không phải PNG của bitmap ở bài trước.

Để tạo ra ảnh .9.png, bạn (hay designer, hay Google cũng có tool cho bạn làm điều này) phải chừa ra 1 pixel cho mỗi cạnh của ảnh PNG gốc. 1 pixel này sẽ dùng để định nghĩa miếng nào có thể co giãn được, miếng nào không nên co giãn, rồi vẽ vào phần chừa ra đó một hay nhiều lằn gạch màu đen. Như hình minh họa sau của Google.

Cấu tạo của ảnh 9-Patch
Cấu tạo của ảnh 9-Patch

Như hình minh họa trên, thì cái khối vuông với bốn cạnh bo tròn ở cả hai hình trên và dưới là một background cho một Button nào đó. Giả sử bạn không định nghĩa 9-Patch (không có các lằn gạch ở các bên như hình), thì khi background này co giãn theo kích cỡ của Button, các góc bo tròn của nó sẽ bị biến dạng. Còn với định nghĩa 9-Path, bạn có thể chỉ định sao cho các góc sẽ được giữ nguyên vẹn, để biết cách thức chỉ định ra sao thì bạn có thể đọc tiếp thông tin dưới đây.

Để giải thích cho việc các đường gạch sẽ tạo nên một ảnh 9-Path thế nào. Thì bạn xem, ở hình trên cùng, bạn chú ý vào hai đường gạch đen ở trên và trái của ảnh. Các đường gạch đen này giúp chỉ định những vùng được phép co giãn theo chiều dài và rộng của ảnh tương ứng. Mình minh họa hình dưới cho bạn thấy 9 miếng của ảnh PNG được tạo ra như thế nào từ sự kết hợp hai đường gạch đen này. Trong đó thì các miếng có màu hồng (các miếng số 24568) sẽ được phép co giãn thoải mái khi chương trình được chạy lên, các miếng còn lại (các miếng số 1379) thì được giữ nguyên. Nhờ định nghĩa như vậy mà bạn có thể bảo toàn kích thước của các vùng có góc bo tròn. Đến bước này bạn đã hiểu cách hoạt động của 9-Path rồi đúng không nào. Tuy nhiên mình chỉ mới nói đến 2 đường gạch đen ở trên và trái thôi, 2 đường còn lại như thế nào thì mời bạn xem tiếp bên dưới.

Bạn đã thấy 9 miếng được định nghĩa ra từ 2 đường gạch trên-trái ảnh chưa nào
Bạn đã thấy 9 miếng được định nghĩa ra từ 2 đường gạch trên-trái ảnh chưa nào

Tiếp tục, mình nói về hai đường gạch đen bên phải và dưới của ảnh. Các đường phải và dưới này không dùng để chỉ định việc co giãn của ảnh nữa, mà giúp chỉ định nội dung (như text hay hình ảnh khác) nếu để vào widget có dùng ảnh 9-Patch này, sẽ được canh chỉnh như thế nào. Hai đường gạch ở trường hợp này là không bắt buộc, bạn có thể để trống nếu muốn sự canh chỉnh nội dung được thực hiện theo mặc định hoặc theo các thuộc tính XML mà bạn đã từng sử dụng.

Bạn sẽ hiểu rõ hơn về các đường định nghĩa cho ảnh 9-Patch này qua ví dụ của mình ở bên dưới.

Ví Dụ Cần Sử Dụng Ảnh 9-Patch

Để dễ hiểu nhất, mình đưa ra một ví dụ cực kỳ thực tế.

Giả sử designer đưa cho bạn một giao diện, và mong muốn bạn thiết kế với một EditText giống như dưới đây.

Ví dụ giao diện buộc phải sử dụng ảnh 9-Patch
Ví dụ giao diện buộc phải sử dụng ảnh 9-Patch

Nếu là bạn, gặp thiết kế như trên, mình khuyên bạn nên xin designer một background như dưới đây.

Chỉ cần xin designer background như này
Chỉ cần xin designer background như này

Bạn có thể thấy, background mà bạn xin là một khung vuông, khung vuông này có sẵn 4 góc bo tròn, và có bao gồm cả hình cái kính lúp luôn. Nếu bạn không dùng đến ảnh 9-Path, mà hiển thị ảnh PNG này làm background của EditText luôn, bạn sẽ thấy 2 điều tệ hại. Một là người dùng sẽ gõ chữ đè lên cả cái kính lúp, chúng ta mong muốn chữ phải xuất hiện bên phải kính lúp cơ. Và hai là các góc bo tròn của khung này sẽ bị kéo dãn ra rất xấu. Mình làm thử cho bạn xem điều tệ hại này.

1
2
3
4
5
6
7
<EditText
    android:id="@+id/activity_main_edt_main"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:hint="Search your place"
    android:background="@drawable/bg_edt_search" />
after
Điều tệ hại nếu không dùng ảnh 9-Path

Bạn thấy chưa, UI trở nên rất xấu. Thế nhưng, trước khi đi vào chi tiết cách thức tạo một ảnh 9-Patch như thế nào, mình nói thêm một tí. Vì mình biết, bạn sẽ không phục nếu mình nói về 9-Path liền, bạn sẽ bảo, xời, có nhiều cách để làm mà, sao cứ phải 9-Path. Mình biết bạn có một cách khác như sau.

Bạn không xin designer background như mình đưa ra trên kia, mà nhờ designer tách dùm mấy thành phần UI ra. Đại loại cũng như dưới đây, để bạn có thể dễ dàng ráp nối chúng lại theo thiết kế. Trong đó ảnh cái kính lúp bạn sẽ dùng đến ImageView để hiển thị,  EditText đó với background bo tròn được hiển thị bên phải ImageView là xong.

search_separate

Uhm… cách này có thể làm được. Nhưng. Thứ nhất, nó sẽ làm bạn mất rất nhiều thời gian, vì bạn phải thao tác với nhiều ảnh hơn và tổ chức nhiều widget hơn. Và kết quả chắc chắn không như bạn mong muốn đâu, vì với hình bo góc ở bên phải mà bạn nhờ tách ra í, khi kéo dài quá (tùy theo kích cỡ màn hình) cũng sẽ gặp phải tình trạng các góc bo không còn còn đẹp nữa. Vậy chúng ta vẫn sẽ nên quay lại với 9-Patch thôi.

Tổ Chức Ảnh 9-Patch Trong res/

Ảnh 9-Patch thực chất cũng là một dạng ảnh trong nhóm drawable mà mình đang nói đến. Do đó giống như bên ảnh bitmap, bạn chỉ có thể để ảnh 9-Patch này vào thư mục res/drawable-xxx/. Đặc biệt hơn, mình khuyên các bạn để thẳng vào res/drawable/ luôn. Vì sao không cần chỉ định alternative resource cho chúng? Bởi đặc tính co giãn linh động của các ảnh 9-Patch, nên thường thì người ta chỉ cần tạo ra một ảnh, rồi chỉ định 9 miếng cho nó. Do chỉ có một ảnh thì để vào default resource luôn cho rồi.

Để bắt đầu xây dựng EditText, bạn hãy vào link này và download ảnh background PNG cho EditText về.

Ảnh down về để vào thư mục drawable luôn
Ảnh down về để vào thư mục drawable luôn

Công Cụ Giúp Chuyển Ảnh PNG Sang Ảnh 9-Patch

Bạn vừa down về ảnh PNG thôi, chúng ta sẽ tạo ra ảnh 9-Patch như thế nào? Có nhiều cách để hô biến một ảnh PNG sang 9-Patch. Như trường hợp của mình, đôi khi lười, mình nhờ luôn các bạn designer thiết kế các đường gạch đen, rồi đổi đuôi file thành .9.patch, hệ thống tự hiểu. Cách nữa là bạn tự thiết kế bằng các công cụ chỉnh sửa ảnh. Tuy nhiên mình có một tin vui rằng Android Studio cũng hỗ trợ chúng ta một công cụ được tích hợp sẵn, giúp chúng ta tạo ra ảnh 9-Patch một cách cực kỳ dễ dàng.

Với ảnh bg_edt_search.png bạn đã để vào thư mục res/drawable/, bạn click phải chuột vào nó và chọn Create 9-Patch file….

Nơi chọn đến công cụ tạo ảnh 9-Path
Nơi chọn đến công cụ tạo ảnh 9-Path

Popup sau đó xuất hiện hỏi bạn muốn save ảnh .9.png ở đâu và tên gì. Mình để như mặc định (tên là bg_edt_search.9.png và vẫn để ở res/drawable/).

Popup hỏi nơi chứa và tên của ảnh 9-Path
Popup hỏi nơi chứa và tên của ảnh 9-Path

Khi bạn nhấn OK ở popup trên, bạn sẽ thấy hai ảnh bg_edt_search.png và bg_edt_search.9.png cùng xuất hiện bên trong thư mục res/drawable/ của bạn. Bạn nên xóa ảnh .png gốc đi nhé, chỉ còn lại ảnh .9.png vừa tạo. Vì ở bài trước mình có nói, không thể có hai ảnh cùng tên (mặc dù khác đuôi) bên trong project của bạn được.

Giờ thì bạn click đúp vào bg_edt_search.9.png ở Android Studio lên. Editor của Android Studio khá thông minh, nó biết bạn đang mở ảnh 9-Patch chứ không phải PNG thường, nên giao diện của nó sẽ như sau.

Cụ thể công cụ tạo ảnh 9-Path
Cụ thể công cụ tạo ảnh 9-Path

Với cửa sổ như trên. Thì khung bên trái cho phép bạn chỉ định các vạch đen 1 pixel xung quanh hình gốc. Khung bên phải cho bạn xem trước kết quả khi co giãn theo các hướng (dọc, ngang, hay cả hai hướng) để bạn có cái nhìn tổng thể. Bạn có thể chỉ định độ zoom của khung bên trái hoặc độ co giãn ở khung bên phải bằng hai thanh trượt ở phía dưới hai khung này. Các checkbox trong popup cũng rất dễ hiểu, bạn cứ click xem thử nhé, nó không làm thay đổi ảnh của bạn đâu, chỉ giúp bạn có cái nhìn tốt hơn khi tương tác với ảnh mà thôi.

Bạn hãy tập trung vào ảnh ở khung bên trái. Hãy đưa chuột vào một pixel ở các biên, nhớ phải cẩn thận vì chỉ có một pixel mà thôi. Bạn có thể kéo một đường dài để định nghĩa các vạch cho 9-Patch. Bạn có thể click chuột vào bất cứ đâu trên phần một pixel đó để vẽ thêm các điểm, hoặc giữ phím Shift và click vào bất cứ nơi nào đã có điểm đen để xóa nó đi. Như mình có nói, bất cứ nơi nào ở đường biên trên và trái đều là chỉ định vùng có thể co giãn, còn đường biên phải và dưới là chỉ định nội dung xuất hiện như thế nào. Bạn hãy nhìn ảnh thiết kế 9-Patch của mình.

Ví dụ về ảnh 9-Path của mình
Ví dụ về ảnh 9-Path của mình

Với vạch ở trên, mình muốn “né” kính lúp ra, chỉ có phần thuộc bên phải kính lúp là được phép co giãn. Với vạch ở trái, mình cũng né cái kính lúp này bằng chỉ định hai đường trên và dưới cái kính. Vạch dưới và phải mình muốn nội dung của EditText rơi vào cái khung bên phải kính lúp, và ở giữa khung theo chiều dọc. Bạn đã hiểu đúng không nào. Bạn nên nhớ là các vạch bên phải và dưới là không bắt buộc nhé, với ví dụ này mình cố tình mang ảnh này ra để làm khó, để bạn có cơ hội vận dụng tất cả các vạch, còn như nếu không có kính lúp bên trái hình, thì nội dung sẽ nằm ở giữa EditText, thì khi đó bạn không cần chỉ định hai vạch phải và dưới này.

“Toàn cảnh” editor sau khi bạn chỉ định 9-Patch như dưới đây, bạn có thể thấy các ảnh xem trước bên khung phải của editor này cho ra các ảnh với độ co giãn tốt hơn rất nhiều so với trước khi bạn chỉ định 9-Patch đúng không, kết nhất là cái kính lúp luôn tròn trịa dù bị giãn ở chiều nào đi nữa.

Xem lại toàn cảnh ảnh 9-Path
Xem lại toàn cảnh ảnh 9-Path

Truy Xuất Đến Ảnh 9-Patch

Do việc sử dụng ảnh 9-Patch không khác so với sử dụng ảnh bitmap ở bài học hôm trước. Nên việc truy xuất đến ảnh 9-Patch này cũng tương tự. Do đó ở bước này bạn hoàn toàn có thể chạy lại project và xem kết quả mà không cần chỉnh sửa gì cho file XML ban đầu cả.

Kết quả cuối cùng
Kết quả cuối cùng

Ảnh Vector

Có thể nói rằng mình đã khá phấn khích với việc Android hỗ trợ ảnh vector. Về cơ bản thì ảnh vector có cấu trúc khác hoàn toàn với ảnh bitmap.

Nếu như ảnh bitmap được cấu thành bởi ma trận các điểm ảnh (cố định). Bạn có thể tưởng tượng giả sử có một ảnh bitmap có kích cỡ 100 x 100 pixel sẽ được biểu diễn bởi ma trận 100 x 100 điểm ảnh. Do đó nếu bạn muốn hiển thị ảnh này với kích cỡ lớn hơn, chẳng hạn như 400 x 400 pixel, thì các pixel này bị giãn nở ra, kết quả cho ta một ảnh bị vỡ. Chính vì đặc tính này của bitmap, mà bạn phải để nhiều kích thước ảnh vào trong các alternative resource khác nhau, để hạn chế tỉ lệ vỡ ảnh do sự co giãn này.

Qua đến ảnh vector, các ảnh dạng này không dùng ma trận tĩnh các điểm ảnh nữa, mà chỉ lưu trữ các thông số chỉ định các đường thẳng, đường cong, màu sắc… cho mỗi ảnh. Đến khi sử dụng ảnh dạng này ở thực tế, thì hệ thống mới chính thức vẽ ảnh lên màn hình dựa vào các thông số đó. Chính vì đặc tính này mà ảnh Vector không bị vỡ dù cho được biểu diễn với bất kỳ kích thước nào.

Ảnh vector tốt như vậy, thì bạn có thể tưởng tượng ra một điều rằng tất cả các project hiện nay đều tập trung vào sử dụng nó? Tuy nhiên, đời không như mơ, và cho dù ảnh vector có biết bao nhiêu là điều tốt đẹp, như là hiển thị đẹp hơn này, tốn ít thời gian để tổ chức ảnh vào các thư mục hơn này, và thậm chí file .apk sau khi xuất xưởng cũng nhẹ nữa này, nhưng ảnh dạng này vẫn còn bị các lập trình viên dè dặt khi sử dụng. Nguyên nhân vì sao thì chúng ta cùng điểm qua một vài bất lợi của chúng, để bạn có một quyết định rõ ràng nhất nếu muốn sử dụng chúng nhé.

  • Bất lợi đầu tiên có thể kể đến là, do ảnh vector được hệ thống vừa đọc thông số vừa vẽ lên màn hình, nên yêu cầu CPU xử lý nhiều hơn so với ảnh bitmap. Điều này làm cho ứng dụng có nhiều ảnh vector sẽ xử lý có vẻ chậm chạp hơn (điều này còn tùy vào số lượng ảnh vector được vẽ lên màn hình cùng lúc). Chính vì vậy mà Google khuyến cáo bạn nên giới hạn kích thước của ảnh dạng này ở 200 x 200 dp khi thiết kế lên UI.
  • Bất lợi kế tiếp là, không phải ảnh vector nào đưa vào Android thì hệ thống cũng chấp nhận. Bạn biết rồi đó, dù sao thì bộ xử lý của các thiết bị điện thoại cũng có giới hạn, chúng không thể xử lý các cấu trúc ảnh phức tạp được, do đó có nhiều ảnh vector khi đưa vào hệ thống sẽ bị báo lỗi, hoặc nếu không thì khi vẽ lên màn hình sẽ bị mất nét, mất điểm, nói chung là mất một số bộ phận.
  • Một bất lợi nữa, là ảnh dạng này không được hỗ trợ từ Android 4.4 (API level 20) trở về trước. Vậy nếu bạn vẫn muốn sử dụng ảnh vector cho tất cả các hệ điều hành trước và sau API level 20? Thì bạn hãy xem và chọn lựa cách thức tương thích ngược ở mục ngay dưới đây.

Vấn Đề Tương Thích Ngược

Vấn đề này xảy ra nếu bạn có khai báo minSdkVersion trong file build.gradle nhỏ hơn 21. Tất nhiên rồi, đa số các project hiện nay vẫn còn hỗ trợ các hệ điều hành Android trước 5.0.

Khi đó hoặc là bạn chấp nhận cho công cụ Vector Asset Studio mà mình sắp nói đến bên dưới, sẽ tạo ra (sau khi build) hai loại ảnh, một là PNG bình thường để có thể chạy với các hệ điều hành API level 20 trở về trước, và một là ảnh vector để chạy trên các hệ điều hành có API level 21 trở về sau. Hai là bạn chỉ định có sử dụng Support Library trong file build.gradle, thì thư viện này sẽ hỗ trợ cho bạn các hàm sử dụng hoàn toàn ảnh vector trong project, tùy bạn chọn thôi. Mình sẽ nói luôn chi tiết cách sử dụng hai cách này.

Chấp Nhận Vector Asset Studio Tạo Ra Hai Loại Ảnh

Với cách này thì bạn không làm gì cả, cứ đọc tiếp các phần tổ chức và tạo ảnh vector bên dưới của mình. Nhưng như bạn biết thì cách này sẽ tạo ra trong file .apk của bạn các ảnh PNG bên cạnh các ảnh vector, vậy thì chắc chắn file .apk này sẽ “phình to” hơn rồi, tùy vào số lượng ảnh vector mà bạn sử dụng.

Sử Dụng Support Library

Nếu bạn dùng cách này, thì trước khi đọc các phần bên dưới của mình, bạn hãy mở file build.gradle cấp độ module lên và thêm vào dòng được tô sáng như sau nhé.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apply plugin: 'com.android.application'
  
android {
    compileSdkVersion 25
    buildToolsVersion '25.0.2'
    defaultConfig {
        applicationId "com.yellowcode.tournote"
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        vectorDrawables.useSupportLibrary = true
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
  
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.3.1'
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:design:25.3.1'
    compile 'com.android.support:recyclerview-v7:25.3.1'
}

Tổ Chức Ảnh Vector Trong res/

Bạn nên để các ảnh vector này trong thư mục res/drawable/. Vì cũng giống như ảnh 9-Patch, các ảnh vector này sẽ tự co giãn khi hiển thị thực tế, nên chúng ta không cần tổ chức chúng theo alternative resource nữa.

Có hai dạng ảnh vector được Android hỗ trợ, đó là SVG (Scalable Vector Graphic) và PSD (Adobe Photoshop Document). Nhưng mình quen dùng SVG, dạng ảnh PSD mình chưa từng sử dụng qua.

Hiện tại do chưa có ảnh vector nào để bạn đặt vào trong thư mục này. Thôi thì chúng ta cùng qua bước dưới đây để có một resource vector nhé.

Công Cụ Vector Asset Studio

Bạn có nhớ ở bài về ảnh bitmap có nói đến công cụ Image Asset? Vậy thì bài hôm nay cũng có Vector Asset, cách sử dụng công cụ này cũng na ná như Image Asset vậy.

Để sử dụng Vector Asset, từ Android Studio, bạn click phải chuột vào thư mục res/ và chọn New > Vector Asset. Như hình.

Đường dẫn đến công cụ Vector Asset
Đường dẫn đến công cụ Vector Asset

Popup xuất hiện sau đó trông như thế này.

Popup Configure Vector Asset
Popup Configure Vector Asset

Bạn có thể thấy rằng giao diện của popup này khá tương đồng với Image Asset của bài hôm trước. Mình nói sơ qua một số thành phần của popup này.

  • Asset Type – Giúp bạn chọn giữa bộ vector icon có sẵn của Google (nếu check Clip Art), hay là từ file bạn upload lên (nếu check Local File).
  • Name – Chỉ là đặt tên cho icon thôi.
  • Clip Art – Click vào hình con robot ở mục này sẽ dẫn bạn đến một bộ các icon có sẵn của Google. Nhưng nếu ở trên kia bạn chọn vào Local File thì field này đổi tên là Patch, cho phép bạn chỉ định file vector của bạn.
  • Size – Chỉ định kích thước của file, bạn yên tâm rằng với ảnh vector, bạn có thể co giãn thoải mái, nên kích thước này cũng chỉ là tương đối, nếu có ảnh hưởng chăng, thì sẽ ảnh hưởng đến cách tương thích ngược có dùng chung ảnh PNG với ảnh vector mà mình nói ở trên kia.
  • Color – Thường các ảnh Vector là các ảnh đơn sắc. Nên chúng cho phép bạn chỉ định màu sắc chủ đạo của ảnh.
  • Opacity – Chỉ định độ trong suốt của ảnh. Mặc định giá trị 100% có nghĩa là không trong suốt.
  • Enable auto mirroring for RTL layout – Check vào nếu bạn muốn ảnh hỗ trợ tự động đảo ngược cho các quốc gia sử dụng hệ chữ được viết từ phải qua trái.

Bạn click vào hình con robot ở mục Clip Art nhé, popup xuất hiện sau đó bạn hãy tìm đến và chọn hình sau đây, hoặc bất cứ hình nào bạn thích. Sau khi nhấn OK ở popup chọn hình, thì bạn hãy đặt lại tên là empty_note_vector và nhấn Next.

Chọn icon mà bạn thích
Chọn icon mà bạn thích

Màn hình sau đó chỉ giúp bạn xác nhận lại ảnh sẽ được để ở đâu. Bạn cứ nhấn Finish để hoàn thành.

Lúc này đây bạn sẽ thấy xuất hiện một ảnh có tên empty_note_vector.xml. Bạn có thể click đúp vào để xem thử, bạn thấy không, ảnh này chỉ toàn là các thông số, đến khi ứng dụng được chạy thực sự, thì hệ thống mới dựa vào đây mà vẽ ảnh cho bạn.

Sử Dụng Ảnh Vector

Sử Dụng Ảnh Vector Từ File XML

Nếu bạn muốn sử dụng đến ảnh vector từ XML, thì còn tùy vào cách bạn chọn lựa phương án tương thích ngược trên kia nữa.

Nếu Bạn Đã Chấp Nhận Vector Asset Studio Tạo Ra Hai Loại Ảnh

Với chọn lựa này, như mình có nói, bạn không cần quan tâm gì ráo, bạn cứ truy xuất đến ảnh thông qua @drawable/tên_file như bình thường. Hệ thống sẽ quyết định là ảnh PNG hay ảnh vector sẽ được sử dụng ở môi trường thực tế. Với cách này bạn hãy vào lại activity_main.xml của TourNote và set ảnh cho ImageView như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.yellowcode.tournote.MainActivity">
  
    <TextView
        android:id="@+id/activity_main_tv_empty"
        style="@style/InformationTextView"
        android:layout_centerInParent="true"
        android:text="@string/mainscreen_empty_note" />
  
    <ImageView
        android:layout_width="@dimen/empty_icon_width"
        android:layout_height="@dimen/empty_icon_height"
        android:layout_below="@id/activity_main_tv_empty"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="@dimen/padding_medium"
        android:scaleType="fitCenter"
        android:src="@drawable/empty_note_vector" />
  
</RelativeLayout>

Nếu Bạn Đã Chấp Nhận Sử Dụng Support Library

Cách này sẽ không có ảnh PNG cho các hệ điều hành Android level 20 trở về trước, mà chỉ toàn các ảnh vector mà thôi. Do đó để các hệ điều hành Android cũ có thể hiểu được các ảnh vector này, bạn phải dùng đến thuộc tính app:srcCombat thay cho android:src ngày xưa. Như thế này.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.yellowcode.tournote.MainActivity">
  
    <TextView
        android:id="@+id/activity_main_tv_empty"
        style="@style/InformationTextView"
        android:layout_centerInParent="true"
        android:text="@string/mainscreen_empty_note" />
  
    <ImageView
        android:layout_width="@dimen/empty_icon_width"
        android:layout_height="@dimen/empty_icon_height"
        android:layout_below="@id/activity_main_tv_empty"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="@dimen/padding_medium"
        android:scaleType="fitCenter"
        app:srcCompat="@drawable/empty_note_vector" />
  
</RelativeLayout>

Truy Xuất Từ Java Code

Mình thì mình thấy không có gì khác biệt khi dùng ảnh vector ở Java Code đối với hai cách hỗ trợ như trên. Bằng cách truyền vào tham số R.drawable.tên_file cho các hàm cần một resource ID, giống như đã từng làm với ảnh PNG bình thường vậy.

1
2
ImageView imvTest = (ImageView) findViewById(R.id.imvTest);
imvTest.setImageResource(R.drawable.empty_note_vector);

Trên đây là hai cách sử dụng ảnh drawable khác bên cạnh ảnh bitmap truyền thống mà Android hỗ trợ cho lập trình viên chúng ta. Cách nào cũng có điểm mạnh và điểm yếu riêng cả. Tùy vào từng trường hợp mà bạn sẽ phải đưa ra quyết định xem project của bạn cần sử dụng ảnh loại nào, đừng quá cứng nhắc trong việc chỉ sử dụng một loại ảnh thôi, trong một project hoàn toàn có thể có nhiều thể loại ảnh drawable khác nhau. Tất nhiên các thể loại ảnh mà Android hỗ trợ cho chúng ta vẫn chưa dừng lại ở đây, bài học sau chúng ta sẽ tiếp tục nói nhiều hơn về resource drawable này.

Sử Dụng Drawable – Ảnh Shape XML & Ảnh Layer List

Bài hôm nay sẽ là bài học tiếp theo trong chuỗi bài về sử dụng ảnh drawable trong Android. Chúng ta hãy cùng nhau ôn lại xem cho đến bài học hôm nay, đã có tất cả bao nhiêu cách để có thể sử dụng ảnh dạng drawable nào.

– Ảnh bitmap – Là dạng drawable được tổ chức theo ma trận các điểm ảnh, các ảnh bitmap được Android hỗ trợ bao gồm PNGJPG và GIF.
– Ảnh 9-Patch và ảnh Vector – Trong đó 9-Patch thì còn tận dụng lại từ PNG gốc rồi phát triển hơn. Còn Vector thì mang đến cho chúng ta một cách tổ chức và hiển thị ảnh hoàn toàn khác.
– Ảnh Shape XML và ảnh Layer List – Thì bài học hôm nay chúng ta sẽ nói đến.

Chú ý là các bài ví dụ hôm nay mình đều lấy source code hiện tại của TourNote ra để làm mẫu. Nếu bạn nào chưa có thực hành TourNote ngày nào thì có thể down source code về từ link này, hoặc bạn có thể tự tạo project mới hoàn toàn để kiểm chứng các dòng code của mình rồi cùng nhau bình luận nếu bạn tìm thấy vấn đề nào đó nhé.

Ảnh Shape XML

Có nhiều bạn gọi ảnh dạng này là Ảnh XML, có nơi chỉ gọi là Shape, nhưng để chính xác nhất mình nghĩ chúng ta nên thống nhất cách gọi là Shape XML.

Shape XML, như tên gọi của nó, ảnh dạng này cho phép bạn thiết kế bất cứ hình khối nào dựa trên XML. Sau đó khi ứng dụng được thực thi, hệ thống sẽ căn cứ vào thiết kế trên XML mà vẽ ảnh ra cho bạn.

Nghe qua có vẻ giống ảnh Vector lắm đúng không nào. Nhưng thực chất chúng chỉ giống nhau là dùng code để vẽ và hệ thống sẽ dựa vào đó mà hiện thực hình vẽ khi ứng dụng được chạy mà thôi. Những cái khác nhau thì nhiều. Như Shape XML dựa trên định nghĩa các hình khối, còn Vector thì có nhiều định nghĩa vô cùng phức tạp. Với Shape XML bạn có thể code các hình khối bằng tay, trong khi với Vector bạn phải dùng công cụ để vẽ, và các công cụ vẽ này sẽ tạo ra ảnh .svg hoặc .psd, rồi bạn phải dùng công cụ Vector Asset Studio để chuyển các ảnh này về lại định dạng .xml. Khác biệt nữa là vẽ ảnh bằng Shape XML không tốn CPU như với ảnh Vector (vì mình không thấy cảnh báo nào như vậy cho Shape XML từ Google cả).

Dựa trên so sánh nhỏ trên thì đủ để bạn biết rằng ảnh Shape XML rất tốt, chúng vừa giúp tốn ít không gian hệ thống để lưu trữ các ảnh bitmap theo kiểu alternative resource khác nhau. Chúng còn không tốn chi phí xử lý của CPU, tức thời gian để vẽ chúng lên màn hình khá nhanh. Khuyết điểm duy nhất của ảnh Shape XML này là độ khó và độ đa dạng của nó. Thường thì với Shape XML bạn chỉ vẽ được các hình khối đơn giản, như background có bo tròn cho Button này, background có viền xám xung quanh cho EditText này,…

Cách Vẽ Ảnh Shape XML

Với việc bạn có thể vẽ bao nhiêu ảnh Shape XML vào trong project của bạn là tùy thuộc vào sự sáng tạo và độ tỉ mẩn của bạn. Mình chỉ đưa công thức cụ thể và ví dụ một số trường hợp cho bạn thấy cách vẽ Shape XML là như thế nào thôi nhé, việc còn lại là ở bạn.

Công thức, hay cú pháp đầy đủ để vẽ ra một Shape XML như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="utf-8"?>
<shape
    android:shape=["rectangle" | "oval" | "line" | "ring"] >
    <corners
        android:radius="integer"
        android:topLeftRadius="integer"
        android:topRightRadius="integer"
        android:bottomLeftRadius="integer"
        android:bottomRightRadius="integer" />
    <gradient
        android:angle="integer"
        android:centerX="float"
        android:centerY="float"
        android:centerColor="integer"
        android:endColor="color"
        android:gradientRadius="integer"
        android:startColor="color"
        android:type=["linear" | "radial" | "sweep"]
        android:useLevel=["true" | "false"] />
    <padding
        android:left="integer"
        android:top="integer"
        android:right="integer"
        android:bottom="integer" />
    <size
        android:width="integer"
        android:height="integer" />
    <solid
        android:color="color" />
    <stroke
        android:width="integer"
        android:color="color"
        android:dashWidth="integer"
        android:dashGap="integer" />
</shape>

Trên đây chỉ là công thức thôi, nó cho bạn xem các trường hợp tổng quát nhất có thể có để giúp bạn định nghĩa các hình khối, không nhất thiết bạn phải có đầy đủ hết các khai báo đã được nêu trong công thức. Và chắc chắn bạn nào chưa từng bao giờ sử dụng Shape XML sẽ hơi khó hiểu với công thức trên, vậy mình giúp bạn ở một số ý nhỏ gạch đầu dòng sau.

– Để bắt đầu vẽ một hình khối bằng Shape XML, bạn nhất định phải khai báo thẻ shape ở gốc. Sau đó, với thuộc tính android:shape của nó sẽ giúp chỉ định một trong các hình khối bạn muốn như sau. “rectangle” là hình chữ nhật, hình khối này là mặc định, có nghĩa là nếu bạn không khai báo thuộc tính này thì hệ thống sẽ hiểu hình khối đang khai báo này là hình chữ nhật. “oval” là hình oval, dĩ nhiên rồi. “line” sẽ vẽ một đường thẳng với chiều dài dãn ra rộng hết không gian chứa nó, loại hình line này cần đến định nghĩa stroke kèm theo sẽ được mình nói đến ở gạch đầu dòng bên dưới. “ring” sẽ vẽ một “vành” tròn được giới hạn bởi hai đường tròn làm biên, nếu bạn đã dùng shape là ring thì bạn phải định nghĩa thêm vài thuộc tính nữa ở thẻ này. Bao gồm: android:innerRadius hoặc android:innerRadiusRatio giúp dịnh nghĩa đường kính vòng tròn bên trong, một cái dùng độ lớn còn một cái dùng tỉ lệ để định nghĩa; android:thickness hoặc android:thicknessRatio giúp định nghĩa khoảng cách giữa hai đường tròn, một cái dùng độ lớn còn một cái dùng tỉ lệ để định nghĩa; android:useLevel cho biết ring này có dùng drawable dạng LevelListDrawable hay không, trong khuôn khổ bài học này bạn cứ để false cho thuộc tính này.

– Bên trong thẻ shape có các thẻ con, đầu tiên mình muốn nói đến thẻ corners. Thẻ này dùng để chỉ định độ bo tròn cho các góc của hình khối rectangle. Bạn có thể dễ dàng thử nghiệm các thuộc tính của corners này.

– Thẻ tiếp theo có thể có bên trong shape là thẻ gradient. Thẻ này rất thú vị, nó giúp bạn chỉ định nhiều nhất là ba điểm màu làm mốc, thông qua các thuộc tính android:startColorandroid:centerColor và android:endColor. Sau đó nó sẽ giúp vẽ ra dãy màu biến thiên dần theo ba màu mốc đó. Thẻ này còn được kèm với các thuộc tính hữu dụng khác như android:type giúp bạn chỉ định kiểu gradient là “linear” – tuyến tính“radial” – tròn như ra-đa, hay “sweep” – kiểu dẻ quạtandroid:angle giúp chỉ định một số nguyên để định nghĩa góc xoay của dãy màu với type là linear. Hay android:gradientRadius giúp định nghĩa bán kính của hình tròn ra-đa.

– Thẻ padding và size giúp chỉ định độ lớn của biên và độ lớn của hình khối.

– Thẻ solid giúp tô màu “đặc” cho hình khối (ngược lại với cách tô màu biến thiên gradient trên kia). Do tô có một màu nên nó chỉ cần một thuộc tính android:color để chỉ định màu.

– Thẻ stroke chỉ định độ rộng cho đường biên của hình khối thông qua thuộc tính android:width. Ngoài ra thuộc tính android:color của nó giúp chỉ định màu cho đường biên. android:dashGap và android:dashWidth (nếu có định nghĩa) sẽ giúp chỉ định đường biên này có dạng đứt nét, các giá trị tương ứng của hai thuộc tính này sẽ giúp định nghĩa khoảng cách giữa hai nét đứt với nhau và độ dài của mỗi nét đứt.

Ví Dụ Vài Ảnh Shape XML

Dưới đây là một vài ảnh Shape XML mà bạn có thể tham khảo.

Một background bo tròn cho Button.

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
    android:shape="rectangle">
 
    <solid android:color="@color/colorAccent" />
 
    <corners android:radius="5dp" />
 
    <stroke
        android:width="2dp"
        android:color="@color/colorPrimary" />
</shape>

Screen Shot 2017-07-18 at 05.36.31

Một hình tròn mang phong cách mới của ứng dụng Skype.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
    android:shape="oval">
 
    <gradient
        android:angle="135"
        android:endColor="#FFFF00"
        android:startColor="#FFB000"
        android:type="linear" />
 
    <size
        android:width="40dp"
        android:height="40dp" />
 
</shape>

Screen Shot 2017-07-18 at 05.49.15

Một vành tròn, có thể dùng làm icon nào đó trong ứng dụng.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
    android:shape="ring"
    android:innerRadiusRatio="6"
    android:thicknessRatio="4"
    android:useLevel="false">
 
    <gradient
        android:endColor="@color/colorAccent"
        android:startColor="@color/colorPrimary"
        android:gradientRadius="15dp"
        android:centerX="0.5"
        android:centerY="0.5"
        android:type="radial" />
 
    <size
        android:width="40dp"
        android:height="40dp" />
 
</shape>

Screen Shot 2017-07-18 at 05.59.52

Tổ Chức Ảnh Shape XML Trong res/

Tương tự như ảnh 9-Patch và ảnh Vector của bài học hôm trước, ảnh Shape XML hôm nay vẫn không cần thiết phải tổ chức theo thư mục alternative resource, vậy bạn chỉ cần đặt tất cả các Shape XML này vào một thư mục duy nhất là res/drawable/ là được.

Để tạo một ảnh Shape XML, bạn tìm đếm thư mục res/drawable/, nhấn phải chuột vào thư mục này và chọn New > Drawable resource file.

Screen Shot 2017-07-18 at 06.08.12

Popup nhỏ tiếp theo xuất hiện, bạn hãy đặt tên cho resource này (như bao cách đặt tên cho resource khác), và chỉ định thẻ gốc, trong trường hợp này thẻ gốc của chúng ta là shape nhé.

Screen Shot 2017-07-18 at 06.15.33

File bg_button.xml sau đó sẽ được tạo ra bên trong res/drawable/. Giờ thì bạn hãy thoải mái thực hành vẽ ra các hình khối như các ví dụ trên đây của mình được rồi đó. Kết quả code của bạn lên editor sẽ được hiển thị tức thời ở cửa sổ Preview.

Screen Shot 2017-07-18 at 06.18.45

Truy Xuất Đến Ảnh Shape XML

Bạn sử dụng ảnh Shape XML bình thường như khi sử dụng ảnh bitmap thôi. Dưới đây là đoạn code sử dụng Shape XML đã được vẽ ở ví dụ trên để làm background cho Button.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.yellowcode.tournote.MainActivity">
 
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="@drawable/bg_button"
        android:padding="4dp"
        android:text="Test Button" />
 
</RelativeLayout>

Ảnh Layer List

Phần này mình xem như là phần mở rộng hơn của Shape XML trên đây thôi. Nếu như Shape XML chỉ giúp bạn vẽ các hình khối đơn giản, thì Layer List giúp kết hợp nhiều hình khối đơn giản đó lại với nhau để tạo thành một hình khối phức tạp hơn.

Cách Vẽ Ảnh Layer List

Như mình có nói, Layer List là sự kết hợp của nhiều Shape XML với nhau. Mỗi Shape XML như vậy được để trong một thẻ item. Các thẻ item này lại nằm trong một thẻ gốc là layer-list. Như cú pháp sau.

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<layer-list
    <item
        android:drawable="@[package:]drawable/drawable_resource"
        android:id="@[+][package:]id/resource_name"
        android:top="dimension"
        android:right="dimension"
        android:bottom="dimension"
        android:left="dimension" />
</layer-list>

Mình nói sơ qua các thành phần trong cú pháp.

– Thẻ layer-list là thẻ gốc, nó hầu như không có tác dụng gì ngoài việc chứa các thẻ item bên trong nó.

– Mỗi thẻ item sẽ có vài thuộc tính kèm theo để định nghĩa bổ sung cho thẻ shape bên trong nó. Các thuộc tính của thẻ item bao gồm. android:drawable giúp chỉ định một resource drawable khác thay cho việc dùng thẻ shapeandroid:id giúp tạo id cho thẻ itemandroid:topandroid:bottomandroid:leftandroid:right giúp chỉ định các khoảng cách so với các biên của item đó. android:widthandroid:heightandroid:gravity là các thuộc tính mà bạn đã quen thuộc từ các bài học trước.

Ví Dụ Vài Ảnh Layer List

Mình để ra đây vài ảnh Layer List, chủ yếu cho vui vui để bạn tham khảo. Khi vào thực tế chắc chắn bạn sẽ cần phải vắt óc suy nghĩ cách để kết hợp các hình khối như thế này. Tin mình đi, nó thú vị lắm.

Một hình trái tim

Hình trái tim này được tạo ra từ một hình thoi (hình chữ nhật xoay nghiêng), và hai hình oval.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
 
    <item
        android:bottom="21dp"
        android:left="32dp"
        android:right="32dp"
        android:top="37dp">
        <rotate android:fromDegrees="45">
            <shape>
                <solid android:color="@color/colorPrimary" />
                <size
                    android:width="100dp"
                    android:height="100dp" />
            </shape>
        </rotate>
    </item>
 
    <item
        android:bottom="52dp"
        android:right="68dp">
        <shape android:shape="oval">
            <solid android:color="@color/colorPrimary" />
        </shape>
    </item>
 
    <item
        android:bottom="52dp"
        android:left="68dp">
        <shape android:shape="oval">
            <solid android:color="@color/colorPrimary" />
        </shape>
    </item>
 
</layer-list>

Screen Shot 2017-07-18 at 12.34.26

Một mô phỏng cho cái gương soi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
 
    <item>
        <shape android:shape="oval">
            <solid android:color="@color/colorPrimaryDark" />
        </shape>
    </item>
 
    <item
        android:bottom="10dp"
        android:left="10dp"
        android:right="10dp"
        android:top="10dp">
        <shape android:shape="oval">
            <solid android:color="@color/colorAccent" />
        </shape>
    </item>
 
    <item
        android:bottom="20dp"
        android:left="20dp"
        android:right="20dp"
        android:top="20dp">
        <shape android:shape="oval">
            <solid android:color="@color/colorAccent" />
            <stroke
                android:width="1.5dp"
                android:color="@color/colorPrimary"
                android:dashGap="2dp"
                android:dashWidth="4dp" />
        </shape>
    </item>
 
</layer-list>

Screen Shot 2017-07-18 at 12.56.12

Pin cho map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
 
    <item
        android:width="20dp"
        android:height="20dp"
        android:left="6dp"
        android:top="28dp">
        <rotate
            android:fromDegrees="45"
            android:pivotX="75%"
            android:pivotY="40%">
            <shape android:shape="rectangle">
                <solid android:color="@color/colorPrimary" />
            </shape>
        </rotate>
    </item>
 
    <item
        android:width="32dp"
        android:height="40dp"
        android:bottom="8dp">
        <shape>
            <solid android:color="@color/colorPrimaryDark" />
            <corners android:radius="4dp" />
        </shape>
 
    </item>
 
    <item
        android:width="32dp"
        android:height="26dp"
        android:bottom="4dp"
        android:top="16dp">
        <shape>
            <solid android:color="@color/colorPrimary" />
            <corners
                android:bottomLeftRadius="4dp"
                android:bottomRightRadius="4dp" />
        </shape>
 
    </item>
 
</layer-list>

Screen Shot 2017-07-18 at 13.29.39

Tổ Chức Ảnh Layer List Trong res/

Phần này tương tự như tổ chức ảnh Shape XML trên kia nhé.

Truy Xuất Đến Ảnh Layer List

Cũng tương tự như truy xuất đến ảnh Shape XML luôn.

Vậy là chúng ta đã đi qua cách sử dụng một dạng resource drawable mới mẻ, bằng cách kết hợp các hình khối lại với nhau. Cách sử dụng resource kiểu này của Android theo mình đánh giá là tương đối thú vị. Bài tiếp theo sẽ là một trong những bài chốt lại chuỗi bài về resource drawable này.

Sử Dụng Drawable – Ảnh State List & Các Ảnh Drawable Còn Lại

Hôm nay chúng ta cùng nhau chốt lại các cách sử dụng đến resource drawable trong Android. Phải nói là có khá nhiều cách để bạn linh hoạt sử dụng, nào là dùng ảnh bitmap này, dùng ảnh 9-Patch hay ảnh Vector này, rồi bạn còn có thể dùng đến XML để mà vẽ các hình khối, và bài hôm nay còn nói đến cách dùng State List và một vài cách khác nữa. Dù rằng cách dùng drawable dạng ảnh bitmap là phổ biến nhất, nhưng các cách dùng khác của drawable vẫn đôi khi phát huy tác dụng của nó ở một số trường hợp. Khả năng mà một project nào đó có dùng đến tất cả các dạng drawable mà mình nêu trên đây là hoàn toàn tồn tại đấy nhé. Vì vậy mà mình mới cất công viết nhiều bài như vậy cho bạn tham khảo.

Bài học hôm nay dùng rất nhiều ảnh động để minh họa, do các hiệu ứng từ bài học mang lại khá là đẹp. Do đó bạn nên cân nhắc đọc bài viết này ở nơi có wifi tốt tốt nhé.

Mời các bạn cùng bắt đầu với drawable State List.

State List Là Gì?

Mình nói thiệt là câu hỏi này rất quan trọng đấy, nếu bạn là người mới toanh đang đang từng bước học lập trình Android, thì bạn sẽ tiết kiệm được kha khá thời gian lục tìm trên mạng kiến thức của bài hôm nay, và bạn không ngờ rằng từ khóa để bạn tìm thấy thứ bạn cần chính là “State List”.

Để mình dẫn dắt từ từ. Bạn biết rằng bất cứ giao diện của ứng dụng nào cũng cần có Button đúng không nào. Bạn hãy thử xây dựng một Button đi, nhưng đừng set background hay src gì cho Button đó cả, chỉ dùng màu mặc định từ hệ thống thôi, khi này nếu bạn thực thi ứng dụng và nhấn vào Button đó, bạn sẽ thấy một hiệu ứng lún xuống của Button, nó báo cho bạn biết rằng Button này đã nhận lệnh. Như này.

Sử dụng drawable - Mô phỏng hiệu ứng button

Giờ thì bạn hãy tiến hành set background hay src cho nó, bằng bất cứ thể loại resource drawable nào mà bạn đã biết. Kết quả sau đó thực thi ứng dụng lên thì sao… Button với background hay src do bạn chỉ định đó, nó không có hiệu ứng lún như Button mặc định, nó đơ đơ sao á!?!

Bạn đã từng gặp vấn đề như vậy chưa. Mình có nhớ khi mới làm quen với Android, cũng với tình huống này, mình tìm trên mạng thế nào mà lại ra một chỉ dẫn từ một developer ở một đất nước nào đó rằng, bạn nên dùng Java code để check trạng thái của Button và set background tương ứng. Wow như vậy thì chuối quá!

Thực tế thì không cần tìm đâu xa, Google có hướng dẫn chúng ta kỹ càng, chỉ là không biết đường tìm mà thôi. Dù sao thì hôm nay, khi viết lại bài này để chia sẻ cách thức xây dựng Button (hay bất kỳ widget nào) có được sự phản hồi lại user, mình vẫn nhớ như in cái cảm giác tìm tòi ban đầu đó. Vừa buồn cười vừa thú vị.

Và State List chính là chìa khóa để bạn tạo ra các Trạng thái khác nhau cho các widget đó bằng cách định nghĩa vào một resource drawable như các bài học trước bạn đã làm quen. Sau đó khi ứng dụng được thực thi, tùy vào ngữ cảnh lúc đó mà hệ thống sẽ quyết định dùng đến Trạng thái tương ứng cho widget mà bạn đã khai báo.

Có hai dạng State List mà bạn phải làm quen, đó là Drawable State List và Color State List. Cách sử dụng của chúng là như nhau. Chúng ta cùng tìm hiểu chúng ở các mục dưới đây.

Drawable State List

Trước hết mời bạn nhìn vào công thức sau để biết các cách có thể có để định nghĩ một Drawable State List.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
    android:constantSize=["true" | "false"]
    android:dither=["true" | "false"]
    android:variablePadding=["true" | "false"] >
    <item
        android:drawable="@[package:]drawable/drawable_resource"
        android:state_pressed=["true" | "false"]
        android:state_focused=["true" | "false"]
        android:state_hovered=["true" | "false"]
        android:state_selected=["true" | "false"]
        android:state_checkable=["true" | "false"]
        android:state_checked=["true" | "false"]
        android:state_enabled=["true" | "false"]
        android:state_activated=["true" | "false"]
        android:state_window_focused=["true" | "false"] />
</selector>

Trong cấu trúc này.

– Thẻ gốc của định nghĩa một Drawable State List chính là thẻ selector. Thẻ này có tác dụng chính là chứa các trạng thái của widget bên trong nó.

– Mỗi thẻ item tiếp theo sẽ chứa đựng các trạng thái. Bạn sẽ chỉ định một hay một vài trạng thái cho item, và một resource drawable tương ứng cho item đó. Mỗi trạng thái chỉ có một trong hai giá trị là “true” hay “false”. Một số trạng thái điển hình được mình liệt kê như sau.

– android:state_pressed, định nghĩa trạng thái khi widget đó có được nhấn (touch) hay click không.

– android:state_focused, định nghĩa trạng thái khi widget đó có được focus vào không, như là với EditText ấy, khi user click vào EditText, thì khi đó trạng thái focus được kích hoạt.

– android:state_hovered, định nghĩa trạng thái khi con trỏ chuột có “lướt” qua Widget hay không, dùng cho một số thiết bị Android có điều khiển bằng trỏ chuột, trạng thái này nhiều khi được hành xử giống với state_focused.

– android:state_selected, định nghĩa trạng thái khi dùng với cần gạt hay các console điều khiển. Khi các widget này được chọn đến sẽ được hệ thống thực thi trạng thái này.

– android:state_checkable, định nghĩa trạng thái khi mà widget đó được set là cho phép check hay không. Bạn nên hiểu là chúng ta dùng trạng thái này để biểu diễn một widget không cho user được check và một widget cho check là như thế nào. Còn để biểu diễn các trạng thái khi check và uncheck là các trạng thái dưới đây.

– android:state_checked, định nghĩa trạng thái check hay uncheck của widget, vừa được nhắc đến bên trên.

– android:state_enabled, tương tự như state_checkablestate_enabled này dùng để biểu diễn một widget có đang cho phép tương tác hay không.

– android:state_activated, định nghĩa trạng thái widget đó đang được kích hoạt hay không.

– android:state_window_focused, định nghĩa trạng thái khi mà cửa sổ chứa đựng widget này có được focus hay không. Bạn nên biết là dù cho cửa sổ nào đó đang được hiển thị, thì không có nghĩa là nó đang được focus. Chẳng hạn như khi user đang tương tác với một cửa sổ, mà một notification show lên, sẽ làm cửa sổ đó đi vào trạng thái không được focus dù là vẫn đang được nhìn thấy bởi user.

– android:drawable, đây không phải là một trạng thái, như mình có nhắc đến ở gạch đầu dòng trên kia, đây là nơi bạn khai báo resource drawable tương ứng cho mỗi item.

Tổ Chức Drawable State List Trong res/

Cũng tương tự như với resource Shape XMLDrawable State List cũng được để trong res/drawable/.

Để tạo một Drawable State List, tương tự như các drawable trước, bạn click chuột phải vào thư mục res/drawable/ và chọn New > Drawable resource file. Hộp thoại xuất hiện tiếp theo bạn vẫn đặt tên cho resource và khai báo thẻ gốc là selector.

Sử dụng drawable - Tạo drawable state list

Ví Dụ Khai Báo Drawable State List

Mình biết lý thuyết trên đây hơi khó hiểu. Vậy chúng ta hãy thử cùng xây dựng một trạng thái hoàn hảo cho Button ở ví dụ đầu bài học nào.

Để có đầy đủ các trạng thái cần thiết, thì trước tiên mình tạo ra một background bo tròn màu xám, dành cho Button bị disable. Background này không gì tốt hơn là sử dụng Shape XML từ bài trước. Bạn có thể đặt tên resource background này là button_disable.xml.

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
 
    <solid android:color="@android:color/darker_gray" />
 
    <corners android:radius="5dp" />
 
</shape>

Sau đó mình tạo background dành cho khi Button được nhấn. Mình đặt tên là button_pressed.xml.

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
 
    <solid android:color="@color/colorPrimaryDark" />
 
    <corners android:radius="5dp" />
 
</shape>

Rồi đến background bình thường dành cho khi Button không bị tác động gì khác. Mình đặt tên là button_normal.xml.

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
 
    <solid android:color="@color/colorPrimary" />
 
    <corners android:radius="5dp" />
 
</shape>

Bạn có thể thấy là việc định nghĩa ra ba trạng thái cho Button trên đây của mình chỉ khác nhau chỗ màu sắc thôi đúng không nào. Nếu bạn không dùng TourNote để thực hành, thì các định nghĩa màu như trên có thể sẽ không có, và khi đó bạn có thể chỉ định một mã màu nào đó bất kỳ trực tiếp vào khai báo.

Khâu quan trọng tiếp theo chúng ta sẽ định nghĩa một Drawable State List. Bạn hãy áp dụng ý mà mình nói ở mục trên đây để tạo ra một Drawable State List có tên là button.xml trong thư mục res/drawable/. Trong trường hợp bạn chưa biết cách tạo các resource dạng này thì có thể tham khảo lại bài học trước của mình nhé.

button.xml có nội dung như sau.

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
 
    <item android:drawable="@drawable/button_pressed" android:state_pressed="true" />
 
    <item android:drawable="@drawable/button_disable" android:state_enabled="false" />
 
    <item android:drawable="@drawable/button_normal" />
     
</selector>

Bạn xem, trong file XML trên, bên trong thẻ selector mình có định nghĩa ba item cho Buttonitem với state_pressed=”true” mình chỉ định resource drawable là button_pressed vừa tạo trên kia, định nghĩa này có nghĩa là khi Button nhận sự kiện click thì resource drawable này được hệ thống tự động hiển thị. Sau đó, item với state_enable=”false” mình chỉ định resource drawable là button_disable vừa tạo trên kia, định nghĩa này có nghĩa là khi Button bị disable thì resource drawable này được hiển thị. Cuối cùng là item đặc biệt, mình không chỉ định trạng thái gì cho nó cả, chỉ khai báo resource drawable là button_normal, định nghĩa này có nghĩa là tất cả các trạng thái còn lại, bao gồm cả trạng thái mà Button không bị tác động gì cả, đều sử dụng resource drawable này.

Truy Xuất Đến Drawable State List

Việc truy xuất đến resource này vẫn giống với truy xuất đến resource Shape XML từ bài trước. Dưới đây là kết quả thử nghiệm các trạng thái của Button, bao gồm cả disable (dựa vào một CheckBox để enable/disable Button này). Bạn thử code đi nhé, đơn giản mà đúng không. 😉

Sử dụng drawable - Mô phỏng drawable state list button

Color State List

Thực ra phần Color State List này không nằm trong khuôn khổ các bài viết về sử dụng resource drawable, vì nó không liên quan đến thư mục này. Vậy vì sao mình nói đến resource này hôm nay? Thứ nhất, vì mình đã hứa với các bạn từ bài 17 – Bài học về Color, rằng Color State List sẽ được nói sau. Thứ hai, Color State List khá giống với Drawable State List về cách sử dụng cũng như cách tổ chức. Chỉ khác nhau ở chỗ một thằng thì dùng resource color, còn một thằng thì dùng resource drawable để biểu diễn các trạng thái. Khác nhau nữa đó là cách tổ chức chúng trong các thư mục res/, chúng ta cùng xem sơ qua Color State List nhé.

Về mặt công thức, thì Color State List không khác mấy so với Drawable State List. Do đó bạn có thể xem sơ qua mà không cần mình giải thích chi tiết.

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:color="hex_color"
        android:state_pressed=["true" | "false"]
        android:state_focused=["true" | "false"]
        android:state_selected=["true" | "false"]
        android:state_checkable=["true" | "false"]
        android:state_checked=["true" | "false"]
        android:state_enabled=["true" | "false"]
        android:state_window_focused=["true" | "false"] />
</selector>

Tổ Chức Color State List Trong res/

Khác với Drawable State List ở trên, Color State List phải được tổ chức trong thư mục res/color/.

Để tạo một Color State List, bạn phải tạo ra thư mục color/ bên trong res/ trước. Thư mục color/ này cũng đã được mình nhắc một chút khi liệt kê tất cả các thư mục có thể có bên trong res/ ở bài 8 rồi đó nhé, hi vọng bạn còn nhớ.

Sau khi có thư mục color/ rồi thì bạn click chuột phải vào nó và chọn New > Color resource file.

Sử dụng drawable - Tạo color state list

Hộp thoại xuất hiện tiếp theo bạn hãy đặt tên cho resource Color State List này. File resource được tạo ra sau bước này sẽ có sẵn thẻ gốc là selector cho bạn.

Sử dụng drawable - Đặt tên khi tạo color state list

Ví Dụ Khai Báo Color State List

Mình lấy lại ví dụ trên, nhưng khi này chúng ta sẽ tập trung vào tạo hiệu ứng cho màu text của Button. Lần này chúng ta làm đơn giản hơn nhiều do không phải khai báo các resource drawable, vì tất cả chỉ là màu sắc.

Bạn xem source code cho button_color.xml như sau.

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
 
    <item android:color="@android:color/darker_gray" android:state_pressed="true" />
 
    <item android:color="@android:color/black" android:state_enabled="false" />
 
    <item android:color="@android:color/white" />
 
</selector>

Bạn đã thấy các trạng thái ở Color State List như ví dụ trên không khác gì so với Drawable State List cả. Chỉ có điều chúng ta sử dụng android:color thay cho android:drawable trên kia mà thôi.

Truy Xuất Đến Color State List

Bạn truy xuất đến chúng giống như truy xuất đến resource color vậy, với XML thì bạn dùng @color/tên_color_state_list, còn với Java code thì bạn dùng R.color.tên_color_state_list.

Kết quả cho việc set button_color vào cho thuộc tính textColor của Button như sau. Bạn có thể thấy hiệu ứng rõ nhất là text ở trạng thái disable, nếu như bình thường không khai báo gì cả thì text này luôn là màu trắng, còn với ví dụ này bạn đã dùng màu đen để thay thế. Bạn có thể tự thử nghiệm.

Sử dụng drawable - Mô phỏng sử dụng color state list button

Các Resource Drawable Còn Lại

Chúng ta đã điểm qua các ảnh dạng drawable phổ biến từ các bài học trước và bài học hôm nay rồi, mục này mình sẽ điểm qua các resource drawable còn lại ít sử dụng hơn, nhưng biết đâu ở đâu đó bạn cần đến chúng.

Ảnh Dạng Level List

Ảnh này cũng na ná với Layer List hay State List mà chúng ta đã biết. Có một điều khác nhau giữa chúng là ảnh Level List giúp chỉ định các level cho từng item. Sau đó khi sử dụng ảnh, chúng ta sẽ dùng đến hàm setLevel() để hệ thống xác định loại ảnh nào cần được load.

Ảnh dạng này thích hợp cho các biểu diễn cần đến các cấp độ, như biểu diễn mức pin còn lại của hệ thống chẳng hạn.

Đây là link gốc cho bạn tham khảo.

Minh họa sau mình dùng đến nhiều ảnh để định nghĩa ra Level List, trong đó mình sử dụng thêm một SeekBar giúp giả lập điều khiển các level tương ứng cho ảnh.

Sử dụng drawable - Mô phỏng sử dụng level list

Ảnh Transition

Ảnh này cũng tương tự các ảnh kiểu List trên kia. Nhưng với ảnh này bạn chỉ được định nghĩa tối đa hai item trong một thẻ gốc transition. Sau đó khi vẽ ảnh này lên, hệ thống sẽ tạo hiệu ứng chuyển dần từ item 1 sang item 2.

Ảnh dạng này thích hợp cho các loại widget có hai trạng thái, và bạn muốn các trạng thái này được chuyển đổi giao diện qua lại có kèm hiệu ứng như thế này.

Bạn có thể xem thêm ở link gốc này.

Ví dụ sau cho bạn thấy việc chuyển đổi giữa hai ảnh kèm hiệu ứng.

Sử dụng drawable - Mô phỏng sử dụng transition

Ảnh Inset

Ảnh này đơn giản lắm, mình không cần hình minh họa. Ảnh này giúp bạn chỉ định một ảnh drawable bên trong thẻ inset, và chỉ định các giá trị inset cho nó. Dựa vào đó, hệ thống sẽ vẽ ảnh nhỏ hơn ảnh thực tế dựa trên các giá trị inset này.

Ảnh dạng này thích hợp cho bạn muốn vẽ một background cho widget nào đó mà không muốn background đó chiếm hết không gian của widget đó. Chắc bạn đang tưởng tượng tới các thông số padding hay margin của widget? Ồ, inset và padding/margin, chúng không giống nhau như bạn tưởng đâu nhé.

Bạn có thể thử vẽ một ảnh Inset như link gốc này, và so sánh với cách sử dụng các thuộc tính padding và margin xem nào.

Ảnh Clip

Bạn có thể đoán ra đây là định nghĩa tạo ra một ảnh bị cắt xén so với ảnh gốc. Vâng bạn đã đoán gần đúng. Thực tế thì ảnh dạng này vừa hỗ trợ bạn cắt xén, vừa hỗ trợ các tùy chọn cắt xén như hướng cắt, phần nào bị cắt, sau đó bạn có thể chỉ định tỉ lệ cắt bằng việc set level cho nó. Ảnh dạng này thích hợp cho các hiệu ứng loading mà bạn bắt gặp đâu đó ở splash screen của một số ứng dụng.

Bạn có thể xem cách sử dụng ở link gốc này.

Còn đây là minh họa của mình, mình lại dùng một SeekBar để điều khiển level clip của ảnh.

Sử dụng drawable - Mô phỏng sử dụng clip

Trên đây là các kiến thức còn lại cuối cùng về cách sử dụng các resource dạng drawable. Hi vọng qua loạt bài học này, bạn sẽ có đầy đủ kiến thức, cũng như các ý tưởng để có thể tạo ra các sản phẩm đẹp-độc-lạ cho thị trường các sản phẩm Android từ cộng đồng các lập trình viên người Việt giỏi giang chúng ta. Vì chung quy lại, sản phẩm có đẹp hay không, bên cạnh màu sắc và các bố trí hợp lý các thành phần UI, thì các resource drawable cũng đóng vai trò quan trọng, quan trọng cả cho bộ mặt sản phẩm, và cả cho hiệu năng của sản phẩm nữa.

Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy đánh giá 5 sao nếu thấy thích bài viết, hãy comment bên dưới nếu có thắc mắc, hãy để lại địa chỉ email của bạn để nhận được thông báo mới nhất khi có bài viết mới, và nhớ chia sẻ các bài viết của Yellow Code Books đến nhiều người khác nữa nhé.

ActionBar

Với việc kết thúc bài học số 23, thì bạn cũng đã biết cách sử dụng một số dạng resource thông dụng trong Android rồi. Nhưng sang bài hôm nay, chúng ta tạm ngưng nói đến việc sử dụng các resource tiếp theo nữa, bởi vì nếu nói hoài một chủ đề sẽ rất chán. Vậy chúng ta chen vào một kiến thức khác thú vị hơn, kiến thức về ActionBar, rồi sau đó sẽ quay lại các bài học về resource hay ho khác ở các bài học kế tiếp.

ActionBar Là Gì?

Bạn hãy mở một ứng dụng nào đó bất kỳ trên thiết bị Android của bạn, thì bạn sẽ thấy, ActionBar là một thanh đẹp đẹp ở trên cùng của các màn hình ứng dụng. Ngoài việc hiển thị nhãn (hay gọi với cái tên tiếng anh là title) của màn hình hiện tại ra, ActionBar còn giúp đặt một số Button lên đó. Và thế là ActionBar bỗng trở thành một “xương sống” cho ứng dụng, nó giúp kết nối các màn hình ứng dụng với nhau, giúp người dùng hiểu rõ sự liền mạch của ứng dụng, cũng như giúp họ hiểu nhanh các chức năng của ứng dụng mà họ đang sử dụng.

Hình minh họa sau cho bạn thấy rõ hơn các thành phần bên trong một màn hình ứng dụng Android.

Các thành phần trên màn hình ứng dụng
Các thành phần trên màn hình ứng dụng

Diện Mạo Của ActionBar Qua Các Đời Android

Qua tiêu đề của mục này, bạn cũng biết được rằng ActionBar không hề giống nhau ở các phiên bản của hệ điều hành Android. Vậy thì biết được sự khác nhau này để làm gì? Mời bạn đọc tiếp bài học hôm nay, còn nhiều thứ hay ho lắm.

Android 1.x & 2.x – Phiên Bản Android Với Title Bar

Với “đời” Android này, như bạn cũng đã biết qua khi đọc bài viết của mình về ThemeActionBar ở giai đoạn này chưa ra đời, phía trên màn hình ứng dụng lúc bấy giờ chỉ có một thanh mỏng hiển thị title nên người ta còn gọi là TitleBar.

ActionBar ở đời Android 1.x và 2.x
ActionBar ở đời Android 1.x và 2.x

Android 3.0 – Phiên Bản Android Cho Tablet

Đến giai đoạn này của hệ điều hành, với nhu cầu muốn tập trung vào giao diện trên tablet, thế là ngoài các tinh chỉnh khác của hệ thống liên quan đến SystemBar mà mình không nói đến, thì ActionBar cũng được ra đời. Ý tưởng của ActionBar lúc này tuy được hướng đến cho tablet thôi, nhưng nó được giữ tới bây giờ, đó là ý tưởng về một ActionBar có thể chứa đựng nhiều thông tin, ngoài title ra còn phải có icon app, rồi các menu mở rộng được gom lại vào cùng một nhóm và nằm bên góc phải của ActionBar.

Bạn có thể xem thêm để thấy mối tương quan giữa giai đoạn ActionBar này với giai đoạn Theme.Holo nhé.

ActionBar ở đời Android 3.0
ActionBar ở đời Android 3.0

Android 4.0 Đến Android 4.4 – Phiên Bản Android Cho Cả Phone Và Tablet

Giai đoạn này không chứng kiến nhiều sự thay đổi cho ActionBar, mà chỉ chú trọng vào sự tương thích tốt giữa các thiết bị phone và tablet.

ActionBar ở đời Android 4.0 đến 4.4
ActionBar ở đời Android 4.0 đến 4.4

Android 5.x Đến Nay

Từ khi xuất hiện Theme.Material ở Android 5.x thì ActionBar cũng được nâng cấp đáng kể. Khi này ứng dụng được cung cấp thêm khả năng bố trí các Button lên ActionBar, bên cạnh đó menu mở rộng bên góc phải vẫn xuất hiện.

ActionBar ở đời Android 5.x đến nay
ActionBar ở đời Android 5.x đến nay

Mặc dù ngày nay các thiết bị Android chạy hệ điều hành trước Android 4.0 đã gần như tuyệt chủng. Nhưng qua bài học này, mình muốn các bạn cũng phải nắm được tổng thể các khác nhau của ActionBar ở các thế hệ Android, để hiểu rõ giao diện của ứng dụng của bạn trên các thiết bị.

Nhưng có một tin khá vui, sẽ khiến bạn đỡ đau đầu hơn sau khi đọc qua các mục trên đây của mình. Đó là hiện nay, chúng ta đang được Android hỗ trợ cho một thư viện tương thích ngược có tên là appcompat-v7 (về sau còn có một thư viện mới hơn mang tên androidx). Thư viện tương thích ngược này được khai báo sẵn khi bạn tạo project có hỗ trợ các thiết bị dưới Android 5.0. Bạn có thể xem lại bài 11 để thấy được khai báo của thư viện này. Thư viện này sẽ giúp mang giao diện Material, cùng với ActionBar về cho các hệ điều hành cũ, làm cho giao diện của các ứng dụng ngày nay trở nên nhất quán trên các phiên bản hệ điều hành. Và như vậy thì coi như chúng ta không cần quan tâm đến sự khác biệt ở các hệ điều hành ở các mục trên đây của mình nữa, các mục trên chỉ còn mang tính tham khảo mà thôi.

Xây Dựng ActionBar

Đến đây chúng ta bắt đầu tìm hiểu xem làm thế nào để xây dựng một ActionBar.

Định Nghĩa Button Trên Action Bar

Với việc bạn tạo một project như TourNote, thì ActionBar mặc định cũng được tạo sẵn, và title của ứng dụng cũng được đặt sẵn lên ActionBar, nên chúng ta không quan tâm đến title ở bài hôm nay. Chúng ta hãy tập trung vào việc đặt các Button lên ActionBar như thế nào. Để đặt các Button vào ActionBar, Android buộc bạn phải làm theo cách này, bạn cần định nghĩa các Button vào một file XML và để nó vào thư mục res/menu/. Việc định nghĩa các Button được thực hiện giống như đoạn XML mẫu như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
  
    <item
        android:id="@+id/add"
        android:icon="@drawable/ic_action_new"
        app:showAsAction="always"
        android:title="@string/add"/>
    <item
        android:id="@+id/reset"
        android:icon="@drawable/ic_action_refresh"
        app:showAsAction="always|withText"
        android:title="@string/reset"/>
    <item
        android:id="@+id/about"
        android:icon="@drawable/ic_action_about"
        app:showAsAction="never"
        android:title="@string/about">
    </item>
  
</menu>

Bạn đừng nên code vội, hãy cùng mình tìm hiểu các thành phần và ý nghĩa của định nghĩa này, chúng ta sẽ có cơ hội được code khi xây dựng ActionBar cho TourNote ở bài thực hành bên dưới bài học này.

  • Đầu tiên, bạn sẽ thấy thẻ gốc của file định nghĩa này là thẻ menu. Tại sao lại là menu mà không phải action_bar hay gì gì đó? Thực ra thẻ này được tái sử dụng lại qua các đời hệ điều hành Android. Sơ khởi ban đầu chức năng của nó đúng với tên thẻ, khi mà ActionBar chỉ mới là TitleBar. Lúc đó người ta gọi các Button mà bạn chuẩn bị tạo cho ActionBar như bài học hôm nay bằng cái tên Option Menu. Thôi không cần nhớ nhiều lằng nhằng, bạn chỉ cần chú ý trong thẻ menu này có khai báo thêm một namespace có tên là app.
  • Bên trong menu sẽ chứa đựng các itemmỗi item này là một Button trên ActionBar của chúng ta.
  • Thuộc tính id của item thì bạn đã biết rồi, thuộc tính này định nghĩa ra ID cho Button để chúng ta sử dụng chúng bên trong Java code.
  • Thuộc tính title của item sẽ định nghĩa ra nhãn cho Button. Mặc định nhãn này sẽ không được hiển thị khi mà Button đó được “nhìn thấy” trên ActionBar (lát nữa bạn sẽ biết), nhãn chỉ hiển thị khi Button nằm ở menu mở rộng của ActionBar này. Nhưng dù cho nhãn đó không được hiển thị, thì bạn vẫn có thể nhấn giữ vào một Button của ActionBar để giúp nó hiển thị ra, lát nữa bạn cứ thử xem.
  • Thuộc tính icon của item đơn giản là hiển thị icon cho Button mà thôi.
  • Thuộc tính showAsAction của item giúp xác định độ ưu tiên hiển thị Button lên ActionBar. Độ ưu tiên này là gì? Bạn cũng nên biết rằng ActionBar trên điện thoại có không gian khiêm tốn đúng không nào, và chúng chỉ dàn hàng ngang các Button trên thanh Action đó. Bạn cũng thấy chúng ta đâu có tùy chọn chỉ định kích cỡ cho các Button đó đâu. Vì vậy mà một ActionBar sẽ chỉ hiển thị được vài Button (số lượng nhiều hay ít sẽ tùy loại thiết bị). Do đó độ ưu tiên sẽ giúp ứng dụng xác định Button nào quan trọng hơn sẽ được hiển thị ra ngoài màn hình, Button kém quan trọng hơn sẽ bị đẩy vào menu mở rộng ở góc bên phải. Các cấp độ ưu tiên này từ cao đến thấp bao gồm alwaysifRoom và never. Trong khi hai cấp độ đầu sẽ tranh giành không gian trên ActionBar, thì never lại luôn mang Button vào trong menu mở rộng. Bạn có thể chỉ định thêm “|withText” kèm theo “always” hay “ifRoom” để có thể hiển thị text song song với Button (chỉ định này không phải lúc nào cũng hữu dụng).

Thao Tác Với Các Button Của Action Bar

Với bước trên đây, nếu thực thi chương trình, bạn vẫn chưa thể trông thấy các Button trên ActionBar được. Bạn phải làm thêm một chút với Java code, bạn cần chỉ định menu là file đã định nghĩa trên đây, và chỉ định các sự kiện click cho từng Button nữa.

Do trong thư mục res/menu/ của chúng ta có thể sẽ chứa nhiều file menu, mỗi file như vậy định nghĩa ra một ActionBar cho một màn hình nào đó. Nên để chỉ định file menu nào cho ActionBar nào, bạn chỉ cần override phương thức onCreateOptionsMenu() trong màn hình đó. Rồi chỉ định như code sau.

1
2
3
4
5
6
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.actions, menu);
          
    return super.onCreateOptionsMenu(menu);
}

Như đã nói, bạn hãy chú ý vào dòng getMenuInflater().inflate(R.menu.actions, menu) của code trên, nó giúp bạn lấy menu đã định nghĩa ở bước trên kia R.menu.actions và gắn nó vào menu của màn hình hiện tại.

Sau khi khai báo menu, bạn phải định nghĩa tiếp sự kiện click cho các Button. Chắc bạn đang nghĩ đến sự kiện onClick? Không đâu, với các Button của ActionBar, chúng ta lại sẽ phải override một phương thức khác, chính là onOptionsItemSelected(). Với phương thức này thì bạn chỉ cần lấy id của từng item mà chúng ta đã định nghĩa ra rồi xây dựng logic click cho nó.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case R.id.add:
            // Làm gì đó
            return true;
        case R.id.reset:
            // Làm cái gì đó
            return true;
        case R.id.about:
            // Làm bất cứ cái gì đó
            return true;
    }
          
    return super.onOptionsItemSelected(item);
}

ActionBar Và ToolBar

Từ phiên bản Android 5.0, Google có định nghĩa thêm một khái niệm ToolBar. Thực chất thì ToolBar cũng là một ActionBar nhưng có sự linh động hơnToolBar cũng được hỗ trợ tương thích ngược cho các hệ điều hành cũ hơn thông qua thư viện appcompat-v7. Tuy nhiên ở bài học hôm nay mình chỉ nói đến ActionBar để các bạn làm quen, chúng ta sẽ nâng cấp TourNote lên ToolBar ở một bài học khác nhé.

Thực Hành Xây Dựng ActionBar Cho TourNote

Bây giờ chúng ta sẽ vận dụng tất cả lý thuyết của bài hôm nay để thực hành xây dựng một ActionBar cho TourNote.

Để dễ hình dung công việc mà chúng ta cần làm, mình mời các bạn xem lại màn hình chính của TourNote. Như mình có nói, mỗi màn hình có thể có một ActionBar riêng, nên buổi hôm nay chúng ta chỉ xây dựng ActionBar cho màn hình chính thôi nhé.

Thiết kế ActionBar của TourNote
Thiết kế ActionBar của TourNote

Chúng ta cũng sẽ không quan tâm đến các tab “Ăn Uống”“Tham Quan”,… các tab này tuy cũng gắn với ActionBar đấy, nhưng sẽ được xây dựng sau và được thêm vào ActionBar một cách động bởi người dùng. ActionBar của TourNote mà chúng ta cần xây dựng sẽ có một Button tìm kiếm luôn nằm ngoài màn hình chính, và hai Button“Về Ứng Dụng” và “Giúp Đỡ” được thu gọn vào trong menu mở rộng.

Download Resource

Trước khi tạo ActionBar, chúng ta phải chuẩn bị đầy đủ resource cho ứng dụng. Trong trường hợp này chúng ta chỉ cần icon hình cái kính lúp.

Nếu bạn còn nhớ công cụ Android Asset Studio thì quá tốt. Bạn có thể vào công cụ đó, tìm Clipart là hình kính lúp, rồi chọn Theme là Dark để icon có màu trắng. Rồi down về. Xong.

Tìm hình cho nút Search của ActionBar
Tìm hình cho nút Search của ActionBar

Hoặc bạn lười thì có thể vào link này để down file zip. Các ảnh trong file này có tên ic_action_search.png. Các ảnh này được mình down sẵn về từ Android Asset Studio cho bạn rồi. Bạn hãy tự giải nén rồi để các ảnh vừa down vào các alternative resource tương ứng nhé.

Để icon search của ActionBar vào các thư mục res
Để icon search của ActionBar vào các thư mục res

Sẵn tiện mình cũng thêm một dòng vào resource string, dòng này sẽ làm nhãn cho Button tìm kiếm của chúng ta. Bạn có thể tự thêm vào hoặc tham khảo trên Github ở link mình để bên dưới bài học hôm nay.

Thên text search cho icon của ActionBar
Thên text search cho icon của ActionBar

Định Nghĩa Các Button Cho ActionBar

Bước này, bởi vì thư mục menu/ chưa có trong res/ của chúng ta, nên bạn phải tạo thư mục này ngay. Cách tạo một thư mục resource đã được thực hành khi bạn tạo res/values-vi/ rồi nhé.

Sau khi tạo xong res/menu/, bạn hãy tạo trong đó một Menu resource file có tên main_actions.xml.

Khi file main_actions.xml vừa được tạo ra, bạn có thể nhìn thấy editor của Android Studio hiển thị resource này dưới dạng menu cho bạn chỉnh sửa. Bạn có thể tự học việc sử dụng giao diện để tạo và chỉnh sửa các menu item sau. Còn bây giờ mình khuyên bạn nên chọn tab Text từ editor này để đi qua màn hình code bằng XML nhé.

Thêm item cho ActionBar
Thêm item cho ActionBar

Bạn hãy khai báo các item cho menu của ActionBar như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
  
    <item
        android:id="@+id/search"
        android:icon="@drawable/ic_action_search"
        android:title="@string/menu_item_search"
        app:showAsAction="always" />
  
    <item
        android:id="@+id/about"
        android:title="@string/menu_item_about_app"
        app:showAsAction="never" />
  
    <item
        android:id="@+id/help"
        android:title="@string/menu_item_help"
        app:showAsAction="never" />
  
</menu>

Thao Tác Với Các Button Của ActionBar

Tiếp tục, chúng ta cũng làm như bài học, code dưới đây sẽ khai báo việc sử dụng main_actions.xml làm menu chính thức cho ActionBar của màn hình chính. Bạn hãy vào lớp MainActivity.java và thêm vào phương thức onCreateOptionMenu() như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends AppCompatActivity {
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
  
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main_actions, menu);
          
        return super.onCreateOptionsMenu(menu);
    }
}

Sau cùng là các đoạn code hiện thực kết quả khi nhấn vào các Button của ActionBar thông qua phương thức onOptionsItemSelected(). Hiện tại chúng ta chưa làm gì cho các sự kiện click này, mà chỉ tạm show ra câu thông báo bằng Toast mà thôi.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class MainActivity extends AppCompatActivity {
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
  
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main_actions, menu);
  
        return super.onCreateOptionsMenu(menu);
    }
  
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.search:
                Toast.makeText(this, "Search button selected", Toast.LENGTH_SHORT).show();
                return true;
            case R.id.about:
                Toast.makeText(this, "About button selected", Toast.LENGTH_SHORT).show();
                return true;
            case R.id.help:
                Toast.makeText(this, "Help button selected", Toast.LENGTH_SHORT).show();
                return true;
        }
  
        return super.onOptionsItemSelected(item);
    }
}

Và đây là thành quả khi khởi chạy TourNote.

Kết quả ActionBar của TourNote
Kết quả ActionBar của TourNote

Thật ấn tượng đúng không nào. Tuy nhiên ActionBar hiện tại của TourNote chưa làm được gì cả, bạn có thể thử nhấn, hay thử nhấn giữ vào các Button của ActionBar để kiểm chứng. Chúng ta sẽ hoàn thiện ActionBar của TourNote ở từng bài học liên quan sau này.

Xây Dựng Navigation Drawer

Với bài học hôm trước, bạn đã “nâng cấp” một tí cho TourNote thông qua việc tạo cho ứng dụng này một ActionBar. Hôm nay chúng ta lại sẽ nâng cái cấp này lên một tí ti nữa, thông qua việc xây dựng thêm cho nó một Navigation Drawer.

Giới Thiệu Về Navigation Drawer

Có lẽ cái tên Navigation Drawer, hay nhiều bạn vẫn gọi là Left Menu, hoặc Slide Menu, không có gì xa lạ với chúng ta cả. Ai cũng biết về nó, ai cũng sử dụng nó hằng ngày. Navigation Drawer được Google giới thiệu vào năm 2013, ngay sau khi ActionBar được trình làng khoảng 2 năm. Navigation Drawer này chỉ đơn giản là một thanh menu được ẩn đi về phía bên trái màn hình. Nó được hiển thị ra khi người dùng nhấn vào icon menu trên Action Bar. Giới thiết kế gọi cái icon menu này bằng một cái tên khá hay: “icon Hamburger”, bởi vì trông nó giống như một cái hamburger… bạn tưởng tượng đi nào.

Navigation Drawer được mở ra khi nhấn vào icon hamburger
Navigation Drawer được mở ra khi nhấn vào icon hamburger

Cũng giống như ActionBarNavigation Drawer mong muốn đem lại cho user một trải nghiệm rõ ràng hơn trên các ứng dụng phức tạp. Khi đó các chức năng chính của ứng dụng dường như được để hết cả lên thanh này (và cả ở ActionBar nữa, nhưng thường thì ActionBar chỉ chứa các chức năng quan trọng và được truy xuất cực kỳ nhiều mà thôi). Và cũng không quá khi nói rằng Navigation Drawer này mới chính thức là “xương sống” rõ ràng nhất cho ứng dụng.

Navigation Drawer mẫu
Navigation Drawer mẫu

Ngoài các thông tin về Navigation Drawer của mình trên đây, bạn có thể vào link này của Google để đọc một số thông tin hữu ích về mặt thiết kế liên quan đến thành phần này nhé.

Tìm Hiểu Các Layout Và Thành Phần Mới

Nếu như bạn còn nhớ, mình đã dành một bài để nói nhiều nhất có thể các layout được dùng phổ biến trong Android, trong bài viết đó, FrameLayoutLinearLayoutRelativeLayout và TableLayout đã được nhắc đến. Và cuối bài viết, mình có nói rằng là sẽ còn nhiều layout khác liên quan đến các giao diện đặc trưng khác chứ không riêng gì các layout phổ biến này. Và đến bài học hôm nay bạn đã có dịp làm quen với một trong số các layout đặc trưng cho Navigation Drawer. Mình xin giới thiệu, một layout mới mang tên DrawerLayout.

DrawerLayout

Như đã nói, DrawerLayout là một layout đặc biệt, nó chuyên dùng để tạo Navigation Drawer. Layout này nằm trong gói support-v4 và support-v13 để hỗ trợ sự tương thích ngược đến với các hệ điều hành cũ hơn. Nếu bạn thắc mắc tại sao lại có nhiều gói support-vx này thì hãy yên tâm, mình dự định dành một bài viết riêng để nói rõ về các thư viện support này nhé.

Và bởi vì layout này đặc biệt là dùng kèm theo Navigation Drawer, nên nó hầu như chẳng có nhiều nguyên tắc lắm đâu. Việc của bạn chỉ cần khai báo một DrawerLayout ở gốc của một Activity nào đó, rồi khai báo bên trong layout đó hai thành phần con, đại loại như thế này, là được.

1
2
3
4
5
6
7
8
9
10
11
<android.support.v4.widget.DrawerLayout
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
  
    <!-- Thành phần con thứ nhất -->
  
    <!-- Thành phần con thứ hai -->
  
</android.support.v4.widget.DrawerLayout>

Mình xin nói rõ một chút về vai trò của hai thành phần con trong DrawerLayout.

  • Thành phần con thứ nhất, chính là giao diện chính của ứng dụng. Một lát nữa khi thực hành xây dựng ứng dụng TourNote, bạn phải thay đổi một chút cấu trúc của màn hình chính TourNote, sao cho trở thành thành phần con thứ nhất này.
  • Thành phần con thứ hai, chính là thành phần của left menu. Thành phần này như đã nói, bình thường sẽ bị ẩn đi, đến khi người dùng nhấn vào icon hamburger hoặc vuốt từ bên trái màn hình sang phải, sẽ được hiển thị. Sau đó nếu người dùng nhấn lại vào icon hamburger, hoặc nhấn vào vùng ngoài left menu, hoặc vuốt từ bên phải màn hình sang trái, thì lại ẩn đi.

Chúng ta sẽ hiểu rõ hơn về DrawerLayout và từng thành phần con của nó khi tiến hành xây dựng Navigation Drawer cho TourNote ở bên dưới đây.

Xây Dựng Navigation Drawer

Trước khi đi vào chi tiết cách xây dựng Navigation Drawer, mình xin được nói rõ rằng, sẽ có hai cách để bạn có thể xây dựng thành phần này cho ứng dụng.

Cách thứ nhất, chẳng tốn công sức gì cả, khi bạn tạo mới bất kỳ project Android nào, đến bước chọn một template (bạn có thể xem lại bài 3 để hiểu các khái niệm liên quan đến tạo mới một project Android), thì bạn cứ chọn Navigation Drawer Activity như hình dưới. Tùy chọn này giúp bạn tạo một ứng dụng có cả Navigation Drawer lẫn Floating Action Button. Thật là đơn giản.

Navigation Drawer Activity lúc tạo mới project
Navigation Drawer Activity lúc tạo mới project

Cách thứ hai, là cách bạn phải bỏ công sức ra xây dựng từng bước như dưới đây chúng ta sẽ làm với TourNote. Mình thích cách trên kia, nhưng với các bài học của mình thì mình muốn cùng các bạn đi theo hướng gian khổ ở cách này. Bởi vì sau khi xây dựng thủ công mọi thứ, chắc chắn các bạn sẽ hiểu rõ hơn về thành phần giao diện này, và các kiến thức liên quan. Sau đó, tự bạn có thể tùy chỉnh, thêm thắt vài thứ vào giao diện một cách dễ dàng hơn.

Thực Hành Xây Dựng Navigation Drawer Cho TourNote

Đây là thiết kế ban đầu về Navigation Drawer của TourNote.

Thiết kế ban đầu về Navigation Drawer của TourNote
Thiết kế ban đầu về Navigation Drawer của TourNote

Như những gì bạn đã đọc qua trên đây, chắc bạn cũng đã hình dung ra các bước mà bài thực hành này sẽ xây dựng. Bao gồm, thay đổi activity_main.xml sao cho layout gốc chính là DrawerLayout nè, rồi xây dựng các thành phần bên trong DrawerLayout nè, và cuối cùng là xây dựng các Java code liên quan nè. Chúng ta cùng vào chi tiết.

Download Resource

Do có xuất hiện thêm một vài icon nữa, bao gồm icon hình dấu chấm than và chấm hỏi, nên bạn có thể vào Android Asset Studio để tự tìm hai icon này.

Dùng công cụ Android Asset Studio để download icon cần thiết
Dùng công cụ Android Asset Studio để download icon cần thiết

Hoặc bạn có thể vào link này để down file zip về rồi giải nén và để các ảnh vừa down vào các alternative resource tương ứng nhé.

Thêm Một Số Thông Số Vào dimens.xml

Chúng ta sẽ tuân thủ các yêu cầu về mặt thiết kế ở link này của Google. Nên bước này chúng ta nên định nghĩa thêm một số kích thước cho Navigation Drawer vào file dimens.xml, bạn chú ý các dòng được tô sáng.

1
2
3
4
5
6
7
8
9
10
11
12
<resources>
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>
 
    <!-- Main Activity components -->
    <dimen name="empty_icon_width">60dp</dimen>
    <dimen name="empty_icon_height">60dp</dimen>
    <dimen name="navigation_header_height">160dp</dimen>
    <dimen name="navigation_item_height">48dp</dimen>
    <dimen name="navigation_item_icon_size">24dp</dimen>
</resources>

Khai Báo Thêm Thư Viện Trong build.gradle

Chúng ta khoan hãy bắt tay vào xây dựng Navigation Drawer ngay, vì nếu bắt tay vào xây dựng, chắc chắn sẽ có báo lỗi xảy ra với project, là do các thành phần trong Navigation Drawer ở các bước sau đòi hỏi project phải có thư viện com.android.support:design bên trong nó.

Chắc hẳn bạn sẽ không hoang mang và hiểu rõ những gì mình nói trên đây nếu đã đọc qua bài 11 của mình. Nếu đã đọc và hiểu rồi, thì bạn hãy mở build.gradle ở cấp độ module lên, và thêm vào dòng được tô sáng sau vào khối dependencies của file này nhé.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apply plugin: 'com.android.application'
 
android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.yellowcode.tournote"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}
 
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'com.android.support:design:28.0.0'
}

Bạn nên sync lại project sau khi thêm vào dòng trên kia, rồi hẵn qua bước tiếp theo bên dưới.

Xây Dựng DrawerLayout

Nào chúng ta bắt đầu xây dựng Navigation Drawer, bằng cách xây dựng Drawer Layout.

Chúng ta cùng mở lại giao diện chính của TourNote ra, chính là file activity_main.xml. Tại đây, như các bài thực hành từ trước giờ, thẻ gốc của file này vẫn là ConstraintLayout.

Với thay đổi ở bước này, bạn chỉ chuyển toàn bộ giao diện gốc của TourNote này thành thành phần thứ nhất. Như vậy giao diện gốc xưa kia giờ đây nằm trong một thẻ có tên android.support.v4.widget.DrawerLayout. Bạn chú ý ghi đầy đủ đường dẫn đến gói support.v4.widget luôn nhé, vì đây là layout được lấy từ thư viện support như mình có nói đến trên kia mà.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main_drawer"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <!-- Content -->
    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
 
        <TextView
            android:id="@+id/activity_main_tv_empty"
            style="@style/InformationTextView"
            android:text="@string/mainscreen_empty_note"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
 
        <ImageView
            android:id="@+id/imageView"
            android:layout_width="@dimen/empty_icon_width"
            android:layout_height="@dimen/empty_icon_height"
            android:layout_marginStart="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginRight="8dp"
            android:scaleType="fitCenter"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/activity_main_tv_empty"
            app:srcCompat="@drawable/empty_note" />
 
    </android.support.constraint.ConstraintLayout>
</android.support.v4.widget.DrawerLayout>

Bạn khoan run ứng dụng lên vội, vì chưa có thay đổi gì đâu.

Xây Dựng Thành Phần Con Thứ Hai

Bạn đã biết thành phần con thứ nhất trong DrawerLayout chính là ConstraintLayout cũ được chúng ta nhét vào trong DrawerLayout rồi đúng không nào. Vậy nên bước này chúng ta chỉ cần xây dựng thành phần con thứ hai, chính là giao diện cho left menu. Dưới đây là một vài ý quan trọng cho thành phần này.

  • Với layout ở thành phần con thứ hai này, bạn xây dựng chúng bằng bất cứ layout nào cũng được. Nhưng mình khuyên bạn nên dùng NavigationView. Đây cũng là một layout được chuyên dùng trong Navigation Drawer, nó giúp định nghĩa ra một menu layout chuẩn, với các màu sắc và kích cỡ mặc định, và khi sử dụng nó, bạn không cần phải lo lắng canh chỉnh chiều rộng của left menu này như thế nào.
  • Bởi vì NavigationView là con cháu của FrameLayout nên nó không giỏi lắm trong việc sắp xếp các thành phần UI con vào trong nó. Tốt nhất bạn nên xây dựng thêm một layout linh động hơn vào con của NavigationView này. Như code dưới đây mình thêm LinearLayout.
  • Dù bạn có dùng layout nào cho thành phần này, cũng đừng quên khai báo thuộc tính android:layout_gravity=”start”, nó giúp neo layout này vào bên trái của thành phần con thứ nhất trên kia.

Và đây là code của toàn bộ giao diện activity_main.xml. Code mới ở mục này được thêm vào như những dòng được tô sáng dưới đây.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main_drawer"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <!-- Content -->
    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
 
        <TextView
            android:id="@+id/activity_main_tv_empty"
            style="@style/InformationTextView"
            android:text="@string/mainscreen_empty_note"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
 
        <ImageView
            android:id="@+id/imageView"
            android:layout_width="@dimen/empty_icon_width"
            android:layout_height="@dimen/empty_icon_height"
            android:layout_marginStart="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginRight="8dp"
            android:scaleType="fitCenter"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/activity_main_tv_empty"
            app:srcCompat="@drawable/empty_note" />
 
    </android.support.constraint.ConstraintLayout>
 
    <!-- Left Menu -->
 
    <android.support.design.widget.NavigationView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true">
 
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/white"
            android:orientation="vertical">
 
            <!-- Header -->
 
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="@dimen/navigation_header_height"
                android:background="@color/colorPrimary"
                android:gravity="bottom"
                android:orientation="vertical"
                android:paddingBottom="@dimen/activity_vertical_margin"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                android:theme="@style/ThemeOverlay.AppCompat.Dark">
 
                <ImageView
                    android:id="@+id/activity_main_imv_avatar"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:paddingTop="@dimen/activity_vertical_margin"
                    app:srcCompat="@mipmap/ic_launcher_round" />
 
                <TextView
                    android:id="@+id/activity_main_tv_user_name"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:paddingTop="@dimen/activity_vertical_margin"
                    android:text="Yellow Code"
                    android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
 
                <TextView
                    android:id="@+id/activity_main_tv_email"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="yellowcode.books@gmail.com" />
 
            </LinearLayout>
 
            <!-- Item Info -->
 
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="@dimen/navigation_item_height"
                android:layout_marginTop="3dp"
                android:background="@android:color/white"
                android:gravity="center_vertical"
                android:orientation="horizontal"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin">
 
                <ImageView
                    android:layout_width="@dimen/navigation_item_icon_size"
                    android:layout_height="@dimen/navigation_item_icon_size"
                    android:src="@drawable/ic_action_info" />
 
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginLeft="32dp"
                    android:text="@string/menu_item_about_app" />
 
            </LinearLayout>
 
            <!-- Item Help -->
 
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="@dimen/navigation_item_height"
                android:background="@android:color/white"
                android:gravity="center_vertical"
                android:orientation="horizontal"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin">
 
                <ImageView
                    android:layout_width="@dimen/navigation_item_icon_size"
                    android:layout_height="@dimen/navigation_item_icon_size"
                    android:src="@drawable/ic_action_help" />
 
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginLeft="32dp"
                    android:text="@string/menu_item_help" />
 
            </LinearLayout>
 
        </LinearLayout>
 
    </android.support.design.widget.NavigationView>
</android.support.v4.widget.DrawerLayout>

Chú ý rằng bên trong NavigationView này mình đã lồng vào một LinearLayout với mục đích như đã nói đến trên đây. Bên trong LinearLayout này mình xây dựng một header để hiển thị thông tin người dùng. Phía dưới header là hai item của left menu, đó là item info và item help. Hai item này tốt nhất nên nằm trong một ListView, nhưng vì chúng ta chưa được học qua ListView, nên mình chỉ xây dựng tạm như thế này để nhìn cho đẹp mắt thôi chứ chưa có logic gì cho việc click lên ẻm đâu nhá.

Chỉnh Sửa MainActivity.java

Đến bước này, nếu bạn thực thi chương trình, cũng sẽ không có bất cứ giao diện cho left menu nào xuất hiện đâu nhé. Thực chất chúng đã nằm sẵn ở bên tay trái màn hình thiết bị nhờ vào việc sắp xếp của DrawerLayout. Việc tiếp theo của chúng ta là xây dựng một số hành động bên MainActivity.java nữa để menu này có cơ hội được xuất hiện.

Thứ nhất, bạn phải đảm bảo cho icon hamburger xuất hiện trên ActionBar bằng hai dòng lệnh sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class MainActivity extends AppCompatActivity {
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
  
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeButtonEnabled(true);
    }
  
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main_actions, menu);
  
        return super.onCreateOptionsMenu(menu);
    }
  
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.search:
                Toast.makeText(this, "Search button selected", Toast.LENGTH_SHORT).show();
                return true;
            case R.id.about:
                Toast.makeText(this, "About button selected", Toast.LENGTH_SHORT).show();
                return true;
            case R.id.help:
                Toast.makeText(this, "Help button selected", Toast.LENGTH_SHORT).show();
                return true;
        }
  
        return super.onOptionsItemSelected(item);
    }
}

Sau đó, dĩ nhiên rồi, bạn khai báo DrawerLayout trong Java code, để thiết lập vài logic cho nó.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class MainActivity extends AppCompatActivity {
  
    private DrawerLayout drawerLayout;
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
  
        drawerLayout = findViewById(R.id.activity_main_drawer);
  
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeButtonEnabled(true);
    }
  
  
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main_actions, menu);
  
        return super.onCreateOptionsMenu(menu);
    }
  
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.search:
                Toast.makeText(this, "Search button selected", Toast.LENGTH_SHORT).show();
                return true;
            case R.id.about:
                Toast.makeText(this, "About button selected", Toast.LENGTH_SHORT).show();
                return true;
            case R.id.help:
                Toast.makeText(this, "Help button selected", Toast.LENGTH_SHORT).show();
                return true;
        }
  
        return super.onOptionsItemSelected(item);
    }
}

Cuối cùng, chúng ta lại dùng đến một thành phần có tên là ActionBarDrawerToggle. Thành phần này được gắn vào ActionBar, làm nhiệm vụ điều khiển việc đóng mở DrawerLayout cho chúng ta. Sau đây là các đoạn code khai báo, gắn vào ActionBar, và đồng bộ thành phần ActionBarDrawerToggle này với Activity. Đoạn code khai báo sau cần sự định nghĩa thêm hai resource string, đó là navigation_drawer_open và navigation_drawer_close, bạn có thể xem thêm định nghĩa string này ở link project ở cuối bài học hôm nay.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class MainActivity extends AppCompatActivity {
  
    private DrawerLayout drawerLayout;
    private ActionBarDrawerToggle drawerToggle;
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
  
        drawerLayout = findViewById(R.id.activity_main_drawer);
        drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
        drawerLayout.addDrawerListener(drawerToggle);
  
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeButtonEnabled(true);
    }
  
    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        // Sync the toggle state after onRestoreInstanceState has occurred.
        drawerToggle.syncState();
    }
  
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        drawerToggle.onConfigurationChanged(newConfig);
    }
  
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main_actions, menu);
  
        return super.onCreateOptionsMenu(menu);
    }
  
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if(drawerToggle.onOptionsItemSelected(item)) {
            return true;
        }
  
        switch (item.getItemId()) {
            case R.id.search:
                Toast.makeText(this, "Search button selected", Toast.LENGTH_SHORT).show();
                return true;
            case R.id.about:
                Toast.makeText(this, "About button selected", Toast.LENGTH_SHORT).show();
                return true;
            case R.id.help:
                Toast.makeText(this, "Help button selected", Toast.LENGTH_SHORT).show();
                return true;
        }
  
        return super.onOptionsItemSelected(item);
    }
}

Giờ thì bạn có thể thực thi ứng dụng lên xem được rồi nhé.

Navigation Drawer kết quả cuối cùng
Navigation Drawer kết quả cuối cùng

Làm Quen Với Activity

Còn nhớ, đã rất lâu rồi, từ bài học số 5, chúng ta đã nói sơ qua cấu tạo cơ bản của một ứng dụng Android, trong đó các bạn đã biết đến một chút khái niệm và vai trò của Activity rồi.

Nhưng trước đó, ở bài học số 3, khi bạn còn chưa biết đến Android cần có những gì, bạn đã phải tạo cho chính bạn một Activity đầu tiên, có tên là MainActivity, và chúng ta đã cùng xây dựng các dòng code trên Activity đó đến tận bây giờ.

Vậy tính ra bạn đã hiểu Activity là gì rồi đúng không. Vậy bài học hôm nay mình sẽ viết gì. Tất nhiên còn một số kiến thức thú vị khác xoay quanh Activity. Như làm sao để tạo thêm nhiều Activity khác. Làm sao để truyền dữ liệu qua các Activity. Cách quản lý các Activity hiệu quả,… sẽ lần lượt được mình trình bày từ bài học hôm nay.

Activity Là Gì?

Vâng, câu trả lời cho câu hỏi này bạn đã biết rồi. Nếu bạn muốn nhớ lại, thì mình cũng đã có nói về Activity ở bài học số 5. Cơ bản là vì yêu cầu về chức năng của các ứng dụng trên thiết bị thông minh rất phức tạp. Như bạn cũng biết thông qua ứng dụng TourNote rồi đó. Nhưng việc nhồi nhét tất cả các yêu cầu và chức năng của một ứng dụng nào đó vào một màn hình nhỏ bé của thiết bị là điều không thể (điều này khác với các ứng dụng trên desktop, đôi khi chỉ cần một cửa sổ là đủ để quản lý). Do đó rất cần thiết khi chúng ta buộc phải tách các thành phần khác nhau của ứng dụng vào từng Activity, mỗi Activity như vậy được xem như một chức năng cơ bản của ứng dụng.

Việc tổ chức ứng dụng thành các Activity như thế nào là ở bạn. Thông thường ở giai đoạn sơ khởi, khi ứng dụng còn là bản thiết kế ở trên giấy, cũng đã rất dễ để chúng ta hình dung ra cấu trúc của các Activity trong ứng dụng. Và như mình có nói, mỗi Activity là một thành phần chức năng cơ bản của ứng dụng. Và cũng vì Activity cũng dính dáng nhiều đến UI của ứng dụng, nên dường như mỗi Activity sẽ chứa tập hợp các UI có chức năng như nhau.

Để dễ hiểu hơn. Bạn hãy xem mình dự định sẽ tổ chức bao nhiêu đây Activity cho TourNote.

Activity - Dự kiến các activity trong TourNote

Vậy TourNote của chúng ta sẽ có các Activity MainActivityDetailActivityContactActivity và EditNoteActivity.

  • MainActivity – Đảm nhiệm vai trò hiển thị danh sách các ghi chú mà người dùng đã tạo trước đó theo chủ đề (mỗi chủ đề là một tab). Mỗi ghi chú này chỉ được hiển thị vắn tắt thông tin. Ngoài ra MainActivity còn chứa đựng Navigation Drawer (chính là left menu) mà bạn đã tạo ra từ bài học trước.
  • DetailActivity – Đảm nhiệm vai trò hiển thị đầy đủ thông tin của một ghi chú, khi mà người dùng click chọn vào một phần tử trên danh sách các ghi chú ở MainActivity.
  • EditActivity – Giao diện này sẽ được dùng chung cho cà trường hợp tạo mới lẫn sửa chữa một ghi chú.
  • ContactActivity – Giao diện giới thiệu ứng dụng và liên hệ tác giả. Giao diện này sẽ được chúng ta xây dựng từ bài học hôm nay.

Tổ chức các Activity trên đây là đối với TourNote, còn bạn, nếu bạn có xây dựng riêng bạn một ứng dụng nào đó, thì hãy tự tổ chức các Activity xem sao nhé.

Tạo Mới Một Activity Như Thế Nào?

Không quá khó để bạn tạo mới một Activity. Tuy nhiên bạn nên nhớ là khi tạo Activity, về lý thuyết thì bạn phải trải qua các bước sau đây, sót một bước là có lỗi xảy ra đấy nhé.

Tạo Mới XyzActivity.java Và activity_xyz.xml

Bộ đôi “quyền lực” này thường đi chung với nhau. Bạn có thể kiểm chứng bằng việc thực hành trên Activity mặc định từ trước đến giờ, chúng là MainActivity.java và activity_main.xml. Sở dĩ chúng đi một cặp với nhau, là vì một file sẽ chịu trách nhiệm chính trong việc hiển thị UI của màn hình (file xml), và một file chịu trách nhiệm xử lý logic cho màn hình đó (file java).

Tương tự như “kế hoạch” xây dựng các Activity cho TourNote của chúng ta trên kia, sau này chúng ta sẽ có các bộ đôi Activity như sau: DetailActivity.java và activity_detail.xmlContactActivity.java và activity_contact.xmlEditNoteActivity.java và activity_edit_note.xml.

Thật đáng mừng rằng Android Studio đã hỗ trợ chúng ta công cụ để tạo mới một bộ các Activity một cách dễ dàng. Lát nữa đến phần thực hành tạo Activity cho TourNote bạn sẽ hiểu rõ hơn.

Khai Báo Activity Mới Với Manifest

Như mình có nói qua, mỗi Activity khi tạo mới đều phải được khai báo thành một thẻ activity trong file Manifest.

Do đó dĩ nhiên là DetailActivityContactActivity, và EditNoteActivity của TourNote sẽ không ngoại lệ. Đều phải nằm trong các thẻ activity này.

Nhưng cũng có thêm một tin đáng mừng rằng là nếu bạn sử dụng công cụ để tạo bộ java và xml cho Activity như mình có nói trên kia, thì mặc định bạn sẽ được hệ thống khai báo luôn vào Manifest. Một lát nữa ở bài thực hành mình sẽ nói cụ thể luôn nhé.

Sử Dụng Intent Để Kích Hoạt Activity

Intent ư? Intent là gì? Ở bài học hôm nay bạn sẽ chưa thực sự đi sâu vào Intent đâu. Bạn chỉ cần biết cách thức dùng Intent để kích hoạt, hay khởi động một Activity mới toanh mà bạn mới tạo. Cách kích hoạt này người ta gọi là kích hoạt kiểu tường minh của Intent. Khi đến bài thực hành bên dưới chúng ta sẽ xem cách kích hoạt tường minh của Intent là như thế nào.

Tuy nhiên nếu bạn quan tâm, Intent đã được mình nhắc đến một chút ở mục này, bạn có thể đọc qua tí để nhớ lại. Các kiến thức đầy đủ liên quan đến Intent sẽ được mình nói rõ hơn ở bài học sau nhé.

Thực Hành Tạo Mới ContactActvity Cho TourNote

Trên kia là các lý thuyết. Nhưng khá quan trọng. Bạn hãy ghi nhớ cho việc thực hành này đây. Các bước của bài thực hành không đi xa hơn những lý thuyết trên kia đã nói đến.

Tạo Mới ContactActivity.java Và activity_contact.xml

Đấy, bạn thấy có khác lý thuyết đâu nào. Để tạo bộ đôi giao diện này, như mình có nói chúng ta sẽ nhờ Android Studio tạo cho. Người ta gọi cách tạo này chính là tạo theo wizard (hiểu nôm na theo nghĩa tiếng Việt là có công cụ hỗ trợ chúng ta tạo theo từng bước).

Để bắt đầu, ở cửa sổ Project, bạn hãy để vệt sáng ở package sẽ chứa file Activity java mà bạn muốn tạo. Rồi chọn theo menu File > New > Activity > Empty Activity. Hoặc nhấn chuột phải và chọn New > Activity > Empty Activity như hình dưới đây.

Đường dẫn giúp tạo mới một Activity từ Android Studio
Đường dẫn giúp tạo mới một Activity từ Android Studio

Lưu ý là tuy bạn nhìn thấy nhiều thể loại Activity khi đi vào menu này, nhưng việc chọn Empty Activity theo mình là tốt nhất. Nó giúp chúng ta có được một Activity và một giao diện vừa đủ để dùng. Các chọn lựa với các Activity còn lại sẽ tự động nhét thêm nhiều đoạn code, để tạo thành các template như khi bạn tạo mới project vậy. Việc tự phát sinh thêm nhiều code như vậy sẽ chỉ khiến bạn khó quản lý hơn thôi.

Sau khi bạn chọn lựa ở bước trên. Thì một dialog xuất hiện, dialog này cũng quen thuộc với bạn rồi.

  • Tại đây bạn nhập tên Activity vào ô Activity NameActivity mới của chúng ta có tên ContactActivity.
  • Đảm bảo checkbox Generate Layout File được check, vì chúng ta muốn wizard khi này sẽ tự tạo tên file xml cho chúng ta. Cơ bản thì tên file xml này được tự động tạo ra dựa vào những gì bạn gõ vào Activity Name trên kia nên khá chuẩn, do đó bạn không cần chỉnh sửa gì cả.
  • Bạn đảm bảo Launcher Activity không check, vì đây không phải là Activity mặc định được mở khi ứng dụng thực thi, một ứng dụng chỉ có một Activity được gọi là Launcher thôi và đó chính là MainActivity của chúng ta rồi.
  • Package name thì chúng ta giữ như mặc định.
  • Cuối cùng, hãy đảm bảo Java là ngôn ngữ được chọn trong mục Source Language, vì bài học Android này chúng ta vẫn viết bằng ngôn ngữ Java.
Cửa số khai báo thông tin cho Activity sắp tạo
Cửa số khai báo thông tin cho Activity sắp tạo

Dễ dàng đúng không nào. Sau khi bạn nhấn Finish, bạn sẽ thấy xuất hiện bộ đôi 2 file mà chúng ta mong muốn. Mình nói sơ qua bộ đôi này một chút.

  • Với ContactActivity.javaActivity này có sẵn phương thức onCreate(). Trong đó, giao diện xml của nó được chỉ định luôn ở hàm setContentView().
  • Với activity_contact.xml. Giao diện này được tạo mới mặc định thẻ gốc là ConstraintLayout.

Khai Báo Activity Mới Với Manifest

Như mình có trình bày ở mục trên. Rằng nếu bạn chọn cách thức tạo mới Activity bằng wizard, bạn sẽ không cần bất cứ khai báo thủ công nào cho Manifest. Tuy nhiên chúng ta cứ mở AndroidManifest.xml ra kiểm tra lại nào.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="utf-8"?>
    package="com.yellowcode.tournote">
 
    <supports-screens
        android:largeScreens="true"
        android:normalScreens="true"
        android:smallScreens="false"
        android:xlargeScreens="true" />
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".ContactActivity"></activity>
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
 
</manifest>

Bạn thấy đó, Activity mới được khai báo với vỏn vẹn một dòng trong Manifest. Thẻ cần khai báo chính là thẻ activity. Trong thẻ này có một thuộc tính duy nhất là android:name với nội dung .ContactActivity. Bạn có thể khai báo đầy đủ đường dẫn đến thuộc tính android:name này như sau com.yellowcode.tournote.ContactActivity. Nhưng vì com.yellowcode.tournote đã được định nghĩa trong thuộc tính package rồi, nên tóm lại chỉ cần khai báo vắn tắt .ContactActivity là được.

Nếu nhìn vào thẻ kế bên, cũng thẻ activity, bạn sẽ thấy thẻ này ngoài thuộc tính android:name ra nó còn có thẻ con intent-filter nữa. Thẻ intent-filter sẽ được mình trình bày đầy đủ ở bài học về Intent. Nhưng bạn có thể biết trước rằng Activity nào có thẻ con intent-filter như này sẽ được hệ thống khởi chạy đầu tiên khi ứng dụng thực thi. Bạn có thể thử chuyển thẻ intent-filter từ .MainActivity sang .ContactActivity rồi thực thi chương trình trên máy thật hoặc máy ảo để tự kiểm chứng nhé. Nhưng nhớ phải chuyển ngược lại sau đó.

Nếu như giờ đây bạn thực thi chương trình, thì cũng chẳng có gì xảy ra cả, vì Activity mới tuy được tạo ra và khai báo đúng đắn, nhưng chưa được kích hoạt bởi bất cứ dòng lệnh nào. Chúng ta cùng qua bước kế tiếp.

Sử Dụng Intent Để Kích Hoạt ContactActivity

Lại một lần nữa chúng ta cứ sử dụng Intent đi rồi sang bài học kế tiếp sẽ cùng thảo luận về em nó.

Như kịch bản thì ContactActivity này sẽ chứa đựng hai nội dung, đó là Về ứng dụng và Giúp đỡ. Việc phân biệt nội dung nào cần hiển thị và hiển thị như thế nào là ở bài học kế tiếp. Việc hôm nay chúng ta cần làm là xây dựng tiếp ở menu ActionBar để khi touch vào bất cứ item menu nào cũng sẽ kích hoạt ContactActvitity này.

Vậy thì chúng ta phải mở MainActivity.java lên. Đến phương thức onOptionsItemSelected(). Xóa bỏ các câu lệnh Toast.makeText() ở hai item R.id.about và R.id.help và thay bằng các câu lệnh kích hoạt ContactActivity. Code như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    if(drawerToggle.onOptionsItemSelected(item)) {
        return true;
    }
  
    switch (item.getItemId()) {
        case R.id.search:
            Toast.makeText(this, "Search button selected", Toast.LENGTH_SHORT).show();
            return true;
        case R.id.about:
            Intent intent = new Intent(this, ContactActivity.class);
            startActivity(intent);
            return true;
        case R.id.help:
            intent = new Intent(this, ContactActivity.class);
            startActivity(intent);
            return true;
    }
  
    return super.onOptionsItemSelected(item);
}

Dòng code kích hoạt trên có các chú ý sau.

  • Chúng ta khởi tạo một Intent để chứa đựng thông tin về kích hoạt Activity mới. Intent này được khởi tạo với hai tham số truyền vào, như đã nói, các bạn nên nhớ rằng đây được gọi là Intent tường minh. Tham số đầu tiên this chính là Context mà chúng ta sẽ tìm hiểu ở bài khác, vì Activity là một Context nên bạn truyền vào từ khóa this như vậy là được. Tham số thứ hai chính là Activity mà bạn muốn kích hoạt, chúng ta truyền vào ContactActivity.class.
  • Phương thức startActivity(Intent) chính thức kích hoạt Activity với các thông tin mà Intent vừa cung cấp.

Giờ thì bạn có thể thực thi chương trình để kiểm chứng rồi. Bạn hãy thử nhấn vào từng item trên menu ActionBar để xem kết quả nhé. Màn hình bên phải dưới đây chính là ContactActivity đấy. Mỗi khi đến ContactActivity, bạn phải nhấn nút back của thiết bị để quay về MainActivity. Để chuyên nghiệp hơn, chúng ta sẽ xây dựng nút back trên ActionBar ở bước tiếp theo dưới đây.

Kiểm tra trên UI hoạt động của Activity mới
Kiểm tra trên UI hoạt động của Activity mới

Xây Dựng Chức Năng Back Trên ActionBar Của ContactActivity

Bởi vì nhấn nút back của thiết bị ở màn hình ContactActivity để quay về MainActivity thật là phiền phức và thiếu chuyên nghiệp. Nên chúng ta sẽ phải làm thêm một bước nhỏ nữa thôi.

Giống như bài học trước. Chúng ta đảm bảo icon back xuất hiện bằng hai dòng code sau.

1
2
3
4
5
6
7
8
9
10
11
public class ContactActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_contact);
 
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeButtonEnabled(true);
    }
}

Rồi đón lấy sự kiện onOptionsItemSelected(), lần này bạn check với item có id là android.R.id.home, đây là id sẵn có của hệ thống đối với item back này. Khi item này được click, chúng ta chỉ đơn giản là gọi phương thức finish() để kết thúc một Activity. Bài học sau chúng ta sẽ xem việc kết thúc một Activity thực sự là như thế nào.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ContactActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_contact);
 
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeButtonEnabled(true);
    }
 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                finish();
                return true;
        }
 
        return super.onOptionsItemSelected(item);
    }
}

Nào giờ thì bạn hãy chạy lại chương trình để kiểm chứng nhé.

Activity mới của TourNote, có nút back trên ActionBar
Activity mới của TourNote, có nút back trên ActionBar

Tìm Hiểu Back Stack

ếu như ở bài hôm trước, chúng ta đã cùng nhau tạo một Activity mới cho TourNote – ContactActivity. Chắc chắn bạn đã cảm thấy thích thú với việc từ một Activity ban đầu, là MainActivity, nhấn vào item menu trên ActionBar một cái, hệ thống sẽ chuyển sang màn hình mới chính là ContactActivity. Rồi nhấn Back button ở ActionBar hay Back ở System Bar sẽ dẫn bạn quay về MainActivity ban đầu. Vậy thì có khi nào bạn thắc mắc rằng các màn hình bên trong một ứng dụng Android sẽ luân phiên hiển thị ra như thế nào? Một màn hình hiển thị ra thì màn hình kia đi về đâu, có bị xóa khỏi hệ thống không? Tại sao nhấn nút Back ở System Bar lại giúp quay về màn hình trước trong khi chúng ta chẳng có code gì chỗ này cả? Vậy nhấn nhiều nút Back thì sẽ đi đến đâu? Vân vân…

Thì bài học hôm nay chúng ta sẽ chỉ đi vào lý thuyết, để trả lời cho các câu hỏi trên kia thông qua việc giải nghĩa khái niệm và cách hoạt động của Back Stack. Mặc dù chỉ là lý thuyết, nhưng những kiến thức của bài hôm nay sẽ làm nền tảng, được ứng dụng nhiều, và sẽ được mình nhắc đến nhiều ở các bài học sau đấy nhé.

Trước khi đi chi tiết vào khái niệm thế nào là Back Stack, chúng ta phải hiểu được phản ứng của hệ thống lên các ứng dụng thông qua việc khảo sát ba nút “thần thánh” trên System Bar của hệ điều hành Android nào.

Back, Home Và Overview

Bất cứ ai sử dụng thiết bị Android cũng đều quen thuộc với ba nút mặc định được đặt ở System Bar phía dưới cùng của màn hình này.

Back Stack - Các nút mặc định trên System Bar

Dù cho hình dáng và vị trí của chúng có thể khác nhau vào từng thời điểm của hệ điều hành, hoặc thiết bị phần cứng. Thì chung quy lại chúng vẫn có ba chức năng chính, đó là BackHome và Overview. Có lẽ mình cũng không cần giải thích tác dụng của ba nút này, vì chắc chắn bạn đang thao tác với chúng hàng ngày rồi. Chúng ta sẽ chỉ nói đến việc hành xử của hệ thống đối với ứng dụng khi người dùng nhấn vào các nút đặc trưng này mà thôi.

Giờ giả dụ bạn vừa mở nguồn điện thoại lên, tức là chưa có ứng dụng nào được chạy. Ở màn hình chính của thiết bị, bạn tìm đến và nhấn vào một icon app nào đó để mở một ứng dụng lên. Mình giả sử TourNote được chọn để khởi chạy. Ngay lập tức, Activtity được đánh dấu là launcher của TourNote sẽ được thực thi. Activity là launcher chính là Activity có khai báo intent-filter ở Manifest như bài học trước bạn có thực hành qua. Người dùng sẽ nhanh chóng nhìn thấy giao diện của MainActivity. Rồi từ MainActivity, bạn lại chọn menu item của ActionBar để đi đến ContactActivity. Bạn hãy nhìn vào sơ đồ sau, các dấu mũi tên đứt nét cho thấy thao tác kích hoạt Actvitiy, còn khung đứt nét chính là Activity đang được hiển thị.

Back Stack - Mô phỏng mở các Activity

Tại ContactActivity, nếu bạn nhấn nút Back. Như bạn biết, ContactActivity sẽ “đóng lại” và nhường màn hình lại cho MainActivity. Nếu nhấn tiếp Back, hệ thống sẽ “thu hồi” màn hình và nhường lại cho màn hình chính của hệ thống. Vậy chuyện gì xảy ra với các màn hình khi bạn nhấn nút Back? Bạn có thể tưởng tượng được rằng nếu như có các Activity được mở bên trong một ứng dụng, việc nhấn Back sẽ giúp quay lại Activity đã mở trước đó một cách tự động, đến khi không còn bất kỳ Activity nào khác của ứng dụng đang mở, ứng dụng sẽ bị đóng lại hoàn toàn. Bạn có thể thấy rằng tư duy lập trình cho ứng dụng Android khác hoàn toàn với các ứng dụng trên desktop ở điểm này. Đó là bạn sẽ không cần phải xây dựng bất kỳ nút nào đại loại như “Thoát ứng dụng” để mà đóng ứng dụng một cách máy móc cả, chính hành động nhấn nút Back của người dùng sẽ là cách tốt nhất để nói cho hệ thống “hãy quay lại màn hình trước”, và màn hình trước ngay cả khi bạn khởi chạy TourNote chính là màn hình chính của thiết bị. Hình sau minh họa việc nhấn nút Back ở các đường mũi tên cong đứt nét, còn dấu chéo cho biết hệ thống sẽ đóng các màn hình đó lại bởi thao tác nhấn nút Back này.

Back Stack - Mô phỏng đóng các Activity

Bạn có thể nghĩ rằng một khi nhấn các nút Back để quay trở về màn hình chính, thì ứng dụng sẽ bị “xóa sổ” hoàn toàn khỏi hệ thống? Không. Ứng dụng vẫn sống một lúc nữa để phòng khi người dùng đổi ý muốn quay lại sử dụng, khi đó họ sẽ không phải đợi quá lâu để ứng dụng khởi động lại từ đầu. Bạn có thể tự tìm hiểu thêm việc này vì chúng ta không đủ giấy mực để nói ở bài hôm nay.

Giờ chúng ta giả dụ đang ở ContactActivity, nhưng thay vì nhấn Back, bạn lại nhấn Home, chuyện gì sẽ xảy ra? Ứng dụng sẽ về màn hình chính ngay. Một tình huống khác hoàn toàn với khi nhấn Back. Nhưng đôi khi cũng sẽ dễ bị nhầm lẫn nếu người dùng đang ở Activity đầu tiên của ứng dụng, khi đó việc nhấn Back hay Home cũng sẽ dẫn người dùng về màn hình chính. Khác nhau cơ bản giữa Back và Home là, việc nhấn Back sẽ kêu hệ thống hủy các Activity đi, và có thể dẫn đến hủy luôn ứng dụng nếu không còn Activity nào khác phải hủy, việc quay lại ứng dụng sau đó sẽ luôn dẫn bạn đi lại từ các màn hình đầu tiên. Còn nhấn Home sẽ không làm hủy dứng dụng hay các Activity, chúng được đưa về background và ở đó, và người dùng hoàn toàn có thể quay trở lại ngay Activity mà trước đó đang hiển thị thì bạn nhấn Home.

Back Stack - Mô phỏng nhấn Home để về màn hình chính

Vậy thì các hành xử khi nhấn Back hoặc Home này của hệ thống có ảnh hưởng gì đến các ứng dụng khác và cả TourNote của chúng ta? Câu trả lời là có. Chính vì vậy mình mới cố gắng viết phần này, mặc dù biết các bạn đang buồn ngủ vì đọc cái điều mà ai cũng biết là gì rồi. TourNote sẽ bị ảnh hưởng như thế nào thì chúng ta cùng xem qua tổ chức của hệ thống thông qua các hành xử này nhé. Bắt đầu với việc tổ chức Task.

Task Là Gì?

Hiển nhiên bạn đã biết nghĩa tiếng Việt của Task chính là Tác vụ. Vậy Tác vụ là gì? Thực ra Task hay Tác vụ trong một hệ thống Android sẽ cần rất nhiều mô tả. Nhưng bạn có thể hiểu một cách trực quan như ví dụ mở ứng dụng trên đây. Mỗi một ứng dụng được khởi chạy trong Android sẽ được hệ thống xem là một Task.

Back Stack - Các Task ứng dụng

Khi mà người dùng nhấn vào một icon app nào đó trên màn hình chính của thiết bị. Hoặc từ danh sách các Task khi nhấn vào nút Overview như hình trên đây. Thì việc đầu tiên hệ thống sẽ xem xem Task hiện tại của ứng dụng đó có tồn tại hay không. Nếu chưa có thông tin Task của ứng dụng đó trong hệ thống, hệ thống sẽ tạo mới một Task cho nó. Còn nếu ứng dụng đã có sẵn một Task, có nghĩa là bạn đã chạy ứng dụng trước đó rồi nhưng bạn đã nhấn Home để về màn hình chính, và Task cũ của nó vẫn còn đang “sống”, thì việc của hệ thống là nhìn vào trong Task đang chạy đó để tìm kiếm vài thông tin, trong đó có thông tin về Activity mà Task đó đang hiển thị là gì, để mà tiếp tục hiển thị lại cho bạn. Ồ vậy trong Task có chứa cấu trúc gì mà hay thế?

Trong Task của mỗi ứng dụng sẽ chứa đựng thông tin về Back Stack. Chính Back Stack này sẽ nói cho hệ thống biết nên hiển thị Activity nào của ứng dụng.

Vậy cụ thể về Back Stack là như thế nào và chúng hoạt động ra sao? Chúng ta cùng qua mục tiếp theo.

Back Stack Là Gì?

Bạn cứ tưởng tượng Back Stack là một ngăn đựng đồ. Khi bạn để một món đồ vào ngăn, nó sẽ nằm ở đáy ngăn (như món số 1 trong hình dưới). Món kế tiếp và kế tiếp nữa sẽ đè lên các món ban đầu (món số 2 và 3). Và món đồ được để vào cuối cùng sẽ là món được lấy ra trước nhất, vì nó nằm trên cùng của ngăn đựng đồ (chính là món số 3).

Mô phỏng Back Stack

Vậy món đồ mà Back Stack sẽ chứa đựng là gì? Đó không gì khác chính là các Activtiy mà chúng ta đã nói đến. Việc hệ thống quản lý xem một Activity nào được hiển thị, và Activity nào phải nhường chỗ cho Activity khác, đều chỉ dựa vào cái ngăn chứa này thôi đấy.

Quay lại ví dụ trên kia. Giả sử vẫn chưa có Task nào của TourNote được tạo. Thì khi bạn nhấn vào icon app TourNote ở màn hình chính. Hệ thống sẽ tạo ra một Task cho TourNote, trong Task này có một Back Stack, trong Back Stack chứa đựng sẵn một Activity ban đầu được khai báo là launcher, chính là MainActivity. Bạn có thể nhìn vào minh họa Back Stack đang chứa Activity nào như bên phải của hình dưới đây.

Back Stack - Mô phỏng mở Activity với Back Stack

Sau đó, với việc gọi phương thức startActivity() từ MainActivity, sẽ dẫn đến việc ContactActivity được kích hoạt như ở bài thực hành trướcViệc kích hoạt này sẽ đặt thêm ContactActivity vào Back Stack. Và hệ thống cũng căn cứ vào Back Stack mà cho MainActivity về background (không bị hủy nhé) vì nó đã không còn nằm trên cùng của ngăn chứa. Còn ContactActivity vì nằm ở trên cùng của Back Stack nên được hiển thị.

Back Stack - Mô phỏng mở Activity với Back Stack

Giờ đây, nếu người dùng nhấn nút Back. Hoặc cũng có thể gọi phương thức finish() như ở bài thực hành trước. Dĩ nhiên, dù bằng đường nào, thì ContactActivity cũng sẽ bị lấy ra khỏi Back Stack. Và ContactActivity lúc này mới chính thức bị hủy. Màn hình lúc này cũng sẽ nhường chỗ lại cho Activity trên cùng của Back Stack lúc bấy giờ, chính là MainActivity.

Back Stack - Mô phỏng đóng Activity với Back Stack

Lúc này, nếu người dùng tiếp tục nhấn BackMainActivity bị hủy. Và khi đã hủy Activity cuối cùng ra khỏi Back Stack, như mình có nói trên kia, hệ thống sẽ xem như ứng dụng đã thoát.

Back Stack - Mô phỏng đóng Activity với Back Stack

Vậy với nút Home thì sao? Nếu bạn đang ở bất kỳ Activity nào, chẳng hạn như ở ContactActivity mà nhấn Home. Thì sẽ không có gì bị xóa khỏi Back Stack cả. Tất cả thông tin trong Task được giữ lại và hệ thống chỉ là chuyển tất cả Task của ứng dụng về background để nhường chỗ cho một Task khác, chính là Task của màn hình chính. Sau này khi bạn mở lại ứng dụng, nếu Task không bị xóa (Task có thể sẽ bị xóa nếu ở background quá lâu và chiếm dụng hiệu năng của hệ thống), thì Back Stack sẽ được hệ thống nhìn vào và tái hiện lại Activity đang ở trên cùng của Stack.

Back Stack - Mô phỏng nhấn Home với Back Stack

Có một lưu ý với Back Stack mà ở ngay bài học hôm nay bạn có thể ráng nhớ. Đó là, chúng ta hoàn toàn có thể đặt vào Back Stack mọi Activity, thậm chí có thể đặt nhiều bản copy của một Activity. Chẳng hạn ở MainActivity bạn thực hiện kích hoạt ContactActivity để đưa Activity này vào Back Stack, nhưng ở ContactActivity, khi bạn muốn trở về MainActivity, thay vì gọi finish() bạn lại startActivity() để kích hoạt lại MainActivity, điều này sẽ tiếp tục đẩy một phiên bản copy nữa của MainActivity vào Back Stack. Thì việc đặt nhiều bản copy của một Activity vào Back Stack như vậy là không nên. Chúng khiến cho thao tác Back của người dùng bị loạn xà ngầu lên, và không tốt cho hiệu năng của hệ thống nữa. Bạn ghi chú điều này để hiểu tại sao lại phải gọi finish() ở ContactActivity, và cho các tình huống cụ thể sau này nữa.

Trên đây là kiến thức thuần lý thuyết về cách mà hệ thống sử dụng Back Stack để điều khiển các Activity của các ứng dụng. Về sau, Back Stack này sẽ còn được nhắc đến nhiều, đặc biệt là khi nói về vòng đời của Activity ở bài kế, và bài học về Fragment ở các bài kế nữa.

 

Vòng Đời Activity

Qua bài học hôm trước, bạn đã thấy mình cố gắng trình bày các kịch bản mà các Activity, hay chính là các màn hình, được hệ thống quản lý như thế nào. Qua đó bạn cũng đã biết khi nào một màn hình vẫn còn được giữ lại trong hệ thống để phòng trường hợp người dùng bất ngờ quay lại. Hoặc khi nào thì hệ thống hủy đi một màn hình.

Bài học hôm nay chúng ta sẽ chỉ xem xét khía cạnh đời sống của duy nhất một màn hình. Để xem dưới sự tác động bởi hệ thống, thì các trạng thái của em nó sẽ phải trải qua là gì. Và với mỗi trạng thái như vậy bạn sẽ biết cách tác động đến ứng dụng để thực hiện một số tác vụ thích hợp nhất theo từng trạng thái như thế nào.

Nghe qua có vẻ hay đúng không. Mời các bạn cùng đến với bài học Android tiếp theo này.

Vòng Đời Activity Là Gì?

Bạn đều biết, ngoài đời thực, vòng đời của một sinh vật nào đó sẽ được tính từ lúc nó sinh ra cho đến lúc chết đi. Thì Activity cũng vậy. Vòng đời Activity sẽ được chúng ta xem xét khi nó được khởi tạo ra (creating) bởi hệ thống, trải qua các trạng thái như dừng lại (stopping), tiếp tục (resuming),… cho đến lúc bị hủy (destroying) khỏi hệ thống. Sở dĩ có nhiều trạng thái như vậy là vì các tương tác từ người dùng mà ra, như khi họ mở một màn hình, khi họ chuyển sang màn hình khác, khi họ chuyển sang một ứng dụng khác, hay nghe một cuộc gọi đến,… Nếu chúng ta nắm được vòng đời và các trạng thái này của Activity, chúng ta sẽ xây dựng ứng dụng trở nên thông minh hơn, và dĩ nhiên, ít lỗi hơn.

Mình giả sử bạn đang xây dựng một ứng dụng phát video. Nếu người dùng đang xem một video mà ứng dụng của bạn đang phát, bỗng đột ngột chuyển sang ứng dụng khác, hay có cuộc gọi đến. Thì lập tức ứng dụng đó phải biết và lưu lại mọi trạng thái của nó, bao gồm cả đoạn video mà người dùng còn đang xem dang dở. Sau đó các tài nguyên của hệ thống nếu có phải được giải phóng tạm thời. Một lúc sau, nếu người dùng quay lại ứng dụng của bạn, họ sẽ được xem tiếp đoạn video lúc nãy, chứ không phải bị reset lại từ đầu. Bạn có thể thấy khi này ứng dụng của bạn rất thông minh. Nhưng tất cả những sự thông minh đó là do bạn chuẩn bị cả đấy, hệ thống chỉ giúp thực thi các trạng thái khi có sự thay đổi. Lát nữa chúng ta cùng làm quen các trạng thái và cách “lắng nghe” các trạng thái này.

Sơ Đồ Minh Họa Vòng Đời Activity

Sơ đồ thần thánh sau là minh họa rõ ràng và đầy đủ nhất về vòng đời Activity.

Vòng đời Activity

Như đã nói, sơ đồ này chính là mô phỏng vòng đời Activity. Mỗi Activity trong một ứng dụng sẽ có một vòng đời khác nhau. Nhưng chúng đều theo chuẩn của sơ đồ này cả.

Bạn hãy chú ý các phương thức được mô tả trong sơ đồ này, bao gồm onCreate()onStart()onRestart()onResume()onPause()onStop() và onDestroy(). Các phương thức này được gọi là các lời gọi về, bạn chỉ cần nhớ nghĩa tiếng Anh của chúng là callback. Các callback chính là các phương thức được bạn override lại từ các phương thức sẵn có của lớp cha, để khi mà hệ thống cần, hệ thống sẽ gọi đến các phương thức đó trong ứng dụng của bạn. Bạn nhớ lại đi, với việc code cho TourNote từ trước tới giờ, bạn đã biết đến callback nào trong các callback mình liệt kê ở trên rồi?

Vòng đời Activity - Minh họa callback đã dùng

Trước khi làm quen với các callback, chúng ta hãy diễn tả sơ về sơ đồ trên.

Mô Tả Sơ Đồ

Sơ đồ bắt đầu từ khi Activity launched, tức là khi Activity được kích hoạt, và được hệ thống để vào BackStack. Sau khi kích hoạt, lần lượt các callback onCreate()onStart()onResume() sẽ được hệ thống gọi đến.

Sau khi gọi đến các callback trên, thì Activity mới chính thức được xem là đang chạy (Activity running).

Lúc này, nếu có bất kỳ Activity nào khác chiếm quyền hiển thị, thì Activity hiện tại sẽ rơi vào trạng thái onPause(). Nếu cái sự hiển thị của Activity khác làm cho Activity mà chúng ta đang nói đến không còn nhìn thấy nữa thì onStop() sẽ được gọi tiếp theo nữa. Lát nữa khi đi vào các chi tiết dưới đây bạn sẽ hiểu sự khác nhau giữa hai callback này thôi.

Nếu Acvitity đã vào onPause() rồi, tức là đang bị Activity khác đè lên, mà người dùng sau đó quay về lại Activity cũ, thì onResume() được gọi. Còn nếu Activity đã vào onStop() rồi, mà người dùng quay về lại Activity cũ thì onRestart() được gọi.

Trong cả hai trường hợp Activity rơi vào onPause() hoặc onStop(), nó sẽ rất dễ bị hệ thống thu hồi (tức là hủy luôn í) để giải phóng tài nguyên, khi này nếu quay lại Activity cũ, onCreate() sẽ được gọi chứ không phải onResume() hay onRestart() đâu nhé.

Và cuối cùng, nếu một Activity bị hủy một cách có chủ đích, chẳng hạn như người dùng nhấn nút Back ở System Bar, hay hàm finish() được gọi,… thì onDestroy() sẽ được kích hoạt và Activity kết thúc vòng đời của nó.

Nếu các mô tả trên đây lan man quá thì bạn có thể chỉ cần nhớ đến bốn trạng thái chính dưới đây.

Các Trạng Thái Chính Trong Vòng Đời Activity

Hoạt Động (Active)

Khi Activity được kích hoạt, và được hệ thống để vào BackStack, nó sẽ bước vào trạng thái active. Với trạng thái active, người dùng hoàn toàn có thể nhìn thấy và tương tác với Activity của ứng dụng.

Tạm Dừng (Pause)

Trạng thái này khá đặc biệt. Trạng thái tạm dừng. Như bạn đã làm quen trên kia, trạng thái này xảy ra khi mà Activity của bạn vẫn đang chạy, người dùng vẫn nhìn thấy, nhưng Activity khi này lại bị che một phần bởi một thành phần nào đó. Chẳng hạn như khi bị một dialog đè lên (bạn sẽ được kiểm chứng khi học qua bài về dialog sau).

Bạn nhớ nhé, cái sự che Activity này không phải hoàn toàn. Chính vì vậy mà Activity đó tuy được người dùng nhìn thấy nhưng không tương tác được.

Dừng (Stop)

Trạng thái này khá giống với trạng thái tạm dừng trên kia. Nhưng khi này Activity bị che khuất hoàn toàn bởi một thành phần giao diện nào đó, hoặc bởi một ứng dụng khác. Và tất nhiên lúc này người dùng không thể nhìn thấy Activity của bạn được nữa.

Hành động mà khi người dùng nhấn nút Home ở System Bar để đưa ứng dụng của bạn về background, cũng khiến Activity đang hiển thị trong ứng dụng rơi vào trạng thái dừng này. Bạn sẽ biết khi đi đến bài thực hành dưới đây.

Chết (Dead)

Nếu Activity được lấy ra khỏi BackStack, chúng sẽ bị hủy và vào trạng thái này. Trường hợp này xảy ra khi user nhấn nút Back ở System Bar để thoát một Activity. Hoặc lời gọi hàm finish() từ một Activity để “giết chính nó”. Cũng có khi ứng dụng ở trạng thái background quá lâu, hệ thống có thể sẽ thu hồi tài nguyên bằng cách dừng hẳn các Activity trong ứng dụng, làm cho tất cả các Activity đều vào trạng thái này.

Khi vào trạng thái dead, Activity sẽ kết thúc vòng đời “trôi nổi” của nó.

Những ý trên giúp bạn nắm được tổng quan các trạng thái mà một Activity có thể trải qua. Chúng ta cùng đi gần đến code hơn bằng cách nói đến công dụng của từng callback.

Làm Quen Với Các Callback

Sau đây là tất cả các callback mà bạn nên biết. Bạn không nhất thiết phải hiện thực cho tất cả các callback này đâu nhé. Chỉ cần dùng đủ thôi, bài thực hành bên dưới sẽ phần nào giúp bạn giải đáp ý này.

onCreate()

Theo mình thì đây là callback tối thiểu trong một Activity mà bạn phải hiện thực, theo mình thôi nhé, bạn có thể bỏ qua nó mà không có báo lỗi gì.

Callback onCreate() này được gọi khá sớm, ngay khi Activity được kích hoạt, và thậm chí người dùng còn chưa nhìn thấy gì của Activity cả, thì callback này đã được gọi rồi. Ngoài ra thì bạn nên biết là callback này chỉ được gọi một lần duy nhất khi Activity được khởi tạo. Nó có thể được gọi lại nếu hệ thống xóa Activity này đi để lấy lại tài nguyên của hệ thống, nhưng rất hiếm khi xảy ra. Và nó còn có thể được gọi lại nếu bạn xoay màn hình ngang/dọc.

Do đặc tính được gọi khá sớm và chỉ được gọi một lần duy nhất trong vòng đời của nó như vậy, nên bạn sẽ tận dụng để load giao diện cho Activity ở giai đoạn này, thông qua phương thức setContentView() mà bạn đã biết từ những ngày đầu tiên. Ngoài giao diện ra, bạn có thể khởi tạo các logic nào đó chỉ chạy một lần ban đầu, như các lời gọi API, load database, tạo item list, tạo Navigation Drawer, và nhiều logic khác mà bạn đã từng làm quen ở callback này từ các bài thực hành trước.

Tham số savedInstanceState được truyền vào phương thức này sẽ được mình nói đến ở bài viết về lưu trữ trong Android sau nhé.

onStart()

Sau khi gọi đến onCreate(), hệ thống sẽ gọi đến onStart(). Hoặc hệ thống cũng sẽ gọi lại onStart() sau khi gọi onRestart() nếu trước đó nó bị che khuất bởi Activity nào khác (một màn hình khác hoặc một ứng dụng khác) che hoàn toàn và rơi vào onStop().

Khi hệ thống gọi đến callback này thì Activity chuẩn bị (có nghĩa là chưa) được nhìn thấy bởi người dùng và tương tác với người dùng. Bởi đặc tính này mà onStart() ít được dùng đến.

onResume()

Khi hệ thống gọi đến callback này thì bạn yên tâm rằng người dùng đã nhìn thấy và đã tương tác được với giao diện.

onResume() được gọi khi Activity được khởi tạo rồi và bước qua onStart() trên kia. Hoặc khi Activity bị một giao diện nào khác che đi một phần (hoặc toàn phần), rồi sau đó quay lại Activity hiện tại. Bạn có thể thấy rằng callback này được gọi rất nhiều lần trong một vòng đời của nó.

Chính đặc điểm này của onResume() mà bạn có thể tận dụng để quay lại tác vụ mà người dùng đang bị dang dở khi onPause() (được nói đến dưới đây) được gọi.

Chẳng hạn như bạn đang soạn nội dung cho TourNote, mà có cuộc gọi đến, bạn sẽ lưu tạm nội dung này khi callback onPause(), để rồi khi onResume() được gọi lại sau đó khi người dùng kết thúc cuộc gọi và quay lại TourNote, bạn sẽ khôi phục nội dung đó để người dùng tiếp tục sử dụng TourNote như chưa có bất kỳ gián đoạn nào.

onPause()

Thông thường nếu có một thành phần nào đó che Activity hiện tại mà người dùng vẫn nhìn thấy Activity đó (nhìn thấy chứ không tương tác được). Chẳng hạn một popup hiện lên trên Activity. Thì onPause() của Activity sẽ được gọi. Sau này khi người dùng quay lại Activity thì onResume() sẽ được gọi.

Bạn có thể tưởng tượng rằng onPause() cũng sẽ được gọi khá nhiều lần trong một vòng đời Activity. Theo như Google thì onPause() được gọi đến khá nhanh, nếu bạn muốn lưu trữ dữ liệu như mình nói trên kia, thì nên lưu những gì nhanh gọn lẹ thôi. Nếu bạn muốn lưu trữ các dữ liệu nặng, hoặc gọi API kết nối server chỗ này, nhiều khả năng ứng dụng sẽ không kịp thực hiện. Do đó, thay vì làm các thao tác nặng nề ở onPause(), bạn có thể cân nhắc gọi chúng ở onStop().

onStop()

Như mình có nói. onStop() được gọi khi Activity không còn được nhìn thấy nữa, có thể một màn hình nào khác che lên hoàn toàn, có thể một ứng dụng nào đó vào foreground, hoặc người dùng nhấn nút Home để về màn hình chính.

Bạn có thể tận dụng onStop() để lưu trữ dữ liệu ứng dụng. Hoặc để giải phóng các tài nguyên đang dùng. Ngưng các API còn đang gọi dang dở.

Tuy nhiên khi onStop() được gọi không phải là lúc chúng ta cũng nói lời tạm biệt Activity. Như mình đã nói, người dùng hoàn toàn có thể quay lại sử dụng Activity sau đó mà không cần phải khởi động lại Activity, khi này thì phương thức onRestart() và onStart() được gọi kế tiếp nhau.

onDestroy()

Trước khi Activity “chết”, hệ thống cho nó cơ hội để nói lời trăn trối, nó sẽ giúp gọi đến callback onDestroy() này của Activity.

Bạn có thể tận dụng callback này để giải phóng các tài nguyên hệ thống mà ở onStop() bạn chưa gọi đến.

Vòng đời của một Activity kết thúc ở đây.

Thực Hành Tìm Hiểu Vòng Đời Của MainActivity

Bài thực hành hôm nay sẽ không có code đẩy lên GitHub. Chúng ta chỉ dùng log để ghi lại các trạng thái của MainActivity. Qua bài thực hành hôm nay, chắc chắn bạn sẽ hiểu vòng đời Activity một cách rõ ràng hơn, chứ không lung tung như lý thuyết trên kia nữa.

Nào, để bắt đầu thực hành, mình mời bạn mở project TourNote lên. Mở MainActivity.java lên. Chúng ta sẽ override hết các callback đã nói trên đây, in log ra ở từng callback để xem chúng được gọi khi nào nhé.

Code của MainActivity.java như sau, code tuy nhiều nhưng là code của mấy bài trước, bạn chỉ cần chú ý các dòng được tô sáng thôi nhé.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public class MainActivity extends AppCompatActivity {
 
    private DrawerLayout drawerLayout;
    private ActionBarDrawerToggle drawerToggle;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        drawerLayout = (DrawerLayout) findViewById(R.id.activity_main_drawer);
        drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
        drawerLayout.addDrawerListener(drawerToggle);
 
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeButtonEnabled(true);
 
        Log.d("MainActivity Lifecycle", "===== onCreate =====");
    }
 
    @Override
    protected void onStart() {
        super.onStart();
 
        Log.d("MainActivity Lifecycle", "===== onStart =====");
    }
 
    @Override
    protected void onRestart() {
        super.onRestart();
 
        Log.d("MainActivity Lifecycle", "===== onRestart =====");
    }
 
    @Override
    protected void onResume() {
        super.onResume();
 
        Log.d("MainActivity Lifecycle", "===== onResume =====");
    }
 
    @Override
    protected void onPause() {
        super.onPause();
 
        Log.d("MainActivity Lifecycle", "===== onPause =====");
    }
 
    @Override
    protected void onStop() {
        super.onStop();
 
        Log.d("MainActivity Lifecycle", "===== onStop =====");
    }
 
    @Override
    protected void onDestroy() {
        super.onDestroy();
 
        Log.d("MainActivity Lifecycle", "===== onDestroy =====");
    }
 
    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        // Sync the toggle state after onRestoreInstanceState has occurred.
        drawerToggle.syncState();
    }
 
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        drawerToggle.onConfigurationChanged(newConfig);
    }
 
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main_actions, menu);
 
        return super.onCreateOptionsMenu(menu);
    }
 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if(drawerToggle.onOptionsItemSelected(item)) {
            return true;
        }
 
        switch (item.getItemId()) {
            case R.id.search:
                Toast.makeText(this, "Search button selected", Toast.LENGTH_SHORT).show();
                return true;
            case R.id.about:
                Intent intent = new Intent(this, ContactActivity.class);
                startActivity(intent);
                return true;
            case R.id.help:
                intent = new Intent(this, ContactActivity.class);
                startActivity(intent);
                return true;
        }
 
        return super.onOptionsItemSelected(item);
    }
}

Nào, giờ chúng ta cùng xem từng trường hợp cụ thể nào.

Khởi Chạy Ứng Dụng

Khi MainActivity được khởi chạy, bạn có thể thấy các dòng log của các callback onCreate()onStart() và onResume() được gọi đến và hiển thị ở Android Monitor.

Vòng đời Activity - Thực hành bước 1

Khởi Chạy ContactActivity

Từ MainActivity, bạn hãy nhấn chọn bất kỳ meu item của ActionBar để đi đến ContactActivity. Bạn có thể thấy các callback onPause() và onStop() của MainActivity được gọi. Vì khi này ContactActivity đã che hết màn hình của MainActivity nên hệ thống không dừng ở onPause() mà gọi tiếp onStop() của MainActivity là vậy.

Ý tưởng: bạn có thể thử xây dựng thêm các callback ở ContactActivity và thử xem log của cả hai Activity để biết được nhiều thông tin hơn nhé.

Vòng đời Activity - Thực hành bước 2

Quay Về MainActivity Từ ContactActivity

Tiếp theo bước trên đây. Nếu bạn nhấn nút Back (từ ActionBar hoặc từ System Bar) để về lại MainActivity. Bạn sẽ thấy các callback onRestart()onStart() và onResume() của MainActivity được gọi.

Vòng đời Activity - Thực hành bước 3

Từ MainActivity Nhấn Home

Lần này bạn thử nhấn Home để đưa ứng dụng về background, chuyện gì sẽ xảy ra với MainActivity? Cũng như bạn đi từ MainActivity đến Activity khác thôi, màn hình chính cũng là một màn hình nào đó che hoàn toàn MainActivity. Nên onPause() và onStop() vẫn được gọi y như trên kia.

Vòng đời Activity - Thực hành bước 4

Từ Màn Hình Chính Quay Về MainActivity

Lần này bạn hãy nhấn nút Overview từ System Bar rồi chọn lại ứng dụng TourNote, bạn sẽ thấy MainActivity xuất hiện lại với các callback onRestart()onStart() và onResumr() y như thao tác quay về từ ContactActivity trên kia.

Vòng đời Activity - Thực hành bước 5

Từ MainActivity Nhấn Back

Lần này bạn đoán là MainActivity bị hủy đúng không? Bạn đã đoán đúng. Các callback onPause()onStop() và cả onDestroy() được gọi. Thật thú vị.

Vòng đời Activity - Thực hành bước 6

Tất nhiên, sau bước này, dù bạn có nhìn thấy như TourNote vẫn đang sống. Nhưng khi quay lại TourNote lần nữa, MainActivity sẽ phải khởi tạo lại qua các callback onCreate()onStart()onResume() như ban đầu.

Bạn có thể thử nghiệm tiếp với việc xoay màn hình ứng dụng, để xem Activity lúc đó bị hủy và khởi tạo lại như thế nào nhé.

Bạn vừa cùng mình xem qua một kiến thức nhẹ nhàng và một bài thực hành nhẹ nhàng để hiểu rõ vòng đời Activity.

 

Truyền Dữ Liệu Qua Lại Giữa Các Activity

Cho đến bài học hôm nay, chắc hẳn các bạn đã rất tự tin với việc thao tác với Activity rồi. Tuy nhiên Activity vẫn còn nhiều điều hay ho khác mà bạn vẫn nên biết. Một trong những điều đó chính là việc làm thế nào mà các Activity có thể “nói chuyện” được với nhau, hay nói cách khác là truyền dữ liệu cho nhau.

Nếu bạn là người mới tiếp cận Android, ngay lúc này đây, có thể bạn vẫn chưa thấy được nhu cầu từ việc truyền dữ liệu qua lại giữa các Activity. Nhưng tin mình đi, mình và những bạn có kinh nghiệm khác đều đã gặp phải nhu cầu này khá sớm. Ngay cả TourNote cũng cần phải xây dựng chức năng truyền dữ liệu qua lại giữa hai Activity rồi đấy. Nếu thấy thú vị thì mời các bạn cùng xem tiếp bài học nhé.

Tại Sao Phải Truyền Dữ Liệu Qua Lại Giữa Các Activity?

Bạn cũng đã biết, mỗi Activity chính là một màn hình của ứng dụng (cho đến bài học hôm nay thì đúng là vậy). Và bạn cũng đã thấy mỗi Activity là một đơn vị độc lập, chẳng hạn mỗi chúng đều có một vòng đời khác nhau. Như vậy sẽ rất khó nếu như chúng không có một cơ chế nào đó truyền đạt dữ liệu qua cho nhau.

Mình giả sử bạn đang xây dựng ứng dụng email. Trong ứng dụng của bạn có một Activity hiển thị danh sách tóm tắt các email đến, giả sử Activity này có tên là ListActivity. Và một Activity hiển thị nội dung cụ thể của từng email, giả sử tên của nó là DetailActivity. Ban đầu khi mở ứng dụng, người dùng sẽ nhìn thấy ListActivity trước, khi họ nhấn vào một email trong danh sách các email ở Activity này, ứng dụng sẽ kích hoạt DetailActivity, và truyền thông tin của item mà người dùng đã nhấn để DetailActivity hiển thị nội dung email tương ứng. Tiếp theo nếu người dùng đọc xong email đó ở DetailActivity mà không thấy email quan trọng, họ có thể nhấn nút xóa email, khi này ứng dụng sẽ quay lại ListActivity, và có thông tin trả về từ DetailActivity cho biết email đã bị xóa, ListActivity sẽ xóa item tương ứng với thông tin trả về đó.

Bạn thấy dó, một ví dụ nhỏ thôi mà có khá nhiều lý do để truyền dữ liệu qua lại giữa các Activity rồi. Và thực tế kinh nghiệm của mình thấy rằng hầu hết các Activity trong ứng dụng đều cần kiến thức của bài hôm nay cả.

Vậy chúng ta cùng xem cụ thể cách để truyền dữ liệu qua lại giữa các Activity như thế nào nhé.

Cách Truyền Dữ Liệu Qua Lại Giữa Các Activity

Nếu bạn đã thử khai báo các thuộc tính static trong ứng dụng để lưu trữ các dữ liệu mà tất cả các Activity có thể đọc được và chỉnh sửa được. Và bạn cho rằng thuộc tính static này là cách trao đổi dữ liệu giữa các Activity, thì bạn đã sai rồi. Các thuộc tính static trong ứng dụng chỉ nên là các hằng số, hoặc các giá trị dùng chung khác như các link ví dụ mình để ở đây cho bạn xem. Nếu bạn khai báo một thuộc tính static, và set giá trị cho nó, rồi bạn kích hoạt một Activity khác có sử dụng dữ liệu ở thuộc tính static này, sẽ không chắc dữ liệu đó đúng như bạn mong muốn đâu. Một lần nữa, đừng dùng thuộc tính static để chia sẻ dữ liệu giữa các Activity nhé.

Chúng ta chỉ có duy nhất một cách để truyền dữ liệu qua lại giữa các Activity. Đó là cách “nhét” dữ liệu vào Intent và nhờ thành phần này chuyển giúp. Hệ thống sẽ đảm bảo dữ liệu được gửi qua “nguyên vẹn” và kịp thời ở Activity mới.

Ồ, vậy Intent ngoài việc kích hoạt các thành phần của Android, trong đó có Activity như bạn đã biết, nay lại có công năng của “người vận chuyển” nữa.

Loại Dữ Liệu Được Sử Dụng Trong Intent

Ở mục trên bạn đã biết rằng, Intent có tác dụng vận chuyển dữ liệu qua các Activity. Vậy dữ liệu sẽ được tổ chức bên trong Intent như thế nào?

Dữ liệu được “nhét” vào trong Intent và được lấy ra khỏi Intent theo các cặp dữ liệu dạng key/valueKey ở đây là một chuỗi, giúp định danh cho dữ liệu value. Nếu bạn để vào trong Intent cặp key/value nào, thì bạn phải lấy ra bởi cặp key/value đó, phải đảm bảo khai báo đúng key và lấy ra đúng kiểu dữ liệu của value khi để vào. Điều này tương tự như khi bạn di chuyển đi chơi xa, thì khi đóng gói hành lý của bạn, nhân viên tiếp nhận hành lý phải dán nhãn tên của bạn hay ID của bạn lên hành lý, để đảm bảo bạn lấy đúng hành lý (chính là value) khi đến nơi dựa vào nhãn tên hay ID đó (chính là key).

Tuy có một cách thôi, nhưng bạn có thể sử dụng một trong hai hình thức sau. Một là sử dụng Extra, hoặc sử dụng Bundle. Sự khác nhau giữa hai cách này theo mình là không quan trọng, bạn nên biết cả hai cách, và sẽ có lời khuyên của mình nên sử dụng cách nào ở bên dưới.

Dùng Extra

Có thể nói, truyền nhận dữ liệu bằng Extra là cách dễ nhất.

Gửi Dữ Liệu

Đầu tiên, để gửi dữ liệu bằng Extra. Sau khi khai báo Intent và trước khi bạn dùng nó để kích hoạt activity nào đó, bạn có thể sử dụng các phương thức được nạp chồng của nó để gửi dữ liệu. Các phương thức đó có chung một tên là putExtra().

Các phương thức giúp đặt dữ liệu vào Extra
Các phương thức giúp đặt dữ liệu vào Extra

Bạn nhớ là các phương thức putExtra() này không có s đằng sau Extra nhé. Extra có s sẽ dành cho mục Bundle dưới đây.

Với mỗi putExtra() như vậy, tham số đầu tiên chính là key mà mình có nói trên kia. Tham số thứ hai tương tự chính là value. Phương thức này được nạp chồng làm nhiều bản để bạn dễ dàng sử dụng từng loại value mà bạn muốn. Tuy nhiên bạn đừng để ý hai kiểu value là Parcelable và Serializable, hai kiểu này hơi phức tạp một chút, chúng ta sẽ có một bài viết riêng về hai kiểu này khi thích hợp.

Đoạn code sau ví dụ cách để đặt dữ liệu vào Intent bằng Extra.

1
2
3
4
5
Intent intent = new Intent(this, ContactActivity.class);
intent.putExtra("Key_1", "Truyền một String");  // Truyền một String
intent.putExtra("Key_2", 5);                    // Truyền một Int
intent.putExtra("Key_3", true);                 // Truyền một Boolean
startActivity(intent);

Nhận Dữ Liệu

Khi này, theo như ví dụ trên thì ContactActivity sẽ được kích hoạt với dữ liệu là ba cặp key/value được truyền qua. Ở phương thức onCreate() hoặc bất cứ chỗ nào của ContactActivity, bạn đều có thể lấy bất cứ cặp key/value nào ra dùng. Bằng cách gọi đến getXxxExtra().

Các phương thức giúp lấy dữ liệu khỏi Extra
Các phương thức giúp lấy dữ liệu khỏi Extra

Mình ghi chung là Xxx, vì Xxx này sẽ được bạn thay thế bằng kiểu dữ liệu phù hợp với key bên “đóng gói”, như getBooleanExtra()getStringExtra()getIntExtra(),… Dĩ nhiên tham số name truyền vào phương thức này phải đúng là key bên đóng gói luôn.

Một số phương thức cần phải có tham số thứ hai, tham số này chính là dữ liệu mặc định nếu như hệ thống không tìm thấy dữ liệu với key mà bạn cung cấp. Việc cung cấp tham số thứ hai này tránh một số lỗi xảy ra đối với chương trình của chúng ta.

Đoạn code sau minh họa cách lấy dữ liệu ra khỏi Intent bằng Extra ở onCreate() của Activity. Bạn có thể thấy từng cặp key/value khớp với khi bạn đặt dữ liệu vào trên kia.

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_contact);
  
    // Các dòng code khác...
  
    Intent intent = getIntent();
    String value1 = intent.getStringExtra("Key_1");
    int value2 = intent.getIntExtra("Key_2", 0);
    boolean value3 = intent.getBooleanExtra("Key_3", false);
}

Dùng Bundle

Thực ra Bundle và Extra không khác gì nhau hết. Nếu như Extra trên kia sẽ “xé lẻ” dữ liệu ra và gởi theo từng dòng. Thì Bundle sẽ giúp bạn “đóng gói” dữ liệu lại và gởi nguyên kiện. Tất nhiên Bundle sẽ tiện hơn trong trường hợp bạn muốn gởi cùng một bộ dữ liệu đến nhiều Activity khác nhau.

Ngoài nhiệm vụ đóng gói dữ liệu để truyền qua lại giữa các Activity ở bài học này, thì Bundle còn dùng trong một số mục đích khác, đơn cử như truyền dữ liệu qua Fragment mà bạn sẽ được biết ở bài sau. Nên tốt hơn hết bạn nên học cách sử dụng Bundle ngay từ bài này nhé.

Gửi Dữ Liệu

Chỉ có phát sinh vài dòng code so với Extra trên kia thôi, đầu tiên là dòng tạo ra Bundle. Sau đó vẫn là các dòng đặt dữ liệu vào Bundle, các dòng này có hơi khác với các dòng đặt dữ liệu vào Extra một chút, nếu với Extra bạn dùng các phương thức nạp chồng với cùng một tên putExtra() thì với Bundle bạn phải dùng đúng phương thức putXxx() với Xxx là kiểu dữ liệu bạn cần dùng.

Các phương thức giúp đặt dữ liệu vào Bundle
Các phương thức giúp đặt dữ liệu vào Bundle

Khi Bundle đã chứa đủ dữ liệu, bạn cần phải đặt Bundle này vào trong Intent bằng một dòng code putExtras() (nhớ là có s nhé). Bạn xem code như sau.

1
2
3
4
5
6
7
Intent intent = new Intent(this, ContactActivity.class);
Bundle bundle = new Bundle();
bundle.putString("Key_1", "Truyền một String"); // Truyền một String
bundle.putInt("Key_2", 5);                      // Truyền một Int
bundle.putBoolean("Key_3", true);               // Truyền một Boolean
intent.putExtras(bundle);
startActivity(intent);

Nhận Dữ Liệu

Cũng tương tự như bên gửi thôi, nếu đã gửi theo Bundle, thì bên nhận cũng sẽ nên nhận theo Bundle trước rồi mới lấy từng dữ liệu ra dùng. Để lấy Bundle ra khỏi Intent thì chúng ta có phương thức getExtras().

Sau khi lấy Bundle ra khỏi Intent, việc tiếp theo sẽ gọi đến các phương thức getXxx() của nó. Các phương thức này của Bundle cũng giống như các phương thức getXxxExtra() của Extra trên kia. Chỉ khác một chỗ getXxx() của Bundle thường có hai phương thức nạp chồng, giúp bạn linh động hơn. Thường thì bạn nên dùng getXxx() với hai tham số, như vậy bạn có thể định nghĩa được giá trị mặc định cho từng phương thức khi mà nó không tìm thấy dữ liệu từ key mà bạn cung cấp, giúp tránh một số lỗi không cần thiết.

Các phương thức giúp lấy dữ liệu khỏi Bundle
Các phương thức giúp lấy dữ liệu khỏi Bundle

Để chắc chắn thì khi nhận dữ liệu với Bundle, bạn nên kiểm tra xem Bundle đó có tồn tại hay không (kiểm tra khác null) trước nhé.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_contact);
  
    // Các dòng code khác...
  
    Intent intent = getIntent();
    Bundle bundle = intent.getExtras();
    if (bundle != null) {
        String value1 = bundle.getString("Key_1", "");
        int value2 = bundle.getInt("Key_2", 0);
        boolean value3 = bundle.getBoolean("Key_3", false);
    }
}

Bạn vừa xem qua cách thức chuyển dữ liệu qua lại giữa các Activity. Giờ là lúc chúng ta thực hành xây dựng một chút cho TourNote rồi.

Thực Hành Gửi Dữ Liệu Từ MainActivity Qua ContactActivity

Ở mục thực hành của bài 26 chúng ta đã xây dựng sự kiện nhấn cho hai menu item trên ActionBar của MainActivity, sao cho khi người dùng nhấn vào About App hay Help cũng sẽ mở ra ContactActvitity cả. Như hình dưới đây.

Nhìn lại việc mở ContactActivity từ bài hôm trước
Nhìn lại việc mở ContactActivity từ bài hôm trước

Thực ra ContactActivity mà mình muốn hướng đến sẽ chứa hai loại nội dung. Một là thông tin về ứng dụng, hai là thông tin giúp đỡ. Việc phân biệt nội dung nào được hiển thị sẽ dựa vào lựa chọn của người dùng trên menu item của MainActivity. Chính vì vậy chúng ta sẽ xây dựng ContactActivity sao cho có thể nhận được dữ liệu từ MainActivity chuyển qua, dữ liệu này chỉ đơn giản báo cho ContactActivity biết người dùng vừa nhấn chọn About App hay Help.

Bạn đã hiểu yêu cầu của bài thực hành hôm nay rồi đúng không nào. Nên nhớ là chúng ta chỉ nói đến cách thức gửi nhận dữ liệu để ContactActivity hiểu được chuyện gì đang xảy ra thôi nhé, do đó bài này chúng ta chỉ sử dụng Toast để kiểm chứng dữ liệu nhận. Còn việc hiển thị cái gì khi đã nhận dữ liệu thì ở bài sau chúng ta sẽ xây dựng tiếp.

Xây Dựng Các Giá Trị Hằng Số

Bạn đã biết việc sử dụng Extra hay Bundle là việc lưu trữ các dữ liệu dạng key/value rồi chuyển đi và nhận chúng ở nơi khác đúng không nào. Vậy để đảm bảo các key luôn luôn được dùng đúng, chúng ta nên định nghĩa nó là một giá trị tĩnh (static) và hằng (final). Giá trị này nên định nghĩa bên lớp nhận dữ liệu thì tốt hơn về mặt quản lý code (theo ý mình thôi nhé, bạn có thể định nghĩa một lớp chuyên chứa các giá trị key này). Do đó mình thêm các thuộc tính được tô sáng sau vào ContactActivity.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ContactActivity extends AppCompatActivity {
  
    public static final String KEY_SHOW_WHAT = "show_what";
    public static final String VALUE_SHOW_ABOUT = "show_about";
    public static final String VALUE_SHOW_HELP = "show_help";
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_contact);
  
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeButtonEnabled(true);
    }
  
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                finish();
                return true;
        }
  
        return super.onOptionsItemSelected(item);
    }
}

Gửi Dữ Liệu Từ MainActivity

Dĩ nhiên bên MainActivity, chúng ta chỉnh sửa một tí nơi kích hoạt ContactActivity, sao cho Intent có thể chứa Bundle mà chúng ta muốn như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    if(drawerToggle.onOptionsItemSelected(item)) {
        return true;
    }
  
    switch (item.getItemId()) {
        case R.id.search:
            Toast.makeText(this, "Search button selected", Toast.LENGTH_SHORT).show();
            return true;
        case R.id.about:
            Intent intent = new Intent(this, ContactActivity.class);
            Bundle bundle = new Bundle();
            bundle.putString(ContactActivity.KEY_SHOW_WHAT, ContactActivity.VALUE_SHOW_ABOUT);
            intent.putExtras(bundle);
            startActivity(intent);
            return true;
        case R.id.help:
            intent = new Intent(this, ContactActivity.class);
            bundle = new Bundle();
            bundle.putString(ContactActivity.KEY_SHOW_WHAT, ContactActivity.VALUE_SHOW_HELP);
            intent.putExtras(bundle);
            startActivity(intent);
            return true;
    }
  
    return super.onOptionsItemSelected(item);
}

Nhận Dữ Liệu Ở ContactActivity

Như mình có nói, ContactActivity chỉ nhận dữ liệu rồi dùng Toast để show ra làm bằng chứng là nó đã nhận đúng dữ liệu. Còn việc hiển thị cái gì thì bài học sau chúng ta sẽ nói tiếp nhé. Như vậy code của ContactActivity như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ContactActivity extends AppCompatActivity {
  
    public static final String KEY_SHOW_WHAT = "show_what";
    public static final String VALUE_SHOW_ABOUT = "show_about";
    public static final String VALUE_SHOW_HELP = "show_help";
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_contact);
  
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeButtonEnabled(true);
  
        Intent intent = getIntent();
        Bundle bundle = intent.getExtras();
        if (bundle != null) {
            String valueShow = bundle.getString(KEY_SHOW_WHAT, "");
            Toast.makeText(this, "Show value: " + valueShow, Toast.LENGTH_SHORT).show();
        }
    }
  
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                finish();
                return true;
        }
  
        return super.onOptionsItemSelected(item);
    }
}

Kết quả là, tùy vào cách người dùng chọn vào menu item mà ContactActivity sẽ hiển thị Toast như sau.

Hình ảnh TourNote sau khi thực hành
Hình ảnh TourNote sau khi thực hành

Làm Quen Với Fragment

Như vậy, đến bài học hôm nay, bạn đã biết rõ khái niệm về Activity trong Android rồi. Bạn đều biết, Activity chính là thành phần được dùng để tổ chức một màn hình trong ứng dụng. Tất nhiên rằng, các thiết bị phần cứng ở những giai đoạn đầu của Android đều có màn hình khá nhỏ và chật chội, nên một Activity khi này đủ mạnh để quản lý một màn hình. Nhưng đời không như mơ, khi mà nhu cầu sử dụng các thiết bị với màn hình lớn ngày càng tăng, không những các tablet mới có màn hình lớn, mà ngay cả các thiết bị được gọi là phone ngày xưa cũng tăng kích thước màn hình lên để trở thành các phaplet (lai tạp giữa phone và tablet). Việc tăng kích thước màn hình trên các thiết bị Android như thế ít nhiều làm tăng thêm “gánh nặng” cho Activity, khiến việc thiết kế UI sao cho vừa có thể chạy tốt trên phone và tablet lẫn phaplet bỗng trở nên khó khăn hơn bao giờ hết cho các lập trình viên chúng ta. Chính vì vậy mà Fragment đã được Google giới thiệu ra nhằm “giảm tải” cái gánh nặng đó.

Qua lời “dẫn truyện” trên đây, bạn đã phần nào hiểu được vai trò của Fragment rồi đúng không nào. Hãy cùng mình xem tiếp các mục bên dưới để có cái nhìn đầy đủ hơn về Fragment này nhé.

Fragment Là Gì?

Với những ý trên đây, bạn có thể thấy Fragment đóng vai trò quản lý một giao diện của màn hình y như Activity vậy. Nhưng Fragment lại không phải là một thành phần quản lý giao diện độc lập như ActivityFragment thuộc về quản lý của Activity.

Như vậy bạn có thể hình dung Fragment chịu trách nhiệm quản lý một không gian màn hình, nhiều khi không gian này cũng chính là toàn màn hình. Và cái không gian màn hình đó của Fragment phải nằm trong một Activity nào đó. Một Activity có thể có nhiều Fragment, có khi nhiều Fragment của Activity đó cùng nhau hiển thị lên một màn hình, cũng có khi chúng luân phiên hiển thị nếu như mỗi chúng đều chiếm cả không gian màn hình. Và một ý nữa, một Fragment nào đó cũng có thể được khai báo và sử dụng bởi nhiều Activity khác nhau.

Bạn có thể thấy rằng, cũng bởi Fragment chịu trách nhiệm quản lý một phần giao diện, nên nó cũng có vòng đời y như với Activity vậy. Tuy nhiên, vì Fragment nằm trong quản lý của Activity, nên vòng đời của Fragment sẽ phụ thuộc rất nhiều vào vòng đời của Activity. Chúng ta sẽ cùng nhau nói rõ hơn về vòng đời của Fragment ở bài học kế tiếp nhé, bây giờ hãy tìm hiểu vì sao chúng ta phải dùng Fragment.

Tại Sao Lại Dùng Fragment?

Câu hỏi này rất hay, và rất quan trọng. Theo như mình có nói, bất kỳ một ứng dụng nào sau khi được thiết kế ra, bạn sẽ dễ biết được ngay ứng dụng đó có bao nhiêu Activity. Nhưng trớ trêu thay, bạn lại khó mà biết được sẽ có bao nhiêu Fragment trong đám Activity đó. Bạn chỉ có thể biết được bao nhiêu và khi nào nên dùng Fragment khi tiếp cận vào nhu cầu thực tế của từng Activity đó mà thôi.

Qua ý trên, bạn có thể thấy sẽ có ứng dụng hoàn toàn không dùng đến Fragment, nếu UI của nó đủ đơn giản. Cũng sẽ có ứng dụng có nhiều Fragment, các Fragment này sẽ được bạn thêm vào trong chương trình trong quá trình xây dựng chúng, nó phụ thuộc rất nhiều vào kinh nghiệm của bạn. Dù sao đi nữa, nếu bạn biết triển khai một Activity thành nhiều Fragment con, có thể sẽ giúp tiết kiệm khá nhiều thời gian thiết kế, xây dựng và sửa lỗi nữa đấy.

Bỏ qua cái sự kinh nghiệm về việc dùng bao nhiêu Fragment đi, chúng ta tiếp tục trả lời câu hỏi tại sao của mục này. Như ở đầu bài viết, mình cũng nói đến vai trò cơ bản của Fragment đó là giúp giảm tải cho lập trình viên khi phải thiết kế giao diện linh động trên nhiều màn hình lớn nhỏ khác nhau. Bạn hãy nhìn vào minh họa cho một tình huống sau.

Mô phỏng sử dụng Fragment cho sự tương thích giữa các màn hình
Mô phỏng sử dụng Fragment cho sự tương thích giữa các màn hình

Với tình huống trên chúng ta có nhu cầu muốn thiết kế một giao diện linh động. Sao cho khi hiển thị trên màn hình lớn, như hình bên trái ở trên đây, thì giao diện hiển thị kiểu danh sách các item ở phía trái, click vào từng item sẽ hiển thị nội dung chi tiết của item đó ở phía phải. Nhưng ở hình bên phải, với thiết bị có màn hình nhỏ, danh sách các item được hiển thị hết toàn màn hình, và click vào từng item sẽ dẫn sang màn hình khác hiển thị nội dung của item đó.

Yêu cầu là vậy đó, nếu bạn chỉ sử dụng một Activity để tự động hiển thị danh sách và nội dung cùng nhau ở màn hình lớn, và hiển thị danh sách tách biệt với nội dung của nó ở màn hình nhỏ trên đây, thì bạn có thiết kế được không? Câu trả lời là được, nhưng phức tạp lắm.

Quay lại hình minh họa. Nếu bạn thiết kế danh sách các item là một Fragment, hình minh họa gọi nó là Fragment A, và nội dung của từng item sẽ là Fragment B. Thì với màn hình lớn bên trái, bạn hiển thị cả hai Fragment A và Fragment B lên cùng một Activity. Còn với màn hình nhỏ bên phải, bạn có thể hiển thị Fragment A và Fragment B thay phiên nhau, trên cùng một Activity hoặc khác Activity là do bạn tổ chức. Cách tổ chức các thành phần UI như vậy lên các Fragment rồi biến tấu chúng trên từng vùng của Activity rõ ràng vừa linh động, dễ dàng, hiệu quả như mình đã nói, lại còn thú vị nữa.

Ngoài ví dụ trên đây cho thấy bạn dễ dàng tạo được sự linh động của UI khi sử dụng Fragment với màn hình nhỏ (phone) và lớn (tablet), thì bạn còn có thể tổ chức sự linh động đối với các chế độ xoay màn hình của cùng một thiết bị. Ở bài học tiếp theo chúng ta sẽ vận dụng kiến thức tạo Fragment hôm nay để từng bước xây dựng một màn hình đạt chuẩn linh động khi xoay ngang/dọc này.

Fragment Và Sự Tương Thích Ngược

Như bạn có thể thấy rằng, Fragment ra đời cũng bởi một lý do chính, đó là hỗ trợ giao diện trên tablet. Nên Fragment rất gắn liền với sự ra đời của tablet. Và hệ điều hành đánh dấu cho việc hỗ trợ chính thức tablet chính là hệ điều hành Android 3.0 (APL level 11), nếu bạn còn nhớ, ở bài về ActionBar chúng ta cũng đã nói về sự hỗ trợ tablet này rồi.

Vậy thì, nếu project của bạn có khai báo minSdkVersion từ 11 trở lên, thì không có gì để nói. Nhưng nếu giá trị thiết lập này nhỏ hơn 11? Tin vui cho bạn rằng hệ thống vẫn hỗ trợ tương thích ngược đến với các ứng dụng như thế này, nhưng sẽ có một chút khác biệt đối với việc quyết định sử dụng lớp Fragment. Khác biệt đó như sau.

Khi xây dựng Fragment, bạn sẽ luôn thấy có hai lựa chọn Fragment ở hai package khác nhau như hai dòng đầu tiên ở hình bên dưới. Một Fragment thuộc về android.app và một Fragment thuộc về android.support.v4.app. Tất nhiên, bạn chỉ nên sử dụng một trong hai thôi, cách chọn lựa Fragment nào để sử dụng thì mình sẽ nói rõ hơn ở dưới đây.

Fragment - Các package hỗ trợ Fragment

Sau đây là cách chọn lựa Fragment nào dành cho bạn.

  • Nếu minSdkVersion của ứng dụng từ 11 trở lên. Bạn cứ thoải mái sử dụng Fragment “chính thức”, đó chính là Fragment trong gói android.app như dòng đầu tiên ở hình trên. Kèm theo đó bạn phải sử dụng phương thức getFragmentManager() khi cần hiển thị động Fragment lên Activity (bạn sẽ được làm quen với cách hiển thị này ở bài tiếp theo).
  • Còn nếu minSdkVersion của ứng dụng nhỏ hơn 11. Bạn hãy dùng đến Fragment ở gói tương thích ngược android.support.v4.app ở dòng thứ hai của hình. Nhưng khi này bạn phải dùng phương thức getSupportFragmentManager() cho mục đích hiển thị động Fragment. Và lại có ràng buộc nữa rằng Activity chứa đựng Fragment khi này không phải Activity thường mà phải là FragmentActivity. Tuy nhiên nếu bạn thấy Activity bạn dùng đang kế thừa từ AppCompatActivity rồi thì cũng yên tâm nhé, vì AppCompatActivity vừa hỗ trợ ActionBar đến các hệ điều hành Android cũ hơn, cũng vừa hỗ trợ cả Fragment.

Cách Xây Dựng Một Fragment

Ý tưởng của việc tổ chức và quản lý UI của Fragment rất giống với Activity. Nếu như với Activity, bạn phải xây dựng bộ đôi “thần thánh” XyzActivity.java và activity_xyz.xml, bạn có thể xem lại ý này ở link này. Thì với Fragment, bạn cũng sẽ phải xây dựng bộ đôi XyzFragment.java và fragment_xyz.xml. Khi đó file xml sẽ chịu trách nhiệm trong việc hiển thị UI, còn file java sẽ chịu trách nhiệm xử lý các logic. Và việc tạo bộ file này cho Fragment cũng được hỗ trợ hoàn toàn bởi wizard của Android Studio.

Nhưng khác với ActivityFragment sẽ không cần phải khai báo với manifest, vì đây không phải là một thành phần cơ bản của ứng dụng. Và Fragment sẽ không cần một Intent để kích hoạt, việc sử dụng Fragment như thế nào sẽ nằm ở bài tiếp theo.

Các Bước Tạo Mới Một Fragment

Phần này chúng ta chỉ làm quen qua các bước để tạo một Fragment. Bạn nên cùng thực hành để quen. Tuy nhiên code của bài thực hành này sẽ không có trên GitHub nhé, vì đến khi chính thức thực hành với TourNote chúng ta cũng sẽ tạo lại một Fragment.

Như đã nói, chúng ta vẫn sẽ dùng wizard để tạo mới Fragment. Cách tạo này sẽ giúp bạn có sẵn bộ java và xml.

Để bắt đầu, ở cửa sổ Project, bạn hãy để vệt sáng ở package sẽ chứa file Fragment java mà bạn muốn tạo. Rồi chọn theo menu File > New > Fragment > Fragment_Nào_Đó. Hoặc nhấn chuột phải và chọn New > Fragment > Fragment_Nào_Đó như hình dưới đây.

Tạo mới một Fragment
Tạo mới một Fragment

Cũng như khi bạn tạo mới Activity, menu trên cho bạn nhiều tùy chọn template Fragment khác nhau. Bạn có thể cân nhắc sử dụng bất kỳ loại Fragment nào có sẵn trong menu này cũng được, mỗi loại như vậy sẽ tự động thêm các source code tương ứng cho bạn. Để có thể dễ dàng để làm quen ban đầu thì mình khuyên bạn nên chọn loại Fragment (Blank).

Sau khi chọn template Fragment ở bước trên, một dialog xuất hiện yêu cầu bạn đặt tên cho Fragment mới này, ở cả file Java và file xml. Chúng ta đặt tên cho nó là FirstFragment nhé. Lưu ý là từ Android Studio 3.0, khi tạo mới một Activity hay Fragment, bạn đã có thể lựa chọn ngôn ngữ là Java hay Kotlin ở mục Source Language ở bước này được rồi đấy nhé.


Một ý bổ sung nữa là, ở dialog trên, với Fragment này bạn đừng check vào include fragment factory method?. Mình sẽ giải thích hai check chọn này là gì cho bạn hiểu rõ ràng hơn.

  • Include fragment factory methods? – Check chọn này cho phép wizard sau đó sẽ tạo mới Fragment có kèm thêm các phương thức truyền dữ liệu từ bên ngoài vào. Dĩ nhiên là chúng ta vẫn sẽ dùng Bundle để truyền dữ liệu từ Activity vào Fragment (khi khởi tạo Fragment), cách dùng Bundle này cũng sẽ giống với cách mà bạn đã làm quen ở Activity vậy. Bài hôm nay các bạn chưa làm quen đến các đoạn code này vì FirstFragment này chưa cần dùng đến. Chúng ta sẽ được tiếp cận việc tạo và truyền dữ liệu qua lại này ở bài sau.
  • Include interface callbacks? – Check chọn này cho phép wizard sau đó sẽ tạo mới Fragment có kèm thêm các phương thức lắng nghe và hiện thực các sự kiện gọi về (Interface Callback). Các sự kiện này sẽ cho phép Activity chứa Fragment đó biết được các hành động mà người dùng tương tác bên trong Fragment, để có các hành xử logic thích hợp. Dĩ nhiên FirstFragment này nên được check vào check chọn này vì MainActivity cần phải biết khi nào và Button nào bên trong FirstFragment được click. Chúng ta cũng sẽ có dịp làm quen với các sự kiện này ở bài sau luôn.

Làm Quen Với Fragment

Bạn đã tạo mới một FirstFragment ở bước trên đây. Việc tạo một Fragment cực dễ như vậy đó. Nhưng code mà bạn nhận được sau đó… đọc vào rối rắm quá đúng không. Mục này chúng ta sẽ làm quen sơ bộ các dòng code này, để có thể hiểu rõ hơn cách mà một Fragment hiển thị nội dung và thực hiện các logic của nó. Bạn sẽ hiểu toàn bộ các dòng code của Fragment thông qua bài viết về vòng đời của Fragment.

Giao Diện Của Fragment (File XML)

Đầu tiên, mời bạn làm quen với giao diện của FirstFragment được chứa trong file xml mà bạn mới tạo trên kia, chính là file fragment_first.xml. Giao diện thì không có gì để nói thêm.

Tuy nhiên, dù cho giao diện hiện tại của FirstFragment thế nào, mình muốn bạn chỉnh sửa cho nó giống như sau. Mình muốn FirstFragment này chính là list các item, như Fragment A ở mục trên kia. Nhưng vì chúng ta chưa có học về list, thì hãy tạo tạm cho mình hai Button nhé, chúng ta giả lập list hiện tại có hai item như vậy thôi.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.yellowcode.tournote.FirstFragment">
  
    <Button
        android:id="@+id/btnItem1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Item 1"/>
  
    <Button
        android:id="@+id/btnItem2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Item 2"/>
  
</LinearLayout>

Logic Của Fragment (File Java)

Fragment cũng như Activity, chúng đều không có constructor. Vậy phương thức nào sẽ dùng để khởi tạo các giao diện và các giá trị cho chúng? Vâng, bạn đã biết Activity dùng đến phương thức khởi tạo là onCreate(). Thì với Fragment bạn phải dùng onCreateView(). Tại sao lại là phương thức này thì đến mục về vòng đời của Fragment bạn sẽ hiểu rõ hơn nhé. Còn bây giờ mời bạn làm quen với onCreateView(). Đây là các dòng code mặc định của phương thức này khi bạn tạo mới một Fragment.

Phương thức onCreateView() của một Fragment
Phương thức onCreateView() của một Fragment

Qua dòng code trên, bạn có thể phát hiện thêm một điều khác biệt nữa giữa Activity và Fragment. Đó là Fragment không dùng phương thức setContentView() để đọc file xml UI như với Activity. Thay vào đó nó phải gọi phương thức inflate() của LayoutInflater, như hình trên. Phương thức này cho phép bạn định nghĩa một xml UI, trong trường hợp này là R.layout.fragment_first, rồi gắn nó vào trong container chính là một ViewGroup.

Nếu kiến thức về inflate gì đó quá lu bu thì bạn có thể tạm chưa cần phải nhớ kỹ lúc này. Cái bạn quan tâm là nếu bây giờ chúng ta muốn tạo sự kiện click cho hai Button đã khai báo ở giao diện xml trên kia thì làm sao? Bạn đã biết, nếu ở Activity chúng ta chỉ việc “lấy” Button từ xml qua Java bằng phương thức findViewById(), như mình có nói đến ở đây. Thì ở Fragment, dòng code findViewById() cũng sẽ được dùng, nhưng thông qua một cấp view mà bạn vừa inflate, bạn hãy xem code sửa đổi sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    View view = inflater.inflate(R.layout.fragment_first, container, false);
  
    Button btnItem1 = (Button) view.findViewById(R.id.btnItem1);
    Button btnItem2 = (Button) view.findViewById(R.id.btnItem2);
  
    btnItem1.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Toast.makeText(getActivity(), "Button 1 clicked", Toast.LENGTH_SHORT).show();
        }
    });
    btnItem2.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Toast.makeText(getActivity(), "Button 2 clicked", Toast.LENGTH_SHORT).show();
        }
    });
  
    return view;
}

Với code trên, mình đã cố tình tách dòng inflate ra để tạo thành một view, để mà dùng đến view đó để gọi các Button thông qua ID của chúng. Cuối cùng là return view đó sau khi đã “đạt được mục đích”.

Mọi thứ không quá khó ha. Tuy nhiên, qua các dòng code show message dạng Toast trên đây, nếu chú ý kỹ, bạn sẽ lại bắt gặp một chi tiết nữa khác nhau giữa Fragment và Activity mà bạn đã biết. Đó là ở tham số thứ nhất của Toast.makeText(), tham số này đòi hỏi một Context, tuy cho đến bài học hôm nay bạn vẫn chưa nắm rõ khái niệm Context lắm, nhưng như những gì đã làm quen với Activity, bạn có biết rằng Activity chính là một Context. Và do đó, bên Activity, bạn truyền vào từ khóa this. Qua Fragment, bạn cần phải có Activity để truyền vào, thế là bạn chỉ cần gọi getActivity(), phương thức này sẽ tự động gọi lấy Activity đang chứa Fragment này. Phương thức getActivity() này cũng sẽ rất hữu dụng cho các tình huống sau này nữa. Bạn nhớ ghi chú lại nhé.

Cơ bản tạo một Fragment là vậy. Chúng ta sẽ cùng nhau xem qua cách thức sắp xếp các Fragment lên Activity, và xem sự tương tác giữa chúng với Activity, và giữa các Fragment với nhau sẽ như thế nào ở bài viết kế tiếp nhé.

Tiếp Tục Làm Quen Với Fragment

Thông qua bài mở màn về Fragment hôm trước, bạn đã biết được Fragment là gì, tại sao phải dùng đến Fragment, và đã biết cách tạo một Frament như thế nào.

Sang bài học hôm nay, mình dự định sẽ nói về các cách để hiển thị Fragment lên Activity một cách linh động như đã hứa. Nhưng thiết nghĩ với việc tiếp cận “sơ sài” với các dòng code của FirstFragment hôm trước, thì sẽ rất khó để có thể vận dụng vào Activity của bài này. Và còn SecondFragment vẫn chưa được tạo nữa chứ. Thế nên mình quyết định sẽ dành thêm một bài nữa để nói rõ về các dòng code quan trọng khi bạn tạo mới các Fragment theo wizard ở bài trước. Đó là các dòng code được phát sinh theo hai check chọn Include fragment factory methods? và Include interface callbacks?.

Các check chọn khi bạn tạo mới một Fragment

Nào mời bạn cùng quay lại với FirstFragment để xem các dòng code quan trọng này nhé.

Hoàn Chỉnh FirstFragment

Vâng, FirstFragment đã được tạo ở bài trước. Tuy nhiên, như mình có nói, hôm trước chúng ta chỉ mới xây dựng giao diện và sự kiện click cho hai Button để mà làm quen với Fragment là gì mà thôi, nên kiến thức về Fragment đó còn sơ sài lắm.

Hôm nay chúng ta sẽ hoàn thiện FirstFragment, sao cho Fragment này có thể nói cho MainActivity (mình dự định Activity này sẽ chứa cả hai Fragment mà chúng ta đang xây dựng) biết Button nào đang được nhấn. Sau đó MainActivity sẽ tự nó báo cho SecondFragment biết nên hiển thị nội dung gì.

Và ở bài học trước, khi tạo mới FirstFragment bằng wizard, bạn có nhớ rằng chúng ta chỉ check chọn vào Include interface callbacks?. Check chọn này giúp Fragment khi đó được tạo ra với các dòng code sẵn sàng cho việc thông báo ngược lại cho Activity chứa nó biết các sự kiện tương ứng, chính là các sự kiện nhấn vào các Button.

Làm Quen Với Các Dòng Code Interface Callback

Dưới đây là các dòng code liên quan đến interface callbacks được tạo ra sẵn, mình tô sáng chúng cho bạn dễ nhìn.

Các dòng code interface callbacks khi tạo mới Fragment

Bạn có thể thấy, code tự phát sinh sẵn cho bạn một interface có tên OnFragmentInteractionListener (khối code số 5). Interface này có định nghĩa sẵn một phương thức trừu tượng có tên onFragmentInteraction(). Dĩ nhiên đây là các code mẫu nên tên của chúng đều mang ý nghĩa chung chung, bạn có thể thay đổi tên, hoặc thêm vào các phương thức tùy ý. Mỗi phương thức bên trong interface này sẽ đại diện cho một hành động nào đó mà bạn muốn thông báo ra bên ngoài.

Khối code số 1 sẽ giúp định nghĩa một thuộc tính là kiểu interface vừa được nói ở trên. Thuộc tính này sẽ được khởi tạo ở khối code số 3 khi mà Fragment được gắn vào Activity. Chính Activity chứa đựng Fragment này sẽ phải implement interface này. Đến bài học sau bạn sẽ hiểu rõ hơn.

Khối code số 2 là nơi mà sự kiện Button được click (khối code này sẽ không còn hữu dụng khi bạn thực hành qua mục sau, tuy nhiên bạn cũng nên biết việc lắng nghe sự kiện click và hành xử của phương thức này sẽ diễn ra như thế nào). Tại đây, phương thức trừu tượng trong mListener sẽ được gọi. Một lần nữa, Chính Activity chứa đựng Fragment này và đang implement phương thức trừu tượng này sẽ phải định nghĩa các hành động cụ thể. Bài sau bạn sẽ thấy Activity biết được các lời gọi này từ Fragment như thế nào.

Khối code số 4 sẽ hủy interface này, tức sẽ không còn gọi đến Activity nữa khi mà Fragment này bị gỡ khỏi Activity. Bạn sẽ được nắm rõ hơn sự kiện onDetach() này ở bài học về vòng đời của Fragment.

Hoàn Thiện Code Cho FirstFragment

Nếu những dẫn giải của mình về code cho FirstFragment trên kia lan man và lu bu quá thì bạn đừng vội nản nhé. Bạn sẽ quen dần với các dòng code này thôi.

Để hoàn thiện cho FirstFragment. Đầu tiên, mình muốn interface của nó phải rõ nghĩa một chút. Mình đổi tên của interface ở khối code số 5 thành OnFirstFragmentListener. Phương thức trừu tượng của nó mình cũng đổi tên luôn thành onItemPressed(), và sửa tham số truyền vào là một nội dung String nào đó. Sau này, MainActivity sẽ implement listener này và phương thức này sẽ lấy String được truyền vào này để gởi qua SecondFragment.

1
2
3
public interface OnFirstFragmentListener {
    void onItemPressed(String content);
}

Vì bạn đổi tên cho listener, nên những chỗ nào có sử dụng listener này bạn đều phải thay thế bằng cái tên mới hết nhé.

Sự thay đổi tiếp theo, mình sẽ không cần khối code số 2 nữa, vì chúng ta đã xây dựng sự kiện click cho hai Button từ bài hôm trước rồi. Như vậy khối code này sẽ bị xóa. Và chúng ta cũng phải thay các dòng hiển thị thông báo dạng Toast hôm trước ở các sự kiện click của Button thành các lời gọi như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    View view = inflater.inflate(R.layout.fragment_first, container, false);
 
    Button btnItem1 = (Button) view.findViewById(R.id.btnItem1);
    Button btnItem2 = (Button) view.findViewById(R.id.btnItem2);
 
    btnItem1.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (mListener != null) {
                mListener.onItemPressed("This is a content when Button 1 click");
            }
        }
    });
    btnItem2.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (mListener != null) {
                mListener.onItemPressed("Hey, this is a Button 2 content");
            }
        }
    });
 
    return view;
}

Và đây là toàn bộ code hoàn chỉnh của FirstFragment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class FirstFragment extends Fragment {
 
    private OnFirstFragmentListener mListener;
 
    public FirstFragment() {
        // Required empty public constructor
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_first, container, false);
 
        Button btnItem1 = (Button) view.findViewById(R.id.btnItem1);
        Button btnItem2 = (Button) view.findViewById(R.id.btnItem2);
 
        btnItem1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mListener != null) {
                    mListener.onItemPressed("This is a content when Button 1 click");
                }
            }
        });
        btnItem2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mListener != null) {
                    mListener.onItemPressed("Hey, this is a Button 2 content");
                }
            }
        });
 
        return view;
    }
 
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof OnFirstFragmentListener) {
            mListener = (OnFirstFragmentListener) context;
        } else {
            throw new RuntimeException(context.toString()
                    + " must implement OnFragmentInteractionListener");
        }
    }
 
    @Override
    public void onDetach() {
        super.onDetach();
        mListener = null;
    }
 
    public interface OnFirstFragmentListener {
        void onItemPressed(String content);
    }
}

Tạo Mới SecondFragment

Thông qua FirstFragment, bạn đã làm quen được cách mà một Fragment thông báo ngược cho Activity biết các sự kiện xảy ra bên trong đó thông qua một interface callback như thế nào.

Giờ thì bạn hãy tự vận dụng các bước tạo FirstFragment để tạo thêm một Fragment nữa nhé. Fragment này có tên là SecondFragment. Lần này thì bạn hãy đảm bảo check chọn Include fragment factory methods? được check mà thôi. Check chọn này cho phép SecondFragment được tạo ra sau đó có chứa các dòng code nhận dữ liệu từ Activity truyền vào khi khởi tạo.

Cửa sổ wizard khi tạo mới Fragment

Tạo Giao Diện Cho SecondFragment

Bạn hãy mở file giao diện của SecondFragment lên, đó chính là file fragment_second.xml. Chúng ta chỉ việc thêm một TextView để hiển thị nội dung nào đó cho Fragment này mà thôi.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#CFCFCF"
    android:padding="8dp"
    tools:context="com.yellowcode.tournote.SecondFragment">
 
    <TextView
        android:id="@+id/tvContent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="Content" />
 
</FrameLayout>

Làm Quen Với Các Dòng Code Factory Method

Giờ thì qua file java của SecondFragment. Và đây là các dòng code liên quan đến check chọn Include fragment factory methods? được mình cho sáng lên như sau.

Các dòng code factory method khi tạo mới Fragment

Đầu tiên, khối code số 1 là các dòng khai báo các key dùng cho Bundle. Nếu bạn chưa biết rõ Bundle là gì thì có thể xem lại mục này. Cơ bản thì Bundle giúp truyền nhận dữ liệu từ ngoài vào Activity và Fragment.

Khối code số 2 khai báo các thuộc tính mParam1 và mParam2, đây chính là các value chứa đựng các giá trị lấy ra từ Bundle.

Phương thức newInstance() ở khối code số 3 sẽ là đầu vào dữ liệu cho Fragment. Ở nơi nào đó khi gọi đến phương thức static này sẽ phải truyền dữ liệu vào theo các tham số param1 và param2. Bên trong phương thức này sẽ tự động tạo ra Bundle, rồi tạo ra các bộ key/value chính là các param vừa nhận, rồi nhét vào Bundle, rồi gửi qua Fragment được khởi tạo ngay chính trong phương thức này luôn, đó chính là SecondFragment. Cách mà phương thức này hoạt động nằm trong một dạng code chuẩn có cái tên là Factory.

Đến phương thức onCreate() ở khối code số 4 thì các bộ key/value sẽ được lấy ra dùng thông qua Bundle đã được “đóng gói” ở newInstance() trên kia và truyền qua.

Như bạn cũng đã làm quen ở FirstFragment, đây cũng chỉ là các code mẫu được đặt những cái tên khá chung chung. Nên chúng ta sẽ phải thay đổi một chút SecondFragment như mục tiếp theo sau.

Hoàn Thiện Code Cho SecondFragment

Do SecondFragment chỉ cần một nội dung đưa vào để hiển thị lên TextView, nên chúng ta chỉ cần một bộ key/value để lấy thông tin text này là đủ.

Ở khối code số 1 ta chỉ cần định nghĩa một key.

1
private static final String ARG_CONTENT = "content";

Tương tự thì khối code số 2 cũng chỉ cần định nghĩa một value.

1
private String mContent;

Khối code số 3 khi này chỉ cần truyền một tham số String vào phương thức. Và đóng gói một bộ key/value vào Bundle rồi truyền qua Fragment vừa được tạo mà thôi.

1
2
3
4
5
6
7
public static SecondFragment newInstance(String content) {
    SecondFragment fragment = new SecondFragment();
    Bundle args = new Bundle();
    args.putString(ARG_CONTENT, content);
    fragment.setArguments(args);
    return fragment;
}

Và cuối cùng khối code số 4 cũng được điều chỉnh tương tự.

1
2
3
4
5
6
7
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (getArguments() != null) {
        mContent = getArguments().getString(ARG_CONTENT);
    }
}

Chúng ta cũng sẽ hoàn thiện phương thức onCreateView() ở SecondFragment này sao cho có thể hiển thị nội dung lên TextView. Bạn xem.

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    View view = inflater.inflate(R.layout.fragment_second, container, false);
 
    TextView tvContent = (TextView) view.findViewById(R.id.tvContent);
    if (!TextUtils.isEmpty(mContent)) {
        tvContent.setText(mContent);
    }
 
    return view;
}

Đây là toàn bộ code hoàn chỉnh của SecondFragment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class SecondFragment extends Fragment {
 
    private static final String ARG_CONTENT = "content";
 
    private String mContent;
 
    public SecondFragment() {
        // Required empty public constructor
    }
 
    public static SecondFragment newInstance(String content) {
        SecondFragment fragment = new SecondFragment();
        Bundle args = new Bundle();
        args.putString(ARG_CONTENT, content);
        fragment.setArguments(args);
        return fragment;
    }
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mContent = getArguments().getString(ARG_CONTENT);
        }
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_second, container, false);
 
        TextView tvContent = (TextView) view.findViewById(R.id.tvContent);
        if (!TextUtils.isEmpty(mContent)) {
            tvContent.setText(mContent);
        }
 
        return view;
    }
}

Như vậy là chúng ta vừa mới hoàn thành xong hai Fragment. Và đã đặt tên chúng là FirstFragment và SecondFragment. Hai Fragment này được tạo ra với hai check chọn khác nhau ở wizard. Và bạn cũng đã làm quen với các code mẫu phát sinh đối với từng check chọn này. Dĩ nhiên nếu bạn tạo mới một Fragment nào đó mà cả hai check chọn này đều được chọn, thì code phát sinh sau đó sẽ bao gồm tất cả những gì mà chúng ta làm quen trong bài học hôm nay. Và nếu như bạn không tạo Fragment bằng wizard, thì sẽ không có các code mẫu như bài học, thì bạn vẫn có thể hoàn toàn áp dụng các dòng code của bài học hôm nay vào Fragment của riêng bạn. Đối với mình, các dòng code được tạo sẵn này rất tiện lợi và thú vị. Các bạn sẽ nắm rõ hơn ý nghĩa của các dòng code của bài hôm nay ở bài học sau thôi.

Hiển Thị Fragment

Như vậy, kết thúc bài học hôm trước, chúng ta đã có trong tay hai FragmentFragment thứ nhất có tên FirstFragment đang hiển thị hai ButtonFragment thứ hai dĩ nhiên là có tên SecondFragment rồi, Fragment này đang chứa một TextView sẽ hiện thị nội dung nào đó tùy theo user sẽ nhấn Button nào ở FirstFragment.

Bài học tiếp theo này chúng ta sẽ tìm cách hiển thị hai Fragment này lên Activity, và thực hiện truyền thông tin qua lại giữa chúng nữa. Bạn sẽ thấy rằng, sử dụng Fragment không khó như bạn nghĩ đâu.

Nào hãy cùng mình xem và thực hành từng bước thông qua các kiến thức của bài học hôm nay nhé.

Các Cách Thức Hiển Thị Fragment

Trước khi chính thức thực hiện việc hiển thị, chúng ta cần hiểu xem có các cách hiển thị nào.

Bạn cũng nên biết rằng, hiện tại mình dùng từ Hiển thị. Nhưng bạn có thể hiểu chúng ta đang tìm cách sắp xếp các Fragment lên Activity, hiển thị chúng, và tương tác với chúng nữa. Tất cả những hành động trên đây mình gom lại thành hai từ Hiển thị mà thôi.

Chúng ta có hai cách để hiển thị các Fragment. Cách thứ nhất là hiển thị Fragment theo kiểu tĩnh. Cách thứ hai đương nhiên là cách hiển thị Fragment theo kiểu động rồi. Vậy thì hai cách này khác nhau ra sao?

Với cách hiển thị Fragment theo kiểu tĩnh. Nghe qua cái tên bạn đã thấy có gì đó mang ý nghĩa cố định rồi. Cố định ở đây có nghĩa là bạn phải chỉ định trước các Fragment cần hiển thị lên màn hình. Khi ứng dụng thực thi, sẽ không có chuyện từ một vùng màn hình đang hiển thị Fragment A, có thể được thay thế nó bởi Fragment B hay bất kỳ Fragment nào khác. Sự linh động của các Fragment đối với cách hiển thị như thế này không cao. Thường thì nếu bạn biết được Activity đó cần hiển thị những Fragment nào và ở vị trí nào, khi đó bạn có thể định nghĩa tĩnh các Fragment như này. Cách hiển thị này có ưu điểm là giúp bạn thiết kế ra các ứng dụng rất nhanh, vì không cần quá nhiều các dòng code. Chắc chắn bạn sẽ được làm quen với cách thức hiển thị Fragment theo kiểu này ở mục bên dưới.

Còn cách hiển thị Fragment theo kiểu động thì đương nhiên sẽ linh động hơn kiểu tĩnh rồi. Nếu bạn sử dụng cách này để hiển thị các Fragment, thông qua việc quản lý bằng Java code, bạn có thể thoải mái đặt Fragment nào đó vào một vùng không gian đã được chỉ định sẵn, hoặc lấy Fragment đó ra khỏi vùng không gian đó, hoặc thay thế bằng một Fragment khác. Cách hiển thị này giúp bạn đạt tới “cảnh giới” cao nhất của sự linh động trong việc sử dụng Fragment. Và dĩ nhiên chúng ta cũng sẽ cùng nhau thực hành hiển thị Fragment theo kiểu này ở bài học hôm nay.

Làm Quen Với Cách Hiển Thị Fragment Theo Kiểu Tĩnh

Giao diện khi sử dụng các Fragmenthttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2017/12/phone_large.png?resize=166%2C300&ssl=1 166w" data-lazy-loaded="1" sizes="(max-width: 300px) 100vw, 300px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px auto; clear: both; text-align: center;">

Chúng ta sẽ hiển thị giao diện như trên đây.

Cách này rất nhanh chóng. Bạn chỉ cần sử dụng một layout có tên là fragment để hiển thị một Fragment mà bạn mong muốn.

Layout fragment này cũng cần bạn chỉ định các thuộc tính android:layout_width và android:layout_height như các layout khác. Chính vì vậy bạn có thể thiết kế bao nhiêu fragment vào trong giao diện của Activity đều được, và đặt chúng vào vào bất cứ vị trí nào bạn muốn. Chính thuộc tính android:name của thẻ fragment này sẽ giúp bạn chỉ định Fragment nào cần hiển thị.

Bạn xem code sau, mình sẽ chỉnh sửa ở file activity_main.xml của TourNote. Mình tìm đến layout chứa TextView và ImageView mà chúng ta đã thực hành, và thay thế hai widget này bằng hai thẻ fragment. Các UI còn lại liên quan đến Navigation Drawer mình vẫn giữ như cũ.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- Content -->
 
<LinearLayout
    android:id="@+id/activity_main_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.yellowcode.tournote.MainActivity">
 
    <fragment
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:name="com.yellowcode.tournote.FirstFragment"/>
 
    <fragment
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="2"
        android:name="com.yellowcode.tournote.SecondFragment"/>
 
</LinearLayout>

Bạn có thể tự tạo một project mới rồi sửa chữa trên layout chính của project đó cũng được nhé. Nhớ là phải đảm bảo có sẵn hai Fragment đã được tạo ra ở bài hôm trước.

Rất nhanh. Bạn đã hiển thị hai Fragment vào trong một LinearLayout rồi đấy. Bạn để ý xem các giá trị android:name=”com.yellowcode.tournote.FirstFragment và android:name=”com.yellowcode.tournote.SecondFragment giúp hệ thống xác định Fragment nào cần hiển thị lên layout fragment nào.

Có lẽ bạn đang rất nôn nóng được xem diện mạo hai Fragment như thế nào. Bình thường với một số Fragment chuyên hiển thị dữ liệu, như SecondFragment, thì đến bước này bạn đã có thể thực thi ứng dụng để kiểm thử được rồi. Nhưng vì FirstFragment có định nghĩa interface OnFirstFragmentListener, và ở phương thức onAttach() của FirstFragment có kiểm tra nếu Activity chứa nó không implement interface này, thì sẽ báo lỗi. Chính vì vậy bạn phải thêm các dòng code sau vào MainActivity.java trước khi thực thi để tránh lỗi.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class MainActivity extends AppCompatActivity implements FirstFragment.OnFirstFragmentListener {
 
    // Các thuộc tính...
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Code như các bài học trước...
    }
 
    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        // Code như các bài học trước...
    }
 
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        // Code như các bài học trước...
    }
 
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Code như các bài học trước...
    }
 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Code như các bài học trước...
    }
 
    @Override
    public void onItemPressed(String content) {
        // Implement phương thức trừu tượng của OnFirstFragmentListener
    }
}

Giờ thì bạn có thể thực thi ứng dụng được rồi. Đây là thành quả của chúng ta.

Giao diện kết quả khi sử dụng các Fragment

Bạn có thể thấy phần không gian màu trắng chứa hai Button ITEM 1 và ITEM 2 chính là giao diện của FirstFragment đấy. Còn phần không gian màu xám chứa một nội dung “Content” thì chính là giao diện của SecondFragment.

Chúng ta chỉ thực hành hiển thị các Fragment lên màn hình ở mục này thôi, chứ không đi sâu hơn với sự tương tác khi user touch lên bất kỳ Button nào ở FirstFragment. Vì thông thường cách hiển thị tĩnh như này chỉ nên dùng khi bạn muốn tổ chức nhanh các Fragment mà không đòi hỏi quá nhiều vào logic của chúng. Nếu bạn không thích cách hiển thị có phần thụ động này, thì hãy đọc tiếp mục sau nhé.

Làm Quen Với Cách Hiển Thị Fragment Theo Kiểu Động

Giao diện khi sử dụng các Fragment

Với cách hiển thị này thì giao diện của màn hình cũng sẽ không đổi. Như hình trên. Vậy sẽ động hơn như thế nào, bạn xem.

Nếu như với cách hiển thị tĩnh trên kia, bạn phải chỉ định thẻ fragment nào sẽ chứa đựng Fragment nào một cách cố định. Thì với cách hiển thị động này, bạn chỉ cần khai báo một vùng không gian nào đó sẽ chứa đựng Fragment, vùng không gian đó được khai báo bằng một FrameLayout.

Nào, vậy chúng ta cần phải thay đổi tí giao diện của activity_main.xml sao cho chứa đựng hai FrameLayout như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- Content -->
 
<LinearLayout
    android:id="@+id/activity_main_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.yellowcode.tournote.MainActivity">
 
    <FrameLayout
        android:id="@+id/firstFrame"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>
 
    <FrameLayout
        android:id="@+id/secondFrame"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="2"/>
 
</LinearLayout>

Giờ chúng ta sẽ qua MainActivity.java để thiết lập logic hiển thị và tương tác giữa các Fragment.

Chúng ta vẫn phải giữ dòng code implements FirstFragment.OnFirstFragmentListener của MainActivity và implement phương thức onItemPressed() mà bạn đã thêm vào ở mục trên. Vì như mình đã có nói, bất cứ khi nào mà bạn có thêm FirstFragment vào Activity, bạn buộc phải có các dòng code này.

Trước khi tiến hành add Fragment vào các FrameLayout mà bạn đã xây dựng sẵn, chúng ta cần làm quen một số lớp quan trọng liên quan sau.

FragmentManager

Đây là một lớp dùng để quản lý các Fragment, lớp này được tích hợp vào trong mỗi Activity để giúp các Activity có thể dễ dàng để thêm (add), xóa (remove) hoặc thay thế (replace) các Fragment ra khỏi một vùng không gian một cách linh động.

Nếu bạn có đọc kỹ về sự tương thích ngược của Fragment, thì có thể thấy có hai lớp FragmentManager mà bạn có thể cân nhắc sử dụng.

Các FragmentManager trong các gói

Mình xin nhắc lại tóm tắt cách chọn các lớp tương thích ngược như sau.

– FragmentManager ở gói android.app – Được gọi thông qua phương thức getFragmentManager() của Activity. Nếu project của bạn khai báo minSdkVersion từ 11 trở lên thì cứ thoải mái sử dụng em này.
– FragmentManager ở gói android.support.v4.app – Được gọi thông qua phương thức getSupportFragmentManager() của Activity. Nếu project của bạn khai báo minSdkVersion nhỏ hơn 11 thì bạn buộc phải dùng lớp quản lý này để có thể mang Fragment đến với các hệ điều hành trước Android 3.0.

Lớp này có một phương thức rất hữu dụng mà mình xin được nói trước như sau.

– findFragmentById() – Khi dùng phương thức này bạn sẽ truyền vào cho nó một ID. ID này có thể là ID của thẻ fragment như với mục hiển thị tĩnh trên kia. Hoặc ID của FrameLayout như bạn mới vừa làm quen ở code trên đây. Kết quả của phương thức này sẽ trả về cho bạn một Fragment được chứa trong layout có ID mà bạn vừa cung cấp.

FragmentTransaction

Khi đã có FragmentManager, bạn bắt đầu thực hiện việc thêm, xóa, thay đổi thoải mái các Fragment dựa vào FragmentTransaction này. Bạn có thể “triệu hồi” FragmentTransaction thông qua phương thức beginTransaction() từ FragmentManager.

FragmentTransaction có các phương thức thú vị để bạn “chơi” với Fragment như sau.

– add() – Khi FrameLayout còn rỗng, tức chưa chứa đựng bất kỳ Fragment nào, thì bạn có thể dùng phương thức này để add Fragment vào cho FrameLayout đó. Như bạn sắp làm quen với code add một FirstFragment sau đây.
– replace() – Khi bạn muốn thay thế một Fragment đang có sẵn ở FrameLayout bằng một Fragment nào đó khác.
– remove() – Khi bạn muốn gỡ bỏ Fragment ra khỏi một FrameLayout nào đó.
– addToBackStack() – Khi bạn quản lý Fragment bởi các phương thức replace() hay remove() trên đây, bạn có thể sử dụng thêm phương thức addToBackStack() này. Nếu bạn gọi đến phương thức này trước khi gọi commit() sẽ được nói ở dưới, thì hệ thống sẽ đưa Fragment ở transaction hiện tại vào Back Stack. Điều này có ý nghĩa là, Fragment bị thay thế hay bị gỡ ra khỏi FrameLayout ở transaction này sẽ không bị xóa khỏi hệ thống mà vẫn còn được quản lý bên trong Back Stack (bạn sẽ được làm quen với Back Stack của Fragment ở bài học sau). Và do Fragment không bị hủy khỏi Back Stack, nên nếu user nhấn nút back sau đó, họ hoàn toàn có thể quay trở lại với Fragment trước đó đã bị gỡ ra.
– commit() – Cho dù bạn có quản lý hiển thị Fragment bằng add()replace() hay remove() thì bạn cũng phải gọi commit() cuối cùng, để FragmentTransaction biết sẽ bắt đầu thực hiện các transaction mà bạn đã ra lệnh đó.

Nếu như thông tin về FragmentManager và FragmentTransaction trên đây khó hiểu quá, thì bạn cứ tiếp tục thực hành tiếp phần sau đây. Bạn sẽ nhanh chóng hiểu rõ hơn thôi.

Chúng ta bắt đầu add FirstFragment vào trong FrameLayout có ID là firstFrame ngay khi MainActivity được hiển thị như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
 
    drawerLayout = (DrawerLayout) findViewById(R.id.activity_main_drawer);
    drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
    drawerLayout.addDrawerListener(drawerToggle);
 
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    getSupportActionBar().setHomeButtonEnabled(true);
 
    FirstFragment firstFragment = new FirstFragment();
    FragmentManager fragmentManager = getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    fragmentTransaction.add(R.id.firstFrame, firstFragment);
    fragmentTransaction.commit();
}

Các dòng code add FirstFragment vào firstFrame là các dòng được tô sáng trên đây. Dòng đầu tiên là dòng khai báo FirstFragment. Kế tiếp bạn nên khai báo FragmentManager, vì Fragment chúng ta đang dùng đã được hệ thống tạo sẵn nằm trong gói android.support.v4.app, nên FragmentManager cũng sẽ được tạo ra thông qua phương thức getSupportFragmentManager(). Sau đó FragmentTransaction được tạo ra, và chúng ta dùng phương thức add() để add firstFragment vào firstFrame. Cuối cùng bạn phải nhớ phương thức commit(). Khi này nếu thực thi ứng dụng, bạn đã có thể trông thấy FirstFragment hiển thị rồi đấy.

Tiếp theo, do bạn đã implement phương thức trừu tượng onItemPresses() ở MainActivity, nên bạn sẽ nhận được một String khi user nhấn vào một Button nào đó ở FirstFragment. Đây là hình ảnh gợi nhớ lại các dòng code truyền một String mà bạn đã xây dựng bên FirstFragment ở bài hôm trước.

Sự kiện click ở FirstFragment

Do đó, mình sẽ căn cứ vào phương thức onItemPressed() này mà thực hiện việc khởi tạo SecondFragment và add vào FrameLayout có ID là secondFrame như sau.

1
2
3
4
5
6
7
8
@Override
public void onItemPressed(String content) {
    SecondFragment secondFragment = SecondFragment.newInstance(content);
    FragmentManager fragmentManager = getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    fragmentTransaction.replace(R.id.secondFrame, secondFragment);
    fragmentTransaction.commit();
}

Bạn thấy, SecondFragment được tạo mới không thông qua từ khóa new như với FirstFragment, vì ở bài hôm trước chúng ta đã xây dựng (thực ra là sửa lại) phương thức khởi tạo newInstance() có kèm các value truyền vào rồi, nên bài này chúng ta sử dụng thôi. Các khởi tạo cho FragmentManager và FragmentTransaction thì vẫn như cách làm với FirstFragment trên kia. Lần này thay vì gọi phương thức add(), chúng ta dùng replace() sẽ hợp lý hơn, vì user có thể sẽ nhấn nhiều lần lên các Button ở FirstFragment, mỗi lần như vậy chúng ta cần thay thế Fragment cũ bằng Fragment mới tại secondFrame này. Và cuối cùng, phương thức commit() luôn phải được gọi.

Dưới đây là tất cả code của activity_main.xml và MainActivity.java mà bạn có thể tham khảo.

– activity_main.xml

– MainActivity.java

Giờ đây bạn có thể thực thi ứng dụng, và nhấn thoải mái lên các Button để kiểm chứng được rồi nhé.

Giao diện kết quả khi sử dụng các Fragment

Làm Quen Với Cảnh Giới Cao Nhất Của Hiển Thị Fragment Theo Kiểu Động

Nếu bạn đã thành công với các bước hiển thị Fragment một cách linh động như trên đây, thì xin chúc mừng bạn, bạn đã hiểu rõ về bản chất của sử dụng Fragment rồi đó.

Tuy nhiên, cách sử dụng hoàn hảo Fragment sẽ được mình trình bày tiếp theo ở mục này.

Giao diện khi sử dụng các Fragment

Các bước sau đây chúng ta sẽ giúp cho MainActivity “thông minh” hơn. Khi biết được nếu thiết bị đang đặt xoay ngang (landscape), sẽ hiển thị hai Fragment lên cùng một màn hình, với FirstFragment bên trái và SecondFragment bên phải, khi click vào một item ở FirstFragment (hiện tại chúng ta đang giả lập là Button) thì sẽ cập nhật nội dung ở SecondFragment, kiểu hiển thị này giống như ví dụ ở mục trên thôi. Còn khi thiết bị đang được đặt đứng (portrait), thì sẽ hiển thị FirstFragment lên toàn màn hình, khi click vào một item ở FirstFragmentActivity sẽ hiển thị nội dung ở SecondFragment cũng toàn màn hình. Tất cả sẽ giống như hình minh họa trên đây.

Xây Dựng Giao Diện Cho Các Màn Hình Ngang/Dọc

Đầu tiên, chúng ta cần chỉnh sửa lại activity_main.xml. Đây là layout mặc định để hiển thị giao diện đứng cho thiết bị, nên chúng ta chỉ cần một Fragment toàn màn hình.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- Content -->
 
<LinearLayout
    android:id="@+id/activity_main_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.yellowcode.tournote.MainActivity">
 
    <FrameLayout
        android:id="@+id/contentFrame"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
 
</LinearLayout>

Sau đó bạn nên tạo thêm một thư mục layout cho màn hình ngang, thư mục mới này có tên và đường dẫn là res/layout-land/. Tại sao lại có tên như vậy thì bạn có thể xem lại trong cách sử dụng Alternative Resource của Android ở bài viết này nhé. Khi bạn tạo xong thư mục layout cho màn hình ngang, thì hãy copy file activity_main.xml hiện tại vào đó.

Fragment - Tạo mới Activity cho landscape

Như đã nói, chúng ta cần chỉnh sửa lại sao cho layout ở thư mục này sao cho chứa đựng hai Fragment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- Content -->
 
<LinearLayout
    android:id="@+id/activity_main_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    tools:context="com.yellowcode.tournote.MainActivity">
 
    <FrameLayout
        android:id="@+id/firstFrame"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"/>
 
    <FrameLayout
        android:id="@+id/secondFrame"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="2"/>
 
</LinearLayout>

Bạn chú ý là các ID của các FrameLayout của cả hai layout ngang và đứng không giống nhau đâu nhé.

Hiển Thị FirstFragment

Không như mục trên kia, nhảy vào onCreate() là bạn có thể add FirstFragment vào firstFrame ngay. Bởi vì chúng ta có hai activity_main.xml, mỗi layout này chứa cách hiển thị Fragment khác nhau, do đó chúng ta phải kiểm tra xem layout hiện tại mà thiết bị đang dùng là layout ngang hay đứng, rồi mới add các Fragment tương ứng vào đúng FrameLayout. Code của onCreate() giờ đây như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
 
    drawerLayout = (DrawerLayout) findViewById(R.id.activity_main_drawer);
    drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
    drawerLayout.addDrawerListener(drawerToggle);
 
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    getSupportActionBar().setHomeButtonEnabled(true);
 
    FirstFragment firstFragment = new FirstFragment();
    if (findViewById(R.id.contentFrame) != null) {
        // Found the ID of only one Fragment ==> Portrait mode
        // Remove the existing fragment before add new one
        if (savedInstanceState != null) {
            getSupportFragmentManager().executePendingTransactions();
            Fragment fragmentById = getSupportFragmentManager().findFragmentById(R.id.contentFrame);
            if (fragmentById != null) {
                getSupportFragmentManager().beginTransaction().remove(fragmentById).commit();
            }
        }
 
        // Add new one
        getSupportFragmentManager().beginTransaction().add(R.id.contentFrame, firstFragment).commit();
    } else {
        // Landscape mode
        // Remove the existing fragments before add new one
        if (savedInstanceState != null) {
            getSupportFragmentManager().executePendingTransactions();
            Fragment firstFragmentById = getSupportFragmentManager().findFragmentById(R.id.firstFrame);
            if (firstFragmentById != null) {
                getSupportFragmentManager().beginTransaction().remove(firstFragmentById).commit();
            }
            Fragment secondFragmentById = getSupportFragmentManager().findFragmentById(R.id.secondFrame);
            if (secondFragmentById != null) {
                getSupportFragmentManager().beginTransaction().remove(secondFragmentById).commit();
            }
        }
 
        // Add new one
        getSupportFragmentManager().beginTransaction().add(R.id.firstFrame, firstFragment).commit();
    }
}

Bạn có thể thấy, dòng if (findViewById(R.id.contentFrame) != null) sẽ giúp kiểm tra xem có tìm thấy layout với ID là contentFrame không, nếu có thì đích thị là activity_main.xml cho màn hình đứng rồi, khi đó chúng ta nên gọi phương thức remove() để gỡ Fragment ra khỏi FrameLayout trước khi add FirstFragment vào. Trường hợp thiết bị đang hiển thị kiểu ngang, thì nên gỡ cả hai Fragment ra khỏi hai FrameLayout của màn hình này rồi mới add FirstFragment vào. Các khởi tạo cho FragmentManager và FragmentTransaction mình viết ngắn gọn hơn. Code không có gì quá khó đúng không bạn.

Tương tự, ở sự kiện onItemPressed(), chúng ta cũng kiểm tra, nếu là màn hình đứng, thì sẽ thay thế FirstFragment bằng SecondFragment cho contentFrame. Nếu là màn hình ngang, thì hành xử giống như cách hiển thị hai Fragment cùng lúc ở mục trên kia thôi. Có một điều cần lưu ý rằng ở cách hiển thị với màn hình đứng, cần phải có thêm phương thức addToBackStack() để cho phép người dùng nhấn nút back ở thiết bị thì quay về Fragment trước đó, như mình có nói đến ở trên kia.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void onItemPressed(String content) {
    SecondFragment secondFragment = SecondFragment.newInstance(content);
    if (findViewById(R.id.contentFrame) != null) {
        // Found the ID of only one Fragment ==> Portrait mode
        FragmentManager fragmentManager = getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.replace(R.id.contentFrame, secondFragment);
        fragmentTransaction.addToBackStack(null);
        fragmentTransaction.commit();
    } else {
        // Landscape mode
        getSupportFragmentManager().beginTransaction().replace(R.id.secondFrame, secondFragment).commit();
    }
}

Đây là code đầy đủ của MainActivity.java cho bạn tham khảo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
public class MainActivity extends AppCompatActivity implements FirstFragment.OnFirstFragmentListener {
 
    private DrawerLayout drawerLayout;
    private ActionBarDrawerToggle drawerToggle;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        drawerLayout = (DrawerLayout) findViewById(R.id.activity_main_drawer);
        drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
        drawerLayout.addDrawerListener(drawerToggle);
 
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeButtonEnabled(true);
 
        FirstFragment firstFragment = new FirstFragment();
        if (findViewById(R.id.contentFrame) != null) {
            // Found the ID of only one Fragment ==> Portrait mode
            // Remove the existing fragment before add new one
            if (savedInstanceState != null) {
                getSupportFragmentManager().executePendingTransactions();
                Fragment fragmentById = getSupportFragmentManager().findFragmentById(R.id.contentFrame);
                if (fragmentById != null) {
                    getSupportFragmentManager().beginTransaction().remove(fragmentById).commit();
                }
            }
 
            // Add new one
            getSupportFragmentManager().beginTransaction().add(R.id.contentFrame, firstFragment).commit();
        } else {
            // Landscape mode
            // Remove the existing fragments before add new one
            if (savedInstanceState != null) {
                getSupportFragmentManager().executePendingTransactions();
                Fragment firstFragmentById = getSupportFragmentManager().findFragmentById(R.id.firstFrame);
                if (firstFragmentById != null) {
                    getSupportFragmentManager().beginTransaction().remove(firstFragmentById).commit();
                }
                Fragment secondFragmentById = getSupportFragmentManager().findFragmentById(R.id.secondFrame);
                if (secondFragmentById != null) {
                    getSupportFragmentManager().beginTransaction().remove(secondFragmentById).commit();
                }
            }
 
            // Add new one
            getSupportFragmentManager().beginTransaction().add(R.id.firstFrame, firstFragment).commit();
        }
    }
 
    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        // Sync the toggle state after onRestoreInstanceState has occurred.
        drawerToggle.syncState();
    }
 
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        drawerToggle.onConfigurationChanged(newConfig);
    }
 
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main_actions, menu);
 
        return super.onCreateOptionsMenu(menu);
    }
 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if(drawerToggle.onOptionsItemSelected(item)) {
            return true;
        }
 
        switch (item.getItemId()) {
            case R.id.search:
                Toast.makeText(this, "Search button selected", Toast.LENGTH_SHORT).show();
                return true;
            case R.id.about:
                Intent intent = new Intent(this, ContactActivity.class);
                Bundle bundle = new Bundle();
                bundle.putString(ContactActivity.KEY_SHOW_WHAT, ContactActivity.VALUE_SHOW_ABOUT);
                intent.putExtras(bundle);
                startActivity(intent);
                return true;
            case R.id.help:
                intent = new Intent(this, ContactActivity.class);
                bundle = new Bundle();
                bundle.putString(ContactActivity.KEY_SHOW_WHAT, ContactActivity.VALUE_SHOW_HELP);
                intent.putExtras(bundle);
                startActivity(intent);
                return true;
        }
 
        return super.onOptionsItemSelected(item);
    }
 
    @Override
    public void onItemPressed(String content) {
        SecondFragment secondFragment = SecondFragment.newInstance(content);
        if (findViewById(R.id.contentFrame) != null) {
            // Found the ID of only one Fragment ==> Portrait mode
            FragmentManager fragmentManager = getSupportFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            fragmentTransaction.replace(R.id.contentFrame, secondFragment);
            fragmentTransaction.addToBackStack(null);
            fragmentTransaction.commit();
        } else {
            // Landscape mode
            getSupportFragmentManager().beginTransaction().replace(R.id.secondFrame, secondFragment).commit();
        }
    }
}

Khi này nếu thực thi chương trình, bạn hãy thử nghiệm sự thông minh của ứng dụng khi xoay màn hình thiết bị ngang dọc nhé.

Chúng ta vừa xem qua các kiến thức về việc hiển thị các Fragment lên cùng một Activity theo dạng tĩnh và động. Chúng ta vẫn sẽ cần phải nói nhiều hơn về Fragment ở bài học sau.

Vòng Đời Fragment

Cho đến bài học này, thì mình tin chắc rằng bạn đã biết rõ cách làm việc với Fragment rồi. Tuy nhiên, cũng giống như với Activity, biết sử dụng không có nghĩa là bạn đã hiểu rõ về nó. Và cũng như với các bài học về Activity, khi bạn đã biết cách sử dụng, bạn sẽ tìm cách hiểu rõ cách thức mà Activity làm việc với hệ thống, cụ thể là các kiến thức về Back Stack và vòng đời của Activity. Và vì Fragment cũng như một Activity, thay vì Activity quản lý giao diện của toàn màn hình, thì Fragment lại quản lý giao diện của một phần màn hình bên trong Activity. Do đó, Fragment cũng sẽ có vòng đời của nó, và được hệ thống quản lý như với Activity vậy. Và bài học hôm nay chúng ta sẽ tìm hiểu kỹ kiến thức về Back Stack và vòng đời của Fragment này.

Nếu như với Activity, mình dành cả hai bài mới nói được Back Stack và vòng đời của nó. Thì với Fragment mình nói ngắn gọn và gộp lại thành một bài duy nhất hôm nay.

Fragment Và Back Stack

Chắc bạn còn nhớ, ở bài học số 27, bạn đã biết Back Stack như là một ngăn chứa, nó dùng để chứa các Activity. Và bạn cũng đã biết cơ chế hoạt động của ngăn chứa này, khi Activity được để vào và lấy ra như thế nào. Nếu bạn muốn biết lại chi tiết các vấn đề về Back Stack này, thì hãy đọc lại bài học cũ nhé.

Vòng đời Fragment - Mô phỏng Back Stack

Vậy với việc lĩnh hội thêm kiến thức về Fragment, và với việc biết rằng Fragment cũng được quản lý bởi Back Stack, thì liệu chúng ta có cần phải biết đến một Back Stack nào khác ngoài Back Stack của Activity mà chúng ta đã làm quen hay không? Câu trả lời là Không cần, hệ thống vẫn dùng Back Stack của Activity để quản lý luôn Fragment. Vì cơ bản thì các Fragment sẽ thuộc về Activity mà.

Chúng ta hãy xem với việc xây dựng một cơ chế quản lý Fragment như ở phần này của bài trước đã làm, thì Back Stack của chúng ta sẽ hoạt động như thế nào ở bài học hôm nay nhé.

Ban đầu, từ màn hình chính, TourNote được chạy, Task của TourNote sẽ được tạo, trong Task có một Back Stack chứa màn hình đầu tiên, chính là MainActivity. Bạn đã biết, khi này FirstFragment đã được add vào MainActivity. Bằng chứng là hai Button Item 1 và Item 2 của Fragment này đang được nhìn thấy đó. Cũng chính vì vậy mà MainActivity và FirstFragment đều nằm trên cùng Back Stack, và đều được người dùng nhìn thấy và tương tác.

Kích hoạt Activity có chứa Fragment

Nếu lúc này đây người dùng nhấn nút Back từ hệ thống, dĩ nhiên là cả MainActivity và FirstFragment sẽ được lấy ra khỏi Back Stack, và xem như ứng dụng sẽ kết thúc. Vì bạn nên nhớ, FirstFragment hoàn toàn không được thêm vào MainActivity thông qua phương thức addToBackStack(), nên nó không chiếm một ngăn cụ thể trong Back Stack, nó đang bị “dính” với MainActivity ở cùng một ngăn.

Nhưng chúng ta không nhấn Back. Giả sử ở bước này bạn nhấn vào Button Item 1. Khi đó, như code của bài học trước, SecondFragment sẽ thay thế cho FirstFragment. Việc thay thế này có gọi đến addToBackStack(). Và khi đó, FirstFragment mới chính thức được đưa vào quản lý bởi Back Stack, còn SecondFragment sẽ hiển thị ở trên cùng. Mình minh họa Back Stack sẽ trông như thế này.

Hiển thị Fragment mới

Nhưng, nếu bạn thắc mắc rằng nếu như không có lời gọi addToBackStack() ở giai đoạn này thì sao? Thì tình huống sẽ bị thay đổi, khi này cái chỗ đang chứa FirstFragment ở Back Stack sẽ bị thay bằng SecondFragment luôn, chứ không có tạo hai ô như hình trên đâu. Bạn có thể thử bằng cách sửa code, nhưng bài học này mình không sửa chữa gì cả, chúng ta cứ theo y kịch bản của code cũ mà thôi.

Tiếo theo, với Back Stack như trên kia, nếu người dùng nhấn Back. Thì cái nằm trên cùng của Back Stack lúc bấy giờ là SecondFragment. Sẽ bị lấy ra trước. MainActivity vẫn còn “sống”, với FirstFragment có sẵn từ trước đó. Nên mọi thứ sẽ trở về như ban đầu.

Quay lại Fragment cũhttps://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2018/03/stack_3_2.png?resize=300%2C105&ssl=1 300w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2018/03/stack_3_2.png?resize=768%2C269&ssl=1 768w, https://i0.wp.com/yellowcodebooks.com/wp-content/uploads/2018/03/stack_3_2.png?resize=1024%2C358&ssl=1 1024w" data-lazy-loaded="1" sizes="(max-width: 1000px) 100vw, 1000px" loading="eager" style="box-sizing: border-box; height: auto; max-width: 100%; border-style: none; margin: 0px;">

FirstFragment lúc bấy giờ trở về là giao diện “dính” vào MainActivity như ban đầu, nên khi này nếu người dùng tiếp tục nhấn nút Back. Cả MainActivity và FirstFragment đều bị “xóa sổ”. Như hình minh họa tiếp theo.

Kết thúc cả Activity và Fragment

Nếu bạn thử nghiệm addToBackStack() ở các dòng code khi gắn FirstFragment vào MainActivity, thì bạn có thể thấy, khi này FirstFragment sẽ là một ngăn trong Back Stack, và nút Back của người dùng như trên đây sẽ vẫn chưa hủy được MainActivity mà chỉ mới gỡ FirstFragment ra khỏi Back Stack mà thôi, bạn thử nhé.

Vòng Đời Fragment

Chúng ta sẽ không nói dông dài về khái niệm vòng đời Fragment nữa, nó hoàn toàn giống với khái niệm vòng đời Activity. Chúng ta đi vào chi tiết sơ đồ của vòng đời Fragment.

Sơ Đồ Minh Họa Vòng Đời Fragment

Sơ đồ vòng đời Fragment

Bạn có thể nhận thấy vòng đời Fragment cũng khá giống với vòng đời Activity mà bạn đã biết. Có khác chăng là nó có nhiều phương thức callback hơn. Chúng ta sẽ từng bước nói rõ về sơ đồ này ở các mục tiếp theo đây.

Mô Tả Sơ Đồ

Sơ đồ bắt đầu khi Fragment được gắn vào Activity. Khi đó các callback onAttach()onCreate()onCreateView()onActivityCreated()onStart() và onResume() lần lượt được gọi.

Sau khi các callback trên được gọi, thì Fragment lúc bấy giờ mới chính thức được xem là đang chạy.

Sau đó, nếu người dùng nhấn nút Back, hay có bất kỳ thao tác gỡ/thay thế (remove/replace) Fragment ra khỏi Activity nào, thì các callback onPause()onStop()onDestroyView()onDestroy() và onDetach() sẽ được gọi.

Nhưng có một cái hay là, nếu Fragment được đưa vào Back Stack kèm với lệnh gỡ/thay thế, thì onDestroy() và onDetach() sẽ chưa được gọi ngay. Để khi rơi vào trường hợp sau đó khi Fragment này được hiển thị lại trong Back Stack, thì onCreateView() sẽ được gọi lại.

Chỉ vậy thôi, chúng ta sẽ xem xét kỹ hơn về từng trạng thái chính của sơ đồ vòng đởi này.

Các Trạng Thái Chính Trong Vòng Đời Fragment

Bạn sẽ thấy các trạng thái chính này cũng không khác gì với Activity cả, nhưng hãy xem các tình huống cụ thể liên quan với nhau giữa Fragment và Activity như thế nào nhé.

Hoạt Động (Active Hay Resume)

Khi Fragment được gắn vào Activity, được nhìn thấy và có thể tương tác được.

Tạm Dừng (Pause)

Cũng khá giống với trạng thái tạm dừng của Activity. Tức là nếu Activity có chứa Fragment này bị che lấp bởi Activity khác (nhưng không bị che hoàn toàn, người dùng vẫn nhìn thấy được Activity bị che lấp, chỉ là không tương tác được), thì cả Activity và Fragment đó đều vào trạng thái tạm dừng.

Dừng (Stop)

Cũng giống với Activity, Fragment bị dừng khi bị thành phần nào đó che khuất hoàn toàn. Hay bị gỡ ra khỏi Activity.

Dừng chưa phải là chấm hết cho đời sống của Fragment. Cụ thể là các trạng thái của nó vẫn còn được lưu trữ, để phòng trường hợp Fragment này được trở lại hiển thị cho người dùng.

Chết (Dead)

Nếu Fragment bị gỡ ra khỏi Activity, nhưng không được đưa vào Back Stack trước đó, thì nó sẽ kết thúc vòng đời. Hoặc khi Activity chứa Fragment này bị gỡ khỏi Back Stack, Fragment cũng sẽ chết theo.

Làm Quen Với Các Callback

Sau đây là ý nghĩa của từng callback

onAttach()

Callback này được gọi khá sớm, ngay khi Activity chứa nó được kích hoạt. Hoặc ngay khi được gắn vào Activity.

Callback này được gọi một lần duy nhất trong vòng đời Fragment. Và ở giai đoạn này Fragment đã “nhận biết” được Activity chứa nó rồi, nên bạn có thể tận dụng để kiểm tra sớm một số điều kiện nào đó như các dòng code ở FirstFragment chúng ta đã từng làm.

Ví dụ callback onAttach() của Fragment

onCreate()

Callback này được gọi khi Fragment bắt đầu khởi tạo từ các dữ liệu đầu vào.

Khác với onCreate() của Activity, rằng bạn có thể tạo giao diện cho màn hình ở callback này, thì với Fragment chúng ta còn phải đợi qua callback tiếp theo mới có thể tạo giao diện được.

Callback này cũng được gọi một lần trong đời sống Fragment. Nên thường tận dụng để lấy dữ liệu từ bên ngoài truyền vào như ở SecondFragment chúng ta có làm quen.

Ví dụ callback onCreate() của Fragment

onCreateView()

Khi Fragment bắt đầu vẽ UI lên màn hình, callback này được gọi. Nên chúng ta sẽ tận dụng callback này cho các thiết lập về giao diện.

Bạn thấy rằng, theo như sơ đồ trên, thì callback này sẽ được gọi lại khi mà Fragment được gỡ ra khỏi Activity nhưng được đưa vào Back Stack, và được gọi lại hiển thị sau đó.

Khi kết thúc callback này, hãy nhớ return một View như những gì bạn đã thử nghiệm với FirstFragment và SecondFragment. Lưu ý là chúng ta hoàn toàn có thể return null nếu Fragment không có UI.

Ví dụ callback onCreateView() của Fragment

onActivityCreated()

Callback này được gọi ngay sau khi onCreateView() được gọi. Nó báo hiệu trạng thái Activity chứa nó được khởi tạo hoàn toàn. Tuy ít được sử dụng hơn các callback khác, nhưng bạn cũng có thể tận dụng nó để thay đổi giao diện hay các tương tác với Activity chứa Fragment này thoải mái được rồi.

onStart()

Khi Fragment bắt đầu được nhìn thấy bởi người dùng và chuẩn bị nhận tương tác.

onResume()

Người dùng hoàn toàn nhìn thấy và tương tác được với Fragment.

onPause()

Callback này như một dấu hiệu cho thấy rằng người dùng đang rời khỏi Fragment hiện tại. Mặc dù không phải lúc nào onPause() được gọi là người dùng sẽ bái bai Fragment này. Nhưng bạn nên sao lưu các dữ liệu cần thiết của Fragment ở callback này, nhỡ đâu người dùng thực sự rời đi không quay lại thì sao.

onStop()

Fragment chính thức không còn được nhìn thấy nữa.

onDestroyView()

Chắc chắn là đối tượng View sẽ bị hủy ở callback này. Và do đó các khởi tạo view của bạn ở onCreateView() sẽ nhanh chóng không còn nữa.

Nếu như Fragment được đưa vào Back Stack, thì khi được lấy ra lại sau đó, callback onCreateView() sẽ được gọi lại.

onDestroy()

Fragment đã sắp “chết”. Nhưng khác với Activity, khi onDestroy() của Activity được gọi thì xem như Activity đã đến “cuối đời”. Còn với Fragment, callback này chỉ như một lời “nhắc nhở” về vận mệnh của Fragment mà thôi.

onDetach()

Callback này gọi đến báo hiệu Fragment sẽ được gỡ khỏi Activity đang chứa nó. Kết thúc vòng đời của Fragment.

Chúng ta vừa xem qua một kiến thức về quản lý Fragment, bởi hệ thống. Thông qua đây bạn được hiểu rõ hơn về các callback và cách sử dụng chúng trong các logic của Fragment sau này.

Phân Loại Fragment

Có thể đến bài học này bạn thắc mắc rằng, ôi các bài về Fragment sao nó dài lê thê thế, đến bao giờ mới kết thúc đây. Thực ra thì có bao nhiêu đây kiến thức về Fragment mà mình xin phép tổng hợp lại ở các bài viết của mình như sau.

  • Làm quen với Fragment – Với bài này bạn đã hiểu tại sao Android lại đẻ ra cái khái niệm Fragment cho việc hiển thị động giao diện trên các thiết bị chạy hệ điều hành này. Bạn cũng biết được Fragment ra đời khi nào và sự hỗ trợ ngược cho các hệ điều hành chưa có Fragment trước đó như thế nào. Bạn cũng thử làm quen với cách tạo một Fragment trong Android Studio.
  • Làm quen với việc tạo mới một Fragment – Bài học này tập trung vào việc tạo và hiểu những đoạn code mà khi bạn tạo mới một Fragment bằng Android Studio. FirstFragment và SecondFragment cũng được tạo ra ở bài học này.
  • Làm quen với việc hiển thị Fragment – Bạn đã được biết đến cách hiển thị Tĩnh và Động các Fragment lên Activity như thế nào.
  • Làm quen với việc hệ thống quản lý Fragment thông qua Back Stack và Vòng đời của nó – Bạn đã hiểu rõ cách hệ thống lưu trữ Fragment trong Back Stack, và cách sử dụng các phương thức callback bên trong vòng đời của Fragment.

Thật thú vị khi được hiểu rõ về Fragment như vậy (chứ không phải đau đầu đâu đúng không nào).

Và bài học hôm nay sẽ là kiến thức cuối cùng về Fragment mà mình muốn cung cấp. Bài học sẽ cho bạn biết, trong Android, có những thể loại Fragment nào mà chúng ta có thể sử dụng.

Fragment Cơ Bản

Minh họa Fragment cơ bản
Minh họa Fragment cơ bản

Chính là Fragment mà bạn đã làm quen từ các bài học trước, mà mình cũng có liệt kê trên kia. Đây là Fragment đơn thuần, với nó, bạn có thể xây dựng mọi thứ theo ý của bạn, với việc theo dõi các bài học về Fragment như mình đã nói, thì bạn đã hiểu rất rõ về thể loại Fragment này, nên mục này mình không nói đến nó nữa, chỉ điểm qua cho nó có mặt mà thôi.

Chúng ta bắt đầu xem qua các thể loại Fragment sau đây, chúng đều là các lớp con của Fragment cả. Chúng được sinh ra để làm gì vậy? Mời bạn cùng xem.

ListFragment

Minh họa ListFragment
Minh họa ListFragment

Đây là một biến thể của Fragment. Dĩ nhiên rồi, vì như mình nói đó, ListFragment là con của Fragment mà. ListFragment giúp cho bạn có thể thiết kế ra một Fragment có chứa ListView một cách nhanh chóng (mặc dù chúng ta hoàn toàn có thể dùng Fragment như trước giờ để thiết kế giao diện có chứa ListView như mình đã nói này).

Và tất nhiên, cũng như FragmentListFragment vẫn được xây dựng trong hai gói android.app và android.support.v4.app dành cho nhu cầu tương thích ngược. Bạn cũng có thể xem lại thông tin về tương thích ngược ở mục này.

ListFragment hay các kiến thức liên quan đến hiển thị UI dạng list này sẽ được mình nói đến cụ thể ở bài học về ListView. Tuy nhiên mình cũng đưa ra một tí code cho bạn tham khảo (và có thể code thử). Code này sẽ không có trên GitHub nhé.

Đầu tiên, bạn có thể tạo mới một Blank Fragment. Bạn có thể không check chọn vào cả ở Include fragment factory methods? và Include interface callbacks? để source code được gọn nhẹ. ListFragment này mình đặt tên là ListFragmentSample.

Tạo ListFragmentSample
Tạo ListFragmentSample

Đây là giao diện của ListFragment, chính là file fragment_list_fragment_sample.xml.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
  
    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >
    </ListView>
  
    <TextView
        android:id="@android:id/empty"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >
    </TextView>
</LinearLayout>

Còn đây là source code của ListFragmentSample. Bạn có thể thấy Fragment này kế thừa từ ListFragment thay vì Fragment như các bài học trước. Và implements sự kiện OnItemClickListener. Với các callback của Fragment này mình vẫn dùng onCreateView() và onActivityCreated() để thiết kế giao diện và xây dựng adapter cho nó. Tất cả những thông tin chi tiết này sẽ được mình nói rõ hơn ở bài học về ListView sau này nhé.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ListFragmentSample extends ListFragment implements AdapterView.OnItemClickListener {
  
    private String[] planets = { "Sun", "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune" };
  
    public ListFragmentSample() {
        // Required empty public constructor
    }
  
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_list_fragment_sample, container, false);
    }
  
    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_expandable_list_item_1, planets);
        setListAdapter(adapter);
        getListView().setOnItemClickListener(this);
    }
  
    @Override
    public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
        Toast.makeText(getActivity(), "Item: " + position, Toast.LENGTH_SHORT).show();
    }
}

Với layout của MainActivity, mình sử dụng cách hiển thị Fragment theo kiểu tĩnh. Bạn xem file activity_main.xml sẽ như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- Content -->
  
<LinearLayout
    android:id="@+id/activity_main_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.yellowcode.tournote.MainActivity">
  
    <fragment
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="com.yellowcode.tournote.ListFragmentSample"/>
  
</LinearLayout>

Và nhờ sử dụng cách hiển thị Fragment theo kiểu tĩnh, chúng ta không cần làm gì nhiều với MainActivity.java (bạn chỉ cần đảm bảo xóa bỏ các dòng code add Fragment động từ các bài học trước, nếu có, ra khỏi code của MainActivity.java mà thôi).

Kết quả thực thi sẽ như hình chụp đầu tiên của mục này trên kia.

DialogFragment

Minh họa DialogFragment
Minh họa DialogFragment

Dĩ nhiên là DialogFragment cũng kế thừa từ Fragment. Nếu như ListFragment là một dạng mở rộng nhưng khá là giống Fragment đến nỗi chúng ta hầu như cũng chẳng cần đến ListFragment nữa (so sánh theo tư tưởng khai triển code, qua bài về ListView bạn sẽ thấy rằng không dùng ListFragment cũng vẫn xây dựng được giao diện danh sách một cách tương tự). Thì DialogFragment lại là một biến thể mới mẻ của FragmentDialogFragment giúp tạo ra một dialog, chính là một giao diện hiển thị đè lên trên giao diện khác và không che hoàn toàn giao diện bên dưới nó.

DialogFragment được Android đưa ra nhằm thay thế cho Dialog cũ ngày xưa. Dĩ nhiên là với DialogFragment thì chúng ta hoàn toàn có thể kế thừa được các callback từ vòng đời của Fragment. Chi tiết về vấn đề này sẽ được mình nói đến ở bài học về Dialog sắp tới đây.

Để xây dựng một DialogFragment “chơi chơi”, bạn vẫn có thể tạo mới một Blank Fragment và vẫn không check chọn vào Include fragment factory methods? hay Include interface callbacks?.

Mình giả sử với việc tạo một DialogFragment có tên là DialogFragmentSample.

Tạo DialogFragmentSample
Tạo DialogFragmentSample

Thì giao diện của nó, fragment_dialog_fragment_sample.xml sẽ đại loại như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
  
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:padding="10dp"
        android:text="This is content of DialogFragment" />
  
</RelativeLayout>

Còn DialogFragmentSample.java, bạn nên kế thừa từ DialogFragment thay vì Fragment như các bài học trước. Chỉ cần một callback onCreateView() (hoặc onCreateDialog() mà bài học về Dialog chúng ta sẽ nói rõ hơn).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DialogFragmentSample extends DialogFragment {
  
    public DialogFragmentSample() {
        // Required empty public constructor
    }
  
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_dialog_fragment_sample, container, false);
        getDialog().setTitle("This is title");
        return view;
    }
}

Ở activity_main.xml chúng ta có thể thiết kế tạm một Button.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- Content -->
  
<LinearLayout
    android:id="@+id/activity_main_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    tools:context="com.yellowcode.tournote.MainActivity">
  
    <Button
        android:id="@+id/activity_main_btn_show_dialogfragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Show DialogFragment"/>
  
</LinearLayout>

Để khi click vào Button đó ở MainActivity.java, chúng ta sẽ hiển thị DialogFragment như sau.

1
2
3
4
5
6
7
8
Button btnShowDislogFragment = (Button) findViewById(R.id.activity_main_btn_show_dialogfragment);
btnShowDislogFragment.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        DialogFragmentSample dialogFragmentSample = new DialogFragmentSample();
        dialogFragmentSample.show(getSupportFragmentManager(), "Dialog Fragment");
    }
});

Kết quả của các đoạn code này khi thực thi sẽ được giao diện như hình chụp mà hình ở phía trên.

PreferenceFragment

Minh họa PreferenceFragment

PreferenceFragment cũng kế thừa từ Fragment. Thành phần này mang lại cho chúng ta một tùy chọn cho việc xây dựng giao diện cho màn hình Cài đặt (Settings) của ứng dụng. Tất nhiên ứng dụng nào cũng sẽ có vài tùy chọn cho người dùng thiết lập các thông số, như cho phép hiện thông báo lên notification hay không, hay thiết lập âm thanh cho một số chức năng nào đó,… tất cả những tùy chọn này có thể được để vào trong màn hình Cài đặt. Và tất nhiên bạn có thể tự xây dựng loại màn hình này. Tuy nhiên, việc sử dụng PreferenceFragment sẽ cho chúng ta một giao diện Cài đặt đồng nhất trên các ứng dụng có sử dụng FreferenceFragment và cả đồng nhất với màn hình Cài đặt của hệ thống nữa.

Để nói sâu về cấu trúc của màn hình Cài đặt hay các thể loại Preference trong màn hình này là rất dài và nhiều kiến thức. Mình sẽ dành một bài riêng để nói về nó. Còn bài hôm nay chúng ta sẽ chỉ làm quen với các dòng code mẫu để hiểu thế nào là PreferenceFragment thôi nhé.

Để thử nghiệm “vọc” chơi PreferenceFragment, thì bạn có thể tạo một Blank Fragment với việc không check chọn vào Include fragment factory methods? hay Include interface callbacks?. Thậm chí chúng ta bỏ luôn cả check chọn Create layout XML?, vì Fragment loại này không cần đến giao diện, chúng ta sẽ dùng chính giao diện của hệ điều hành. Wow làm sao mà được như vậy? Mời bạn cùng xem tiếp.

Tạo PreferenceFragmentSample
Tạo PreferenceFragmentSample

Các phần sau đây của mục này mình sẽ nói khá vắn tắt nhưng cũng đủ để bạn muốn thử nghiệm.

Nói là không có giao diện cho PreferenceFragment, nhưng không hẳn là như vậy. Chúng ta chỉ dùng lại các loại Preference có sẵn từ Android, để làm giao diện cho nó, bạn có thể xem kỹ hơn về thông tin này ở link này.

Vậy việc đầu tiên sau khi chúng ta tạo PreferenceFragmentSample như trên kia, đó là thêm thư viện có hỗ trợ các loại Preference, chúng ta sẽ thêm vào dependencies của module. Nếu bạn chưa rõ lắm về cách thức thêm một thư viện vào dependencies thì có thể xem lại bài học này.

1
implementation 'androidx.preference:preference:1.1.0'

Và đây là “giao diện” của PreferenceFragment. Giao diện này được tạo ở thư mục res/xml/ với tên là preferences.xml. Nội dung của giao diện này chính là các Preference định nghĩa sẵn bởi hệ thống.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
  
    <PreferenceCategory
        android:title="First Section" >
  
        <CheckBoxPreference
            android:key="checkbox_preference"
            android:title="Checkbox Preference"
            android:defaultValue="true" />
  
        <EditTextPreference
            android:key="edittext_preference"
            android:title="Edit Text Preference"
            android:summary="This is a sample edit text preference."
            android:dialogTitle="Dialog Edit Text Preference"
            android:dependency="checkbox_preference" />
  
    </PreferenceCategory>
  
    <PreferenceCategory
        android:title="Second Section" >
  
        <ListPreference
            android:key="list_preference"
            android:title="List Preference"
            android:dialogTitle="Dialog List Preference"
            android:entries="@array/entries_list_preference"
            android:entryValues="@array/entryvalues_list_preference" />
  
        <Preference
            android:title="Custom Intent" >
  
            <intent android:action="android.intent.action.VIEW"
                android:data="https://yellowcodebooks.com/" />
  
        </Preference>
  
    </PreferenceCategory>
  
</PreferenceScreen>

Giao diện này có sử dụng đến hai resource string array đã được khai báo như sau.

1
2
3
4
5
6
7
8
9
10
11
<string-array name="entries_list_preference">
    <item>Entry 1</item>
    <item>Entry 2</item>
    <item>Entry 3</item>
</string-array>
  
<string-array name="entryvalues_list_preference">
    <item>Value 1</item>
    <item>Value 2</item>
    <item>Value 3</item>
</string-array>

Đến PreferenceFragmentSample.java, chúng ta nên kế thừa từ PreferenceFragment, sau đó chỉ định “giao diện” của Fragment này chính là file preferences.xml mà chúng ta vừa tạo ra ở trên kia thông qua phương thức addPreferencesFromResource() như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PreferenceFragmentSample extends PreferenceFragment {
  
    public PreferenceFragmentSample() {
        // Required empty public constructor
    }
  
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
  
        addPreferencesFromResource(R.xml.preferences);
    }
}

Bạn có thể gắn Fragment này theo kiểu tĩnh như với ListFragmentSample trên kia vào MainActivity cho nhanh. Kết quả của các đoạn code này khi thực thi sẽ được giao diện như hình chụp trên kia.

WebViewFragment

Chú ý: lần chỉnh sửa bài viết lần này của mình đối với bài học thứ 34 này mình phát hiện ra rằng lớp WebViewFragment đã bị Google loại bỏ từ phiên bản API level 28. Do đó kiến thức về WebViewFragment này chỉ còn là ký ức. Tuy nhiên mình cũng không xóa bỏ nội dung bài học lẫn code trên Github. Nội dung bài học và bài thực hành vẫn còn dưới đây, và link source code vẫn được để trên một branch riêng, nhưng từ các bài học sau, các source code này sẽ không xuất hiện trong TourNote của chúng ta nữa nhé. Chúng ta sẽ xây dựng màn hình hiển thị web này ở bài học liên quan khác.

Minh họa WebViewFragment
Minh họa WebViewFragment

Dĩ nhiên như các anh em của nó đã được nói đến trên kia, WebViewFragment cũng được kế thừa từ Fragment.

Trước hết, WebView là một vùng giao diện trên màn hình, mà ở đó chúng ta có thể hiển thị nội dung một trang web nào đó. Kiến thức về WebView rất thú vị, và mình dành một bài riêng để nói về nó sau này.

Vậy còn WebViewFragment thì sao? WebViewFragment cũng là một Fragment mở rộng từ Fragment gốc, giúp bạn thiết kế một Fragment với WebView bên trong nó một cách nhanh hơn. Bạn có thể không cần phải sử dụng WebViewFragment mới có thể đặt WebView vào trong nó, bản thân Fragment cũng có thể làm được. Code sau giúp bạn có cái nhìn ban đầu về WebView.

Để thử nghiệm WebViewFragment, bạn cũng nên tạo một Blank Fragment với việc không check chọn vào Include fragment factory methods? hay Include interface callbacks?. Bạn cũng nên bỏ luôn cả check chọn Create layout XML?, vì fragment loại này cũng không cần đến giao diện, vì là web mà, mọi giao diện đều dựa vào website hết cả rồi.

Tạo WebViewFragmentSample
Tạo WebViewFragmentSample

Một điều hiển nhiên khi hiển thị một website, đó là bạn phải cần kết nối mạng. Dù cho thiết bị của bạn đã có kết nối rồi, thì bạn vẫn cần phải xin quyền được sử dụng kết nối mạng của hệ thống thông qua thẻ uses-permission được định nghĩa thêm vào file AndroidManifest.xml bởi dòng sau.

1
<uses-permission android:name="android.permission.INTERNET" />

Như mình đã nói, chúng ta chẳng cần đến giao diện gì ráo, chỉ cần truyền url vào cho WebView như code dưới đây của file WebViewFragmentSample.java mà thôi.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class WebViewFragmentSample extends WebViewFragment {
  
  
    public WebViewFragmentSample() {
        // Required empty public constructor
    }
  
  
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = super.onCreateView(inflater, container, savedInstanceState);
  
        getWebView().getSettings().setJavaScriptEnabled(true);
        getWebView().getSettings().setSupportZoom(true);
        getWebView().getSettings().setBuiltInZoomControls(true);
        getWebView().loadUrl("https://yellowcodebooks.com/");
  
        return view;
    }
  
}

Kết quả của việc thực thi ứng dụng khi này như hình minh họa đầu tiên của mục này.

Thực Hành Xây Dựng Fragment Cho TourNote

Đã lâu rồi chúng ta không xây dựng gì cho TourNote cả, đây là một sự buồn tẻ đúng không nào.

Hôm nay, sau khi đã trở thành “chuyên gia” trong lĩnh vực Fragment rồi, chúng ta cùng quay lại với nhau để xây đắp thêm các viên gạch mới cho ngôi nhà TourNote của chúng ta.

Ôn lại một tí những gì đã làm ở bài thực hành trước, đó là chúng ta đã làm sao cho ContactActivity “hiểu” được user đã chọn tùy chọn gì từ MainActivity thông qua Bundle. Khi đó, chúng ta có dùng Toast để hiển thị lên xem nội dung mà Bundle gửi qua là gì. Mọi thứ đã trơn tru.

Bài thực hành hôm nay chúng ta sẽ xây dựng một Fragment, Fragment này có nhiệm vụ hiển thị nội dung trang web tương ứng với những gì mà user đã chọn lựa. Cụ thể là, nếu từ MainActivity, nếu user chọn About app thì qua ContactActivity sẽ hiển thị trang Giới Thiệu, còn nếu user đã chọn Help thì qua ContactActivity sẽ hiển thị trang Liên Hệ. Nói rằng ContactActivity sẽ hiển thị, nhưng thực chất ContactActivity chứa đựng một WebViewFragment giúp đảm đương công việc này.

Tổ Chức Lại Package

Trước đây, do có rất ít file source code, nên chúng ta cứ tạo đại vào trong package mặc định của project như hình dưới đây.

Cách tổ chức package từ những bài học trước
Cách tổ chức package từ những bài học trước

Vấn đề sẽ trở nên rất phức tạp nếu như sau này chúng ta tạo ngày càng nhiều file source code. Vậy nhân tiện hôm nay, khi tạo mới một Fragment, chúng ta nên tổ chức các package sao cho có thể tách bạch giữa Activity và Fragment với nhau, khi đó bạn sẽ thấy project trở nên gọn gàng hơn như thế nào.

Đầu tiên, chúng ta nên tạo một package có tên là fragments. Package fragments này sẽ chứa đựng tất cả các Fragment sau này có thể có.

Để tạo mới một package trong Android Studio, bạn nên để vệt sáng vào package nào muốn tạo package con cho nó ở cửa sổ Project. Rồi chọn theo menu File > New > Package hoặc click phải chuột vào package đó rồi chọn như hình dưới đây.

Tạo mới một package
Tạo mới một package

Popup xuất hiện sau đó, bạn gõ vào tên package muốn tạo, trong trường hợp này chùng ta gõ “fragments”.

Khai báo tên của package sắp tạo
Khai báo tên của package sắp tạo

Bạn hãy làm tương tự để chúng ta có thêm một package nữa có tên activities. Kết quả của các package vừa mới tạo sẽ như sau.

Bạn hãy tạo sao cho có 2 package mới là activities và fragments
Bạn hãy tạo sao cho có 2 package mới là activities và fragments

Bước tiếp theo sau đó, bạn hãy click chọn cả hai ContactActivity và MainActivity ở cửa sổ Project bên trái rồi kéo chúng vào package activities vừa mới tạo cũng ở cửa sổ này. Popup xác nhận sau đó xuất hiện bạn cứ nhấn vào nút Refactor.

Kết quả cuối cùng chúng ta có được cấu trúc như sau.

Kéo ContactActivity và MainActivity vào packages activities
Kéo ContactActivity và MainActivity vào packages activities

Thật là đẹp đẽ. Giờ hãy tạo mới Fragment nào.

Tạo Mới Fragment

Chúng ta sẽ tạo mới một BlankFragment theo hướng dẫn ở bài trước, ở cửa sổ thiết lập Fragment mới bạn đặt tên Fragment này là AboutHelpFragment. Bạn nên nhớ không check chọn vào Create layout XML?, vì như mục trên kia có nói rằng giao diện của Fragment này chính là web. Nhưng nhớ phải check chọn vào Include fragment factory methods? để Fragment này có thể nhận dữ liệu đầu vào là url. Và phải nhớ tạo Fragment mới này bên trong package fragments nhé.

Tạo mới AboutHelpFragment
Tạo mới AboutHelpFragment

Khi đó cấu trúc project của chúng ta như sau.

Cấu trúc package sau khi thêm AboutHelpFragment
Cấu trúc package sau khi thêm AboutHelpFragment

Đừng Quên Permission

Như những gì bạn đã làm quen với WebView trên kia, ở bước này bạn cần định nghĩa một uses-permisson để ứng dụng có thể kết nối vào internet. Và đây là nội dung của file AndroidManifest.xml.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="utf-8"?>
    package="com.yellowcode.tournote">
 
    <uses-permission android:name="android.permission.INTERNET"/>
 
    <supports-screens
        android:largeScreens="true"
        android:normalScreens="true"
        android:smallScreens="false"
        android:xlargeScreens="true" />
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".activities.ContactActivity"></activity>
        <activity android:name=".activities.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
 
</manifest>

Xây Dựng AboutHelpFragment

Như những gì bạn đã làm quen với code được tạo sẵn với SecondFragment ở bài học trước, và code của WebViewFragmentSample của bài hôm nay. Thì code của AboutHelpFragment sẽ trông như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class AboutHelpFragment extends WebViewFragment {
 
    private static final String ARG_URL = "url";
 
    private String mUrl;
 
    public AboutHelpFragment() {
        // Required empty public constructor
    }
 
    public static AboutHelpFragment newInstance(String url) {
        AboutHelpFragment fragment = new AboutHelpFragment();
        Bundle args = new Bundle();
        args.putString(ARG_URL, url);
        fragment.setArguments(args);
        return fragment;
    }
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mUrl = getArguments().getString(ARG_URL);
        }
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = super.onCreateView(inflater, container, savedInstanceState);
 
        getWebView().getSettings().setJavaScriptEnabled(true);
        getWebView().getSettings().setSupportZoom(true);
        getWebView().getSettings().setBuiltInZoomControls(true);
        getWebView().loadUrl(mUrl);
 
        return view;
    }
 
}

Chỉnh Sửa Giao Diện Của ContactActivity

Chúng ta sẽ dùng phương pháp hiển thị Fragment động. Dĩ nhiên rồi, vì còn tùy vào chọn lựa của user mà chúng ta sẽ thêm Fragment với url tương ứng vào cho ContactActivity mà.

Để hiển thị Fragment theo kiểu động như thế này, bạn đã biết, chúng ta cần có một FrameLayout trong activity_contact.xml như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".activities.ContactActivity">
 
    <FrameLayout
        android:id="@+id/contactMainFrame"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">
 
    </FrameLayout>
</android.support.constraint.ConstraintLayout>

Thêm AboutHelpFragment Động Vào ContactActivity

Nào, code của ContactActivity.java bây giờ sẽ như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class ContactActivity extends AppCompatActivity {
  
    public static final String KEY_SHOW_WHAT = "show_what";
    public static final String VALUE_SHOW_ABOUT = "show_about";
    public static final String VALUE_SHOW_HELP = "show_help";
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_contact);
  
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeButtonEnabled(true);
  
        Intent intent = getIntent();
        Bundle bundle = intent.getExtras();
        if (bundle != null) {
            String valueShow = bundle.getString(KEY_SHOW_WHAT, "");
  
            FragmentManager fragmentManager = getFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            if (valueShow.equals(VALUE_SHOW_ABOUT)) {
                AboutHelpFragment aboutHelpFragment = AboutHelpFragment.newInstance("https://yellowcodebooks.com/about/");
                fragmentTransaction.add(R.id.contactMainFrame, aboutHelpFragment);
                fragmentTransaction.commit();
            } else if (valueShow.equals(VALUE_SHOW_HELP)) {
                AboutHelpFragment aboutHelpFragment = AboutHelpFragment.newInstance("https://yellowcodebooks.com/contact/");
                fragmentTransaction.add(R.id.contactMainFrame, aboutHelpFragment);
                fragmentTransaction.commit();
            }
        }
    }
  
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                finish();
                return true;
        }
  
        return super.onOptionsItemSelected(item);
    }
}

Tìm Hiểu Về Danh Sách Tập 1 – Adapter & AdapterView

Nên nhớ Danh sách mà chúng ta nói đến, là một giao diện. Về mặt kiến thức, nó khác với cấu trúc dữ liệu dạng Danh sách (List) mà bài học Java mình sẽ nói đến một cách cụ thể. Và giao diện Danh sách hôm nay có liên quan đến hai khái niệm mở màn: Adapter và AdapterView. Mời bạn cùng đến với 2 thành phần cơ bản này trước tiên.

Adapter & AdapterView Là Gì?

Cảm nhận ban đầu về khái niệm Adapter
Cảm nhận ban đầu về khái niệm Adapter

Nói về Adapter trước. Thú thiệt ngày trước, khi mình mới bắt đầu làm quen với Android, cảm nhận của mình khi đọc đến Adapter, chính là cái cục Adapter như hình minh họa của mình trên đây.

Khi mình tìm hiểu kỹ, thì mới ngộ ra rằng, nếu hiểu theo hướng công năng của nó, thì rõ ràng Adapter của Android nó mang công năng của bộ Adapter ngoài đời thực. Adapter ngoài đời thực mang trách nhiệm chuyển đổi đặc tính của một thiết bị ở đầu vào, thành các đặc tính phù hợp hơn với thiết bị ở đầu ra. Còn Adapter trong Android mang trách nhiệm chuyển đổi dữ liệu, từ dữ liệu thô ở đầu vào (mà mình gọi là DataSource như hình bên dưới), thành dữ liệu hiển thị lên View mà người dùng có thể dễ dàng đọc, hiểu và tương tác. Cơ bản thì công năng của Adapter sẽ được minh họa bằng sơ đồ sau.

Sơ đồ cho thấy vai trò của Adapter
Sơ đồ cho thấy vai trò của Adapter

Đến đây hẳn sẽ làm bạn hoang mang. Bạn sẽ tự hỏi rằng, tại sao phải cần sự chuyển đổi dữ liệu này? Các bài học trước giờ có nói đến cái gì liên quan đến chuyển đổi dữ liệu này đâu? Thực ra thì sự chuyển đổi này rất quan trọng, và trước đây bạn chưa làm quen là vì chưa đụng đến nó, nay đụng đến rồi bạn cũng nên biết.

Chúng ta sẽ tìm hiểu từ mong muốn thực tế trước. Các bạn đều biết. Theo như lời mở đầu bài viết trên kia. Hầu hết các ứng dụng, không riêng gì các ứng dụng trên thiết bị di động đâu, đều cần hiển thị một dạng giao diện đặc biệt: giao diện theo kiểu Danh sách. Đó có thể là danh sách các hình ảnh, danh sách các email đến. Hay như TourNote sau này còn có danh sách các ghi chú mà bạn sắp sửa bắt tay vào xây dựng đây. Các danh sách này được sắp xếp theo chiều ngang, hoặc theo chiều dọc, hoặc theo dạng bảng (hay còn gọi dạng lưới) trên một vùng không gian màn hình, hoặc mỗi phần tử danh sách là một màn hình (dạng view pager). Nói chung là có rất nhiều các loại danh sách mà bạn đã biết. Như các hình ảnh chụp màn hình của mình dưới đây.

Bạn có nhìn thấy các danh sách mà mình muốn nói đến không?
Bạn có nhìn thấy các danh sách mà mình muốn nói đến không?

Các danh sách này như bạn thấy, chứa rất nhiều phần tử. Mỗi phần tử có thể chỉ là text, có thể là ảnh, có thể vừa ảnh vừa text, hay vừa icon vừa text,… Số lượng phần tử trong các danh sách có thể là cố định, hoặc cũng có thể được thêm động vào danh sách khi ứng dụng đã thực thi (như các ghi chú của TourNote, hay gần gũi hơn đó là các phần tử feed trên danh sách feed của Facebook).

Từ những yêu cầu đặc biệt trên đây của giao diện Danh sách, thì bạn cũng có thể thấy rằng không thể nào mà bạn có thể tự viết các dòng code rồi thêm từng phần tử vào trong một View (như ScrollView chẳng hạn) để giả lập thành một danh sách được. Mà bạn phải dùng đến AdapterAdapter sẽ giúp bạn chuyển đổi dữ liệu từ DataSource ban đầu thành một danh sách chuyên nghiệp. Từ đây chúng ta gọi các danh sách này là các AdapterView luôn nhé. Vậy để nhớ lâu, sơ đồ của chúng ta nên viết lại đúng đắn như sau.

Sơ đồ cho thấy vai trò của Adapter (cập nhật)
Sơ đồ cho thấy vai trò của Adapter (cập nhật)

Với sơ đồ trên đây thì chuỗi kiến thức về Adapter này chúng ta sẽ tìm hiểu gì?

  • Về khối DataSource: đọc lên thấy cao siêu nhưng thực ra chúng chỉ là kiểu mảng hoặc kiểu List là được. Adapter sẽ có cách (hoặc bạn cũng giúp Adapter ít nhiều) hiểu được các thể loại hoặc các cấu trúc này để chuyển chúng lên AdapterView hiển thị ra cho người dùng.
  • Về khối Adapter: chúng ta sẽ làm quen với ArrayAdapter được cung cấp sẵn bởi hệ thống trước. Rồi các bài học sau chúng ta sẽ tìm cách tùy chỉnh (hay còn gọi custom) lại ArrayAdapter này sao cho có thể hiển thị các danh sách phức tạp theo ý của chúng ta. Ngoài ArrayAdapter ra thì sau này các bạn sẽ gặp một vài thể loại Adapter khác cho một số danh sách đặc thù. Tuy cái tên của chúng khác nhau, nhưng có điểm chung là đều có chữ Adapter ở cuối mỗi cái tên, như ExpandapleListAdapterPagerAdapter. Cộng với việc xây dựng các Adapter là gần như giống nhau nên bạn cứ yên tâm là không đau đầu lắm đâu nhé.
  • Về khối AdapterView: uhm việc tìm hiểu cũng sẽ dài hơi không kém Adapter. Các bạn sẽ được làm quen với ListViewGridViewExpandableListViewGalleryStackViewSpinner. Và cả RecyclerView. Tuy RecyclerView bản chất không thuộc khối AdapterView đâu, nhưng do công năng và cách sử dụng tương tự nên mình nêu ra ở đây luôn, chúng ta sẽ nói về RecyclerView riêng nó ở một bài học khác.

Ôi thôi nhức đầu quá. Yên tâm, chúng ta sẽ tìm hiểu từ từ. Và chỉ cần mình nêu một vài ví dụ thôi, thì bạn sẽ hiểu hết cách vận hành bộ ba DataSource – Adapter – AdapterView này nhanh thôi.

Xây Dựng ListView Từ ArrayAdapter

Nào dựa trên những gì mình nói trên đây, bạn đã biết ListView chính là một AdapterView. Còn ArrayAdapter là một AdapterDataSource một lát nữa đây chúng ta sẽ dùng một mảng được xây dựng sẵn. Bạn sẽ thấy sự kết hợp của bộ ba DataSource – Adapter – AdapterView ngay trong bài ví dụ hôm nay để hiểu rõ hơn về giao diện Danh sách.

Tuy nhiên mình cũng xin nói thêm, rằng mục này mình không nói cụ thể vào Adapter hay AdapterView gì đâu nhé. Mình chỉ vận dụng những gì có sẵn của hệ thống để bạn nắm được ý của bài học. Bạn sẽ hiểu tường tận Adapter hơn khi vào các bài kế tiếp. Và bạn cũng sẽ hiểu tường tận về ListView hay các AdapterView khác ở các phần tiếp theo của chuỗi bài Danh sách nữa. Các bạn chờ đọc nhé.

Và một ý nữa trước khi chính thức vào xây dựng ListView. Đó là code của bài học hôm nay tuy được vận dụng trên TourNote nhưng sẽ không được đưa lên GitHub. Vì ListView và Adapter ở bài này còn khá sơ sài, chúng ta sẽ cần một giao diện Danh sách cao cấp hơn, phức tạp hơn và sẽ được giới thiệu ở các bài học sau.

Nào bạn hãy mở project TourNote lên nhé. Mình sẽ xây dựng ListView trên Activity ContactActivity, vì code và giao diện ở Activity này còn đơn giản, rất dễ để chúng ta code và chạy thử. Bạn cũng có thể tự tạo mới một project rồi làm theo các bước như mình dưới đây cũng được nhé.

Xây Dựng DataSource

Nói xây dựng nghe ghê quá. Cái chính là chúng ta khai báo một mảng các String như sau ở ContactActivity. Chúng chính là DataSource của chúng ta.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ContactActivity extends AppCompatActivity {
 
    // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
 
    private static final String[] items = {"lorem", "ipsum", "dolor",
            "sit", "amet", "consectetuer",
            "adipiscing", "elit", "morbi",
            "vel", "ligua", "vitae",
            "arcu", "aliquet", "mollis",
            "eiam", "vel", "erat",
            "placerat", "ante", "porttitor",
            "sodales", "pellentesque", "augue",
            "purus"};
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_contact);
 
        // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
    }
 
    // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
}

Mảng items trên đây chính là DataSource. Bạn đừng cố hiểu nghĩa các String bên trong mảng nhé, mình chỉ tạo đại chúng thôi, tạo nhiều nhiều phần tử mảng xíu để lát cái Danh sách hiển thị lên màn hình đủ dài để bạn thử cuộn lên/xuống. Bạn có thể sử dụng List thay cho mảng. Nhưng đơn giản nhất ở bài học hôm nay vẫn là mảng, chúng ta sẽ có dịp dùng đến List sau này.

Xây Dựng AdapterView

AdapterView của chúng ta đang muốn xây dựng chính là ListView. Và bở vì AdapterView là một View, nên nó có thể được “nhét” vào giao diện của Activity. Mặt khác bạn còn có thể nhìn thấy chúng trong Pallet của editor giao diện nữa. Bạn hãy mở file giao diện activity_contact.xml lên, nhớ chọn tab Design ở editor này, và tìm xem nhé.

ListView hay các AdapterView khác đều xuất hiện trong Pallete
ListView hay các AdapterView khác đều xuất hiện trong Pallete

Như vậy để nhét ListView vào màn hình activity_contact, bạn có thể kéo thả ListView từ Pallete vào màn hình trực quan thiết kế. Rồi sau đó canh chỉnh các thông số của ListView này sao cho nó chiếm không gian toàn màn hình nhé, như hình minh họa dưới đây của mình, và hãy chú ý một số canh chỉnh của mình trong khung màu đỏ (bạn có thể xem thêm bài viết về ContraintLayout này của mình nếu như vẫn chưa tự tin lắm với việc canh chỉnh bằng giao diện đối với layout này).

Kéo thả ListView và thiết lập vài thông số
Kéo thả ListView và thiết lập vài thông số

Nhớ đừng quên đặt ID cho ListView này là my_lisview nhé. Chúng ta sẽ cần đến ID khi dùng Adapter đổ dữ liệu từ DataSource lên ListView này ở Java code sau.

Nếu chuyển sang tab Text của cửa sổ thiết kế, thì ListView mới thêm vào của chúng ta sẽ như sau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ContactActivity">
 
    <ListView
        android:id="@+id/my_listview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

Dùng Adapter Để Kết Nối DataSource Với AdapterView

Chúng ta đã có DataSource chính là mảng các String rồi, và cũng đã khai báo AdapterView chính là ListView rồi. Vậy Adapter dùng như thế nào? Như đã nói, chúng ta sẽ dùng đến ArrayAdapter. Bạn hãy thêm vào ContactActivity các dòng code được tô sáng sau, rồi mình sẽ giải thích sau nhé.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ContactActivity extends AppCompatActivity {
 
    // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
 
    private static final String[] items = {"lorem", "ipsum", "dolor",
            "sit", "amet", "consectetuer",
            "adipiscing", "elit", "morbi",
            "vel", "ligua", "vitae",
            "arcu", "aliquet", "mollis",
            "eiam", "vel", "erat",
            "placerat", "ante", "porttitor",
            "sodales", "pellentesque", "augue",
            "purus"};
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_contact);
 
        // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
 
        // Khai báo myListView và kết nối với my_listview bên XML
        ListView myListView = findViewById(R.id.my_listview);
        // Khai báo myArrayAdapter
        ArrayAdapter<String> myArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items);
        // Gắn myArrayAdapter vào cho myListView
        myListView.setAdapter(myArrayAdapter);
    }
 
    // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
}

Dòng code đầu tiên.

1
ListView myListView = findViewById(R.id.my_listview);

Theo mình nghĩ đây dòng code quen thuộc với các bạn rồi, do đó không nên nói nhiều. Cơ bản nó giúp chúng ta khai báo ra một ListView ở Java code, có tên là myListViewListView này cũng chính là cái ListView đã vẽ bên giao diện XML, để một lát nữa chúng ta sẽ cần đến cái ListView Java code vừa được khai báo này.

Đến các dòng code quan trọng tiếp theo, chính là dòng code khởi tạo một ArrayAdapter có tên myArrayAdapterArrayAdapter này được khai báo là ArrayAdapter<String>, cho biết rằng Adapter này giúp chuyển đổi danh sách các kiểu String từ DataSource sang AdapterView.

1
ArrayAdapter<String> myArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items);

Chúng ta cùng làm quen với 3 thông số truyền vào cho phương thức khởi tạo ArrayAdapter này.

  • Tham số đầu tiên đòi hỏi một ContextContext là gì thì chúngta tìm hiểu sau. Trong trường hợp này Activity chính là một Context, do đó chúng ta dùng từ khóa this, ý nói chính là Activity này (ContactActivity).
  • Tham số thứ hai chúng ta truyền vào một android.R.layout.simple_list_item_1. Đây là một view được xây dựng sẵn của hệ thống, chúng ta lấy tạm ra dùng. View này giúp hiển thị vỏn vẹn một String của chúng ta lên TextView của một phần tử trong Danh sách. Hay nói cách khác, Danh sách, hay ListView của chúng ta sẽ là danh sách các TextView, mỗi TextView sẽ giúp hiển thị một String của chúng ta. Và từng phần tử danh sách này chúng ta dùng tạm cái hệ thống đang có. Tuy việc dùng tạm này cho ra một ListView “tầm thường” thôi nhưng đủ để chúng ta hiểu về Adapter. Chúng ta sẽ cùng nhau xây dựng một ListView “hoành tráng” ở bài học sau nhé.
  • Tham số thứ ba chính là DataSource đã khai báo. Bạn đã biết, DataSource này được truyền vào để ArrayAdapter biết mỗi String có nội dung là gì, mà hiển thị lên từng TextView của từng phần tử Danh sách.

Dòng code cuối cùng cần tìm hiểu. Tuy ngắn nhưng quan trọng. Thiếu nó thì ListView cũng chưa có dữ liệu. Đó chính là dòng gán ArrayAdapter vừa khai báo vào cho ListView.

1
myListView.setAdapter(myArrayAdapter);

Đây là kết quả khi bạn thực thi chương trình (và nhớ là sau khi thực thi, bạn phải vào màn hình ContactActivity thông qua lựa chọn trên ActionBar của màn hình chính MainActivity nhé).

ListView của bài học hôm nay
ListView của bài học hôm nay

Wow một khi ListView đã hiện lên như trên đây thì có nghĩa là bạn đã thành công, và đã hiểu sơ qua khái niệm về Adapter & AdapterView của bài học hôm nay rồi đó. Bạn có thể thử cuộn lên/xuống danh sách vừa tạo này. Thậm chí hãy thử nhấn vào từng phần tử của danh sách để xem giao diện của chúng ra sao. Dĩ nhiên khi bạn nhấn vào từng phần tử của danh sách, mọi thứ vẫn cứ “trơ trơ”, vì chúng ta chưa code gì cho sự kiện này cả. Bài học đã dài và mình sẽ trình bày tiếp việc bắt các sự kiện trên AdapterView, cụ thể là ListView ở bài học sau nhé.

ìm Hiểu Về Danh Sách Tập 2 – ListView & ListActivity

Sau khi hoàn thành xong tập đầu tiên về giao diện Danh sách, bạn đã hiểu được phần nào khái niệm và cách vận dụng của bộ ba DataSource – Adapter – AdapterView để tạo nên một Danh sách từ mảng các String rồi đúng không nào. Và thông qua bộ ba phần tử của Danh sách này, bạn cũng đã được làm quen với một AdapterView khá hay có tên ListView.

Ở phần này, chúng ta tạm gác việc tìm hiểu thêm về Adapter. Mà hãy tiếp tục nói về ListViewAdapterView này còn có nhiều điều để nói lắm đấy. Rồi mở rộng hơn, dựa trên kiến thức khá ổn của ListView, ở bài sau chúng ta sẽ nói đến nhiều các thể loại AdapterView khác, dù giao diện chúng có khác nhau, nhưng cách thức xây dựng cũng sẽ gần như ListView mà thôi. Sau cùng, chúng ta cùng nhau quay lại Adapter để mổ xẻ phần tử này, khi mà kiến thức sử dụng AdapterView đã khá vững vàng.

Thôi lan man quá. Mời các bạn bắt đầu bài học.

ListView Và ListActivity

Vâng, bài hôm trước chúng ta làm quen với ListView. Bài này chúng ta nói rõ hơn về ListView. Nhưng… có cả ListActivity nữa ư, nó là cái gì? Có giống với ListView không?

Thực ra thì, ListView và ListActivity cũng cùng một công năng là xây dựng một giao diện dạng Danh sách theo chiều dọc màn hình. Với việc làm quen ở bài trước, chúng ta xây dựng Danh sách này bên trong một Activity có sẵn. Tức là trong Activity đó chúng ta kéo thả một ListView vào như một View bình thường, canh chỉnh vị trí của ListView đó ở trên giao diện của Activity. Nhưng Google không muốn chúng ta sống một cách giản đơn như vậy. Họ nhận thấy rằng, nếu một Activity mà chỉ có một ListView bên trong nó, thì họ có một cách nữa để chúng ta xây dựng ListView, họ đẻ ra một Activity đặc biệt có tên ListActivity.

Về cơ bản ListActivity giúp thiết kế ra một giao diện Danh sách cực nhanh (có thể mục đích chính của Google là đây). Chỉ cần vài thao tác bạn đã có một Danh sách, thậm chí ListActivity không cần đến file giao diện (tức là file XML luôn), ghê không. Bạn chỉ cần khai báo file XML cho ListActivity khi bạn muốn thiết kế giao diện vừa ListView vừa các thành phần giao diện khác nữa. Nào chúng ta cùng nhau xây dựng nhanh một ListActivity như thế này.

Xây Dựng ListActivity Không Cần Giao Diện XML

Đầu tiên, bạn hãy mở lại project TourNote, mở lại lớp ContactActivity đã thực hành xây dựng ListView từ bài học trước. Hoặc bạn có thể mở lại project mới của bạn đã tạo ra hôm trước. Hoặc siêng hơn có thể tạo mới một project cho bài hôm nay cũng được. Nhưng hãy đảm bảo ContactActivity, hay Activity nào đó của riêng bạn, kế thừa từ ListActivity nhé. Đại loại như thế này.

1
2
3
4
public class ContactActivity extends ListActivity {
 
    // Code trong này lát tính tiếp
}

Tiếp theo. Về DataSource, bạn hãy giữ nguyên việc khai báo danh sách các mảng String mang tên items như bài học trước.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ContactActivity extends ListActivity {
 
    // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
 
    private static final String[] items = {"lorem", "ipsum", "dolor",
            "sit", "amet", "consectetuer",
            "adipiscing", "elit", "morbi",
            "vel", "ligua", "vitae",
            "arcu", "aliquet", "mollis",
            "eiam", "vel", "erat",
            "placerat", "ante", "porttitor",
            "sodales", "pellentesque", "augue",
            "purus"};
 
    // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
}

Cũng chưa có gì khác nhiều đúng không nào. Tiếp tục, ở code của phương thức onCreate(). Bạn chú ý các dòng được tô sáng của mình nhé.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class ContactActivity extends ListActivity {
 
    // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
 
    private static final String[] items = {"lorem", "ipsum", "dolor",
            "sit", "amet", "consectetuer",
            "adipiscing", "elit", "morbi",
            "vel", "ligua", "vitae",
            "arcu", "aliquet", "mollis",
            "eiam", "vel", "erat",
            "placerat", "ante", "porttitor",
            "sodales", "pellentesque", "augue",
            "purus"};
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Không cần dòng này, vì ListActivity có sẵn ListView cho chúng ta rồi
//        setContentView(R.layout.activity_contact);
 
        // ListActivity không hỗ trợ các hàm này
//        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
//        getSupportActionBar().setHomeButtonEnabled(true);
 
        // Các khai báo khác của lớp này cứ để nguyên vậy, vì không quan trọng nên mình tạm ẩn đi cho bạn dễ nhìn
 
//        // Khai báo myArrayAdapter
        ArrayAdapter<String> myArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items);
//        // Gắn myArrayAdapter vào cho ListView có sẵn
        setListAdapter(myArrayAdapter);
    }
 
    // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
}

Do onCreate() có nhiều khác biệt, nên mình xin giải thích rõ ràng hơn như sau.

  • Đầu tiên, bạn có thể thấy chúng ta không cần đến phương thức setContentView() nữa. Bạn đã biết phương thức này giúp định nghĩa giao diện cho Activity. Và vì ListActivty đã có sẵn ListView rồi nên chúng ta có thể bỏ luôn việc khai báo giao diện này. Lát nữa chúng ta sẽ thử tạo giao diện cho ListActivity để mà hiển thị vừa ListView vừa TextView sau xem thế nào nhé.
  • Tiếp theo, các dòng code liên quan đến getSupportActionBar() chúng ta cũng không thể thêm vào ListActivity. Vì sao ư? Mình nghĩ có lẽ ListActivity không hỗ trợ ActionBar. Đây là một điểm trừ cực kỳ lớn khiến trước giờ mình hầu như không dùng đến ListActivity.
  • Tuy dòng code khai báo myArrayAdapter giống hệt bài học trước. Nhưng dòng gán Adapter vào ListView thì rất khác. Hôm trước bạn phải gọi myListView.setAdapter(myArrayAdapter), thì hôm nay chỉ cần gọi setListAdapter(myArrayAdapter), vì như mình đã nói, ListActivity có sẵn ListView rồi nên phương thức setListAdapter() sẽ biết và set thẳng Adapter vào cho ListView sẵn có luôn.

Giờ thị bạn có thể thực thi chương trình để xem ListActivity của chúng ta ở bài này có khác gì với cách dùng ListView ở bài hôm trước không nhé.

ListActivity của bài hôm nay
ListActivity của bài hôm nay

Bạn có thể thấy rằng ListActivity này không có ActionBar đúng không. Tuy nhiên mình không nói nhiều về khía cạnh này của ListActivity. Mình chỉ biết có ý kiến rằng nếu muốn ListActivity có ActionBar thì phải set lại Theme về Theme.Holo, nhưng vậy thì chán quá. Thôi tùy bạn quyết định có dùng ListActivity hay không nhé.

Xây Dựng ListActivity Với Giao Diện XML

Dù cho bạn có thích ListActivity hay không. Nhưng mình đã lỡ nói rồi nên sẽ nói cho trót. Mục trên đây giúp bạn xây dựng nhanh một giao diện dạng Danh sách mà không cần phải khai báo file XML, tức file giao diện cho Activity. Cách làm này rất nhanh, nhưng không phải lúc nào trong thực tế giao diện cũng chỉ có mỗi một ListView như thế này. Chính vì vậy chúng ta giả sử cần một giao diện phức tạp hơn nhưng cũng vừa đủ đơn giản thôi, giao diện chúng ta sắp xây dựng sẽ vừa có TextView vừa có ListView xem sao nhé.

Bạn hãy mở activity_contact.xml lên. Hoặc bất kỳ file XML nào của project của bạn. Chúng ta tiến hành kéo thả vào màn hình trực quan thiết kế TextView và ListView sao cho nó trông như thế này.

Thiết kế giao diện bao gồm TextView và ListView
Thiết kế giao diện bao gồm TextView và ListView

Bạn chú ý, đoạn này của mình rất quan trọng này. Bạn nhớ set ID cho TextView là myTextView. Còn ID cho ListView là @android:id/list. Bạn có thể set ID cho TextView là bất kỳ, như những gì bạn từng làm với ID trước kia. Nhưng ID cho ListView lúc này luôn luôn phải là @android:id/list bạn nhé. Đó là quy định của ListActivity, hệ thống sẽ tìm đến @android:id/list để thiết lập Adapter cho nó thông qua phương thức setListAdapter() trên kia.

Nếu bạn muốn xem file XML của giao diện trên, thì đây.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ContactActivity">
 
    <ListView
        android:id="@android:id/list"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="16dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/myTextView" />
 
    <TextView
        android:id="@+id/myTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:gravity="center_horizontal"
        android:text="Bellow is a ListView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

Với sự chỉnh sửa này thì ContactActivity chỉ cần bạn để lại phương thức setContentView() ở onCreate() là được. Code hoàn chỉnh giống như sau, khác biệt duy nhất so với code ở mục trên kia chỉ là dòng mà mình đã tô sáng.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ContactActivity extends ListActivity {
 
    // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
 
    private static final String[] items = {"lorem", "ipsum", "dolor",
            "sit", "amet", "consectetuer",
            "adipiscing", "elit", "morbi",
            "vel", "ligua", "vitae",
            "arcu", "aliquet", "mollis",
            "eiam", "vel", "erat",
            "placerat", "ante", "porttitor",
            "sodales", "pellentesque", "augue",
            "purus"};
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_contact);
 
        // ListActivity không hỗ trợ các hàm này
//        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
//        getSupportActionBar().setHomeButtonEnabled(true);
 
        // Các khai báo khác của lớp này cứ để nguyên vậy, vì không quan trọng nên mình tạm ẩn đi cho bạn dễ nhìn
 
        // Khai báo myArrayAdapter
        ArrayAdapter<String> myArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items);
        // Gắn myArrayAdapter vào cho ListView có sẵn
        setListAdapter(myArrayAdapter);
    }
 
    // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
}

Kết quả khi thực thi chương trình. Bạn chú ý đã thấy có TextView hiển thị ở trên ListView đấy nhé.

ListActivity với giao diện gồm TextView và ListView
ListActivity với giao diện gồm TextView và ListView

Sự Kiện Click Trên ListView

Qua các kiến thức về ListView mà bạn đã làm quen hôm trước đến giờ, thì bạn đã phần nào hiểu về ListView rồi. Tuy nhiên chúng ta cũng chỉ mới hiển thị ListView lên màn hình thôi. Một công năng quan trọng khác của ListView mà mình muốn nói tới, đó là giúp chúng ta bắt được các sự kiện trả về khi người dùng thao tác trên Danh sách. Đó có thể là sự kiện click chọn trên một phần tử Danh sách, hay sự kiện nhấn giữ (long click) trên một phần tử Danh sách, hay sự kiện chọn (select) trên một phần tử Danh sách (dành cho các thiết bị có các phím điều hướng chuyên dụng như TV chẳng hạn),… Có nhiều sự kiện trả về là thế, nhưng mục này mình sẽ nói đến một sự kiện tiêu biểu, được sử dụng rất nhiều, hầu như có Danh sách là có sự kiện này, đó là sự kiện click chọn (hay nói ngắn gọn là click, hay touch) trên Danh sách.

Nói về mặt sử dụng, thì kiến thức về bắt sự kiện click ở mục này nói một mục thôi là đủ. Nhưng do chúng ta đã tách ra làm hai cách khai báo, là ListView của bài trước, và ListActivity của bài hôm nay, nên sự kiện click cũng sẽ được mình tách ra làm 2 mục nhỏ cho các bạn dễ phân biệt và nhớ lâu.

Sự Kiện Click Trên ListView Bài Hôm Trước

Với bài hôm trước, chúng ta có một myListView được khai báo tường minh từ ListView. Do đó để lấy sự kiện click trên myListView, chúng ta gọi đến sự kiện setOnItemClickListView() trên myListView này. Code hoàn chỉnh của ContactActivity khi này như sau (bạn hãy chú ý đoạn được tô sáng).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class ContactActivity extends AppCompatActivity {
 
    // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
 
    private static final String[] items = {"lorem", "ipsum", "dolor",
            "sit", "amet", "consectetuer",
            "adipiscing", "elit", "morbi",
            "vel", "ligua", "vitae",
            "arcu", "aliquet", "mollis",
            "eiam", "vel", "erat",
            "placerat", "ante", "porttitor",
            "sodales", "pellentesque", "augue",
            "purus"};
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_contact);
 
        // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
 
        // Khai báo myListView và kết nối với my_listview bên XML
        ListView myListView = findViewById(R.id.my_listview);
        // Khai báo myArrayAdapter
        ArrayAdapter<String> myArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items);
        // Gắn myArrayAdapter vào cho myListView
        myListView.setAdapter(myArrayAdapter);
 
        // Khai báo sự kiện Click cho myListView
        myListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Toast.makeText(ContactActivity.this, items[position] + " clicked", Toast.LENGTH_SHORT).show();
            }
        });
    }
 
    // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
}

Nếu bạn đã từng hiểu cách thiết lập sự kiện click trên Button ra sao, thì sự kiện click trên phần tử của Danh sách trên đây cũng thiết kế tương tự. Có điều trong interface OnItemClickListener trên có khai báo một phương thức gọi về onItemClick() với vài tham số hữu dụng, như position, tham số này trả về một con số, con số này chính là vị trí của phần tử trong Danh sách được click. Dựa vào thông số position này mà trong phương thức onItemClick() mình có thể hiển thị một Toast với nội dung chính là phần tử thứ position trong danh sách String items.

Kết quả khi thực thi chương trình, và khi nhấn vào một item trên list.

Khi nhấn vào phần tử trên ListView
Khi nhấn vào phần tử trên ListView

Sự Kiện Click Trên ListActivity

Còn đây là cách set sự kiện click cho phần tử của ListActivity hôm nay. Ngoại trừ các dòng code khác nhau liên quan đến việc khai báo ListView mà bạn đã biết trên kia, thì việc set sự kiện click dưới đây chỉ có việc thay thế myListView.setOnItemClickListener() thành getListView().setOnItemClickListener() mà thôi, tại sao có sự khác biệt này thì chắc bạn cũng hình dung ra được rồi.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class ContactActivity extends ListActivity {
 
    // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
 
    private static final String[] items = {"lorem", "ipsum", "dolor",
            "sit", "amet", "consectetuer",
            "adipiscing", "elit", "morbi",
            "vel", "ligua", "vitae",
            "arcu", "aliquet", "mollis",
            "eiam", "vel", "erat",
            "placerat", "ante", "porttitor",
            "sodales", "pellentesque", "augue",
            "purus"};
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_contact);
 
        // ListActivity không hỗ trợ các hàm này
//        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
//        getSupportActionBar().setHomeButtonEnabled(true);
 
        // Các khai báo khác của lớp này cứ để nguyên vậy, vì không quan trọng nên mình tạm ẩn đi cho bạn dễ nhìn
 
        // Khai báo myArrayAdapter
        ArrayAdapter<String> myArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items);
        // Gắn myArrayAdapter vào cho myListView
        setListAdapter(myArrayAdapter);
 
        // Khai báo sự kiện Click cho item của list
        getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Toast.makeText(ContactActivity.this, items[position] + " clicked", Toast.LENGTH_SHORT).show();
            }
        });
    }
 
    // Các khai báo khác của lớp này cứ để nguyên vậy trong code của bạn, mình tạm ẩn đi cho bạn dễ nhìn
}

Kết quả thực thi cũng tương tự như trên mà thôi.

Khi nhấn vào phần tử trên ListView
Khi nhấn vào phần tử trên ListView

Chúng ta kết thúc bài học hôm nay tại đây. Qua đây hẳn bạn đã thủ sẵn cho bản thân 2 cách xây dựng ListView rồi. Và bạn cũng đã có thể bắt sự kiện click trên phần tử của ListView nữa. Như phần mở đầu bài viết có nói, xong phần này, tiếp theo chúng ta sẽ nói về các cách xây dựng một số AdapterView khác. Sẽ có nhiều thú vị lắm đấy.

Cám ơn bạn đã đọc tài liệu của chúng tôi

Công ty cổ phần thương mại Vạn Tín Việt

0936.006.058
0936.006.058