導覽與返回堆疊

1. 事前準備

在先前的程式碼研究室中,您已開始實作 Cupcake 應用程式,現將在本程式碼研究室中完成其餘步驟。Cupcake 應用程式有多個畫面,且會顯示杯子蛋糕的訂購流程。完成的應用程式必須讓使用者能夠瀏覽應用程式,以執行下列操作:

  • 建立杯子蛋糕訂單
  • 使用「向上」或「返回」按鈕前往訂購流程的上一個步驟
  • 取消訂單
  • 將訂單傳送至其他應用程式 (例如電子郵件應用程式)

過程中,您將會瞭解 Android 如何處理應用程式的工作和返回堆疊。這可讓您操控各種情況下的返回堆疊 (例如取消訂單),讓使用者返回應用程式的第一個畫面,而非訂購流程的前一個畫面。

必要條件

  • 可在活動的不同片段中建立和使用共用檢視模型
  • 熟悉使用 Jetpack 導覽元件
  • 已將資料繫結與 LiveData 搭配使用,讓 UI 與檢視模式模型保持同步
  • 可建立意圖,以開始新活動

課程內容

  • 導覽對於應用程式返回堆疊的影響
  • 如何實作自訂返回堆疊行為

建構項目

  • 杯子蛋糕訂購應用程式可讓使用者將訂單傳送到其他應用程式,且可取消訂單

需求條件

  • 已安裝 Android Studio 的電腦。
  • 完成先前程式碼研究室所獲得的 Cupcake 應用程式程式碼

2. 範例應用程式總覽

本程式碼研究室使用先前程式碼研究室的 Cupcake 應用程式。您可以使用完成先前程式碼研究室所獲得的程式碼,或從 GitHub 下載範例程式碼。

下載本程式碼研究室的範例程式碼

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

如要取得本程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作。

取得程式碼

  1. 按一下上面顯示的網址。系統會在瀏覽器中開啟專案的 GitHub 頁面。
  2. 在專案的 GitHub 頁面中,按一下「Code」按鈕開啟對話方塊。

5b0a76c50478a73f.png

  1. 在對話方塊中,按一下「Download ZIP」按鈕,將專案儲存到電腦。等待下載作業完成。
  2. 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
  3. 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。

在 Android Studio 中開啟專案

  1. 啟動 Android Studio。
  2. 在「Welcome to Android Studio」視窗中,按一下「Open an existing Android Studio project」

36cc44fcf0f89a1d.png

注意:如果 Android Studio 已開啟,請依序選取「File」>「New」>「Import Project」選單選項。

21f3eec988dcfbe9.png

  1. 在「Import Project」對話方塊中,前往解壓縮專案資料夾所在的位置 (可能位於「下載」資料夾中)。
  2. 按兩下該專案資料夾。
  3. 等待 Android Studio 開啟專案。
  4. 按一下「Run」按鈕 11c34fc5e516fb1c.png 即可建構並執行應用程式。請確認應用程式的建構符合預期。
  5. 在「Project」工具視窗中瀏覽專案檔案,查看應用程式的設定方式。

接著,執行應用程式,如下所示。

45844688c0dc69a2.png

本程式碼研究室會引導您先在應用程式中實作「Up」按鈕,讓使用者可藉由輕觸該按鈕回到訂購流程的上一步。

fbdc1793f9fea6da.png

接著則會新增「Cancel」按鈕,讓改變心意的使用者能夠在訂購過程中取消訂單。

d66fdafeac1b0dcf.gif

您將會擴充應用程式,以便在輕觸「Send Order to Another App」(傳送訂單至其他應用程式) 時與其他應用程式分享訂單。接著,即可透過電子郵件等方式將訂單傳送至杯子蛋糕店。

170d76b64ce78f56.png

讓我們深入探索並完成 Cupcake 應用程式吧!

3. 實作向上按鈕行為

Cupcake 應用程式中,應用程式列會顯示可返回上一個畫面的箭頭,此為「Up」按鈕,您在先前的程式碼研究室中曾學過。「向上」按鈕目前沒有任何作用,因此請先在應用程式中修正此導覽錯誤。

fbdc1793f9fea6da.png

  1. 您的 MainActivity 中應該已有使用導覽控制器設定應用程式列 (也稱為動作列) 的程式碼。將 navController 設為類別變數,以利於其他方法中使用。
class MainActivity : AppCompatActivity(R.layout.activity_main) {

    private lateinit var navController: NavController

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val navHostFragment = supportFragmentManager
                .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        navController = navHostFragment.navController

        setupActionBarWithNavController(navController)
    }
}
  1. 在同一個類別中,新增程式碼來覆寫 onSupportNavigateUp() 函式。此程式碼會要求 navController 處理應用程式的向上導覽。否則,請返回至處理「Up」按鈕的父類別實作 (AppCompatActivity 中)。
