[Android] Activity元件

這篇文章主要是學習Activity的整理筆記(寫到最後基本上都是翻譯了....好累),原文可參考官方文章

學習建立Mobile App時,view的建立一定是最基本的,有了View,再去安排UI,以及View之間的傳遞,這些都是循序漸進的學習方向,本篇會參考官方文章,進行整理,說明建立Activity的基本要求與步驟,雖然不會比原文深入到哪,但算是提供一個參考吧?

Create Activity

首先我們要建立Activity的子類別,客製化我們要的Activity。在此子類別中,實作系統需要的回呼方法,處理Activity的生命週期。創建一個Activity類別需要實作這兩個回呼:

  • onCreate:一定要實作的方法。系統在建立Activity時會呼叫它。你必須在此實作中初始化該Activity內基本的元件,更重要的是要呼叫setContentView()方法,告訴系統該Activity的layout為何(以xml檔建立的使用者介面)。
  • onPause:使用者離開Activity時就會呼叫它。

UI實作

Android內有許多建立好的view可供使用:
  • Widgets:繼承View而來,作為可視(互動式的)的View顯示在螢幕上,像是按鈕
  • Layouts:繼承ViewGroup而來(ViewGroup也是繼承View),為其子View提供layout。
對於UI的實作可以看User Interface這文件。

在manifest裡面宣告Activity

做法是,在Manifest裡面的<application> 元素下面增加子元素<activity>

<manifest ... >
  <application ... >
      <activity android:name=".ExampleActivity" />
      ...
  </application ... >
  ...</manifest >
可以在其中添加的屬性請參考<activity>的文件。另外有些東西在上架後就不能改變了,請參考這篇文章

使用intent filter

在<activity>底下使用<intent-filter>子元素可以定義intent filter,以下是當你建立一個新的App時自動建立的第一個activity的設定:

<activity android:name=".ExampleActivity" android:icon="@drawable/app_icon">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
<action>是MAIN,表示這是這個App主要的(main)進入點。<category>是LAUNCHER表示這個activity應該列於系統的App launcher中(允許使用者啟動此activity)。

若你希望你的App是完備的(self-contained)並且不允許其他的App啟動你的activity,那你就不需要其他的intent filters了。只有一個activty應該要有"main" action以及"launcher" category。其他你不要讓別的App使用的activity都不要插入intent filter,到時你直接用explicit intent呼叫就可以了。

但如果你希望你的activity可以回應其他App的implicit intent的話(包括你自己的),那你就必須為了這些activity去定義intent filters才行。你可以參考官方文件intent filter標籤教學文章

開始Activity

將描述你想要的Activity的intent遞入startActivity()內並呼叫此方法,就可啟動activity。Intent會清楚指明你要啟動的activity或是描述你要執行的action的類型,intent也可以攜帶小量的資料以供啟動activity時使用。

如果你要啟動特定的activity,你需要指明它的名稱,例如以下是啟動名為SignInActivity的activity:

Intent intent = new Intent(this, SignInActivity.class);
startActivity(intent);
如果你是要執行某種action,比方說送出電子郵件:

Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_EMAIL, recipientArray);
startActivity(intent);
EXTRA_EMAIL extra會把存有電子郵件信箱的字串陣列加入到intent內,當email App回應時,就可以把這些信箱加入到"to"裡面。

Starting an activity for a result(為了要求某些結果而啟動activity)

有時你想從你啟動的activity中接收結果,這時,你不要用startActivity()來啟動activity,而是用startActivityForResult()方法來啟動。舉例來說,實作Activity A的回呼函數onActivityResult()以便取得由startActivityForResult()所啟動的Activity B回傳的資料,當Activity B結束的時候,Activity B會把資料存入intent傳回Activity A的startActivityForResult(int, int, intent)。

private void pickContact() {
    // Create an intent to "pick" a contact, as defined by the content provider URI
    Intent intent = new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI);
    startActivityForResult(intent, PICK_CONTACT_REQUEST);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // If the request went well (OK) and the request was PICK_CONTACT_REQUEST
    if (resultCode == Activity.RESULT_OK && requestCode == PICK_CONTACT_REQUEST) {
        // Perform a query to the contact's content provider for the contact's name
        Cursor cursor = getContentResolver().query(data.getData(),
        new String[] {Contacts.DISPLAY_NAME}, null, null, null);
        // Do something...
    }
}
在這例子中,onActivityResult先檢查request是否成功,若成功,會得到RESULT_OK,以及結果所回應的要求是否是已知的(我們要檢查據是哪一個要求所回傳的結果,才知道要怎麼處理),在此例中,requestCode應該和startActivityForResult的第二個參數是一樣的。

關閉Activity

注意:大多數的情況下系統會協助處理activity的生命週期,你不用直接呼叫這些方法去關閉activity,否則會影響到使用者經驗,除非你絕對不要使用者回到這個activtiy你才要呼叫這些方法去關閉之。

Activity生命週期之管理

