Intent trong Android

Nếu bạn đã làm việc với Android trong một thời gian thì hẳn bạn đã ít nhiều sử dụng tới Intent. Quen thuộc nhất là phương thức startActivity(Intent(Context context, Class nextActivity)) để chuyển từ Activity này sang Activity khác trong cùng ứng dụng. Tuy nhiên, Intent không chỉ dừng lại ở đó. Ngoài việc “bắn” extra trong nội bộ ứng dụng, bạn còn có thể “bắn” extra từ ứng dụng của bạn sang một hoặc nhiều ứng dụng khác.

1. Sơ lược về Intent

An intent is an abstract description of an operation to be performed. It can be used with startActivity to launch an Activity, broadcastIntent to send it to any interested BroadcastReceiver components, and startService(Intent) or bindService(Intent, ServiceConnection, int) to communicate with a background Service.

An Intent provides a facility for performing late runtime binding between the code in different applications. Its most significant use is in the launching of activities, where it can be thought of as the glue between activities. It is basically a passive data structure holding an abstract description of an action to be performed.

Sẽ là rất khó hiểu nếu bạn chỉ đọc và hiểu các dòng trên mà không thực hành. Nói một cách ngắn gọn nhất, để thực hiện việc liên lạc và truyền dữ liệu giữa các thành phần của ứng dụng (Activity, Service, BroadcastReceiver) thì Intent chính là thứ bạn cần dùng. Ví dụ, để gọi một Activity khác, hoặc cho chạy một Service, hoặc gửi thông tin broadcast, hoặc hơn nữa là gửi một dữ liệu nào đó tới một ứng dụng khác.

Intent bao giờ cũng có hai phía, một là phía xuất phát, vốn luôn là cái package của bạn, mà cụ thể hơn là cái Activity bạn đang viết code, và phần đích đến. Đích đến có thể là một thành phần trong nội bộ ứng dụng của bạn, hoặc một ứng dụng khác. Căn cứ vào độ tường minh của đích đến, trang Android Developer chia Intent thành hai loại: Explicit intent – tôi dịch là intent tường minh, và implicit intent – intent không tường minh.

2. Explicit intent – Intent tường minh

Gỉa sử bạn gọi một chiếc taxi và yêu cầu tài xế chở bạn đến một địa chỉ cụ thể nào đó. Nếu đem bối cảnh đó chuyển về việc lập trình Android, thì cái yêu cầu trên chính là một explicit intent. Như vậy, một explicit intent là một intent có đích đến vô cùng cụ thể, là một intent chứa đích danh đích đến. Chẳng hạn, việc bạn startActivity(new Intent(this, NextActivity.class)) chính là một explicit intent, chỉ cụ thể đích đến là đâu – tức NextActivity.

Nếu bạn gửi một intent tới một ứng dụng cụ thể khác (đương nhiên bạn đã biết package label của nó rồi) thì intent của bạn cũng là một explicit intent, vì đích đến rất rõ ràng, là cái ứng dụng cụ thể kia. Và Android System sẽ tự tìm và gọi cái Activity nào trong ứng dụng kia có chức năng nhận loại intent mà ứng dụng của bạn gửi qua. Chẳng hạn, tôi muốn gửi một file qua ứng dụng Dropbox, thì tôi sẽ chỉ việc setPackage cho Intent của mình là “com.dropbox.android”. Kết quả là, nếu thiết bị khách có ứng dụng Dropbox thì Android system sẽ gọi một Activity có chức năng nhận intent tương thích lên. Còn không, thì nó sẽ “thảy” ra một Exception và dừng hoạt động của hàm tại đó, chứ nó không có cố tình “chuyển” intent đó sang nơi nào khác.

3. Implicit intent – Intent không tường minh

Quay lại việc bạn đi taxi. Nếu bạn không biết rõ đích đến là đâu, chỉ có thể mô tả một cách sơ sơ thì độ chính xác của thông tin sẽ bị giảm đi. Tương tự như vậy, mức độ chính xác của intent cũng sẽ giảm đi khi các thông tin bạn set cho intent không đầy đủ. Và nó trở thành một implicit intent. Việc Android system xử lí implicit intent không chỉ phụ thuộc vào các thông tin do bạn cung cấp, mà còn phụ thuộc vào cả cách bạn muốn Android system xử lí intent đó.

Chẳng hạn, nếu bạn muốn chọn một hay nhiều file trong hệ thống tập tin có thể truy cập được trên thiết bị, chẳng hạn như bạn đang làm một ứng dụng về ảnh, thì bạn dùng Intent và Intent.ACTION_PICK để hệ thống mở trình chọn tập tin hoặc đưa ra danh sách các ứng dụng được thiết kế để nhận Intent và Intent.ACTION_PICK để người dùng lựa chọn.

4. Cách Android system xử lí các implicit intent.