override fun onSupportNavigateUp(): Boolean {
   return navController.navigateUp() || super.onSupportNavigateUp()
}
  1. 執行應用程式。「Up」按鈕現在應可在 FlavorFragmentPickupFragmentSummaryFragment 中運作。前往訂購流程中的上一個步驟時,片段應透過檢視模型顯示正確的口味和自取日期。

4. 瞭解工作和返回堆疊

現在,請在應用程式的訂購流程中加入「Cancel」按鈕。在訂購過程中的任何時間點取消訂單,會讓使用者返回 StartFragment若要處理此行為,您需瞭解 Android 中的工作和返回堆疊。

工作

Android 的活動存在於工作中。首次從啟動器圖示開啟應用程式時,Android 會建立一個包含主要活動的新工作。「工作」是使用者執行特定工作 (例如查看電子郵件、建立杯子蛋糕訂單、拍照) 時,可進行互動的一系列活動。

系統會以返回堆疊的排列方式來顯示活動,此方式會將使用者造訪的新活動推送至工作的返回堆疊上。您可以將上述過程看成鬆餅的堆疊,每一份新的鬆餅皆會添加於堆疊頂端。堆疊頂端的活動是指使用者目前正在互動的活動。堆疊中,位於該活動下方的活動已移至背景且停止。

517054e483795b46.png

使用者想要往回瀏覽時,返回堆疊功能就能派上用場。Android 可以從堆疊頂端移除目前活動、將其刪除,並重新啟動其下方的活動。這稱為從堆疊中移除活動,並將先前的活動移至前景,以便使用者進行互動。如果使用者想要反覆返回查看,Android 會持續將活動從堆疊頂端移除,直到接近堆疊底部為止。如果返回堆疊中沒有任何活動,系統會將使用者導回至裝置的啟動器畫面,或至啟動此應用程式的應用程式。

讓我們看看您透過以下 2 個活動實作的 Words 應用程式版本:MainActivityDetailActivity

初次啟動應用程式時,MainActivity 會開啟,並新增至工作的返回堆疊中。

4bc8f5aff4d5ee7f.png

只要按一下字母,DetailActivity 就會啟動,然後推送到返回堆疊上。這表示已建立、啟動並重新啟用 DetailActivity,因此使用者可與其互動。系統會將 MainActivity 置於背景,並以灰色的背景顏色顯示於圖表中。

80f7c594ae844b84.png

若輕觸「返回」按鈕,系統就會從返回堆疊中彈出 DetailActivity,並刪除及結束 DetailActivity 例項。

80f532af817191a4.png

接著,返回堆疊頂端的下一個項目 (MainActivity) 就會移至前景。

85004712d2fbcdc1.png

如同返回堆疊可追蹤使用者已開啟的活動,只要與 Jetpack Navigation 元件搭配運作,返回堆疊也可透過相同方式追蹤使用者造訪過的片段目的地。

fe417ac5cbca4ce7.png

只要使用 Navigation 程式庫,即可在使用者每次點選「返回」按鈕時,從返回堆疊中彈出片段目的地。此預設行為無需實作任何設定。如果您需要自訂返回堆疊行為,才需要編寫程式碼,您將為 Cupcake 應用程式進行此操作。

Cupcake 應用程式的預設行為

讓我們看看返回堆疊在 Cupcake 應用程式中的運作方式。應用程式中只有一個活動,但使用者會瀏覽多個片段目的地。因此,「Back」按鈕在使用者輕觸時才返回上一個片段目的地較為理想。

初次開啟應用程式時,系統會顯示 StartFragment 目的地。該目的地會推送至堆疊頂端。

cf0e80b4907d80dd.png

選取要訂購的杯子蛋糕數量後,將會前往 FlavorFragment,這個目的地會推送至返回堆疊上。

39081dcc3e537e1e.png

當您選取口味並輕觸「Next」後,將會前往 PickupFragment,並推送至返回堆疊上。

37dca487200f8f73.png

最後,選取自取日期並輕觸「Next」後,將會前往 SummaryFragment,這個目的地會新增至返回堆疊頂端。

d67689affdfae0dd.png

如果您從 SummaryFragment 中輕觸「返回」或「向上」按鈕,SummaryFragment 會從堆疊中移除並刪除。

215b93fd65754017.png

PickupFragment 現在位於返回堆疊頂端,並向使用者顯示。

37dca487200f8f73.png

再次輕觸「返回」或「向上」按鈕,將會從堆疊中移除 PickupFragment,接著顯示 FlavorFragment

再次輕觸「Back」或「Up」按鈕,將會從堆疊中移除 FlavorFragment,接著顯示 StartFragment

