Tina Tang's Blog

在哪裡跌倒了,就在哪裡躺下來

0%

Android筆記(22)-Activities和Intents

目前為止,您使用的應用程式只執行一項活動(activity)。但實際上很多 Android 應用程式需要執行多項活動(activity),並透過導覽(navigation)來切換應用程式。

學習目標

  • 使用明確意圖(intent)導覽(navigation)至特定活動(activity)。
  • 使用隱含意圖(intent)導覽(navigation)至其他應用程式的內容。
  • 新增選單(menu)選項,並新增按鈕至應用程式列(app bar)

在本程式碼研究室中,您要建構一個字典應用程式,讓應用程式使用多項活動,並使用意圖(intent)切換應用程式,同時傳遞資料至其他應用程式。


範例程式碼

在後續步驟中,您要使用 Words 應用程式。Words 應用程式是簡單的字典應用程式,包含字母清單、每個字母的字詞,以及在瀏覽器中查詢個別字詞定義的功能。

下載範例程式碼

請注意,GitHub 下載範例程式碼的資料夾名稱是 android-basics-kotlin-words-app-starter。在 Android Studio 中開啟專案時,請選取這個資料夾。

範例程式碼網址: https://github.com/google-developer-training/android-basics-kotlin-words-app
分支版本名稱: starter

在 Android Studio 中開啟專案

  1. 啟動 Android Studio。
  2. 開啟 android-basics-kotlin-words-app-starter 專案
  3. 按一下「Run」按鈕,確認應用程式的建構符合預期。

Word 應用程式總覽

繼續操作前,請花點時間熟悉專案內容。您必須熟悉上一個單元的所有概念。應用程式目前有兩項活動(activities),包含 recycler viewadapter

具體而言,您會使用下列檔案:

  1. MainActivityRecyclerView 會使用 LetterAdapter。每個字母都是包含 onClickListener 的按鈕,這些按鈕目前沒有任何內容。您可以在這裡管理按鈕點按操作以導覽至 DetailActivity
  2. DetailActivityRecyclerView 使用 WordAdapter,以顯示字詞清單(list)。雖然您暫時無法前往此畫面,但請記得每個字詞都有對應的按鈕和 onClickListener。在這個步驟中,為了導覽至瀏覽器並顯示字詞的定義,您要加入程式碼。
  3. MainActivity 也需要進行一些變更。在這個步驟中,為了顯示按鈕,讓使用者切換清單(list)和格線版面配置,您要實作選項選單(option menu)。

Intent簡介

您已完成初始專案設定,接著我們要探討意圖(intent),以及如何在應用程式中使用意圖(intent)。

意圖(intent)是物件,代表要執行的一些動作。我們最常看到意圖用來啟動活動(activity) (當然意圖不只這個用途)。意圖(intent)分為兩種類型:隱含(implicit)明確(explicit)

  • 明確意圖(explicit intent) :極為精確,您確切知道要啟動的活動(activity) (通常是應用程式的畫面)。
  • 隱含意圖(implicit intent) :較抽象,系統收到動作的類型 (例如開啟連結、撰寫電子郵件或撥打電話),然後負責判斷如何完成要求。

您可能已看過這兩種意圖(intent),只是未察覺到。一般而言,在您的應用程式中 顯示活動(activity) 時,即是使用 明確意圖(explicit intent)

但如果動作不涉及目前的應用程式 (例如您找到有趣的 Android 文件資訊頁面,並想分享給朋友),即使用 隱含意圖(implicit intent) 。您可能會看到類似選單,詢問您要使用什麼應用程式分享資訊頁面。


設定 Explicit Intent

現在可以實作第一個意圖(Intent)了。在第一個畫面上,當使用者輕觸字母後,就會前往列有字詞清單的第二個畫面。因為已實作 DetailActivity,所以只需使用Intent啟動此動作。因為應用程式已經知道要啟動特定的活動(Activity),您可以使用明確意圖(Explicit Intent)。

建立並使用Intent只需幾個步驟:

  1. 開啟 LetterAdapter.kt 並向下捲動至 onBindViewHolder()。在這一行程式碼下,設定按鈕文字,並設定 holder.buttononClickListener
1
2
3
holder.button.setOnClickListener {

}
  1. 然後取得 context 的參考。
1
val context = holder.itemView.context
  1. 建立 Intent,並傳入目的地活動的結構定義和 class name。