實作回呼方法去管理activity的生命週期是很重要的。一個activity的生命週期是受到它與其他activities、task和back stack之間的關聯所直接影響。

Activity會存在於下列三個情況:
  1. Resumed:Activity在前台,同時使用者正使用它。
  2. Paused:另一個Activity在前台並且正被使用,此Activity仍可見,通常是有另一個Activity在頂層,而且是部分透明或是沒有遮住整個畫面。此Activity在暫停時仍存活,但在低記憶體空間狀態下時可能會被系統刪除(此Activity仍在記憶體中,所有的成員狀態和資訊都還在,並且仍然與window manager連結)。
  3. Stopped:該Activity給其他的Activity完全遮住了(該Activity目前就是在背景),Stopped的Activity還是存在的(保存在記憶體中,包括所有的狀態和成員資訊,只是沒有和window manager連結)。但使用者已經完全看不到它了,而且當系統需要記憶空間時就會刪除它。
如果Activity是Paused或是Stopped的話,系統可以要求它結束(呼叫它的finish方法)或是刪除它的程序來移除它,之後如果這個Activity要再度被開啟的話,全部都要重新建立。

實作生命週期回呼

當Activity在上面提及的各個狀態之間出入時,Activity會透過不同的回呼被告知,你可以覆寫這些回呼方法,讓它們在狀態改變時做出適當的處理。基本的生命週期方法如下:
  • onCreate
  • onStart
  • onResume
  • onPause
  • onStop
  • onDestroy
實作這些方法,你可以監控在生命週期內的三個巢狀迴圈:
  • entire lifetime:是Activity在onCreate()呼叫與onDestroy()呼叫之間的狀態,在onCreate()作為全部的設定,在onDestroy()釋放全部的資源。舉例來說,如果你的Activity會在背景運行一個tread從網路下載資料,那它應該是在onCreate()時被建置,在onDestroy()被停止。
  • visible lifetime:是Activity在onStart()呼叫與onStop()呼叫之間的狀態,在這段時間內,使用者可以看到Activity顯示在螢幕上而且可以跟它互動,舉例來說,onStop()是當新的Activity啟動,舊的已經看不到時被呼叫。在這兩個方法之間,你可以維持著用於顯示Activity給使用者看的資源,舉例來說,你可以在onStart()註冊一個BroadcastReceiver偵測會影響你UI的改變,並且當你所顯示的東西已經不再需要時於onStop()取消它的註冊。在整個生命週期中,當Activity改變它的顯示狀態時(可見或不可見),系統可能會多次呼叫onStart()和onStop()。
  • foreground lifetime:是Activity在onResume()呼叫與onPause()呼叫之間的狀態,在此期間內,Activity是在畫面上所有的Activities前面並且獲得使用者的注視。Activity可以頻繁的進出前景狀態,例如,當手機休眠或是跳出對話框時,onPause()就會被呼叫。因為這個狀態的轉換時常發生,所以在這兩個方法內的程式碼應該要輕量化,以避免拖慢轉換讓使用者等待。
(譯註:在這邊開始跳了一大段,都是表格的部分,請參考原文吧,不是太難的東西,表格的形狀可以看成是該方法屬於相同的生命週期階段)

Saving activity state

在Activity生命週期之管理的介紹中提到,在paused和stopped狀態中,還是維持著Activity的狀態。當paused和stopped時,Activity物件還在記憶體內,所有關於它的成員和目前的狀態都還是存在著。因此,使用者所做出的任何改變仍然被保持,這樣當Activity回到前台時,那些改變依舊在那。

然而,當系統為了獲取記憶體而刪除Activity時,Activity物件被刪除,所以系統無法僅憑它的狀態資料就回復它,若使用者返回時,系統必須重新創建它,然而,使用者並沒有注意系統刪除了這個Activity然後又創建它,因此,會期望Activity還是原來的樣子,在這情況下,你必須確定有關Activity狀態的資訊都有透過實作另外的回呼方法去儲存保留下來。

系統會在Activity被毀棄前呼叫onSaveInstanceState(),系統會遞一個Bundle給這個方法,讓你透過鍵值對來儲存關於Activity的資訊,諸如putString()putInt()。然後,若系統刪除你的App程序,使用者又回到你的Activity,系統會重新創建Activity並且遞送一個BundleonCreate()onRestoreInstanceState()這兩個方法。透過這兩個方法,你可以將你儲存的狀態從Bundle中抽取出來並且恢復Activity的狀態。若那沒有狀態資訊以供恢復,Bundle會給你null(這是當你的Activity是第一次創建時)。
注意:不保證onSaveInstanceState()一定會於你的activity被毀去之前被呼叫,因為也有可能是你的activity並不需要儲存狀態(像是用back鍵回上一頁,因為使用者是明確的關閉了activity)。如果系統呼叫onSaveInstanceState(),那會是在onStop()之前,有可能是在onPause()之前。
然而,即使你甚麼都不做而且也沒實作onSaveInstanceState(),有些activity的狀態可能也會透過Activity類別預設的onSaveInstanceState()實作被恢復。特別的是,預設的實作會呼叫布局內每個view的onSaveInstanceState()方法,這使得每一個view提供了關於它們自己應該被儲存的資訊。在Android framework中幾乎每一個widgets都有適當的實作了這個方法,使得任何對UI可視的改變都會自動被儲存以及在你的activity要被除新建制時被恢復。舉例來說,widget EditText會儲存任何使用者輸入的文字,CheckBox widget會儲存他是否被check了。你唯一需要做的事情是給每一個你希望他會儲存自己狀態的widget提供一個唯一的ID(用android:id屬性來提供),如果widget沒有ID,系統就不能儲存它的狀態。