當您在訂購流程中返回到上一個步驟時,一次只會移除一個目的地。但在下一個工作中,您會在應用程式中新增取消訂單功能。為此,您必須一次移除返回堆疊中的多個目的地,讓使用者返回 StartFragment 建立新訂單。

e3dae0f492450207.png

修改 Cupcake 應用程式中的返回堆疊

修改 FlavorFragmentPickupFragmentSummaryFragment 類別和版面配置檔案,為使用者提供取消訂單按鈕。

新增導覽動作

請先在應用程式的導覽圖中加入導覽動作,讓使用者能從後續目的地返回 StartFragment

  1. 前往「res」>「navigation」>「nav_graph.xml」檔案,然後選取「Design」檢視畫面,以開啟導覽編輯器
  2. 目前有 startFragmentflavorFragment 的動作、flavorFragmentpickupFragment 的動作,以及 pickupFragmentsummaryFragment 的動作。
  3. 按住並拖曳即可建立從 summaryFragmentstartFragment 的新導覽動作。如想複習如何在導覽圖中連結目的地,請參閱這些操作說明
  4. 按住並拖曳 pickupFragment 即可建立至 startFragment 的新動作。
  5. 按住並拖曳 flavorFragment 即可建立至 startFragment 的新動作。
  6. 完成後,導覽圖應如下所示。

dcbd27a08d24cfa0.png

進行上述變更後,使用者即可從訂購流程中較後方的某個片段返回至訂購流程的起始處。現在,您需要實際使用這些動作進行瀏覽的程式碼。輕觸「Cancel」按鈕處即為適當位置。

在版面配置中新增「Cancel」按鈕

首先,請在所有片段 (StartFragment 除外) 的版面配置檔案中新增「Cancel」按鈕。如果您已位於訂購流程的第一個畫面,便無需取消訂單。

  1. 開啟 fragment_flavor.xml 版面配置檔案。
  2. 您可以使用「Split」檢視畫面直接編輯 XML,且並排檢視預覽畫面。
  3. 在小計文字檢視區塊和「Next」按鈕之間新增「Cancel」按鈕。為其指派資源 ID @+id/cancel_button,並以文字顯示 @string/cancel

該按鈕應與「繼續」按鈕平行放置,以一列按鈕的形式呈現。針對垂直限制,請將「Cancel」按鈕頂部限制與「Next」按鈕頂部同高。針對水平限制,請將「Cancel」按鈕的起始處限制於父項容器中,並將其結尾處限制於「Next」按鈕的起始處。

此外,請將「Cancel」按鈕的高度設為 wrap_content,寬度設為 0dp,以便將螢幕寬度平均分配給另一個按鈕。請注意,進入下一個步驟前,「Preview」窗格不會顯示此按鈕。

...

<TextView
    android:id="@+id/subtotal" ... />

<Button
    android:id="@+id/cancel_button"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="@string/cancel"
    app:layout_constraintEnd_toStartOf="@id/next_button"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="@id/next_button" />

<Button
    android:id="@+id/next_button" ... />

...
  1. fragment_flavor.xml 中,您也需將「Next」按鈕的起始限制從 app:layout_constraintStart_toStartOf="parent" 變更為 app:layout_constraintStart_toEndOf="@id/cancel_button"此外,在「Cancel」按鈕上新增結束邊界,讓兩個按鈕之間留有空白。現在,Android Studio 的「Preview」窗格中應會顯示「Cancel」按鈕。
...

<Button
    android:id="@+id/cancel_button"
    android:layout_marginEnd="@dimen/side_margin" ... />

<Button
    android:id="@+id/next_button"
    app:layout_constraintStart_toEndOf="@id/cancel_button"... />

...
  1. 在視覺樣式方面,請使用 Material Outlined Button 樣式 (含屬性 style="?attr/materialButtonOutlinedStyle"),使「Cancel」按鈕不會過於醒目,因「Next」按鈕是您希望使用者專注的主要動作。
<Button
    android:id="@+id/cancel_button"
    style="?attr/materialButtonOutlinedStyle" ... />

按鈕和位置現在看起來十分完美!

1fb41763cc255c05.png

  1. 以同樣的方式,在 fragment_pickup.xml 版面配置檔案中新增「Cancel」按鈕。
...

<TextView
    android:id="@+id/subtotal" ... />

<Button
    android:id="@+id/cancel_button"
    style="?attr/materialButtonOutlinedStyle"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginEnd="@dimen/side_margin"
    android:text="@string/cancel"
    app:layout_constraintEnd_toStartOf="@id/next_button"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="@id/next_button" />

<Button
    android:id="@+id/next_button" ... />

...
  1. 請一併更新「Next」按鈕的起始限制。接著,預覽畫面中會顯示「Cancel」按鈕。
