1. 簡介
目前為止,您使用的應用程式只執行一項活動。但實際上很多 Android 應用程式需要執行多項活動,並透過導覽來切換應用程式。
在本程式碼研究室中,您要建構一個字典應用程式,讓應用程式使用多項活動,並使用意圖切換應用程式,同時傳遞資料至其他應用程式。
必要條件
您必須可以:
- 在 Android Studio 中瀏覽專案。
- 在 Android Studio 中使用並新增 XML 資源。
- 覆寫並實作現有類別的方法。
- 建立 Kotlin 類別的執行個體、存取屬性和呼叫方法。
- 請參閱 developer.android.com 的文件,深入瞭解特定類別。
課程內容
教學示範
- 使用明確意圖導覽至特定活動。
- 使用隱含意圖導覽至其他應用程式的內容。
- 新增選單選項,並新增按鈕至應用程式列。
建構項目
- 使用意圖並新增選項選單,修改字典應用程式,並實作切換畫面。
軟硬體需求
- 安裝 Android Studio 的電腦。
2. 範例程式碼
在後續步驟中,您要使用 Words 應用程式。Words 應用程式是簡單的字典應用程式,包含字母清單、每個字母的字詞,以及在瀏覽器中查詢個別字詞定義的功能。
步驟很多,但別擔心,您不必建構完整的應用程式,就能輕鬆瞭解意圖。而且我們提供完整版本的專案或範例專案。
實作所有畫面後,您會暫時無法切換畫面。您的工作是使用意圖,讓整個專案不必從頭開始建構所有項目,即可工作。
下載本程式碼研究室的範例程式碼
本程式碼研究室提供範例程式碼,可延伸至本程式碼研究室所教授的功能。範例程式碼可能包含先前介紹過的程式碼。也可能含有您不熟悉的程式碼,您可以在後續的程式碼研究室中學習。
請注意,GitHub 下載範例程式碼的資料夾名稱是 android-basics-kotlin-words-app-starter
。在 Android Studio 中開啟專案時,請選取這個資料夾。
如果您熟悉 Git 指令,請注意,範例程式碼位於名為「範例」的分支版本中。複製存放區後,請查看 origin/starter
分支版本中的程式碼。如果您不曾使用 Git 指令,請按照下列步驟,從 GitHub 下載程式碼。
- 前往專案所在的 GitHub 存放區頁面。
- 驗證分支版本名稱與程式碼研究室中指定的分支版本名稱相符。例如,在下列螢幕截圖中,分支版本名稱為「main」。
- 在專案的 GitHub 頁面中,按一下「Code」按鈕,畫面上會出現彈出式視窗。
- 在彈出式視窗中,按一下「Download ZIP」按鈕,將專案儲存至電腦。等待下載作業完成。
- 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
- 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。
在 Android Studio 中開啟專案
- 啟動 Android Studio。
- 在「Welcome to Android Studio」視窗中,按一下「Open」。
注意:如果 Android Studio 已開啟,請改為依序選取「File」>「Open」選單選項。
- 在檔案瀏覽器中,前往已解壓縮的專案資料夾所在的位置 (可能位於「Downloads」資料夾中)。
- 按兩下該專案資料夾。
- 等待 Android Studio 開啟專案。
- 按一下「Run」按鈕 即可建構並執行應用程式。請確認應用程式的建構符合預期。
3. Words 應用程式總覽
繼續操作前,請花點時間熟悉專案內容。您必須熟悉上一個單元的所有概念。應用程式目前有兩項活動,活動個別包含回收器檢視畫面和配接器。
具體而言,您會使用下列檔案:
MainActivity
的RecyclerView
會使用LetterAdapter
。每個字母都是包含onClickListener
的按鈕,這些按鈕目前沒有任何內容。您可以在這裡管理按鈕點按操作以導覽至DetailActivity
。DetailActivity
的RecyclerView
使用WordAdapter
,以顯示字詞清單。雖然您暫時無法前往此畫面,但請記得每個字詞都有對應的按鈕和onClickListener
。在這個步驟中,為了導覽至瀏覽器並顯示字詞的定義,您要加入程式碼。MainActivity
也需要進行一些變更。在這個步驟中,為了顯示按鈕,讓使用者切換清單和格線版面配置,您要實作選項選單。
熟悉專案目前的操作後,請繼續下一節瞭解意圖。
4. 意圖簡介
您已完成初始專案設定,接著我們要探討意圖,以及如何在應用程式中使用意圖。
意圖是物件,代表要執行的一些動作。我們最常看到意圖用來啟動活動 (當然意圖不只這個用途)。意圖分為兩種類型:隱含和明確。明確意圖極為精確,您確切知道要啟動的活動 (通常是應用程式的畫面)。
隱含意圖較抽象,系統收到動作的類型 (例如開啟連結、撰寫電子郵件或撥打電話),然後負責判斷如何完成要求。您可能已看過這兩種意圖,只是未察覺到。一般而言,在您的應用程式中顯示活動時,即是使用明確意圖。
但如果動作不涉及目前的應用程式 (例如您找到有趣的 Android 文件資訊頁面,並想分享給朋友),即使用隱含意圖。您可能會看到類似選單,詢問您要使用什麼應用程式分享資訊頁面。
您必須針對動作使用明確意圖,或在應用程式中分享螢幕畫面,並負責完成整個程序。您通常會使用隱含意圖執行涉及其他應用程式的動作,然後透過系統判斷最終結果。在 Word 應用程式中,您會使用這兩種意圖。
5. 設定明確意圖
現在可以實作第一個意圖了。在第一個畫面上,當使用者輕觸字母後,就會前往列有字詞清單的第二個畫面。因為已實作 DetailActivity
,所以只需使用意圖啟動此動作。因為應用程式已經知道要啟動特定的活動,您可以使用明確意圖。
建立並使用意圖只需幾個步驟:
- 開啟
LetterAdapter.kt
並向下捲動至onBindViewHolder()
。在這一行程式碼下,設定按鈕文字,並設定holder.button
的onClickListener
。
holder.button.setOnClickListener {
}
- 然後取得
context
的參考。
val context = holder.itemView.context
- 建立
Intent
,並傳入目的地活動的結構定義和類別名稱。
val intent = Intent(context, DetailActivity::class.java)
您要顯示的活動名稱指定為 DetailActivity::class.java
。實際的 DetailActivity
物件會在幕後建立。
- 呼叫
putExtra
方法,然後傳入「letter」做為第一個引數,按鈕文字則做為第二個引數。
intent.putExtra("letter", holder.button.text.toString())
額外資料是什麼?請記得,意圖只是一組操作說明,但目的地動作目前沒有意圖。而額外資料是一段資料,例如數字或字串,即之後擷取的指定名稱。這類似於呼叫函式時傳遞引數。因為 DetailActivity
可顯示任何字母,您必須指定顯示哪個字母。
此外,您認為為什麼需要呼叫 toString()
?按鈕的文字是字串,對吧?
可以這麼說。它其實是 CharSequence
類型,即所謂的介面。您目前不需瞭解 Kotlin 介面的任何資訊,只需知道介面是用於確定字串等類型,以及實作特定函式和屬性的方式。您可以將 CharSequence
聯想為類似字串類別的一般表示法。按鈕的 text
屬性可以是字串,或同時是 CharSequence
的任何物件。但 putExtra()
方法接受 String
,但不是任何 CharSequence
,所以必須呼叫 toString()
。
- 呼叫結構定義的
startActivity()
方法,並傳入intent
。
context.startActivity(intent)
接著執行應用程式,並嘗試輕觸字母。隨即顯示詳細資料畫面!但無論使用者輕觸哪個字母,詳細資料畫面會一律顯示字母 A 的字詞。在詳細資料動作中,一些工作仍有待完成,才會顯示傳遞字母的字詞,作為 intent
額外資料。
6. 設定 DetailActivity
您已建立第一個明確意圖!現在進入詳細資料畫面。
在 DetailActivity
的 onCreate
方法中,呼叫 setContentView
後,以程式碼取代硬式編碼字母,從 intent
取得傳入的 letterId
。
val letterId = intent?.extras?.getString("letter").toString()
這裡有很多要注意的事項,所以接著我們要查看每個項目:
首先,intent
屬性的來源為何?這不是 DetailActivity
的屬性,而是任何活動的屬性。此屬性會持續參考啟動活動使用的意圖。
額外屬性是 Bundle
類型,或許您已猜到,該屬性提供方法來存取傳入意圖的所有額外屬性。
這兩個屬性會以問號標示。原因是什麼呢?原因是 intent
和 extras
屬性可為空值,換句話說,您可以使用值,也可以不使用值。有時,您可能想要變數為 null
。intent
屬性可能不是 Intent
(如果活動不是從意圖啟動),此外,額外的屬性可能不是 Bundle
,而是名為 null
的值。在 Kotlin 中,null
代表沒有值。物件可能存在,或可能是 null
。如果您的應用程式嘗試在 null
物件上存取屬性或呼叫函式,該應用程式就會異常終止。若要安全存取這個值,您必須在名稱後方加上 ?
。如果 intent
是 null
,應用程式不會嘗試存取額外的屬性,此外,如果 extras
為空值,程式碼也不會嘗試呼叫 getString()
。
如何知道哪些屬性需要問號才能確保空值的安全性?您可以透過類型名稱後方是否有問號或驚嘆號來判斷。
最後請注意,使用 getString
擷取實際字母會傳回 String?
,所以呼叫 toString()
可確保它是 String
,而不是 null
。
您現在執行應用程式,並導覽至詳細資料畫面時,應該會看到每個字母的字詞清單。
清除
兩個程式碼會執行意圖,並擷取選取 extra
名稱「letter」(字母) 的字母硬式編碼。雖然小型樣本可以使用這方法,但對於大型應用程式 (其中包含需持續追蹤的大量意圖額外資料) 就不是最佳做法。
雖然您可以只建立名為「letter」的常數,但應用程式加入較多意圖額外資訊後,就不適合使用常數;更何況常數要放置在哪個類別也是個問題。請記得,字串會同時用於 DetailActivity
和 MainActivity
。您必須定義常數,才可以跨多個類別使用,並維持程式碼井然有序。
值得慶幸的是,我們可透過一項實用的 Kotlin 功能來區隔常數,而且不必使用名為「隨附物件」類別的特定例項,一樣可以使用常數。隨附物件類似於類別例項這種其他物件。但在程式執行期間,隨附物件只會存在一個例項,所以有時稱為「單例模式」。除了本程式碼研究室的適用範圍外,單例模式仍有許多用途,但目前您要使用隨附物件規劃常數,並使其可從 DetailActivity
外部存取。您可以開始使用隨附物件,重構「letter」(字母) 額外資料的程式碼。
- 在
onCreate
上方的DetailActivity
中,新增以下內容:
companion object {
}
請注意,這做法類似於定義類別,只是使用了 object
關鍵字。另外還有關鍵字 companion
,表示該關鍵字與 DetailActivity
類別相關聯,所以我們不必為其指定額外的類型名稱。
- 在大括號中加入字母常數的屬性。
const val LETTER = "letter"
- 若要使用新的常數,請更新
onCreate()
中呼叫的硬式編碼字母,如下所示:
val letterId = intent?.extras?.getString(LETTER).toString()
再次提醒您,常數通常會參考點標記法,但仍屬於 DetailActivity
。
- 切換至
LetterAdapter
,並修改呼叫putExtra
,以使用新的常數。
intent.putExtra(DetailActivity.LETTER, holder.button.text.toString())
大功告成!重構後,您的程式碼會更容易閱讀和維護。如果您要變更程式碼,或新增的其他常數,只需在一個位置進行變更。
若要深入瞭解隨附物件,請參閱物件運算式和宣告的 Kotlin 說明文件。
7. 設定隱含意圖
在多數情況下,您的應用程式會顯示特定的活動。但在部分情況下,您不會知道要啟動什麼活動或應用程式。在我們的詳細資料畫面上,每個字詞按鈕會顯示使用者的字詞定義。
例如,使用 Google 搜尋提供的字典功能。您不是在應用程式中加入新活動,而是啟動裝置瀏覽器,顯示搜尋網頁。
所以您可能需要意圖以在 Chrome (Android 預設的瀏覽器) 中載入資訊頁面嗎?
答錯了。
部分使用者可能慣用第三方瀏覽器,或手機隨附製造商預先安裝的瀏覽器。他們也許已安裝 Google 搜尋應用程式,或是第三方字典應用程式。
您無法得知使用者安裝哪些應用程式,也無法假定他們要查詢的字詞。這範例正適合使用隱含意圖。您的應用程式會提供系統採用動作的資訊,然後系統會判斷處置動作的方式,並在必要時提示使用者其他資訊。
請按照下列步驟建立隱含意圖:
- 在這個應用程式中,您要執行 Google 搜尋字詞。第一個搜尋結果是字詞的字典定義。由於每次搜尋都會使用相同的基準網址,因此建議您將網址定義為常數。在
DetailActivity
中修改隨附物件,增加新的常數SEARCH_PREFIX
。這是 Google 搜尋的基準網址。
companion object {
const val LETTER = "letter"
const val SEARCH_PREFIX = "https://www.google.com/search?q="
}
- 接著,開啟
WordAdapter
,然後在onBindViewHolder()
方法中呼叫按鈕的setOnClickListener()
。開始建立搜尋查詢的Uri
。呼叫parse()
以從String
建立Uri
時,您必須使用字串格式,才能將字詞附加至SEARCH_PREFIX
。
holder.button.setOnClickListener {
val queryUrl: Uri = Uri.parse("${DetailActivity.SEARCH_PREFIX}${item}")
}
如果您想知道「URI」是什麼,URI 不是錯字,而是「統一資源識別項」。您可能已經知道網址,或「統一資源定位器」是指向網頁的字串。URI 是格式的一般用語。所有網址 (URL) 都是 URI,但不是所有 URI 都是網址。其他 URI (例如電話號碼位址) 會以 tel:
開頭,但系統會視為 URN 或統一資源名稱,而不是網址。用來代表兩者的資料類型稱為 URI
。
請注意,以下沒有任何與應用程式相關的活動。您只需提供 URI
,但不知道最終用法。
- 定義
queryUrl
後,請將新的intent
物件初始化:
val intent = Intent(Intent.ACTION_VIEW, queryUrl)
但不必傳入結構定義和活動,而是傳入 Intent.ACTION_VIEW
和 URI
。
ACTION_VIEW
是通用意圖,可採用 URI,在本案例即是網址。接著,系統會在使用者的網路瀏覽器中,開啟 URI 處理意圖。其他意圖類型:
CATEGORY_APP_MAPS
:啟動地圖應用程式CATEGORY_APP_EMAIL
:啟動電子郵件應用程式CATEGORY_APP_GALLERY
:啟動圖片庫 (相簿) 應用程式ACTION_SET_ALARM
:在背景設定鬧鐘ACTION_DIAL
:撥打電話
若要瞭解詳情,請參閱部分常用意圖的說明文件。
- 最後,即使您不在應用程式中啟動任何特定活動,但您仍會呼叫
startActivity()
並傳入intent
,指示系統啟動其他應用程式。
context.startActivity(intent)
現在當您啟動應用程式、前往字詞清單,並輕觸其中一個字詞時,您的裝置應會導覽至該網址 (或根據安裝的應用程式,顯示選項清單)。
確切的行為會因使用者而有所差異,但會提供順暢的使用者體驗,而無需使用複雜的程式碼。
8. 設定選單和圖示
新增明確和隱含意圖,更方便瀏覽應用程式後,您可以新增選單選項,讓使用者可以在字母和清單與格線版面配置間切換。
您目前可能看到很多應用程式畫面的頂端,使用此選項列。這是應用程式列,除了顯示應用程式名稱外,應用程式列也可以自訂並代管許多實用的功能,例如實用動作的快速鍵或溢位選單。
在本應用程式中,我們不會新增完備的選單,您會瞭解如何在應用程式列中新增自訂按鈕,方便使用者變更版面配置。
- 首先,您必須匯入兩個圖示,代表格狀和清單檢視。新增名為「檢視模組」(將其命名為 ic_grid_layout) 和「檢視清單」(將其命名為 ic_linear_layout) 的插圖向量素材資源。如需複習新增質感設計圖示的操作方式,請參閱此資訊頁面的操作說明。
- 您必須設法告知系統應用程式列要顯示的選項,以及使用的圖示。方法是在「res」資料夾上按一下滑鼠右鍵,然後依序選取「New」>「Android Resource File」,藉此新增資源檔案。將「Resource Type」設為
Menu
,並將「File Name」設為layout_menu
。
- 按一下「OK」。
- 開啟「res/Menu/layout_menu」。以下列內容取代
layout_menu.xml
的內容:
<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
:告訴系統如何顯示按鈕。設為「總是」時,這個按鈕會總是顯示在應用程式列中,且不會併入溢位選單。
當然,設定屬性並不代表選單會實際執行任何動作。
您仍須在 MainActivity.kt
中新增一些程式碼,才能讓選單順利運作。
9. 實作選單按鈕
若要查看選單按鈕實際的運作情形,您必須在 MainActivity.kt
中執行以下步驟。
- 首先,建議您建立屬性,追蹤應用程式所在的版面配置狀態,這樣做可讓您更輕鬆切換版面配置按鈕。將預設值設為
true
,因為預設會使用線性版面配置管理員。
private var isLinearLayoutManager = true
- 當使用者切換按鈕時,您會希望項目清單轉換成項目的格狀清單。不曉得您是否記得,我們在回收器檢視畫面的課程中提到很多不同的版面配置管理工具,其中的
GridLayoutManager
可以在單一資料列顯示多個項目。
private fun chooseLayout() {
if (isLinearLayoutManager) {
recyclerView.layoutManager = LinearLayoutManager(this)
} else {
recyclerView.layoutManager = GridLayoutManager(this, 4)
}
recyclerView.adapter = LetterAdapter()
}
在這個步驟中,您可以使用 if
陳述式指派版面配置管理員。除了設定 layoutManager
外,此程式碼也會指派轉換器。LetterAdapter
會用於清單和格線版面配置。
- 一開始在 xml 中設定選單時,您會指定靜態圖示。但切換版面配置後,為了反映新功能,建議您更新圖示,並切換回清單版面配置。在這個步驟中,您只要設定線性和格線版面配置圖示,而在下次輕觸按鈕後,按鈕會根據版面配置切換回圖示。
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
屬性,系統會有條件地設定圖示。
若要應用程式順利使用選單,您必須覆寫另外兩種方法。
onCreateOptionsMenu
:這會加載選項選單,並執行其他設定onOptionsItemSelected
:選取按鈕後,這會實際呼叫chooseLayout()
。
- 依照下列方式覆寫
onCreateOptionsMenu
:
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
}
以下方式並不難。加載版面配置後,請呼叫 setIcon()
,確保圖示符合版面配置。這個方法會傳回 Boolean
,因為您要建立選項選單,所以會傳回 true
。
- 只要在
onOptionsItemSelected
加入幾行程式碼實作,如下所示。
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)
}
}
每次輕觸選單項目都會呼叫這個程式碼,所以請務必檢查輕觸的選單項目。您會使用上述的 when
陳述式。如果 id
與 action_switch_layout
選單項目相符,系統會取消 isLinearLayoutManager
的值。接著,請呼叫 chooseLayout()
和 setIcon()
,更新使用者介面。
此外,執行應用程式前,因為 chooseLayout()
中已設定版面配置管理員和轉換器,您必須在 onCreate()
中更換程式碼,才能呼叫新方法。變更完成後,onCreate()
應該會如下所示。
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()
}
接著請執行應用程式,您可以使用選單按鈕,切換清單和格狀檢視。
10. 解決方案程式碼
本程式碼研究室的解決方案程式碼位於下方的專案中:
- 前往專案所在的 GitHub 存放區頁面。
- 驗證分支版本名稱與程式碼研究室中指定的分支版本名稱相符。例如,在下列螢幕截圖中,分支版本名稱為「main」。
- 在專案的 GitHub 頁面中,按一下「Code」按鈕,畫面上會出現彈出式視窗。
- 在彈出式視窗中,按一下「Download ZIP」按鈕,將專案儲存至電腦。等待下載作業完成。
- 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
- 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。
在 Android Studio 中開啟專案
- 啟動 Android Studio。
- 在「Welcome to Android Studio」視窗中,按一下「Open」。
注意:如果 Android Studio 已開啟,請改為依序選取「File」>「Open」選單選項。
- 在檔案瀏覽器中,前往已解壓縮的專案資料夾所在的位置 (可能位於「Downloads」資料夾中)。
- 按兩下該專案資料夾。
- 等待 Android Studio 開啟專案。
- 按一下「Run」按鈕 ,即可建構並執行應用程式。請確認應用程式的建構作業符合預期。
11. 摘要
- 明確意圖會用來前往應用程式特定的活動。
- 隱含意圖會對應特定動作 (例如開啟連結或共用圖片),並讓系統決定如何執行意圖。
- 選單選項讓您可在應用程式列加入按鈕和選單。
- 透過隨附物件,您可以將能重複使用的常數和類型建立關聯,而非與該類型的例項建立關聯。
若要執行意圖:
- 取得結構定義的參考。
- 建立
Intent
物件提供活動或意圖類型 (視意圖為明確或隱含而定)。 - 呼叫
putExtra()
傳遞任何必要資料。 - 呼叫
startActivity()
傳入intent
物件。