Đối với explicit intent, thì Android system sẽ làm hành động “có người nhận thì thực hiện tiếp, không có thì quăng ra một Ngoại lệ”. Nhưng đối với implicit intent, vì do tính thiếu chính xác của nó, nên Android system có một số hướng xử lí như bên dưới, và đó không phải là những cách xử lí mang tính “chữa cháy” vì thực sự, nó đã được thiết kế như vậy để các lập trình viên – chính là bạn – tận dụng.

  • Gọi ứng dụng duy nhất có tính năng nhận loại intent bạn gửi.
  • Tạo một app chooser.
  • Gọi app mặc định mà người dùng đã định.

Khi có từ hai ứng dụng trở lên có khả năng thực hiện một tác vụ nào đó, thì theo mặc định, Hệ thống Android sẽ liệt kê chúng ra khi ứng dụng đang được sử dụng gửi đi một implicit intent yêu cầu xử lí tác vụ đó. Nếu bạn đã sử dụng Android được một thời gian, thì bạn sẽ không còn lạ lẫm gì với bảng chọn tương tự như bên dưới.

Tuy nhiên, nếu chỉ có một ứng dụng có khả năng thực hiện tác vụ trên, thì nghiễm nhiên nó phải được lựa chọn. Và do đó, intent sẽ được gửi trực tiếp tới nó mà không cần thông qua bảng chọn. Như vậy, implicit intent trong tình huống này có hướng xử lí giống như một explicit intent.

Một tình huống khác, là người dùng đã chọn một ứng dụng làm ứng dụng mặc định để thực hiện tác vụ đó. Và lúc này, implicit intent cũng được xử lí như là một explicit intent. Tất nhiên, về bản chất gốc thì nó vẫn là một implicit intent và nếu Hệ thống Android (các bản Android càng mới hơn) đặt ra những giới hạn nghiêm ngặt về implicit intent thì nó cũng sẽ gặp những giới hạn đó.

5. Sử dụng Intent trong nội bộ ứng dụng

Tất nhiên, việc gửi nhận Intent chỉ diễn ra giữa Activity-Activity, Activity-Service và Service-Activity, vì việc tương tác Intent và ContentProvider nếu có thì cũng chẳng có ý nghĩa gì cả, ngoại trừ một hành động duy nhất là truy cập file (Intent.ACTION_PICK và FileProvider) – và hoạt động này diễn ra bởi hệ thống Android nên bạn không cần quan tâm.

Bạn đã quá quen thuộc với việc dùng Intent để hướng người dùng di chuyển từ Activity này sang Activity khác trong ứng dụng với startActivity(Intent). Ngoài ra, chí ít bạn cũng đã dùng qua Intent#putExtra(String key, Object whatToSend) để gửi dữ liệu sang các Activity khác. Không giới hạn ở đó, Intent còn được dùng để gửi dữ liệu sang Service nữa. Chẳng hạn:

Tuy nhiên, để Service gửi Intent lại Activity thì bạn không bao giờ được dùng startActivity(), bởi Activity đã hoạt động rồi mà, nó phải đang hoạt động thì mới gửi Intent qua startService được chứ! Do đó, bạn sẽ phải tính tới cách sendBroadcast(Intent). Tuy nhiên, nói dễ hơn làm, việc nhận Intent từ Service phức tạp hơn rất nhiều so với startService(Intent). Bạn có thể dùng cách này đối với cả started services và bound services. Dưới đây, thay vì sử dụng thư viện framework thì tôi dùng thư viện support, cụ thể là android.support.v4.content. Lí do cụ thể thì tôi sẽ giải thích bên dưới. Đầu tiên, bạn tạo một BroadcastReceiver, một LocalBroadcastManager và một IntentFilter instances trong Activity.

Còn ở phía service, thì bạn thực hiện tạo một LocalBroadcastManager, và sendBroadcast với các dòng sau:

Giải thích: Bạn có để ý là cả Activity và Service đều có một LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(Context); và nếu tìm hiểu kĩ thì hàm getInstance(Context) là như sau:

Vì mInstance là biến static nên nó tất cả các hàm LocalBroadcastManager.getInstance(Context) đều trỏ tới nó mặc kệ cái Context đó là cái Context nào đi nữa, vì một là nó được khởi tạo một lần, và hai là Context được truyền vào constructor của nó là applicationContext mà. Do đó, biến mInstance sẽ là duy nhất trong toàn bộ ứng dụng của bạn. Cả broadcastManager của Activity hay Service chỉ là một, là chính cái mInstance kia. Do đó, việc Context leak là điều cực kì khó xảy ra hơn so với dùng trực tiếp các hàm Context#sendBroadcast và Context#receiveBroadcast của Activity và Service, vì có thể Service là duy nhất nhưng Activity sẽ được khởi tạo lại khi bạn xoay màn hình và do đó dễ gây leak Context.