<Button
    android:id="@+id/next_button"
    app:layout_constraintStart_toEndOf="@id/cancel_button" ... />
  1. fragment_summary.xml 檔案套用類似的變更,但這個片段的版面配置稍有不同。您將在父項垂直容器 LinearLayout 中的「傳送」按鈕下方新增「取消」按鈕,並在兩個按鈕之間保留一定間距。

741c0f034397795c.png

...

    <Button
        android:id="@+id/send_button" ... />

    <Button
        android:id="@+id/cancel_button"
        style="?attr/materialButtonOutlinedStyle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/margin_between_elements"
        android:text="@string/cancel" />

</LinearLayout>
  1. 執行並測試應用程式。現在,FlavorFragmentPickupFragmentSummaryFragment 的版面配置中應該會顯示「Cancel」按鈕。不過,輕觸該按鈕目前並不會執行任何動作。請在下一個步驟中為這些按鈕設定點選監聽器。

新增「Cancel」按鈕的點擊事件監聽器

在每個片段類別 (StartFragment 除外) 中新增 Helper 方法,以便在使用者點選「Cancel」按鈕時進行處理。

  1. 將這個 cancelOrder() 方法新增至 FlavorFragment。如果使用者在看到口味選項時決定取消訂單,請呼叫 sharedViewModel.resetOrder(). 清除檢視模型。接著,使用 ID 為 R.id.action_flavorFragment_to_startFragment. 的導覽動作返回 StartFragment
fun cancelOrder() {
    sharedViewModel.resetOrder()
    findNavController().navigate(R.id.action_flavorFragment_to_startFragment)
}

如果您看到與動作資源 ID 相關的錯誤,可能需要返回 nav_graph.xml 檔案,確認您的導覽動作也命名為相同名稱 (action_flavorFragment_to_startFragment)。

  1. 如要在 fragment_flavor.xml 版面配置的「Cancel」按鈕上設定點選監聽器,請使用事件監聽器繫結。點選此按鈕可叫用您剛才在 FragmentFlavor 類別中建立的 cancelOrder() 方法。
<Button
    android:id="@+id/cancel_button"
    android:onClick="@{() -> flavorFragment.cancelOrder()}" ... />
  1. 針對 PickupFragment 重複執行相同的程序。在片段類別中新增 cancelOrder() 方法,藉此重設訂單,並從 PickupFragment 瀏覽至 StartFragment
fun cancelOrder() {
    sharedViewModel.resetOrder()
    findNavController().navigate(R.id.action_pickupFragment_to_startFragment)
}
  1. fragment_pickup.xml 中,於「Cancel」按鈕上設定點選監聽器,以便在使用者點選時呼叫 cancelOrder() 方法。
<Button
    android:id="@+id/cancel_button"
    android:onClick="@{() -> pickupFragment.cancelOrder()}" ... />
  1. SummaryFragment 中的「取消」按鈕新增類似的程式碼,讓使用者可以返回 StartFragment如果 androidx.navigation.fragment.findNavController 未自動匯入,您可能需要自行匯入.
fun cancelOrder() {
    sharedViewModel.resetOrder()
    findNavController().navigate(R.id.action_summaryFragment_to_startFragment)
}
  1. fragment_summary.xml 中按下「取消」按鈕時,將隨即呼叫 SummaryFragmentcancelOrder() 方法。
<Button
    android:id="@+id/cancel_button"
    android:onClick="@{() -> summaryFragment.cancelOrder()}" ... />
  1. 執行並測試應用程式,確認您剛才新增至每個片段的邏輯。建立杯子蛋糕訂單時,輕觸 FlavorFragmentPickupFragmentSummaryFragment 上的「取消」按鈕,即可返回 StartFragment繼續建立新訂單時,請注意系統已清除先前訂單中的資訊。

成效看起來不錯,但返回 StartFragment 後,向後瀏覽實際上有錯誤。請按照下列步驟重現錯誤。

  1. 按照訂購流程建立新的杯子蛋糕訂單,直到到達摘要畫面為止。舉例來說,您可以訂購 12 個巧克力口味的杯子蛋糕,並選擇未來的自取日期。
  2. 接著,輕觸「Cancel」。您應該會返回 StartFragment
  3. 這看起來沒問題,但如果您輕觸系統自帶的「返回」按鈕,就會回到訂單摘要畫面,其中將顯示訂單摘要:訂購 0 個杯子蛋糕,未選擇任何口味。這是錯誤現象,不應向使用者顯示。

1a9024cd58a0e643.png

使用者可能不想回到訂購流程。此外,檢視模型中的所有訂單資料均已清除,因此這項資訊不實用。反之,輕觸 StartFragment 中的「Back」按鈕,應離開 Cupcake 應用程式。