1
val intent = Intent(context, DetailActivity::class.java)
  • 您要顯示的活動名稱指定為 DetailActivity::class.java。實際的 DetailActivity 物件會在幕後建立。
  1. 呼叫 putExtra 方法,然後傳入「letter」做為第一個引數,按鈕文字則做為第二個引數。
1
intent.putExtra("letter", holder.button.text.toString())

extra 是什麼?請記得,intent 只是一組操作說明,但目的地動作目前沒有 intent。而 extra 是一段資料,例如數字或字串,即之後擷取的指定名稱。這類似於呼叫函式時傳遞引數。因為 DetailActivity 可顯示任何字母,您必須指定顯示哪個字母。

此外,您認為為什麼需要呼叫 toString()?按鈕的文字是字串,對吧?

可以這麼說。它其實是 CharSequence 類型,即所謂的介面。您目前不需瞭解 Kotlin 介面的任何資訊,只需知道介面是用於確定字串等類型,以及實作特定函式和屬性的方式。您可以將 - 聯想為類似字串 class 的一般表示法。按鈕的 text 屬性可以是字串,或同時是 CharSequence 的任何物件。但 putExtra() 方法接受 String,不是 CharSequence,所以必須呼叫 toString()

  1. 呼叫結構定義的 startActivity() 方法,並傳入 intent
1
context.startActivity(intent)

接著執行應用程式,並嘗試輕觸字母。隨即顯示詳細資料畫面!但無論使用者輕觸哪個字母,詳細資料畫面會一律顯示字母 A 的字詞。在詳細資料動作中,一些工作仍有待完成,才會顯示傳遞字母的字詞,作為 intent 額外資料。


設定 DetailActivity

您已建立第一個明確意圖!現在進入詳細資料畫面。

DetailActivityonCreate 方法中,呼叫 setContentView 後,以程式碼取代硬式編碼字母,從 intent 取得傳入的 letterId

1
val letterId = intent?.extras?.getString("letter").toString()

這裡有很多要注意的事項,所以接著我們要查看每個項目:

首先,intent 屬性的來源為何?這不是 DetailActivity 的屬性,而是任何活動的屬性。此屬性會持續參考啟動活動使用的意圖。

額外屬性是 Bundle 類型,或許您已猜到,該屬性提供方法來存取傳入意圖的所有額外屬性。

這兩個屬性會以問號標示。原因是什麼呢?原因是 intentextras 屬性可為空值,換句話說,您可以使用值,也可以不使用值。有時,您可能想要變數為 nullintent 屬性可能不是 Intent (如果活動不是從意圖啟動),此外,額外的屬性可能不是 Bundle,而是名為 null 的值。在 Kotlin 中,null 代表沒有值。物件可能存在,或可能是 null。如果您的應用程式嘗試在 null 物件上存取屬性或呼叫函式,該應用程式就會異常終止。若要安全存取這個值,您必須在名稱後方加上 ?。如果 intentnull,應用程式不會嘗試存取額外的屬性,此外,如果 extras 為空值,程式碼也不會嘗試呼叫 getString()

如何知道哪些屬性需要問號才能確保空值的安全性?您可以透過類型名稱後方是否有問號或驚嘆號來判斷。

最後請注意,使用 getString 擷取實際字母會傳回 String?,所以呼叫 toString() 可確保它是 String,而不是 null

您現在執行應用程式,並導覽至詳細資料畫面時,應該會看到每個字母的字詞清單。

清除(Cleaning Up)

兩個程式碼會執行意圖,並擷取選取 extra 名稱「letter」(字母) 的字母硬式編碼。雖然小型樣本可以使用這方法,但對於大型應用程式 (其中包含需持續追蹤的大量意圖額外資料) 就不是最佳做法。

雖然您可以只建立名為「letter」的常數,但應用程式加入較多意圖額外資訊後,就不適合使用常數;更何況常數要放置在哪個 class 也是個問題。請記得,字串會同時用於 DetailActivityMainActivity。您必須定義常數,才可以跨多個 class 使用,並維持程式碼井然有序。

值得慶幸的是,我們可透過一項實用的 Kotlin 功能來區隔常數,而且不必使用名為「companion object」 class 的特定 instance,一樣可以使用常數。Companion object 類似於 class instance 這種其他物件。但在程式執行期間,companion object 只會存在一個 instance,所以有時稱為「單例模式(singleton)」。除了本程式碼研究室的適用範圍外,單例模式(singleton)仍有許多用途,但目前您要使用 companion object 規劃常數,並使其可從 DetailActivity 外部存取。您可以開始使用 companion object,重構「letter」(字母) 額外資料的程式碼。

  1. onCreate 上方的 DetailActivity 中,新增以下內容:
