Use Case分析與手機畫面佈局之規劃
By 高煥堂 2008/04/14
1.1 善用Use Case分析
一個手機系統通常會藉由多個畫面來與使用者溝通。那麼,一個系統到底需要幾個幕前的佈局(Layout)呢?而且需要多少個幕後的Activity類別呢?一般而言,這是需求分析的一環,而且Use Case是業界最流行的需求分析利器。
UML(Unified Model Language)的Use Case圖能有效表達使用者與Android應用程式的互動過程。從這互動過程中,引導出應用程式的畫面佈局及Activity類別之規劃。如此,確保Layout與Activity的安排能符合使用者的需要或期待。簡而言之,其開發過程分為兩個階段:
一、 進行需求分析,分析出一個或多個Use Case,以Use Case圖表示之。
二、 規劃出一個或多個畫面佈局(Layout),來支持這些Use Case。並且規劃出一個或多個幕後的Activity類別來支持目前的Layout。
Layout範例二 Layout範例三
這兩大階段又各分為兩個步驟,所以總共有4個步驟:
Step-1: 繪製Use Case圖。如下:
圖1-1 第一階段的產出:Use Case圖
Step-2: 撰寫Use Case敘述(Use Case Description, 簡稱UCD)。其描述使用者與應用程式的對話流程(Dialog Process)。如下圖:
圖2-2 第一階段的產出:Use Case敘述
以上第一端階段Use Case分析技巧建議你閱讀由筆者所著的Use Case入門與實例 一書。於此把焦點放在第二階段:規劃Layout佈局和Activity類別來支撐所分析出來的Use Case圖。
Step-3: 從UCD對應到Android的畫面佈局(Layout)。
現在進行第二階段。由於Use Case主要呈現使用者的觀點,並未考慮畫面安排和佈局之種種限制(例如手機螢幕小),所以上述的Use Case偏向純粹是使用者的觀點,並未納入畫面的設計。仔細閱讀Use Case敘述,並考量畫面之限制,會發現一些不同的情況,例如:
l 一個Use Case敘述可能需要多個畫面佈局才能完成;
l 也有可能將兩個或多個Use Case敘述統合起來,由一個畫面佈局(Layout)來完成。
如下圖所示,UCDx和UCDy的整合互動過程都在「尋找和下載」畫面佈局裡完成;而UCDz則運用了三個不同的畫面佈局才能完成。
圖1-3 分解或合併Use Case敘述,對應到Layout
至此,幕前的畫面佈局已經規劃完畢了。
Step-4: 從畫面佈局(Layout)對應到Activity類別。
由於Use Case主要呈現使用者的觀點,就繼續規劃幕後的Activity類別。一個幕後的Activity類別可以支持一個或多個畫面佈局,如下圖1-4所示。Activity_A類別支持三個畫面佈局,而Activity_B則只支持一個畫面佈局。這是畫面佈局與Activity類別之間形成多對1 的關係。
此外,也可以採取1對1的設計策略,每一個Activity都各支持一個畫面佈局,如下圖1-5所示。
圖1-4 畫面佈局與Activity類別間為N:1之關係
其中,Activity_A類別支持3個畫面佈局,其「支持」的涵義是:當User操作這3個畫面中的任一個畫面時,所觸發的事件(Event)會被送給Activity_A的物件處理,其事件處理函數(Event Handler)就被定義在Activity_A類別裡。
到底是採取圖1-4的N:1策略,還是採取圖1-5的1:1策略呢?這完全看Android應用程式師的決定了,並沒有固定的規則可循。
在下一章裡,將會針對1:1和N:1兩種策略而說明如何撰寫Android程式碼來落實上述的Use Case分析圖。
圖1-5 畫面佈局與Activity類別間為1:1之關係
5.2 以Android實踐Use Case分析之策略
如果在第一階段已經分析出使用者的需要,並繪出Use Case圖(如下圖1-6),那麼如何寫出適當的Android程式來實踐這個Use Case圖,以滿足使用者的需求呢?
圖1-6 以世界標準UML的Use Case圖表達使用者需求
其實踐策略有許多,本章將以最常見的兩種策略來舉例說明之:
1. 策略-A:
l 一個Use Case對應到一個畫面佈局;
l 一個幕前的畫面佈局對應到一個Activity類別。
這是最單純的實踐策略。
2. 策略-B:
l 一個Use Case對應到一個畫面佈局;
l 多個幕前的畫面佈局可對應到一個Activity類別。
在此策略下,如何使用最適當的技巧來撰寫Android程式碼呢?
1.2 Android範例一
兩個Use Case之間可以有「包含」(include)關係,用以表示某一個Use Case的對話流程中,包含著另一個Use Case的對話流程。一旦出現數個Use Case都有部份相同的對話流程時,將相同的流程記錄在另一個Use Case中,前者稱為「基礎Use Case」(Base Use Case),後者稱為「包含Use Case」(Inclusion Use Case)。如此,這些基礎Use Case就可以共享包含Use Case,而且未來其它的Use Case只要建立包含關係,就可以立即享用已經在其它Use Case定義好的相同對話流程了。茲採取最常見策略-A來實現Use Case的包含關係:策略-A:
l 一個Use Case對應到一個畫面佈局;
l 一個幕前的畫面佈局對應到一個Activity類別。
所以需要定義兩個Activity子類別:於此分別稱之為BaseActivity和InclusionActivity,各支持一個畫面佈局。此時,BaseActivity的畫面在跟User互動過程中,可呼叫框架的startActivity()函數來啟動InclusionActivity,呈現另一個畫面佈局,展開Inclusion Use Case的互動流程,結束之後就返回到BaseActivity的畫面,繼續原來的互動流程。如此,以Android程式技巧完美地實現Use Case的重要關係。
1.2.1 畫面佈局:
一個Use Case代表使用者與應用程式之間的一項互動,例如uc: PickUp An Item就表示使用者需要從畫面上挑選一個產品細項。於是,依據策略-A:一個Use Case對應到一個畫面佈局。就規劃一個名為“pu_layout”的佈局來實現這個Use Case所敘述的互動過程。同理,也針對uc: DisplayChoice而規劃一個名為“ac01_layout”的佈局來對應之。
1.2.2 Activity類別:
再依據策略-A:一個幕前的畫面佈局對應到一個Activity類別。於是規劃出名為“ac01”的Activity子類別來支持ac01_layout佈局。同理,也針對pu_layout的佈局而規劃出一個名為“puckup”的Activity子類別來對應之。
規劃出幕前的佈局和幕後的Activity子類別之後,可以善用UML的順序圖(Sequence Diagram)來表達更詳細的互動流程,作為程式轉寫的藍圖。如下圖:
圖1-7 以UML的順序圖作為Andriod程式碼編寫的藍圖
茲說明如下:
---- 訊息1:create()表示使用者啟動程式時,透過Android框架來誕生(或啟動)ac01類別的物件,並在螢幕上呈現出ac01_layout的畫面佈局。
---- 訊息2:pressPickup()表示使用者從佈局上的Menu菜單選取選項。此刻,ac01物件立即正向呼叫Android框架裡的startActivity()函數來誕生(或啟動)pickup類別(為Activity的子類別)的物件,並在螢幕上呈現出pu_layout的畫面佈局。
---- 訊息3:pickUpAnItem()表示使用者從pu_layout佈局上的Menu選單裡選取一個細項。此刻,pickup物件立即把剛才選取的細項資料存入框架裡的SharedPreferences物件裡面,以便回傳給ac01物件。
---- 訊息4:pressShowResult()表示使用者從佈局上的Menu菜單選取選項。此刻,ac01物件立即呼叫SharedPreferences物件,從它取得剛才pickup物件所寄存的細項資料,然後顯示於畫面上。
1.2.3 畫面操作:
1. 此程式一開始,出現ac01_layout畫面,其提供Menu選單如下:
2. 如果選取選項,立即切換到pu_layout畫面佈局,如下:
3. 此佈局提供Menu選單如下:
4. 選取任一個選項之後,立即切換回到ac01_layout畫面佈局,如下:
5. 選取選項,就把資料顯示於畫面佈局的TextView裡如下:
6. 程式的任務就達成了。
1.2.4 撰寫步驟:
Step-1: 建立Android專案:ex01_01。
Step-2: 撰寫Activity的子類別:ac01,其程式碼如下:
// ---- ac01.java 程式碼 ----
package com.misoo.ex01_01;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.Menu;
import android.widget.TextView;
public class ac01 extends Activity {
public static final int PICKUP_ID = Menu.FIRST;
public static final int SHOW_ID = Menu.FIRST + 1;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
menu.add(0, PICKUP_ID, "Pick Up An Item");
menu.add(1, SHOW_ID, "Show Result"); return true;
}
@Override
public boolean onOptionsItemSelected(Menu.Item item) {
switch (item.getId()) {
case PICKUP_ID:
Intent in = new Intent(ac01.this, pickup.class);
startActivity(in); return true;
case SHOW_ID:
SharedPreferences passwdfile = getSharedPreferences( "ITEM", 0);
String im = passwdfile.getString("ITEM", null);
TextView tv = (TextView)findViewById(R.id.tv);
tv.setText("choice: " + im); return true;
}
return super.onOptionsItemSelected(item);
}}
Step-3: 撰寫Activity的子類別:pickup,其程式碼如下:
// ---- pickup.java 程式碼 ----
package com.misoo.ex01_01;
import android.app.Activity;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.view.Menu;
public class pickup extends Activity {
public static final int ITEM_1_ID = Menu.FIRST;
public static final int ITEM_2_ID = Menu.FIRST + 1;
public static final int ITEM_3_ID = Menu.FIRST + 2;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
menu.add(0, ITEM_1_ID, "item-1"); menu.add(0, ITEM_2_ID, "item-2");
menu.add(0, ITEM_3_ID, "item-3");
return true;
}
@Override
public boolean onOptionsItemSelected(Menu.Item item) {
Editor passwdfile = getSharedPreferences("ITEM", 0).edit();
passwdfile.putString("ITEM",item.getTitle().toString());
passwdfile.commit();
finish();
return super.onOptionsItemSelected(item);
}}
Step-4: 執行之。
1.2.5 說明:
1. ac01類別裡的指令:
Intent in = new Intent(ac01.this, pickup.class);
startActivity(in);
就指名啟動Activity:pickup。
2. pickup類別裡的指令:
Editor passwdfile = getSharedPreferences("ITEM", 0).edit();
passwdfile.putString("ITEM",item.getTitle().toString());
passwdfile.commit();
finish();
就在呼叫finish()而於結束此畫面之前,先將item的值存入SharedPreferences物件裡。
3. 返回到ac01的畫面後,指令:
SharedPreferences passwdfile = getSharedPreferences("ITEM", 0);
String im = passwdfile.getString("ITEM", null);
TextView tv = (TextView)findViewById(R.id.tv);
tv.setText("choice: " + im);
就從SharedPreferences物件取出item的值,顯示於畫面的TextView上。
1.3 Android範例二
茲採策略-B來實現Use Case包含關係:
策略-B:
l 一個Use Case對應到一個畫面佈局;
l 多個幕前的畫面佈局可對應到一個Activity類別。
圖1-8 如何落實這Use Case圖呢?
所以需要定義一個Activity子類別:假設取名為ac01,它支持兩個不同的畫面佈局,各佈局擔任各Use Case與User的互動畫面。或者可由一個畫面佈局來實現各個Use Case與User的互動。本章就來介紹Android所提供的實現技巧。
1.3.1 一個Activity支持兩個畫面佈局
Use Case表達User的一項期待,也表達了User與應用程式的互動過程;而畫面佈局則是應用程式用來與User互動的窗口,User透過此窗口來取得應用程式的服務,滿足他的期待。一個房子可以有多個窗子,同理,一個Activity子類別可以支持多的佈局。
所謂「支持」的涵義是:當User操作這兩個畫面所觸發的事件(Event)都會呼叫這個Activity子類別的事件處理函數(Event Handler)去處理之。
1.3.2 畫面佈局:
一個Use Case代表使用者與應用程式之間的一項互動,例如uc: PickUp An Item就表示使用者需要從畫面上挑選一個產品細項。於是,依據策略-B:一個Use Case對應到一個畫面佈局。就規劃一個名為“pu_layout”的佈局來實現這個Use Case所敘述的互動過程。同理,也針對uc: DisplayChoice而規劃一個名為“ac01_layout”的佈局來對應之。
1.3.3 Activity類別:
再依據策略-B:多個幕前的畫面佈局對應到一個幕後的Activity類別,也就是說,一個Activity可以支持多個畫面佈局。於是規劃出名為“ac01”的Activity子類別來支持ac01_layout佈局,也支持pu_layout畫面佈局。
Android手機螢幕就像布袋戲的演出舞台窗口,而內容則一幕一幕(即Layout)演出。每一幕都有不同的角色(View, 如Button等),這角色就像布袋戲的木偶,每一幕通常由一群木偶的互動而構成的。然而,布袋戲裡,有時一幕可由多位演員聯合演出。在Android裡,每一幕(Activity)則只能由一位演員(Activity)負責演出。
1.3.4 順序圖:
規劃出幕前的佈局和幕後的Activity子類別之後,可以善用UML的順序圖來表達更詳細的互動流程,作為程式轉寫的藍圖。如下圖所示:
圖1-9 ac01類別支持兩個畫面佈局
茲說明如下:
---- 訊息1:create()表示使用者啟動程式時,透過Android框架來誕生(或啟動)ac01類別的物件。
---- 訊息 1.1:此刻,設定ac01_layout為目前呈現的畫面佈局。
---- 訊息2:pressPickupAnItem()表示使用者從佈局上的Menu菜單選取選項。此刻,ac01物件立即設定pu_layout為目前呈現的畫面佈局。
---- 訊息3:pickUpAnItem()表示使用者從pu_layout佈局上的ListView選單裡選取一個細項。然後經所選取的item值顯示出來。
1.3.5 畫面操作:
1. 此程式一開始,出現ac01_layout畫面如下:
2. 如果按下按鈕,立即切換到pu_layout畫面佈局,如下:
3. 從這List選單選取任一細項,就返回ac01_layout畫面佈局了。
1.3.6 撰寫步驟:
Step-1: 建立Android專案:ex01_02。
Step-2: 撰寫Activity的子類別:ac01,其程式碼如下:
// ---- ac01.java 程式碼 ----
package com.misoo.ex01_02;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.AdapterView.OnItemClickListener;
public class ac01 extends Activity implements OnClickListener {
private String[] data = {"Item-1", "Item-2", "Item-3"};
private Button btn, btn2;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
set_ac01_layout(); }
public void set_ac01_layout() {
setContentView(R.layout.ac01);
btn = (Button)findViewById(R.id.pu_btn);
btn.setOnClickListener(this);
btn2 = (Button)findViewById(R.id.exit_btn);
btn2.setOnClickListener(this); }
public void onClick(View v) {
if (v == btn) this.set_pu_layout();
if(v == btn2) this.finish(); }
public void set_pu_layout(){
setContentView(R.layout.pickup);
ListView lv = (ListView)findViewById(R.id.list);
ArrayAdapter arrayAdapter
= new ArrayAdapter(this, android.R.layout.simple_list_item_1, data);
lv.setAdapter(arrayAdapter);
lv.setOnItemClickListener(listener);
}
OnItemClickListener listener = new OnItemClickListener() {
public void onItemClick(AdapterView arg0, View arg1, int arg2, long arg3) {
set_ac01_layout();
TextView tv = (TextView)findViewById(R.id.tv);
tv.setText("choice: " + data[arg2]);
}};
}
Step-3: 撰寫/res/layout/ac01.xml的內容,更改為:
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="3dip"
android:text="@string/dialog" />