以下我們將介紹返回堆疊目前的情況,以及修正錯誤的方法。透過訂單摘要畫面建立訂單時,每個目的地都會推送至返回堆疊上。

fc88100cdf1bdd1.png

您在 SummaryFragment 取消了訂單。當您使用 SummaryFragmentStartFragment 的動作進行瀏覽時,Android 會新增另一個 StartFragment 執行個體,做為返回堆疊上的新目的地。

5616cb0028b63602.png

因此,當您輕觸 StartFragment 中的「返回」按鈕時,應用程式最終會重新顯示 SummaryFragment (包含空白訂單資訊)。

如要修正這個導覽錯誤,請瞭解導覽元件如何讓使用者在使用動作進行瀏覽時,從返回堆疊中移除其他目的地。

從返回堆疊中移除其他目的地

在導覽圖的導覽動作中加入 app:popUpTo 屬性後,即可從返回堆疊中移除多個目的地,直到到達指定目的地為止。如果指定 app:popUpTo="@id/startFragment",則會移除返回堆疊中的目的地,直到到達 StartFragment 為止,此片段會保留在堆疊中。

將此變更新增至程式碼並執行應用程式時,您將會發現只要取消訂單,就會回到 StartFragment。但這次,當您輕觸 StartFragment 的「Back」按鈕時,會再次看到 StartFragment (而不是結束應用程式)。這也不是預期出現的行為。如先前所述,由於您正在前往 StartFragment,Android 實際上會在返回堆疊中新增 StartFragment 做為新目的地,因此現在返回堆疊會有 2 個 StartFragment 執行個體。因此,您必須輕觸「返回」按鈕兩次才能結束應用程式。

dd0fedc6e231e595.png

為修正這個新錯誤,請要求將所有目的地從返回堆疊中移除,直到 (且包含) StartFragment 為止。請在適當的導覽動作指定 app:popUpTo="@id/startFragment"

app:popUpToInclusive="true",以達到此目標。如此一來,返回堆疊中就只會有一個新的 StartFragment 執行個體。接著輕觸 StartFragment 中的「Back」按鈕,結束應用程式。我們現在要進行這項變更。

cf0e80b4907d80dd.png

修改導覽動作

  1. 開啟「res」>「navigation」>「nav_graph.xml」檔案,前往導覽編輯器
  2. 選取從 summaryFragmentstartFragment 的動作,使其以藍色醒目顯示。
  3. 展開右側的「Attributes」(如果尚未開啟)。在可修改的屬性清單中尋找「Pop Behavior」。

8c87589f9cc4d176.png

  1. 透過下拉式選單的選項,將 popUpTo 設為 startFragment。這表示返回堆疊中的所有目的地皆會移除 (從堆疊頂端開始往下移除),直到 startFragment 為止。

a9a17493ed6bc27f.png

  1. 接著按一下「popUpToInclusive」核取方塊,直到畫面上顯示勾號和「true」標籤為止。這表示您想要移除目的地,直到 (且包含) 返回堆疊中已經存在的 startFragment 執行個體為止。透過此方式,返回堆疊中就不會出現兩個 startFragment 執行個體。

4a403838a62ff487.png

  1. 針對將 pickupFragment 連結到 startFragment 的動作重複以上變更。

4a403838a62ff487.png

  1. 針對將 flavorFragment 連結到 startFragment 的動作重複以上操作。
  2. 完成後,請查看導覽圖檔案的「Code」檢視畫面,確認應用程式變更內容正確無誤。
<navigation
    android:id="@+id/nav_graph" ...>
    <fragment
        android:id="@+id/startFragment" ...>
        ...
    </fragment>
    <fragment
        android:id="@+id/flavorFragment" ...>
        ...
        <action
            android:id="@+id/action_flavorFragment_to_startFragment"
            app:destination="@id/startFragment"
            app:popUpTo="@id/startFragment"
            app:popUpToInclusive="true" />
    </fragment>
    <fragment
        android:id="@+id/pickupFragment" ...>
        ...
        <action
            android:id="@+id/action_pickupFragment_to_startFragment"
            app:destination="@id/startFragment"
            app:popUpTo="@id/startFragment"
            app:popUpToInclusive="true" />
    </fragment>
    <fragment
        android:id="@+id/summaryFragment" ...>
        <action
            android:id="@+id/action_summaryFragment_to_startFragment"
            app:destination="@id/startFragment"
            app:popUpTo="@id/startFragment"
            app:popUpToInclusive="true" />
    </fragment>
</navigation>

請注意,這 3 項動作 (action_flavorFragment_to_startFragmentaction_pickupFragment_to_startFragmentaction_summaryFragment_to_startFragment) 應新增 app:popUpTo="@id/startFragment"app:popUpToInclusive="true" 屬性。

  1. 接著執行應用程式。請按照訂購流程中的步驟操作,然後輕觸「Cancel」。返回 StartFragment 時,請輕觸「Back」按鈕 (僅限一次!) 退出應用程式。