1
2
3
companion object {

}
  • 請注意,這做法類似於定義 class,只是使用了 object 關鍵字。另外還有關鍵字 companion,表示該關鍵字與 DetailActivity class 相關聯,所以我們不必為其指定額外的類型名稱。
  1. 在大括號中加入字母常數的屬性。
1
const val LETTER = "letter"
  1. 若要使用新的常數,請更新 onCreate() 中呼叫的硬式編碼字母,如下所示:
1
val letterId = intent?.extras?.getString(LETTER).toString()

再次提醒您,常數通常會參考點標記法,但仍屬於 DetailActivity

  1. 切換至 LetterAdapter,並修改呼叫 putExtra,以使用新的常數。
1
intent.putExtra(DetailActivity.LETTER, holder.button.text.toString())

大功告成!重構後,您的程式碼會更容易閱讀和維護。如果您要變更程式碼,或新增的其他常數,只需在一個位置進行變更。

若要深入瞭解 companion object,請參閱物件運算式和宣告的 Kotlin 說明文件。


設定 Implicit Intent

在多數情況下,您的應用程式會顯示特定的活動(Activity)。但在部分情況下,您不會知道要啟動什麼活動(Activity)或應用程式(Application)。在我們的詳細資料畫面上,每個字詞按鈕會顯示使用者的字詞定義。

例如,使用 Google 搜尋提供的字典功能。您不是在應用程式中加入新活動,而是啟動裝置瀏覽器,顯示搜尋網頁。
所以您可能需要意圖(Intent)以在 Chrome (Android 預設的瀏覽器) 中載入資訊頁面嗎?

答錯了。
部分使用者可能慣用第三方瀏覽器,或手機隨附製造商預先安裝的瀏覽器。他們也許已安裝 Google 搜尋應用程式,或是第三方字典應用程式。

您無法得知使用者安裝哪些應用程式,也無法假定他們要查詢的字詞。這範例正適合使用隱含意圖(Implicit Intent)。您的應用程式會提供系統採用動作的資訊,然後系統會判斷處置動作的方式,並在必要時提示使用者其他資訊。

請按照下列步驟建立隱含意圖:

  1. 在這個應用程式中,您要執行 Google 搜尋字詞。第一個搜尋結果是字詞的字典定義。由於每次搜尋都會使用相同的基準網址,因此建議您將網址定義為常數。在 DetailActivity 中修改隨附物件(companion object),增加新的常數 SEARCH_PREFIX。這是 Google 搜尋的基準網址。
1
2
3
4
companion object {
const val LETTER = "letter"
const val SEARCH_PREFIX = "https://www.google.com/search?q="
}
  1. 接著,開啟 WordAdapter,然後在 onBindViewHolder() 方法中呼叫按鈕的 setOnClickListener()。開始建立搜尋查詢的 Uri。呼叫 parse() 以從 String 建立 Uri 時,您必須使用字串格式,才能將字詞附加至 SEARCH_PREFIX
1
2
3
holder.button.setOnClickListener {
val queryUrl: Uri = Uri.parse("${DetailActivity.SEARCH_PREFIX}${item}")
}

如果您想知道「URI」是什麼,URI 不是錯字,而是「統一資源識別項」。您可能已經知道網址,或「統一資源定位器」是指向網頁的字串。URI 是格式的一般用語。所有網址 (URL) 都是 URI,但不是所有 URI 都是網址。其他 URI (例如電話號碼位址) 會以 tel: 開頭,但系統會視為 URN 或統一資源名稱,而不是網址。用來代表兩者的資料類型稱為 URI

請注意,以下沒有任何與應用程式相關的活動。您只需提供 URI,但不知道最終用法。

  1. 定義 queryUrl 後,請將新的 intent 物件初始化:
1
val intent = Intent(Intent.ACTION_VIEW, queryUrl)

但不必傳入結構定義和活動,而是傳入 Intent.ACTION_VIEWURI