Khi Service gửi một Intent thông qua broadcastManager.sendBroadcast(intent) thì cái nguyên cái intent đó chính là cái intent nằm trong onReceive(Context context, Intent intent) của BroadcastReceiver receiver của Activity. Và các thao tác khác liên quan tới biến intent đó thì tôi nghĩ bạn đã tự hiểu được rồi.

6. Các lầm tưởng về Intent trong nội bộ ứng dụng

a) Gửi intent vào ListView, hoặc ListViewAdapter hoặc RecyclerView.Adapter.

Thực tế cũng khó có thể gọi đó là lầm tưởng vì nếu bạn tạo được các hàm xử lí được tình huống, chẳng hạn bạn đặt được hàm getIntent() trong Adapter cho ListView và RecyclerView thì cứ thoải mái mà dùng. Khoảng đầu tháng 6 thì tôi có nhận được câu hỏi của 2 bạn với cùng một nội dung là làm sao get được Intent trong Adapter, vì các bạn đó muốn add thêm một phần tử mới vào ListView và RecyclerView, nên các bạn dùng Intent để gửi nội dung thông qua putExtra.

Thực tế là trong ListView, BaseAdapter, RecyclerView và RecyclerView.Adapter và các subclasses của chúng không có hàm getIntent() return Intent cả cũng như trong Activity và các subclasses của nó cũng không có hàm nào để đưa Intent vào Adapter cả. Và nếu có thì cũng không ai đi sử dụng Intent để truyền dữ liệu vào chúng. Chẳng hạn tôi tự đặt ra các hàm đó như sau:

Để truyền Intent tử MyActivity vào MyAdapter thì tôi đương nhiên dùng sendIntentToAdapter(Intent, MyAdapter) rồi. Như vậy, tôi khởi tạo một biến Intent, và putExtra vào. Nhưng chưa đủ đâu, tôi cần một cái String key nữa, vậy tôi đặt tiếp một biến String key. Quay sang Adapter, tôi lại getIntent đó ra, getExtra ra, đem so với String key trên coi có sai không, nếu đúng thì xử lí tiếp, và hướng xử lí tiếp theo đó hoàn toàn giống y chang với việc xử lí trực tiếp mà không cần qua Intent: Add phần tử vào danh sách các đối tượng ban đầu trong Adapter rồi notifyDataSetChanged là xong nồi cơm. Vậy, dùng Intent có lợi ích gì? Đặc biệt hơn khi tôi hỏi thì các bạn đó lại “lầm tưởng” (lần này là thật) là dùng startActivity(new Intent(Activity.this, Adapter.class) để truyền Intent vào Adapter. Bảo đảm, bạn chạy hàm trên chục lần thì app sẽ crash đủ chục lần.

b) Gửi Intent vào Fragment

Một “lầm tưởng” khác là việc truyền Intent vào Fragment. Ở đây không phải là việc truyền dữ liệu từ Activity mẹ vào trong Fragment con, mà là nhận dữ liệu từ Activity khác. Và khi nghe câu “Em tìm trong Fragment không thấy có hàm getIntent thì làm sao?” là tôi lắc đầu đủ chục cái. Một là nếu không có thì bạn có thể tự định nghĩa. Còn hai là Fragment luôn nằm trong Activity, không bao giờ tách rời Activity cả, vậy tại sao bạn không lấy Intent từ Activity và truyền vào Fragment?

Một lí do rất lớn là việc Google chưa bao giờ cân nhắc tới việc tạo hàm getIntent trong Fragment, bởi lẽ một Fragment có thể nằm trong nhiều Activity khác nhau. Chẳng hạn, khi bạn tối ưu hóa giao diện cho máy tính bảng, bạn không có cách nào khác là dùng Fragment để tận dụng code thay cho làm hàng loạt Activity khác nhau cho các thiết bị có kích thước khác nhau. Do đó, việc getIntent trong Fragment đôi lúc sẽ xảy ra hiện tượng xung đột không đáng có và ứng dụng của bạn sẽ liên tục hiện cái bảng bên dưới cho tới khi mặt bạn xanh lên thì mới thôi, và nếu bạn “mới học” thì có khi bị đau tim cũng không chừng.Unfortunately-app-has-stopped

c) Intent là cách duy nhất để gửi dữ liệu giữa các thành phần trong ứng dụng

Thực tế là không phải. Gửi nhận Intent chỉ là một cách mà thôi. Cá nhân tôi “thích” dùng callback interface hơn, đặc biệt là về Bound Services thì tôi luôn dùng interface và mọi chuyện diễn ra đều “ngon cơm ngọt canh” mà lại có phần đơn giản hơn so với Intent nữa. Tất nhiên, về tính chất của project tôi làm thì dùng interface dễ hơn, nhưng đối với project của bạn thì có thể khác. Do đó, nói chung dùng cách nào là tùy bạn, tùy vào tính chất project và cả khả năng của bạn nữa, nhưng nói tóm lại: INTENT KHÔNG PHẢI LÀ CÁCH DUY NHẤT.

 

Leave a Reply

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

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