簡而言之,當您取消訂單並返回應用程式的第一個畫面時,返回堆疊中的所有片段目的地都會移除,包括第一個出現的 StartFragment。完成導覽動作後,StartFragment 會做為新目的地新增至返回堆疊。輕觸該處的「Back」後,會從堆疊移除 StartFragment,使返回堆疊中完全沒有片段。Android 即完成活動,且使用者離開應用程式。

應用程式應如下所示:2e0599d9b55401f1.png

5. 傳送訂單

到目前為止,應用程式看起來很棒!但還剩下一部分。當您輕觸 SummaryFragment 上的「傳送訂單」按鈕時,仍會彈出 Toast 訊息。

90ed727c7b812fd6.png

如果訂單能夠自應用程式發出,即可打造更加實用的體驗。善用在先前程式碼研究室中學到的知識,運用隱含意圖將應用程式資訊分享至其他應用程式。如此一來,使用者就可以在裝置上與電子郵件應用程式分享杯子蛋糕訂單資訊,讓系統將訂單透過電子郵件傳送到杯子蛋糕店。

170d76b64ce78f56.png

如要實作這項功能,請參閱上方的螢幕截圖,瞭解電子郵件主旨和電子郵件內文的結構。

您將會使用 strings.xml 檔案中已包含的字串。

<string name="new_cupcake_order">New Cupcake Order</string>
<string name="order_details">Quantity: %1$s cupcakes \n Flavor: %2$s \nPickup date: %3$s \n Total: %4$s \n\n Thank you!</string>

order_details 是一個字串資源,其中包含 4 種不同的格式引數,此為杯子蛋糕實際數量、所需口味、所需自取日期和總價的預留位置。引數編號為 1 到 4,語法為 %1%4。引數類型也已指定 ($s 代表字串預期在此處)。

在 Kotlin 程式碼中,您可以在 R.string.order_details 上呼叫 getString(),後接 4 個引數 (順序很重要!)。舉例來說,呼叫 getString(R.string.order_details, "12", "Chocolate", "Sat Dec 12", "$24.00") 會建立下列字串,而這正是您所需的電子郵件內文。

Quantity: 12 cupcakes
Flavor: Chocolate
Pickup date: Sat Dec 12
Total: $24.00

Thank you!
  1. SummaryFragment.kt 中修改 sendOrder() 方法。移除現有的 Toast 訊息。
fun sendOrder() {

}
  1. sendOrder() 方法中,建構訂單摘要文字。從共用檢視模型取得訂單數量、口味、日期和價格,建立格式化的 order_details 字串。
val orderSummary = getString(
    R.string.order_details,
    sharedViewModel.quantity.value.toString(),
    sharedViewModel.flavor.value.toString(),
    sharedViewModel.date.value.toString(),
    sharedViewModel.price.value.toString()
)
  1. sendOrder() 方法中,建立將訂單分享至其他應用程式的隱含意圖。請參閱說明文件,瞭解如何建立電子郵件意圖。請為意圖動作指定 Intent.ACTION_SEND、將類型設為 "text/plain",並加入電子郵件主旨 (Intent.EXTRA_SUBJECT) 和電子郵件內文 (Intent.EXTRA_TEXT) 的意圖額外項目。視需要匯入 android.content.Intent
val intent = Intent(Intent.ACTION_SEND)
    .setType("text/plain")
    .putExtra(Intent.EXTRA_SUBJECT, getString(R.string.new_cupcake_order))
    .putExtra(Intent.EXTRA_TEXT, orderSummary)

另提供額外提示,如果您將此應用程式調整為個人用途,可將電子郵件收件者預先填入為杯子蛋糕店的電子郵件地址。在意圖中,您將以意圖額外項目 Intent.EXTRA_EMAIL 指定電子郵件收件者。

  1. 由於此為隱含意圖,您不必事先得知哪個特定元件或應用程式會處理這項意圖。使用者會決定要使用哪一款應用程式來達到意圖。但是,在使用這項意圖啟動活動前,請先檢查是否有應用程式能處理此意圖。如果沒有能處理此意圖的應用程式,這項檢查可防止 Cupcake 應用程式當機,讓程式碼更加安全。
if (activity?.packageManager?.resolveActivity(intent, 0) != null) {
    startActivity(intent)
}