ACTION_VIEW 是通用意圖,可採用 URI,在本案例即是網址。接著,系統會在使用者的網路瀏覽器中,開啟 URI 處理意圖。其他意圖類型:

  • CATEGORY_APP_MAPS:啟動地圖應用程式
  • CATEGORY_APP_EMAIL:啟動電子郵件應用程式
  • CATEGORY_APP_GALLERY:啟動圖片庫 (相簿) 應用程式
  • ACTION_SET_ALARM:在背景設定鬧鐘
  • ACTION_DIAL:撥打電話

若要瞭解詳情,請參閱部分常用意圖的說明文件。

  1. 最後,即使您不在應用程式中啟動任何特定活動,但您仍會呼叫 startActivity() 並傳入 intent,指示系統啟動其他應用程式。
1
context.startActivity(intent)

現在當您啟動應用程式、前往字詞清單,並輕觸其中一個字詞時,您的裝置應會導覽至該網址 (或根據安裝的應用程式,顯示選項清單)。


設定 Menu 和 Icons

新增明確和隱含意圖,更方便瀏覽應用程式後,您可以新增選單選項,讓使用者可以在字母和清單與格線版面配置間切換。

您目前可能看到很多應用程式畫面的頂端,使用此選項列。這是應用程式列,除了顯示應用程式名稱外,應用程式列也可以自訂並代管許多實用的功能,例如實用動作的快速鍵或溢位選單。

在本應用程式中,我們不會新增完備的選單,您會瞭解如何在應用程式列中新增自訂按鈕,方便使用者變更版面配置。

  1. 首先,您必須匯入兩個圖示,代表格狀和清單檢視。新增名為「view module」(將其命名為 ic_grid_layout) 和「view list」(將其命名為 ic_linear_layout) 的 vector assets。如需複習新增 material icons 的操作方式,請參閱此資訊頁面的操作說明。
  1. 您必須設法告知系統應用程式列要顯示的選項,以及使用的圖示。方法是在「res」資料夾上按一下滑鼠右鍵,然後依序選取「New」>「Android Resource File」,藉此新增資源檔案。將「Resource Type」設為 Menu,並將「File Name」設為 layout_menu
  1. 按一下「OK」。

  2. 開啟「res/Menu/layout_menu」。以下列內容取代 layout_menu.xml 的內容:

1
2
3
4
5
6
7
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_switch_layout"
android:title="@string/action_switch_layout"
android:icon="@drawable/ic_linear_layout"
app:showAsAction="always" />
</menu>

此選單檔案的結構很簡單。就像版面配置會在開頭透過版面配置管理工具來保留個人檢視畫面,選單 XML 檔案會在開頭使用包含個別選項的選單標記。

您的選單只有一個按鈕,並包含一些屬性:

  • id:就像檢視畫面,選單選項在程式碼中也有可以參考的識別碼。
  • title:在本範例中其實不會顯示文字,但螢幕閱讀器可能使用文字識別選單
  • icon:預設為 ic_linear_layout。但選取按鈕後,系統會開啟或關閉按鈕,顯示網格圖示。
  • showAsAction:告訴系統如何顯示按鈕。由於它設定為 always,這個按鈕會始終在應用程式列中可見,並且不會成為溢出選單(overflow menu)的一部分。

您仍須在 MainActivity.kt 中新增一些程式碼,才能讓選單順利運作。


實作 Menu button

若要查看 menu button 實際的運作情形,您必須在 MainActivity.kt 中執行以下步驟。

  1. 首先,建議您建立屬性(property),追蹤應用程式所在的版面配置狀態,這樣做可讓您更輕鬆切換 layout 按鈕。將預設值設為 true,因為預設會使用 linear layout manager。
1
private var isLinearLayoutManager = true
  1. 當使用者切換按鈕時,您會希望 item 清單(item list)轉換成 item 的格狀清單(grid list)。不曉得您是否記得,我們在 recycler views 的課程中提到很多不同的版面配置管理工具,其中的 GridLayoutManager 可以在單一資料列顯示多個 item。