雖然onSaveInstanceState()預設的實作儲存了有關你activity的UI的有用的資訊,你仍可能需要去覆寫它來儲存額外的資訊。比方說,你可能需要去儲存在activity的生命週期中的改變的成員的值(這可能跟在UI中恢復的值彼此相關,但是擁有那些UI值的成員預設上是不會被恢復的)。
你可以在你的布局內確實的停止view去儲存它的狀態,將屬性android:saveEnabled設定成false就可以了,或是呼叫方法setSaveEnabled()。通常,你不應該關閉這個功能,但你也許希望用不同的方法去恢復activty的UI的狀態。
因為預設的onSaveInstanceState()實作協助去儲存UI的狀態,如果你覆寫這個方法去儲存額外的資訊,你就應該要在執行任何步驟前呼叫其superclass的onSaveInstanceState()方法。同樣的,在你覆寫onRestoreInstanceState()時也要先呼叫其superclass實作的onRestoreInstanceState()方法,這樣預設的實作才能恢復view的狀態。
注意:因為onSaveInstanceState()不保證一定會被呼叫,你只應使用於紀錄activity暫時的狀態(UI的狀態),你永遠不應該使用它去紀錄persistent data,取而代之的,當使用者離開activity時,你應該使用onPause()去儲存persistent data(比如說應該要儲存在database的data)。
檢查你的app能否恢復它的狀態的好方法是,旋轉你的設備,這樣的話螢幕會改變它的方位
,此時,系統會為了可能將運用於新的螢幕設置的代替資源,毀棄並重製activity,以呈現新的畫面。因為這樣的理由,當你的Activity重製時,要能完整的恢復它的狀態,因為使用者在使用app的時候會經常的旋轉螢幕。

Handling configuration changes

有些設備的設定(configuration)會在運作時改變(像是螢幕方位、鍵盤取得性以及語言)。當諸如此類的改變發生時,Android會重製正運作中的activity(系統會呼叫onDestroy(),然後立刻呼叫onCreate()),這種行為是使用已提供的替代資料(向是為了不同的螢幕方位和尺寸而定義的布局)來重新載入你的app這樣的設計,來讓你的app適應新設定。

如果你適當的設計你的activity來處理如上描述的由於螢幕方位改變並恢復activity狀態造成的重啟動,你的app在其activity生命週期內面對其他事件時就能具備更大的韌性。

處理重啟動(restart)最好的方法是使用onSaveInstanceState() 和 onRestoreInstanceState() (or onCreate())來儲存與恢復activity,一如之前章節的討論。

更多有關於運作中設定改變以及你要如何處理的資訊可以讀這篇文章

Coordinating activities

當一個activity繼另一個activity之後啟動時,他們同時經歷了生命周期的轉換,第一個activity暫停(pause)並且停止(stop)(雖然,如果它仍然在背景可視的話,它不會停止),而另一個activity被創建。為避免這些activities分享儲存在disc或其他地方的data,你要了解第一個activity在第二個activity被創建前是不會完全停止的,然而,第二個activity的啟動程序會和第一個activity的停止程序重疊到。

生命週期回呼的次序是well-defined,特別當兩個activities都在相同的程序而且其中一個啟動了另一個activity,以下是當Activity A 啟動Acivity B時操作發生的次序:
  1. 執行Activity A 的 onPause() 方法。
  2. 依序執行Activity B 的 onCreate()、onStart() 以及 onResume()方法(Activity B現在受到使用者關注)。
  3. 然後如果Activity A已經再也看不到了的話,Activity A的onStop()方法會執行。

這個可預測的生命週期回呼順序允許你去管理資訊在activity之間的傳送。比方說,如果要在第一個activity停止然後接下來的activity要能夠讀取它的話,你必須要寫入database,然後,你應該是在onPause() 而不是在onStop()期間內寫入到database。


譯尾:翻譯這篇主題也三周以上了吧,真的好累啊,我想,重點就是activity的生命週期的介紹,大家對此有了解後,後面的小節是有關Activity的一些相關的注意事項,這些事項會和生命週期的各個回呼方法有點關係,最後介紹最重要的activity狀態儲存,大致上這樣看完後,應該就能努力試著去處理自己的activity了吧?希望大家有甚麼想法要跟我說的,都可以留言,覺得翻譯有問題的,告訴我哪一段,我再去看看,多謝各位的指教啦。

留言