透過存取 PackageManager 執行這項檢查,其具備裝置上所安裝應用程式套件的相關資訊。只要 activitypackageManager 並非空值,就可以透過片段的 activity 存取 PackageManager。使用您建立的意圖呼叫 PackageManagerresolveActivity() 方法。如果結果不是空值,可以放心使用意圖呼叫 startActivity()

  1. 執行應用程式測試程式碼。建立杯子蛋糕訂單,然後輕觸「Send Order to Another App」。畫面上顯示分享對話方塊彈出式視窗時,即可選取 Gmail 應用程式。如有需要,亦可選擇其他應用程式。如果您選擇 Gmail 應用程式,可能需要在裝置上設定帳戶 (如果尚未設定,例如您正在使用模擬器)。如果電子郵件內文中未顯示最新的杯子蛋糕訂單,您可能需要先捨棄目前的電子郵件草稿。

170d76b64ce78f56.png

在不同情況下進行測試時,如果只有 1 個杯子蛋糕,可能會發現錯誤。訂單摘要顯示「1 cupcakes」,但是這種說法在英文中屬於文法錯誤。

ef046a100381bb07.png

反之,應顯示「1 cupcake」(非複數型態)。如要根據數量值選擇單數或複數型態的字詞,可以在 Android 中使用數量字串。只要宣告 plurals 資源,即可根據數量指定不同的字串資源,例如單數或複數型態。

  1. strings.xml 檔案中新增 cupcakes 複數資源。
<plurals name="cupcakes">
    <item quantity="one">%d cupcake</item>
    <item quantity="other">%d cupcakes</item>
</plurals>

在單數情況 (quantity="one") 下,會使用單數字串。在所有其他情況下 (quantity="other"),將會使用複數字串。請注意,%d 為整數引數,而非 %s 的字串引數,當您格式化字串時,將會傳入此引數。

在 Kotlin 程式碼中呼叫:

getQuantityString(R.plurals.cupcakes, 1, 1) 會傳回 1 cupcake 字串

getQuantityString(R.plurals.cupcakes, 6, 6) 會傳回 6 cupcakes 字串

getQuantityString(R.plurals.cupcakes, 0, 0) 會傳回 0 cupcakes 字串

  1. 前往 Kotlin 程式碼前,請更新 strings.xml 中的 order_details 字串資源,使杯子蛋糕的複數版本不再以硬式編碼的方式寫入。
<string name="order_details">Quantity: %1$s \n Flavor: %2$s \nPickup date: %3$s \n
        Total: %4$s \n\n Thank you!</string>
  1. SummaryFragment 類別中,更新 sendOrder() 方法以使用新的數量字串。最簡單的方式是先從檢視模型找出數量,然後儲存在變數中。由於檢視模型中的 quantity 屬於 LiveData<Int> 類型,因此 sharedViewModel.quantity.value 可能為空值。如果為空值,請使用 0 做為 numberOfCupcakes 的預設值。

請將此新增為 sendOrder() 方法中的第一行程式碼。

val numberOfCupcakes = sharedViewModel.quantity.value ?: 0

elvis 運算子 (?:) 表示左側的運算式並非空值時,請使用該運算式。如果左側的運算式為空值,請使用 elvis 運算子右側的運算式 (本例中為 0)。

  1. 接著和先前一樣,將 order_details 字串格式化。請勿以 numberOfCupcakes 做為數量引數直接傳入,而是使用 resources.getQuantityString(R.plurals.cupcakes, numberOfCupcakes, numberOfCupcakes) 建立格式化的杯子蛋糕字串。

完整的 sendOrder() 方法如下所示:

fun sendOrder() {
    val numberOfCupcakes = sharedViewModel.quantity.value ?: 0
    val orderSummary = getString(
        R.string.order_details,
        resources.getQuantityString(R.plurals.cupcakes, numberOfCupcakes, numberOfCupcakes),
        sharedViewModel.flavor.value.toString(),
        sharedViewModel.date.value.toString(),
        sharedViewModel.price.value.toString()
    )

    val intent = Intent(Intent.ACTION_SEND)
        .setType("text/plain")
        .putExtra(Intent.EXTRA_SUBJECT, getString(R.string.new_cupcake_order))
        .putExtra(Intent.EXTRA_TEXT, orderSummary)

    if (activity?.packageManager?.resolveActivity(intent, 0) != null) {
        startActivity(intent)
    }
}
  1. 執行並測試程式碼。檢查電子郵件內文中的訂單摘要是否顯示 1 個杯子蛋糕、6 個杯子蛋糕或 12 個杯子蛋糕。

透過此方法,您已完成 Cupcake 應用程式的所有功能!恭喜!!這是一款極具挑戰性的應用程式,而您在成為 Android 開發人員的旅程中,獲得大幅的進展!您已成功整合目前為止學到的所有概念,同時在過程中整理出一些新的問題解決方式。

最後步驟