1
2
3
4
5
6
7
8
private fun chooseLayout() {
if (isLinearLayoutManager) {
recyclerView.layoutManager = LinearLayoutManager(this)
} else {
recyclerView.layoutManager = GridLayoutManager(this, 4)
}
recyclerView.adapter = LetterAdapter()
}
  • 在這個步驟中,您可以使用 if 陳述式指派 layout manager。除了設定 layoutManager 外,此程式碼也會指派 adapter。LetterAdapter 會用於 list 和 grid layout。
  1. 一開始在 xml 中設定 menu 時,您會指定靜態圖示。但切換 layout 後,為了反映新功能,建議您更新圖示,並切換回 list layout。在這個步驟中,您只要設定 liner 和 grid layout 圖示,而在下次輕觸按鈕後,按鈕會根據 layout 切換回圖示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private fun setIcon(menuItem: MenuItem?) {
if (menuItem == null)
return

// Set the drawable for the menu icon based on which LayoutManager is currently in use

// An if-clause can be used on the right side of an assignment if all paths return a value.
// The following code is equivalent to
// if (isLinearLayoutManager)
// menu.icon = ContextCompat.getDrawable(this, R.drawable.ic_grid_layout)
// else menu.icon = ContextCompat.getDrawable(this, R.drawable.ic_linear_layout)
menuItem.icon =
if (isLinearLayoutManager)
ContextCompat.getDrawable(this, R.drawable.ic_grid_layout)
else ContextCompat.getDrawable(this, R.drawable.ic_linear_layout)
}

根據 isLinearLayoutManager 屬性,系統會有條件地設定圖示。

若要應用程式順利使用 menu,您必須覆寫另外兩種方法。

  • onCreateOptionsMenu:這會加載 option menu,並執行其他設定
  • onOptionsItemSelected:選取按鈕後,這會實際呼叫 chooseLayout()
  1. 依照下列方式覆寫 onCreateOptionsMenu
1
2
3
4
5
6
7
8
9
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.layout_menu, menu)

val layoutButton = menu?.findItem(R.id.action_switch_layout)
// Calls code to set the icon based on the LinearLayoutManager of the RecyclerView
setIcon(layoutButton)

return true
}

以下方式並不難。加載 layout 後,請呼叫 setIcon(),確保圖示符合 layout。這個方法會傳回 Boolean,因為您要建立 option menu,所以會傳回 true

  1. 只要在 onOptionsItemSelected 加入幾行程式碼實作,如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_switch_layout -> {
// Sets isLinearLayoutManager (a Boolean) to the opposite value
isLinearLayoutManager = !isLinearLayoutManager
// Sets layout and icon
chooseLayout()
setIcon(item)

return true
}
// Otherwise, do nothing and use the core event handling

// when clauses require that all possible paths be accounted for explicitly,
// for instance both the true and false cases if the value is a Boolean,
// or an else to catch all unhandled cases.
else -> super.onOptionsItemSelected(item)
}
}

每次輕觸 menu item 都會呼叫這個程式碼,所以請務必檢查輕觸的 memu item。您會使用上述的 when 陳述式。如果 idaction_switch_layout memu item 相符,系統會取消 isLinearLayoutManager 的值。接著,請呼叫 chooseLayout()setIcon(),更新使用者介面。

此外,執行應用程式前,因為 chooseLayout() 中已設定 layout manager 和 adapter,您必須在 onCreate() 中更換程式碼,才能呼叫新方法。變更完成後,onCreate() 應該會如下所示。

1
2
3
4
5
6
7
8
9
10
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

recyclerView = binding.recyclerView
// Sets the LinearLayoutManager of the recyclerview
chooseLayout()
}

接著請執行應用程式,您可以使用 menu button,切換 list 和格狀檢視。


解決模擬器WIFI無網路

以MAC示範:

  1. 打開系統設定 > 網路 > 點選 WIFI 的詳細資料
  1. 切換到 DNS > 新增 8.8.8.8 和 8.8.4.4 並套用
  1. 最後重啟模擬器,即可看到 AndroidWifi 顯示已連線(Connected)

總結

  • 明確意圖(explicit intent)會用來前往應用程式特定的活動。
  • 隱含意圖(implicit intent)會對應特定動作 (例如開啟連結或共用圖片),並讓系統決定如何執行意圖。
  • 選單選項(Menu options)讓您可在應用程式列加入按鈕和選單。
  • 透過隨附物件(Companion objects),您可以將能重複使用的常數和類型建立關聯,而非與該類型的 instance 建立關聯。

若要執行意圖(intent):

  • 取得結構定義(context)的參考(reference)。
  • 建立 Intent 物件提供活動(activity)或 intent type (視 intent 為 explicit 或 implicit 而定)。
  • 呼叫 putExtra() 傳遞任何必要資料。
  • 呼叫 startActivity() 傳入 intent 物件。

參考資料

如何修復 Android 模擬器 Wi-Fi 連線無網際網路的問題
在 Mac 上輸入 DNS 和搜尋網域設定