1. 事前準備
在先前的程式碼研究室中,您已開始實作 Cupcake 應用程式,現將在本程式碼研究室中完成其餘步驟。Cupcake 應用程式有多個畫面,且會顯示杯子蛋糕的訂購流程。完成的應用程式必須讓使用者能夠瀏覽應用程式,以執行下列操作:
- 建立杯子蛋糕訂單
- 使用「向上」或「返回」按鈕前往訂購流程的上一個步驟
- 取消訂單
- 將訂單傳送至其他應用程式 (例如電子郵件應用程式)
過程中,您將會瞭解 Android 如何處理應用程式的工作和返回堆疊。這可讓您操控各種情況下的返回堆疊 (例如取消訂單),讓使用者返回應用程式的第一個畫面,而非訂購流程的前一個畫面。
必要條件
- 可在活動的不同片段中建立和使用共用檢視模型
- 熟悉使用 Jetpack 導覽元件
- 已將資料繫結與 LiveData 搭配使用,讓 UI 與檢視模式模型保持同步
- 可建立意圖,以開始新活動
課程內容
- 導覽對於應用程式返回堆疊的影響
- 如何實作自訂返回堆疊行為
建構項目
- 杯子蛋糕訂購應用程式可讓使用者將訂單傳送到其他應用程式,且可取消訂單
需求條件
- 已安裝 Android Studio 的電腦。
- 完成先前程式碼研究室所獲得的 Cupcake 應用程式程式碼
2. 範例應用程式總覽
本程式碼研究室使用先前程式碼研究室的 Cupcake 應用程式。您可以使用完成先前程式碼研究室所獲得的程式碼,或從 GitHub 下載範例程式碼。
下載本程式碼研究室的範例程式碼
請注意,如果您是從 GitHub 下載範例程式碼,專案的資料夾名稱為 android-basics-kotlin-cupcake-app-viewmodel
。在 Android Studio 中開啟專案時,請選取這個資料夾。
如要取得本程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作。
取得程式碼
- 按一下上面顯示的網址。系統會在瀏覽器中開啟專案的 GitHub 頁面。
- 在專案的 GitHub 頁面中,按一下「Code」按鈕開啟對話方塊。
- 在對話方塊中,按一下「Download ZIP」按鈕,將專案儲存到電腦。等待下載作業完成。
- 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
- 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。
在 Android Studio 中開啟專案
- 啟動 Android Studio。
- 在「Welcome to Android Studio」視窗中,按一下「Open an existing Android Studio project」。
注意:如果 Android Studio 已開啟,請依序選取「File」>「New」>「Import Project」選單選項。
- 在「Import Project」對話方塊中,前往解壓縮專案資料夾所在的位置 (可能位於「下載」資料夾中)。
- 按兩下該專案資料夾。
- 等待 Android Studio 開啟專案。
- 按一下「Run」按鈕 即可建構並執行應用程式。請確認應用程式的建構符合預期。
- 在「Project」工具視窗中瀏覽專案檔案,查看應用程式的設定方式。
接著,執行應用程式,如下所示。
本程式碼研究室會引導您先在應用程式中實作「Up」按鈕,讓使用者可藉由輕觸該按鈕回到訂購流程的上一步。
接著則會新增「Cancel」按鈕,讓改變心意的使用者能夠在訂購過程中取消訂單。
您將會擴充應用程式,以便在輕觸「Send Order to Another App」(傳送訂單至其他應用程式) 時與其他應用程式分享訂單。接著,即可透過電子郵件等方式將訂單傳送至杯子蛋糕店。
讓我們深入探索並完成 Cupcake 應用程式吧!
3. 實作向上按鈕行為
在 Cupcake 應用程式中,應用程式列會顯示可返回上一個畫面的箭頭,此為「Up」按鈕,您在先前的程式碼研究室中曾學過。「向上」按鈕目前沒有任何作用,因此請先在應用程式中修正此導覽錯誤。
- 您的
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)
}
}
- 在同一個類別中,新增程式碼來覆寫
onSupportNavigateUp()
函式。此程式碼會要求navController
處理應用程式的向上導覽。否則,請返回至處理「Up」按鈕的父類別實作 (AppCompatActivity
中)。
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
- 執行應用程式。「Up」按鈕現在應可在
FlavorFragment
、PickupFragment
和SummaryFragment
中運作。前往訂購流程中的上一個步驟時,片段應透過檢視模型顯示正確的口味和自取日期。
4. 瞭解工作和返回堆疊
現在,請在應用程式的訂購流程中加入「Cancel」按鈕。在訂購過程中的任何時間點取消訂單,會讓使用者返回 StartFragment
。若要處理此行為,您需瞭解 Android 中的工作和返回堆疊。
工作
Android 的活動存在於工作中。首次從啟動器圖示開啟應用程式時,Android 會建立一個包含主要活動的新工作。「工作」是使用者執行特定工作 (例如查看電子郵件、建立杯子蛋糕訂單、拍照) 時,可進行互動的一系列活動。
系統會以返回堆疊的排列方式來顯示活動,此方式會將使用者造訪的新活動推送至工作的返回堆疊上。您可以將上述過程看成鬆餅的堆疊,每一份新的鬆餅皆會添加於堆疊頂端。堆疊頂端的活動是指使用者目前正在互動的活動。堆疊中,位於該活動下方的活動已移至背景且停止。
使用者想要往回瀏覽時,返回堆疊功能就能派上用場。Android 可以從堆疊頂端移除目前活動、將其刪除,並重新啟動其下方的活動。這稱為從堆疊中移除活動,並將先前的活動移至前景,以便使用者進行互動。如果使用者想要反覆返回查看,Android 會持續將活動從堆疊頂端移除,直到接近堆疊底部為止。如果返回堆疊中沒有任何活動,系統會將使用者導回至裝置的啟動器畫面,或至啟動此應用程式的應用程式。
讓我們看看您透過以下 2 個活動實作的 Words 應用程式版本:MainActivity
和 DetailActivity
。
初次啟動應用程式時,MainActivity
會開啟,並新增至工作的返回堆疊中。
只要按一下字母,DetailActivity
就會啟動,然後推送到返回堆疊上。這表示已建立、啟動並重新啟用 DetailActivity
,因此使用者可與其互動。系統會將 MainActivity
置於背景,並以灰色的背景顏色顯示於圖表中。
若輕觸「返回」按鈕,系統就會從返回堆疊中彈出 DetailActivity
,並刪除及結束 DetailActivity
例項。
接著,返回堆疊頂端的下一個項目 (MainActivity
) 就會移至前景。
如同返回堆疊可追蹤使用者已開啟的活動,只要與 Jetpack Navigation 元件搭配運作,返回堆疊也可透過相同方式追蹤使用者造訪過的片段目的地。
只要使用 Navigation 程式庫,即可在使用者每次點選「返回」按鈕時,從返回堆疊中彈出片段目的地。此預設行為無需實作任何設定。如果您需要自訂返回堆疊行為,才需要編寫程式碼,您將為 Cupcake 應用程式進行此操作。
Cupcake 應用程式的預設行為
讓我們看看返回堆疊在 Cupcake 應用程式中的運作方式。應用程式中只有一個活動,但使用者會瀏覽多個片段目的地。因此,「Back」按鈕在使用者輕觸時才返回上一個片段目的地較為理想。
初次開啟應用程式時,系統會顯示 StartFragment
目的地。該目的地會推送至堆疊頂端。
選取要訂購的杯子蛋糕數量後,將會前往 FlavorFragment
,這個目的地會推送至返回堆疊上。
當您選取口味並輕觸「Next」後,將會前往 PickupFragment
,並推送至返回堆疊上。
最後,選取自取日期並輕觸「Next」後,將會前往 SummaryFragment
,這個目的地會新增至返回堆疊頂端。
如果您從 SummaryFragment
中輕觸「返回」或「向上」按鈕,SummaryFragment
會從堆疊中移除並刪除。
PickupFragment
現在位於返回堆疊頂端,並向使用者顯示。
再次輕觸「返回」或「向上」按鈕,將會從堆疊中移除 PickupFragment
,接著顯示 FlavorFragment
。
再次輕觸「Back」或「Up」按鈕,將會從堆疊中移除 FlavorFragment
,接著顯示 StartFragment
。
當您在訂購流程中返回到上一個步驟時,一次只會移除一個目的地。但在下一個工作中,您會在應用程式中新增取消訂單功能。為此,您必須一次移除返回堆疊中的多個目的地,讓使用者返回 StartFragment
建立新訂單。
修改 Cupcake 應用程式中的返回堆疊
修改 FlavorFragment
、PickupFragment
和 SummaryFragment
類別和版面配置檔案,為使用者提供取消訂單按鈕。
新增導覽動作
請先在應用程式的導覽圖中加入導覽動作,讓使用者能從後續目的地返回 StartFragment
。
- 前往「res」>「navigation」>「nav_graph.xml」檔案,然後選取「Design」檢視畫面,以開啟導覽編輯器。
- 目前有
startFragment
至flavorFragment
的動作、flavorFragment
至pickupFragment
的動作,以及pickupFragment
至summaryFragment
的動作。 - 按住並拖曳即可建立從
summaryFragment
至startFragment
的新導覽動作。如想複習如何在導覽圖中連結目的地,請參閱這些操作說明。 - 按住並拖曳
pickupFragment
即可建立至startFragment
的新動作。 - 按住並拖曳
flavorFragment
即可建立至startFragment
的新動作。 - 完成後,導覽圖應如下所示。
進行上述變更後,使用者即可從訂購流程中較後方的某個片段返回至訂購流程的起始處。現在,您需要實際使用這些動作進行瀏覽的程式碼。輕觸「Cancel」按鈕處即為適當位置。
在版面配置中新增「Cancel」按鈕
首先,請在所有片段 (StartFragment
除外) 的版面配置檔案中新增「Cancel」按鈕。如果您已位於訂購流程的第一個畫面,便無需取消訂單。
- 開啟
fragment_flavor.xml
版面配置檔案。 - 您可以使用「Split」檢視畫面直接編輯 XML,且並排檢視預覽畫面。
- 在小計文字檢視區塊和「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" ... />
...
- 在
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"... />
...
- 在視覺樣式方面,請使用 Material Outlined Button 樣式 (含屬性
style="?attr/materialButtonOutlinedStyle"
),使「Cancel」按鈕不會過於醒目,因「Next」按鈕是您希望使用者專注的主要動作。
<Button
android:id="@+id/cancel_button"
style="?attr/materialButtonOutlinedStyle" ... />
按鈕和位置現在看起來十分完美!
- 以同樣的方式,在
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" ... />
...
- 請一併更新「Next」按鈕的起始限制。接著,預覽畫面中會顯示「Cancel」按鈕。
<Button
android:id="@+id/next_button"
app:layout_constraintStart_toEndOf="@id/cancel_button" ... />
- 對
fragment_summary.xml
檔案套用類似的變更,但這個片段的版面配置稍有不同。您將在父項垂直容器LinearLayout
中的「傳送」按鈕下方新增「取消」按鈕,並在兩個按鈕之間保留一定間距。
...
<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>
- 執行並測試應用程式。現在,
FlavorFragment
、PickupFragment
和SummaryFragment
的版面配置中應該會顯示「Cancel」按鈕。不過,輕觸該按鈕目前並不會執行任何動作。請在下一個步驟中為這些按鈕設定點選監聽器。
新增「Cancel」按鈕的點擊事件監聽器
在每個片段類別 (StartFragment
除外) 中新增 Helper 方法,以便在使用者點選「Cancel」按鈕時進行處理。
- 將這個
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
)。
- 如要在
fragment_flavor.xml
版面配置的「Cancel」按鈕上設定點選監聽器,請使用事件監聽器繫結。點選此按鈕可叫用您剛才在FragmentFlavor
類別中建立的cancelOrder()
方法。
<Button
android:id="@+id/cancel_button"
android:onClick="@{() -> flavorFragment.cancelOrder()}" ... />
- 針對
PickupFragment
重複執行相同的程序。在片段類別中新增cancelOrder()
方法,藉此重設訂單,並從PickupFragment
瀏覽至StartFragment
。
fun cancelOrder() {
sharedViewModel.resetOrder()
findNavController().navigate(R.id.action_pickupFragment_to_startFragment)
}
- 在
fragment_pickup.xml
中,於「Cancel」按鈕上設定點選監聽器,以便在使用者點選時呼叫cancelOrder()
方法。
<Button
android:id="@+id/cancel_button"
android:onClick="@{() -> pickupFragment.cancelOrder()}" ... />
- 為
SummaryFragment
中的「取消」按鈕新增類似的程式碼,讓使用者可以返回StartFragment
。如果androidx.navigation.fragment.findNavController
未自動匯入,您可能需要自行匯入.
fun cancelOrder() {
sharedViewModel.resetOrder()
findNavController().navigate(R.id.action_summaryFragment_to_startFragment)
}
- 在
fragment_summary.xml
中按下「取消」按鈕時,將隨即呼叫SummaryFragment
的cancelOrder()
方法。
<Button
android:id="@+id/cancel_button"
android:onClick="@{() -> summaryFragment.cancelOrder()}" ... />
- 執行並測試應用程式,確認您剛才新增至每個片段的邏輯。建立杯子蛋糕訂單時,輕觸
FlavorFragment
、PickupFragment
或SummaryFragment
上的「取消」按鈕,即可返回StartFragment
。繼續建立新訂單時,請注意系統已清除先前訂單中的資訊。
成效看起來不錯,但返回 StartFragment
後,向後瀏覽實際上有錯誤。請按照下列步驟重現錯誤。
- 按照訂購流程建立新的杯子蛋糕訂單,直到到達摘要畫面為止。舉例來說,您可以訂購 12 個巧克力口味的杯子蛋糕,並選擇未來的自取日期。
- 接著,輕觸「Cancel」。您應該會返回
StartFragment
。 - 這看起來沒問題,但如果您輕觸系統自帶的「返回」按鈕,就會回到訂單摘要畫面,其中將顯示訂單摘要:訂購 0 個杯子蛋糕,未選擇任何口味。這是錯誤現象,不應向使用者顯示。
使用者可能不想回到訂購流程。此外,檢視模型中的所有訂單資料均已清除,因此這項資訊不實用。反之,輕觸 StartFragment
中的「Back」按鈕,應離開 Cupcake 應用程式。
以下我們將介紹返回堆疊目前的情況,以及修正錯誤的方法。透過訂單摘要畫面建立訂單時,每個目的地都會推送至返回堆疊上。
您在 SummaryFragment
取消了訂單。當您使用 SummaryFragment
至 StartFragment
的動作進行瀏覽時,Android 會新增另一個 StartFragment
執行個體,做為返回堆疊上的新目的地。
因此,當您輕觸 StartFragment
中的「返回」按鈕時,應用程式最終會重新顯示 SummaryFragment
(包含空白訂單資訊)。
如要修正這個導覽錯誤,請瞭解導覽元件如何讓使用者在使用動作進行瀏覽時,從返回堆疊中移除其他目的地。
從返回堆疊中移除其他目的地
導覽動作:popUpTo 屬性
在導覽圖的導覽動作中加入 app:popUpTo
屬性後,即可從返回堆疊中移除多個目的地,直到到達指定目的地為止。如果指定 app:popUpTo="@id/startFragment"
,則會移除返回堆疊中的目的地,直到到達 StartFragment
為止,此片段會保留在堆疊中。
將此變更新增至程式碼並執行應用程式時,您將會發現只要取消訂單,就會回到 StartFragment
。但這次,當您輕觸 StartFragment
的「Back」按鈕時,會再次看到 StartFragment
(而不是結束應用程式)。這也不是預期出現的行為。如先前所述,由於您正在前往 StartFragment
,Android 實際上會在返回堆疊中新增 StartFragment
做為新目的地,因此現在返回堆疊會有 2 個 StartFragment 執行個體。因此,您必須輕觸「返回」按鈕兩次才能結束應用程式。
導覽動作:popUpToInclusive 屬性
為修正這個新錯誤,請要求將所有目的地從返回堆疊中移除,直到 (且包含) StartFragment
為止。請在適當的導覽動作指定 app:popUpTo="@id/startFragment"
和 app:popUpToInclusive="true"
,以達到此目標。如此一來,返回堆疊中就只會有一個新的 StartFragment
執行個體。接著輕觸 StartFragment
中的「Back」按鈕,結束應用程式。我們現在要進行這項變更。
修改導覽動作
- 開啟「res」>「navigation」>「nav_graph.xml」檔案,前往導覽編輯器。
- 選取從
summaryFragment
至startFragment
的動作,使其以藍色醒目顯示。 - 展開右側的「Attributes」(如果尚未開啟)。在可修改的屬性清單中尋找「Pop Behavior」。
- 透過下拉式選單的選項,將 popUpTo 設為
startFragment
。這表示返回堆疊中的所有目的地皆會移除 (從堆疊頂端開始往下移除),直到startFragment
為止。
- 接著按一下「popUpToInclusive」核取方塊,直到畫面上顯示勾號和「true」標籤為止。這表示您想要移除目的地,直到 (且包含) 返回堆疊中已經存在的
startFragment
執行個體為止。透過此方式,返回堆疊中就不會出現兩個startFragment
執行個體。
- 針對將
pickupFragment
連結到startFragment
的動作重複以上變更。
- 針對將
flavorFragment
連結到startFragment
的動作重複以上操作。 - 完成後,請查看導覽圖檔案的「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_startFragment
、action_pickupFragment_to_startFragment
和 action_summaryFragment_to_startFragment
) 應新增 app:popUpTo="@id/startFragment"
和 app:popUpToInclusive="true"
屬性。
- 接著執行應用程式。請按照訂購流程中的步驟操作,然後輕觸「Cancel」。返回
StartFragment
時,請輕觸「Back」按鈕 (僅限一次!) 退出應用程式。
簡而言之,當您取消訂單並返回應用程式的第一個畫面時,返回堆疊中的所有片段目的地都會移除,包括第一個出現的 StartFragment
。完成導覽動作後,StartFragment
會做為新目的地新增至返回堆疊。輕觸該處的「Back」後,會從堆疊移除 StartFragment
,使返回堆疊中完全沒有片段。Android 即完成活動,且使用者離開應用程式。
應用程式應如下所示:
5. 傳送訂單
到目前為止,應用程式看起來很棒!但還剩下一部分。當您輕觸 SummaryFragment
上的「傳送訂單」按鈕時,仍會彈出 Toast
訊息。
如果訂單能夠自應用程式發出,即可打造更加實用的體驗。善用在先前程式碼研究室中學到的知識,運用隱含意圖將應用程式資訊分享至其他應用程式。如此一來,使用者就可以在裝置上與電子郵件應用程式分享杯子蛋糕訂單資訊,讓系統將訂單透過電子郵件傳送到杯子蛋糕店。
如要實作這項功能,請參閱上方的螢幕截圖,瞭解電子郵件主旨和電子郵件內文的結構。
您將會使用 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!
- 在
SummaryFragment.kt
中修改sendOrder()
方法。移除現有的Toast
訊息。
fun sendOrder() {
}
- 在
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()
)
- 在
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
指定電子郵件收件者。
- 由於此為隱含意圖,您不必事先得知哪個特定元件或應用程式會處理這項意圖。使用者會決定要使用哪一款應用程式來達到意圖。但是,在使用這項意圖啟動活動前,請先檢查是否有應用程式能處理此意圖。如果沒有能處理此意圖的應用程式,這項檢查可防止 Cupcake 應用程式當機,讓程式碼更加安全。
if (activity?.packageManager?.resolveActivity(intent, 0) != null) {
startActivity(intent)
}
透過存取 PackageManager
執行這項檢查,其具備裝置上所安裝應用程式套件的相關資訊。只要 activity
和 packageManager
並非空值,就可以透過片段的 activity
存取 PackageManager
。使用您建立的意圖呼叫 PackageManager
的 resolveActivity()
方法。如果結果不是空值,可以放心使用意圖呼叫 startActivity()
。
- 執行應用程式測試程式碼。建立杯子蛋糕訂單,然後輕觸「Send Order to Another App」。畫面上顯示分享對話方塊彈出式視窗時,即可選取 Gmail 應用程式。如有需要,亦可選擇其他應用程式。如果您選擇 Gmail 應用程式,可能需要在裝置上設定帳戶 (如果尚未設定,例如您正在使用模擬器)。如果電子郵件內文中未顯示最新的杯子蛋糕訂單,您可能需要先捨棄目前的電子郵件草稿。
在不同情況下進行測試時,如果只有 1 個杯子蛋糕,可能會發現錯誤。訂單摘要顯示「1 cupcakes」,但是這種說法在英文中屬於文法錯誤。
反之,應顯示「1 cupcake」(非複數型態)。如要根據數量值選擇單數或複數型態的字詞,可以在 Android 中使用數量字串。只要宣告 plurals
資源,即可根據數量指定不同的字串資源,例如單數或複數型態。
- 在
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
字串
- 前往 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>
- 在
SummaryFragment
類別中,更新sendOrder()
方法以使用新的數量字串。最簡單的方式是先從檢視模型找出數量,然後儲存在變數中。由於檢視模型中的quantity
屬於LiveData<Int>
類型,因此sharedViewModel.quantity.value
可能為空值。如果為空值,請使用0
做為numberOfCupcakes
的預設值。
請將此新增為 sendOrder()
方法中的第一行程式碼。
val numberOfCupcakes = sharedViewModel.quantity.value ?: 0
elvis 運算子 (?:) 表示左側的運算式並非空值時,請使用該運算式。如果左側的運算式為空值,請使用 elvis 運算子右側的運算式 (本例中為 0
)。
- 接著和先前一樣,將
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 個杯子蛋糕、6 個杯子蛋糕或 12 個杯子蛋糕。
透過此方法,您已完成 Cupcake 應用程式的所有功能!恭喜!!這是一款極具挑戰性的應用程式,而您在成為 Android 開發人員的旅程中,獲得大幅的進展!您已成功整合目前為止學到的所有概念,同時在過程中整理出一些新的問題解決方式。
最後步驟
現在,請花點時間清理程式碼,這是您從先前程式碼研究室中學到的良好程式編寫做法。
- 最佳化匯入
- 將檔案重新格式化
- 移除未使用或註解排除的程式碼
- 視需要在程式碼中新增註解
為了讓您的應用程式更易於存取,請在啟用 Talkback 的情況下測試應用程式,以確保流暢的使用者體驗。在適當情況下,互動朗讀有助於傳達畫面上每個元素的用途。此外,還能確認應用程式的所有元素都能透過滑動手勢前往。
確認實作的用途是否能在最終應用程式中如預期正常運作。範例:
- 資料應在裝置旋轉時保留 (歸因於檢視模型)。
- 如果您輕觸「Up」或「Back」按鈕,訂單資訊仍會在
FlavorFragment
和PickupFragment
上正確顯示。 - 傳送訂單到其他應用程式應分享正確的訂單詳細資料。
- 取消訂單後,系統應清除訂單中的所有資訊。
如果發現任何錯誤,請加以修正。
做得好!
6. 解決方案程式碼
本程式碼研究室的解決方案程式碼位於下方所示專案中。
如要取得本程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作。
取得程式碼
- 按一下上面顯示的網址。系統會在瀏覽器中開啟專案的 GitHub 頁面。
- 在專案的 GitHub 頁面中,按一下「Code」按鈕開啟對話方塊。
- 在對話方塊中,按一下「Download ZIP」按鈕,將專案儲存到電腦。等待下載作業完成。
- 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
- 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。
在 Android Studio 中開啟專案
- 啟動 Android Studio。
- 在「Welcome to Android Studio」視窗中,按一下「Open an existing Android Studio project」。
注意:如果 Android Studio 已開啟,請依序選取「File」>「New」>「Import Project」選單選項。
- 在「Import Project」對話方塊中,前往解壓縮專案資料夾所在的位置 (可能位於「下載」資料夾中)。
- 按兩下該專案資料夾。
- 等待 Android Studio 開啟專案。
- 按一下「Run」按鈕 即可建構並執行應用程式。請確認應用程式的建構符合預期。
- 在「Project」工具視窗中瀏覽專案檔案,查看應用程式的設定方式。
7. 摘要
- Android 會保留您造訪過所有目的地的返回堆疊,並將每個新目的地推送到堆疊上。
- 輕觸「Up」或「Back」按鈕,即可將目的地從返回堆疊中移除。
- 使用 Jetpack 導覽元件時,可協助將片段目的地推送至返回堆疊,或從返回堆疊中移除,因此預設的「Back」按鈕行為無需實作任何設定。
- 指定導覽圖動作的
app:popUpTo
屬性,以便從返回堆疊中移除目的地,直到到達屬性值中指定的目的地為止。 app:popUpTo
中指定的目的地也需從返回堆疊中移除時,指定動作的app:popUpToInclusive="true"
。- 您可以使用
Intent.ACTION_SEND
和填入的意圖額外項目 (例如Intent.EXTRA_EMAIL
、Intent.EXTRA_SUBJECT
和Intent.EXTRA_TEXT
) 來建立隱含意圖,將內容分享到電子郵件應用程式。 - 如果想要依據數量使用不同字串資源 (例如單數或複數情況),請使用
plurals
資源。
8. 瞭解詳情
9. 自行練習
使用杯子蛋糕訂購流程上的自身變數擴充 Cupcake 應用程式。範例:
- 提供具備特殊條件的特殊口味,例如自取當天無法供應。
- 詢問使用者杯子蛋糕訂單的姓名。
- 如果數量超過 1 個,則允許使用者在訂單中選擇多種杯子蛋糕口味。
您需要更新應用程式的哪些部分以支援這項新功能?
檢查操作:
應用程式成品應能夠順利執行。
10. 挑戰工作
請運用您從建構 Cupcake 應用程式所獲得的經驗,根據自己的用途建構應用程式。可以是用於訂購披薩、三明治或任何其他想法的應用程式!建議您在實作應用程式之前,先擬定不同的目的地。
如果想從其他設計概念中汲取靈感,可以嘗試使用 Shrine 應用程式質感設計研究,其可讓您瞭解如何為自己的品牌運用質感主題設定及元件。Shrine 應用程式比您建構的 Cupcake 應用程式更為複雜,所以與其想建構一款極具挑戰性的應用程式,不如先考慮可優先處理的小功能。透過不斷累積成功經驗的方式,逐漸建立信心。
應用程式製作完成後,您可以在社群媒體上分享建構的內容。使用 #LearningKotlin 主題標記,讓我們看見您的作品!