現在,請花點時間清理程式碼,這是您從先前程式碼研究室中學到的良好程式編寫做法。

  • 最佳化匯入
  • 將檔案重新格式化
  • 移除未使用或註解排除的程式碼
  • 視需要在程式碼中新增註解

為了讓您的應用程式更易於存取,請在啟用 Talkback 的情況下測試應用程式,以確保流暢的使用者體驗。在適當情況下,互動朗讀有助於傳達畫面上每個元素的用途。此外,還能確認應用程式的所有元素都能透過滑動手勢前往。

確認實作的用途是否能在最終應用程式中如預期正常運作。範例:

  • 資料應在裝置旋轉時保留 (歸因於檢視模型)。
  • 如果您輕觸「Up」或「Back」按鈕,訂單資訊仍會在 FlavorFragmentPickupFragment 上正確顯示。
  • 傳送訂單到其他應用程式應分享正確的訂單詳細資料。
  • 取消訂單後,系統應清除訂單中的所有資訊。

如果發現任何錯誤,請加以修正。

做得好!

6. 解決方案程式碼

本程式碼研究室的解決方案程式碼位於下方所示專案中。

如要取得本程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作。

取得程式碼

  1. 按一下上面顯示的網址。系統會在瀏覽器中開啟專案的 GitHub 頁面。
  2. 在專案的 GitHub 頁面中,按一下「Code」按鈕開啟對話方塊。

5b0a76c50478a73f.png

  1. 在對話方塊中,按一下「Download ZIP」按鈕,將專案儲存到電腦。等待下載作業完成。
  2. 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
  3. 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。

在 Android Studio 中開啟專案

  1. 啟動 Android Studio。
  2. 在「Welcome to Android Studio」視窗中,按一下「Open an existing Android Studio project」

36cc44fcf0f89a1d.png

注意:如果 Android Studio 已開啟,請依序選取「File」>「New」>「Import Project」選單選項。

21f3eec988dcfbe9.png

  1. 在「Import Project」對話方塊中,前往解壓縮專案資料夾所在的位置 (可能位於「下載」資料夾中)。
  2. 按兩下該專案資料夾。
  3. 等待 Android Studio 開啟專案。
  4. 按一下「Run」按鈕 11c34fc5e516fb1c.png 即可建構並執行應用程式。請確認應用程式的建構符合預期。
  5. 在「Project」工具視窗中瀏覽專案檔案,查看應用程式的設定方式。

7. 摘要

  • Android 會保留您造訪過所有目的地的返回堆疊,並將每個新目的地推送到堆疊上。
  • 輕觸「Up」或「Back」按鈕,即可將目的地從返回堆疊中移除。
  • 使用 Jetpack 導覽元件時,可協助將片段目的地推送至返回堆疊,或從返回堆疊中移除,因此預設的「Back」按鈕行為無需實作任何設定
  • 指定導覽圖動作的 app:popUpTo 屬性,以便從返回堆疊中移除目的地,直到到達屬性值中指定的目的地為止。
  • app:popUpTo 中指定的目的地也需從返回堆疊中移除時,指定動作的 app:popUpToInclusive="true"
  • 您可以使用 Intent.ACTION_SEND 和填入的意圖額外項目 (例如 Intent.EXTRA_EMAILIntent.EXTRA_SUBJECTIntent.EXTRA_TEXT) 來建立隱含意圖,將內容分享到電子郵件應用程式。
  • 如果想要依據數量使用不同字串資源 (例如單數或複數情況),請使用 plurals 資源。

8. 瞭解詳情

9. 自行練習

使用杯子蛋糕訂購流程上的自身變數擴充 Cupcake 應用程式。範例:

  • 提供具備特殊條件的特殊口味,例如自取當天無法供應。
  • 詢問使用者杯子蛋糕訂單的姓名。
  • 如果數量超過 1 個,則允許使用者在訂單中選擇多種杯子蛋糕口味。

您需要更新應用程式的哪些部分以支援這項新功能?

檢查操作:

應用程式成品應能夠順利執行。

10. 挑戰工作

請運用您從建構 Cupcake 應用程式所獲得的經驗,根據自己的用途建構應用程式。可以是用於訂購披薩、三明治或任何其他想法的應用程式!建議您在實作應用程式之前,先擬定不同的目的地。

如果想從其他設計概念中汲取靈感,可以嘗試使用 Shrine 應用程式質感設計研究,其可讓您瞭解如何為自己的品牌運用質感主題設定及元件。Shrine 應用程式比您建構的 Cupcake 應用程式更為複雜,所以與其想建構一款極具挑戰性的應用程式,不如先考慮可優先處理的小功能。透過不斷累積成功經驗的方式,逐漸建立信心。

應用程式製作完成後,您可以在社群媒體上分享建構的內容。使用 #LearningKotlin 主題標記,讓我們看見您的作品!