(원문: http://developer.android.com/guide/components/loaders.html)

(위치: Develop > API Guides > App Components > Activities 
> Loaders)

(전체 번역 글 목록으로 이동하기)


로더 (Loaders)


안드로이드3.0(허니컴)에서 처음 소개된 로더(loader)는 액티비티 및 프레그먼트에서 데이터를 비동기적으로 가져오는 작업을 이전보다 더 쉽게 할 수 있도록 해줍니다. 로더의 특징은 아래와 같습니다:

  • 모든 액티비티프레그먼트에서 사용가능합니다.
  • 비동기적으로 데이터를 가져오는 기능을 제공합니다.
  • 데이터를 모니터링하고 있다가 값에 변화가 생기면 그 새로운 결과를 전달해 줍니다.
  • 설정이 변경되어 로더가 재생성될 때, 자동적으로 마지막 위치를 다시 연결함으로써, 데이터를 다시 요청하지 않아도 됩니다.


로더 API 요약


아래 표에서는 로더와 관련된 클래스 및 인터페이스에 대하여 요약하고 있습니다:

 클래스/인터페이스

  설명 

 LoaderManager

 액티비티프레그먼트에서 하나 이상의 로더를 관리하기 위해 사용하는 클래스입니다(추상 클래스라서 객체를 직접 생성할 수 없고, getLoaderManager()를 호출하여 객체를 얻습니다). 이것은 앱이 액티비티나 프레그먼트의 생명주기와 함께, 시간이 오래걸리는 작업을 관리하는 것을 도와줍니다. CursorLoader와 함께 사용되는 것이 일반적이지만, 로더를 내 앱에 맞게 만들어 사용할 수 있습니다. 

 액티비티나 프레그먼트에는 오직 하나의 LoaderManager가 있지만, 하나의 LoaderManager는 여러개의 로더를 가질 수 있습니다.

 LoaderManager
 .LoaderCallbacks

 클라이언트와 LoaderManager 간의 상호작용을 위한 콜백 인터페이스입니다. 예를 들어, onCreateLoader()는 새로운 로더를 생성할 때 호출되는 콜백 메소드입니다.

 Loader

 데이터를 비동기적으로 가져오는 기능을 하는 추상 클래스입니다. 보통은 CursorLoader를 사용하면 되지만, Loader클래스를 상속받아서 내 앱에 맞는 서브 클래스를 만들 수 있습니다. 로더는 실행되고 있는 동안에 데이터를 모니터링하고 있다가 변화가 생기면 그 결과를 전달해 줍니다.

 AsyncTaskLoader

 Loader를 상속받아 구현한 추상 클래스로서, 기존에 AsyncTask가 하던 일을 하는 로더입니다.

 CursorLoader

 AsyncTaskLoader의 서브 클래스로서, ContentResolver에 질의하여 Cursor객체를 얻어 옵니다. 이 클래스는 커서에 질의하는 표준적인 방법으로 로더의 프로토콜을 구현하는데, 앱의 UI가 멈추지 않게 하기 위하여 백그라운드 쓰레드에서 동작하는 AsyncTaskLoader를 상속받아 만들어졌습니다. ContentProvider로부터 데이터를 가져올 때는, 프레그먼트나 액티비티에서 직접 질의하는 대신에, CursorLoader를 사용하는 것이 최선의 방법입니다.


위의 표에 소개된 클래스와 인터페이스들은 내 앱에 로더를 구현하기 위해 필수적인 요소들입니다. 로더를 만들 때 위의 모든 것들이 다 사용되지는 않을 수도 있지만, 로더를 초기화하기 위해 LoaderManager는 반드시 필요하고, CursorLoader와 같이 Loader클래스를 상속받은 로더가 필요합니다. 아래 이어지는 섹션들에서 이러한 클래스 및 인터페이스를 이용하는 방법에 대하여 학습할 것입니다.


앱에서 로더 사용하기


이번 섹션에서는 안드로이드 앱에서 로더를 사용하는 방법을 설명합니다. 로더를 사용하는 앱은 보통 아래 내용들을 포함합니다:

  • 액티비티 또는 프레그먼트
  • LoaderManager 객체
  • ContentProvider로부터 데이터를 가져오기 위한 CursorLoader, 또는 LoaderAsyncTaskLoader를 상속받아 구현한 서브 클래스
  • LoaderManager.LoaderCallbacks의 구현체. 여기서 로더를 만들고 관리합니다.
  • 로더의 데이터를 보여줄 수단들. 예를 들면, SimpleCursorAdapter.
  • 데이터 제공자. 예를 들면, ContentProvider.


로더 시작하기

LoaderManager는 액티비티나 프레그먼트에서 하나 이상의 Loader객체를 관리합니다. 하나의 액티비티나 프레그먼트에 하나의 LoaderManager가 사용됩니다(싱글톤). 

보통 액티비티의 onCreate()나 프레그먼트의 onActivityCreated()에 아래와 같은 코드를 추가하여 로더를 초기화합니다:

// Prepare the loader.  Either re-connect with an existing one,
// or start a new one.
getLoaderManager
().initLoader(0, null, this);

위 코드의 initLoader()에 들어가는 인자들은 아래와 같습니다:

  • 로더 구분을 위해 사용하는 유일한 ID값. 위의 예제에서는 0입니다.
  • 로더 생성시 전달할 인자들을 담는 Bundle객체. 위의 예제에서는 null입니다.
  • LoaderManager.LoaderCallbacks의 구현객체로서, 로더에서 각종 이벤트가 발생할 때, 이 객체의 콜백메소드들이 호출됩니다. 위의 예제에서는 현재 클래스(액티비티나 프레그먼트)에 해당 인터페이스를 구현했다고 전제하고 this를 넘깁니다. 

initLoader() 호출은 로더를 초기화하고 활성화시키며, 아래와 같이 경우에 따라 로더를 재사용하거나 생성합니다:

  • 인자로 넘기는 ID로 지정된 로더가 이미 존재한다면, 그 로더를 재사용합니다.
  • 인자로 넘기는 ID로 지정된 로더가 없다면, LoaderManager.LoaderCallbacksonCreateLoader()가 호출되며, 여기서 로더를 생성하여 리턴해 줘야 합니다. 자세한 내용은 아래의 onCreateLoader 섹션에서 학습하실 수 있습니다.

어떠한 경우건, LoaderManager.LoaderCallbacks의 구현객체는 로더와 관련이 있고, 로더의 상태가 변할 때 그에 따른 콜백 메소드가 호출됩니다. initLoader()가 호출된 순간에 로더매니저가 시작된 상태이고, 요청받은 로더가 이미 존재하며 데이터를 다 가져왔다면, 시스템은 initLoader()가 끝나지 않았더라도 즉시 onLoadFinished()를 호출합니다. 따라서 이러한 상황에 대해서도 대비해야 합니다. 자세한 내용은 onLoadFinished에서 학습하실 수 있습니다.

initLoader() 메소드는 Loader객체를 리턴하지만, 굳이 멤버변수로 저장해 둘 필요는 없습니다. LoaderManager는 로더를 관리하며, 데이터 로딩을 시작하거나 정지하고, 로더의 상태 및 데이터를 유지하는 역할을 합니다. 이것은 로더의 메소드들을 직접 사용하는 일이 거의 없다는 것을 의미합니다(LoaderThrottle 샘플에서는 로더의 메소드를 사용하긴 합니다). 데이터를 로딩하는 과정에서 이벤트 발생시 해야할 일들은 대부분 LoaderManager.LoaderCallbacks의 콜백 메소드들에 구현합니다. 자세한 내용은 LoaderManager의 콜백 사용하기에서 학습하실 수 있습니다.


로더 재시작하기

위에서 학습한 바에 따르면, initLoader()를 호출할 때, 로더가 이미 있으면 그 로더를 재사용하고, 없으면 생성한다고 했습니다. 그러나 가끔은 로더가 이미 있더라도 데이터를 삭제하고 다시 시작하고 싶을 것입니다.

기존 데이터를 삭제하고 다시 시작하기 위해서는 restartLoader()를 사용하면 됩니다. 예를 들면, 아래 예제의 SearchView.OnQueryTextListener 구현 메소드는 SearchView에서 사용자의 질의문이 변경되었을 때 호출되며, 로더를 재시작합니다. 로더를 재시작함으로써, 새로운 질의를 할 수 있는 검색 필터를 사용할 수 있는 것입니다. 

public boolean onQueryTextChanged(String newText) {
   
// Called when the action bar search text has changed.  Update
   
// the search filter, and restart the loader to do a new query
   
// with this filter.
    mCurFilter
= !TextUtils.isEmpty(newText) ? newText : null;
    getLoaderManager
().restartLoader(0, null, this);
   
return true;
}


LoaderManager의 콜백 메소드들 사용하기

LoaderManager.LoaderCallbacks는 클라이언트가 LoaderManager와 상호작용할 수 있도록 해주는 콜백 인터페이스입니다.

Loader, 특히 CursorLoader는 실행이 중지된 후에도 데이터를 유지합니다. 이것은 앱이 액티비티나 프레그먼트의 onStop()onStart() 호출 사이에 데이터를 유지한다는 의미이고, 또한 액티비티나 프레그먼트가 stopped상태에서 다시 started상태가 되었을 때 데이터를 리로드(reload)할 필요가 없다는 의미이기도 합니다. LoaderManager.LoaderCallbacks 메소드들은 새로운 로더가 생성될 때나, 로더의 데이터를 그만 사용하라고 앱에게 알릴 때 등에 호출됩니다.

LoaderManager.LoaderCallbacks 인터페이스에는 아래 3개의 메소드가 정의되어 있습니다:

  • onCreateLoader() - 주어진 ID에 해당하는 로더를 생성 및 리턴합니다. 
  • onLoadFinished() - 로더의 데이터 로딩 작업이 끝났을 때 호출됩니다.
  • onLoaderReset() - 로더가 리셋될 때 현재의 데이터들을 무효화하기 위하여 호출됩니다.

위 메소드들은 이어지는 섹션에서 더 자세히 학습할 수 있습니다.


onCreateLoader

로더를 사용하기 위해 initLoader()를 호출하면, 인자로 넘긴 ID값에 해당하는 로더가 있는지 확인한 후, 해당 로더가 없으면 onCreateLoader() 콜백 메소드가 호출되며, 여기서 새로운 로더를 생성하여 리턴해야 합니다. 보통은 CursorLoader를 리턴하지만, Loader클래스를 상속받아 만든 서브클래스를 리턴할 수도 있습니다.

아래 예제에서 onCreateLoader()CursorLoader를 생성하여 리턴합니다. 여기서 CursorLoader 객체 생성시, 컨텐트 프로바이더에 질의할 때 필요한 몇가지 정보를 인자로 넘겨야 합니다:

  • uri - 가져올 데이터에 대한 URI. 데이터의 위치.
  • projection - 리턴될 컬럼(column) 목록. null을 넘기면 모든 컬럼이 리턴되지만 다소 비효율적입니다.
  • selection - 리턴될 row들의 필터링 정의. SQLWHERE 절에서 "WHERE" 단어만 제외된 구문입니다. null을 넘기면 모든 row가 리턴됩니다.
  • selectionArgs - selection에 실제값 대신 ?(물음표 표시)로 표시된 부분이 있다면, 그 부분들을 selectionArgs에 있는 값들로 치환하게 됩니다. (이것은, SQL 쿼리문과 조건에 들어가는 값들을 분리함으로써 DB의 부담을 줄이기 위함이며, 자세한 내용은 "SQL, 바인딩"으로 구글링하여 학습하시기 바랍니다.)
  • sortOrder - 리턴될 row들의 정렬 기준. SQLORDER BY 절에서 "ORDER BY"만 제외된 구문입니다. null을 넘기면 시스템의 기본 정렬 기준을 사용하는데, 보통은 기본 정렬 기준이 없습니다.

예제 코드:

 // If non-null, this is the current filter the user has provided.
String mCurFilter;
...
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
   
// This is called when a new Loader needs to be created.  This
   
// sample only has one Loader, so we don't care about the ID.
   
// First, pick the base URI to use depending on whether we are
   
// currently filtering.
   
Uri baseUri;
   
if (mCurFilter != null) {
        baseUri
= Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
                 
Uri.encode(mCurFilter));
   
} else {
        baseUri
= Contacts.CONTENT_URI;
   
}

   
// Now create and return a CursorLoader that will take care of
   
// creating a Cursor for the data being displayed.
   
String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
           
+ Contacts.HAS_PHONE_NUMBER + "=1) AND ("
           
+ Contacts.DISPLAY_NAME + " != '' ))";
   
return new CursorLoader(getActivity(), baseUri,
            CONTACTS_SUMMARY_PROJECTION
, select, null,
           
Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
}


onLoadFinished

이 메소드는 로더의 데이터 로딩 작업이 끝났을 때 호출되며, 로더가 로딩한 가장 최근의 데이터들이 날아가기(release) 전에 호출됩니다. 여기서 예전 데이터들을 사용하는 부분들을 제거해야 하지만, 로더가 새 데이터들을 다루기 전까지는 따로 저장하고 있던 데이터들을 날리면 안됩니다.

로더는 앱이 예전 데이터를 더이상 사용하지 않는다는 것을 알게되면 그 데이터를 날립니다(release). 예를 들어, 데이터가 CursorLoader로부터 얻은 cursor객체라면, 개발자가 구현하는 부분에서 close()를 호출하지 않도록 합니다. cursor객체가 CursorAdapter 안에 있다면, swapCursor()를 호출해야 합니다. 그러면 swapCursor()가 결과를 리턴하기 전까지는 예전 커서가 닫히지(closed) 않습니다.

예제 코드:

// This is the Adapter being used to display the list's data.
SimpleCursorAdapter mAdapter;
...

public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
   
// Swap the new cursor in.  (The framework will take care of closing the
   
// old cursor once we return.)
    mAdapter
.swapCursor(data);
}

 

onLoaderReset

이 메소드는 로더가 리셋될 때 현재의 데이터들을 무효화하기 위하여 호출됩니다. 이 콜백 메소드에서는 사용하던 데이터들의 참조를 제거하여 그 데이터들을 날리는 작업을 구현합니다.

아래 예제에서는 swapCursor()에 null을 넘기고 있습니다:

// This is the Adapter being used to display the list's data.
SimpleCursorAdapter mAdapter;
...

public void onLoaderReset(Loader<Cursor> loader) {
   
// This is called when the last Cursor provided to onLoadFinished()
   
// above is about to be closed.  We need to make sure we are no
   
// longer using it.
    mAdapter
.swapCursor(null);
}


예제


본 예제에서는, 주소록 컨텐트 프로바이더에 질의한 결과들을 담고 있는 리스트뷰를 화면에 보여주는 프레그먼트의 전체적인 구현내용을 보여줍니다. 여기서는 프로바이더에 질의하기 위하여 CursorLoader를 사용합니다. 

내 앱에서 사용자의 주소록 정보를 사용하기 위해서는, 매니페스트 파일에 READ_CONTACTS 퍼미션이 선언되어 있어야 합니다.

public static class CursorLoaderListFragment extends ListFragment
       
implements OnQueryTextListener, LoaderManager.LoaderCallbacks<Cursor> {

   
// This is the Adapter being used to display the list's data.
   
SimpleCursorAdapter mAdapter;

   
// If non-null, this is the current filter the user has provided.
   
String mCurFilter;

   
@Override public void onActivityCreated(Bundle savedInstanceState) {
       
super.onActivityCreated(savedInstanceState);

       
// Give some text to display if there is no data.  In a real
       
// application this would come from a resource.
        setEmptyText
("No phone numbers");

       
// We have a menu item to show in action bar.
        setHasOptionsMenu
(true);

       
// Create an empty adapter we will use to display the loaded data.
        mAdapter
= new SimpleCursorAdapter(getActivity(),
                android
.R.layout.simple_list_item_2, null,
               
new String[] { Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS },
               
new int[] { android.R.id.text1, android.R.id.text2 }, 0);
        setListAdapter
(mAdapter);

       
// Prepare the loader.  Either re-connect with an existing one,
       
// or start a new one.
        getLoaderManager
().initLoader(0, null, this);
   
}

   
@Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
       
// Place an action bar item for searching.
       
MenuItem item = menu.add("Search");
        item
.setIcon(android.R.drawable.ic_menu_search);
        item
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
       
SearchView sv = new SearchView(getActivity());
        sv
.setOnQueryTextListener(this);
        item
.setActionView(sv);
   
}

   
public boolean onQueryTextChange(String newText) {
       
// Called when the action bar search text has changed.  Update
       
// the search filter, and restart the loader to do a new query
       
// with this filter.
        mCurFilter
= !TextUtils.isEmpty(newText) ? newText : null;
        getLoaderManager
().restartLoader(0, null, this);
       
return true;
   
}

   
@Override public boolean onQueryTextSubmit(String query) {
       
// Don't care about this.
       
return true;
   
}

   
@Override public void onListItemClick(ListView l, View v, int position, long id) {
       
// Insert desired behavior here.
       
Log.i("FragmentComplexList", "Item clicked: " + id);
   
}

   
// These are the Contacts rows that we will retrieve.
   
static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
       
Contacts._ID,
       
Contacts.DISPLAY_NAME,
       
Contacts.CONTACT_STATUS,
       
Contacts.CONTACT_PRESENCE,
       
Contacts.PHOTO_ID,
       
Contacts.LOOKUP_KEY,
   
};
   
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
       
// This is called when a new Loader needs to be created.  This
       
// sample only has one Loader, so we don't care about the ID.
       
// First, pick the base URI to use depending on whether we are
       
// currently filtering.
       
Uri baseUri;
       
if (mCurFilter != null) {
            baseUri
= Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
                   
Uri.encode(mCurFilter));
       
} else {
            baseUri
= Contacts.CONTENT_URI;
       
}

       
// Now create and return a CursorLoader that will take care of
       
// creating a Cursor for the data being displayed.
       
String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
               
+ Contacts.HAS_PHONE_NUMBER + "=1) AND ("
               
+ Contacts.DISPLAY_NAME + " != '' ))";
       
return new CursorLoader(getActivity(), baseUri,
                CONTACTS_SUMMARY_PROJECTION
, select, null,
               
Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
   
}

   
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
       
// Swap the new cursor in.  (The framework will take care of closing the
       
// old cursor once we return.)
        mAdapter
.swapCursor(data);
   
}

   
public void onLoaderReset(Loader<Cursor> loader) {
       
// This is called when the last Cursor provided to onLoadFinished()
       
// above is about to be closed.  We need to make sure we are no
       
// longer using it.
        mAdapter
.swapCursor(null);
   
}
}


다른 예제들

ApiDemos 샘플앱에 로더를 사용하는 방법에 대한 몇가지 다른 예제들이 있습니다:

  • LoaderCursor - 위의 예제 코드의 완성 버전.
  • LoaderThrottle - 데이터가 변함에 따라 발생하는 컨텐트 프로바이더로의 질의의 수를 어떻게 효율적으로 줄일 수 있는지 보여주는 예제.

안드로이드 SDK의 디렉토리 아래에 있는 samples 디렉토리에 각 OS 버전별 샘플앱들이 있습니다.


Posted by 개발자 김태우
,

(원문: http://developer.android.com/guide/components/fragments.html)

(위치: Develop > API Guides > App Components > Activities 
> Fragments)

(전체 번역 글 목록으로 이동하기)


프레그먼트 (Fragments)


프레그먼트는 액티비티가 담당하는 전체 사용자 인터페이스 중 독립적인 일부분을 맡습니다. 여러개의 화면이 하나의 액티비티에 표현되도록 하기 위해, 하나의 액티비티는 여러개의 프레그먼트를 가질 수 있습니다. 프레그먼트는 액티비티처럼 생명주기를 갖으며 사용자와 상호작용할 수 있습니다. 즉, 액티비티의 한 모듈이라고 볼 수 있습니다. 프레그먼트는 액티비티가 실행중일 때 액티비티에 추가 또는 제거될 수 있으며, 다른 액티비티들에 재사용될 수도 있습니다.

프레그먼트는 액티비티 안에 들어가며, 프레그먼트의 생명주기는 그 액티비티의 생명주기에 직접적으로 영향을 받습니다. 예를 들어, 액티비티가 paused상태가 되면 그 안에 있는 모든 프레그먼트들도 paused상태가 되며, 액티비티가 종료되면 프레그먼트들도 종료됩니다. 하지만 액티비티가 resumed상태(액티비티 생명주기 참고)로 있는 동안에, 그 안에 있는 각각의 프레그먼트는 독립적으로 추가되거나 제거될 수 있습니다. 액티비티에 추가될 때, 액티비티에 의해 관리되는 백스택에 추가될 수도 있습니다. 프레그먼트에서 다른 프레그먼트를 실행했다가, 뒤로버튼을 누르면 다시 이전 프레그먼트가 보이게 하고 싶다면, 백스택을 사용합니다.

프레그먼트가 액티비티 레이아웃의 일부가 되길 원한다면, 액티비티의 뷰 계층구조 안에서 ViewGroup 안에 해당 프레그먼트가 정의한 뷰 레이아웃이 들어가게 됩니다. 액티비티의 레이아웃 xml파일 안에 <fragment>요소로 추가할 수도 있고, 소스코드에서 동적으로 프레그먼트 객체를 생성하여 액티비티의 레이아웃에 있는 ViewGroup 객체 안에 추가할 수도 있습니다. 하지만 프레그먼트가 반드시 액티비티 레이아웃의 일부가 되어야 하는 것은 아닙니다. 프레그먼트는 눈에 보이는 UI 없이 액티비티가 필요로 하는 작업을 수행할 수도 있습니다. 

본 문서에서는 프레그먼트를 이용하여 어떻게 앱을 개발할 수 있는지에 대해 설명할 것입니다. 여기에는, 프레그먼트를 액티비티의 백스택에 추가할 때 상태를 어떻게 유지할 수 있는지, UI에서 발생한 이벤트를 어떻게 액티비티 및 다른 프레그먼트에 전달할 수 있는지, 액티비티의 액션바와 어떻게 상호작용할 수 있는지 등이 포함됩니다.


디자인 철학


프레그먼트는 태블릿과 같은 큰 화면 디바이스에서 동적이고 유연한 UI를 지원하기 위해 안드로이드 3.0(API레벨 11, 허니컴)부터 추가되었습니다. 태블릿의 화면은 스마트폰보다 훨씬 크기 때문에, UI 컴포넌트가 들어갈 공간이 상대적으로 더 많습니다. 따라서 스마트폰용으로 개발된 앱이 태블릿을 적절히 지원하기 위해서는 레이아웃의 수정이 필요한데, 프레그먼트를 이용하면 레이아웃의 수정 없이 액티비티의 전체 화면을 분할하여 프레그먼트들을 배치할 수 있는 것입니다. 

뉴스앱을 예로 들어 보겠습니다. 기사 목록을 보여주는 프레그먼트가 왼쪽에 있고, 기사 내용을 보여주는 프레그먼트가 오른쪽에 있다면, 화면의 전환 없이 한 화면에서 기사를 선택하여 읽을 수 있을 것입니다. 프레그먼트들은 각각 생명주기 콜백 메소드들을 구현할 수 있고, 각각이 따로 사용자와 상호작용할 수 있습니다. 따라서 기사 목록과 기사 내용을 각각의 화면에 보여주는 대신에 아래 그림1의 태블릿 레이아웃에서 보이는 바와 같이 한 화면에 보여줄 수 있는 것입니다.

프레그먼트는 모듈화가 되어 액티비티에서 재사용 가능하도록 개발되어야 합니다. 프레그먼트는 독립적인 존재로서 레이아웃을 정의하고, 생명주기를 갖기 때문에, 여러개의 액티비티에 재사용될 수 있습니다. 따라서, 재사용 가능하도록 설계되어야 하고, 다른 프레그먼트를 직접 참조하는 등의 의존성을 없애야 합니다. 이것은 다양한 크기의 스크린에서 그에 맞게 프레그먼트들을 조합할 수 있도록 하기 위해 중요합니다. 태블릿과 스마트폰을 동시에 지원하는 앱을 설계할 때, 각각의 레이아웃에 프레그먼트들을 적절히 배치할 수 있습니다. 예를 들어, 스마트폰에서는 태블릿과는 다르게 화면이 작기 때문에 두개의 프레그먼트를 한꺼번에 보여주기 어려울 것이며, 각각의 프레그먼트를 각각의 액티비티에 담아 별도의 화면에서 보여줄 수 있을 것입니다.

그림 1. 태블릿과 스마트폰에서, 프레그먼트로 정의된 2개의 UI모듈이 보여지는 예.


그림1을 보면
, 태블릿에서는 액티비티A에 두개의 프레그먼트를 모두 보여주고 있고, 스마트폰에서는 크기가 작기 때문에 액티비티A에서 프레그먼트A를, 액티비티B에서 프레그먼트B를 보여주고 있습니다. 프레그먼트를 재사용함으로써 태블릿과 스마트폰을 모두 지원하는 앱을 만들 수 있는 것입니다.

다양한 스크린을 지원하기 위해 프레그먼트들을 어떻게 설계하고 조합할 것인지에 대한 자세한 내용은 태블릿과 스마트폰 지원하기에서 학습하실 수 있습니다. 


프레그먼트 만들기


프레그먼트를 만들기 위해서는 Fragment 클래스나 이미 만들어진 Fragment의 서브클래스를 상속받아서, 생명주기 콜백 메소드나 그 외 필요한 기능들을 구현하면 됩니다. 프레그먼트의 콜백 메소드는 onCreate(), onStart(), onPause(), onStop() 등으로서 액티비티의 콜백 메소드들과 유사합니다. 그래서 액티비티 기반으로 이미 개발된 앱을 프레그먼트 기반으로 바꾸려고 한다면, 액티비티의 콜백 메소드에 구현된 내용들을 거의 그대로 프레그먼트의 콜백 메소드로 옮기기만 하면 됩니다.

보통 아래 몇가지 콜백 메소드들을 구현합니다.

onCreate()
프레그먼트가 생성될 때 호출됩니다. 여기서는 프레그먼트가 살아있는 동안 사용하게 될 중요한 구성요소들을 셋팅하며, 이는 프레그먼트가 paused나 stopped상태가 되었다가 다시 resumed상태가 될 때 유지가 되길 원하는 것들입니다.

onCreateView()
프레그먼트의 사용자 UI가 화면에 보여지는 첫번째 시점에 호출됩니다. 여기서는 화면에 보여줄 뷰, 즉 프레그먼트의 레이아웃을 생성하여 리턴해줘야 합니다. 만약 프레그먼트가 UI를 제공하지 않는다면, null을 리턴하면 됩니다.

onPause()
사용자가 해당 프레그먼트에서 벗어날 때 가장 먼저 호출되는 콜백 메소드입니다. 이후에 다시 resumed상태가 될 수도 있고, 아예 종료될 수도 있습니다. 따라서 여기서는 유지되길 원하는 값이 있다면 저장해 주는 일을 하도록 합니다.

대부분의 앱에서 프레그먼트를 만들 때, 위의 세가지 콜백 메소드를 구현하지만, 그 외에도 콜백 메소드가 몇가지 더 있습니다. 자세한 내용은 프레그먼트의 생명주기 다루기에서 학습하실 수 있습니다.

위에서도 언급했듯이 프레그먼트를 만들 때는 기본적으로 Fragment클래스를 상속받을 수 있지만, 이미 만들어진 서브클래스를 상속 받을 수도 있는데 그 몇가지를 소개합니다:

DialogFragment
현재 화면 위에 뜨는 다이얼로그를 구현한 프레그먼트입니다. 이것은 기존에 액티비티에서 AlertDialog를 이용해 다이얼로그를 띄우던 방식을 대체할 수 있습니다. 액티비티에 의해 관리되는 백스택에 들어가기 때문에, 뒤로 버튼을 눌렀을 때, 다이얼로그의 바닥에 보이는 이전 화면으로 돌아갈 수 있습니다.

ListFragment
ListActivity처럼 어댑터에 의해 관리되는 리스트를 보여주는 프레그먼트입니다. onListItemClick()과 같은 리스트뷰 관련 콜백 메소드들을 제공합니다.

PreferenceFragment
PreferenceActivity처럼 Preference객체를 보여주는 프레그먼트입니다. 앱의 "설정" 화면을 만들 때 사용하 수 있습니다.


사용자 인터페이스(UI) 추가하기

프레그먼트는 보통 액티비티의 사용자 인터페이스의 일부로 사용되고, 독립적인 레이아웃을 갖습니다. 

프레그먼트의 레이아웃을 구현하기 위해서는, 안드로이드 시스템이 프레그먼트의 레이아웃을 화면에 그리려고 할때 호출되는 onCreateView() 콜백 메소드를 구현하여, 여기서 프레그먼트 레이아웃의 루트가 되는 View 객체를 리턴해주면 됩니다. 

메모: 만약 ListFragment를 상속받아 프레그먼트를 만들었다면, 기본적으로 onCreateView()에서 ListView객체를 리턴하고 있기 때문에, onCreateView()를 구현하지 않아도 됩니다.

onCreateView()에서 레이아웃을 리턴하기 위해서, XML로 정의된 레이아웃 리소스로부터 뷰객체를 만들 수 있습니다. 이를 위하여 onCreateView()에서는 LayoutInflater객체를 매개변수로 제공해 줍니다.

아래 예제에서는 리소스에 있는 example_fragment.xml파일을 이용하여 레이이웃을 만드는 것을 보여줍니다.

public static class ExampleFragment extends Fragment {
   
@Override
   
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             
Bundle savedInstanceState) {
       
// Inflate the layout for this fragment
       
return inflater.inflate(R.layout.example_fragment, container, false);
   
}
}

위 예제의 container 매개변수는, 프레그먼트의 레이아웃이 들어가게 되는 부모 ViewGroup객체(액티비티 레이이웃의 일부)를 가리킵니다. 그리고 savedInstanceState 매개변수는 프레그먼트가 resumed상태가 될 때 이전 상태를 복구하기 위해 제공되는 Bundle객체입니다. (자세한 내용은 프레그먼트 생명주기 다루기에서 학습하실 수 있습니다.)

inflate()메소드는 3개의 인자를 받습니다:

  • 만들고자 하는 레이아웃의 리소스 ID.
  • 프레그먼트 레이아웃의 부모 ViewGroup객체. 레이아웃을 자식으로 추가하거나, 레이아웃이 뭔지 미리 알려줌으로써 레이아웃에 LayoutParameter를 만들어 셋팅하기 위해 필요합니다.
  • 위의 두번째 매개변수로 받은 ViewGroup객체에 추가할지 여부(boolean). 위 예제에서는 상위 클래스의 onCreateView()에서, 리턴하는 뷰를 container에 추가하는 로직이 이미 구현되어 있기 때문에 이 값이 false입니다.

지금까지는 화면에 보여질 레이아웃을 갖는 프레그먼트를 어떻게 만드는지에 대해서 알아봤습니다. 다음으로 프레그먼트를 액티비티에 어떻게 추가하는지에 대해 알아보겠습니다.

레이아웃 생성: 위 예제에서 R.layout.example_fragment는 앱의 리소스로 저장된 example_fragment.xml파일을 가리킵니다. 레이아웃 xml파일을 만드는 방법에 대한 자세한 내용은 사용자 인터페이스에서 학습하실 수 있습니다. 


프레그먼트를 액티비티에 추가하기

프레그먼트는 액티비티의 뷰 계층구조의 일부로 추가되어, 액티비티 UI의 한 부분을 구성합니다. 액티비티 레이아웃에 추가되는 방법은 2가지가 있습니다.

● 액티비티의 레이아웃 파일에 프레그먼트 요소 선언하기

이 경우, 뷰그룹이나 뷰위젯을 추가할 때처럼 레이아웃 속성값을 지정할 수 있습니다. 아래 예제는 액티비티 레이아웃에 2개의 프레그먼트를 추가하는 것을 보여줍니다:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   
android:orientation="horizontal"
   
android:layout_width="match_parent"
   
android:layout_height="match_parent">
   
<fragment android:name="com.example.news.ArticleListFragment"
           
android:id="@+id/list"
           
android:layout_weight="1"
           
android:layout_width="0dp"
           
android:layout_height="match_parent" />
   
<fragment android:name="com.example.news.ArticleReaderFragment"
           
android:id="@+id/viewer"
           
android:layout_weight="2"
           
android:layout_width="0dp"
           
android:layout_height="match_parent" />
</LinearLayout>

위 예제에서 <fragment>의 android:name 속성에는 Fragment클래스의 패키지명 포함한 이름을 지정합니다.

시스템은 액티비티의 레이아웃을 생성할 때, 각 프레그먼트의 onCreateView()를 호출하여 리턴받은 뷰를 <fragment>요소 대신에 추가합니다.

메모: 각 프레그먼트는 유일한 식별자를 필요로 합니다. 이는 액티비티가 재시작될 때 시스템이 프레그먼트를 식별하기 위함이며, 내 액티비티에서 프레그먼트를 찾아 제거하거나 뭔가 작업을 수행하기 위해서이기도 합니다. 프레그먼트에 식별자를 제공하는 방법은 아래와 같이 3가지가 있습니다.

  • android:id에 유일한 ID를 지정합니다.
  • android:tag에 유일한 문자열을 지정합니다.
  • 위의 2가지를 모두 지정하지 않았다면, 시스템은 부모뷰의 ID를 사용합니다.

소스코드에서 ViewGroup객체에 프레그먼트 추가하기

액티비티가 실행되고 있는 동안, 프레그먼트를 액티비티의 레이아웃에 추가할 수 있습니다. 그러기 위해서는 프레그먼트가 들어갈 ViewGroup객체를 지정해야 합니다. 

프레그먼트를 액티비티에 추가하거나 제거하거나 다른 프레그먼트로 대체하거나 하려면, FragmentTransaction을 사용해야 합니다. FragmentTransaction객체는 액티비티에서 아래와 같이 얻을 수 있습니다.

FragmentManager fragmentManager = getFragmentManager()
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

그리고 나서 아래와 같이 add()메소드를 사용하여 프레그먼트를 추가할 수 있습니다. 

ExampleFragment fragment = new ExampleFragment();
fragmentTransaction
.add(R.id.fragment_container, fragment);
fragmentTransaction
.commit();

add()메소드의 첫번째 인자는 프레그먼트가 추가될 부모뷰(ViewGroup)의 리소스 ID이고, 두번째 인자는 추가할 프레그먼트입니다. 

FragmentTransaction객체에 뭔가 변화가 생겼을 때는, commit()메소드를 호출해야 그것이 실제로 반영됩니다.


UI 없이 프레그먼트 추가하기

위에서는, UI를 제공하는 프레그먼트를 어떻게 액티비티에 추가할 것인가를 학습했지만, UI 없이 액티비티에서 백그라운드 작업을 하기 위해 프레그먼트를 활용할 수도 있습니다.

UI 없는 프레그먼트를 추가하려면, add(Fragment, String) 메소드를 호출하면 됩니다. 여기서 두번째 인자인 String은 프레그먼트를 구분하기 위한 유일한 "태그"이며, 뷰의 리소스 ID는 인자로 들어가지 않습니다. 이 메소드를 호출함으로써 프레그먼트를 액티비티에 추가하기는 하지만, 뷰 레이아웃을 필요로 하지는 않기 때문에 프레그먼트의 onCreateView()가 호출되지 않습니다. 따라서 onCreateView() 콜백 메소드를 구현할 필요도 없습니다.

UI가 없는 프레그먼트를 추가할 때 태그를 반드시 넣어야만 하는 것은 아닙니다. 그리고 태그는 UI가 있는 프레그먼트에도 넣을 수 있습니다. 다만, UI가 없는 프레그먼트에서는 "태그"가 프레그먼트를 구분할 수 있는 유일한 방법이기 때문에 보통 태그를 넣습니다. 그리고 나서 프레그먼트를 찾을 때는 findFragmentByTag()메소드를 호출하면 됩니다.

프레그먼트로 백그라운드 작업을 하는 예는 FragmentRetainInstance.java 샘플에서 확인하실 수 있습니다.


프레그먼트 다루기 


액티비티에서 프레그먼트를 다루기 위해서는, FragmentManager객체가 필요하며, 이는 getFragmentManager()를 호출하여 얻을 수 있습니다.

FragmentManager객체로 할 수 있는 몇가지는 아래와 같습니다:

  • 액티비티에 있는 프레그먼트를 얻고자 할때는, findFragmentById()findFragmentByTag()를 사용합니다. findFragmentById()는 UI를 제공하는 프레그먼트의 경우 사용하고, findFragmentByTag()는 UI의 제공유무와 상관없이 사용할 수 있습니다.
  • 사용자가 뒤로버튼을 눌렀을 때와 같이 프레그먼트를 백스택에서 제거할 때는 popBackStack()을 호출합니다.
  • 백스택에 변화가 생기는 순간을 잡기 위해 리스너를 등록하는 메소드는 addOnBackStackChangedListener()입니다.

FragmentManager객체의 메소드들에 대한 더 자세한 내용은 FragmentManager 클래스 문서에서 학습하실 수 있습니다.

이전 섹션에서 학습한 바와 같이, 프레그먼트를 추가 및 제거하는 등의 작업을 할 때 필요한 FragmentTransactionFragmentManager에서 얻을 수 있습니다.


프레그먼트 트랜잭션 수행하기


액티비티에서 프레그먼트를 사용함으로써 얻을 수 있는 가장 큰 이점은, 하나의 액티비티 안에서 사용자와의 상호작용을 통해 프레그먼트들을 추가하거나 제거하거나 대체하거나 하는 등의 작업을 할 수 있는 것입니다. 이러한 액티비티의 변화를 트랜잭션이라 칭하며, FragmentTransaction의 API들을 호출함으로써 트랜잭션을 수행할 수 있습니다. 액티비티에 의해 관리되는 백스택에 해당 트랜잭션을 저장함으로써, 사용자가 뒤로버튼을 누르면 이전 액티비티가 보이게 하는 등의 처리를 할 수 있습니다. (이는 액티비티 백스택의 동작과 유사합니다.)

아래와 같은 방법으로 FragmentManager로부터 FragmentTransaction객체를 얻을 수 있습니다:

FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

한번의 트랜잭션은, 동시에 처리되기 위한 변화들의 집합을 의미합니다. FragmentTransaction객체의 add(), remove(), replace()와 같은 메소드들을 호출함으로써 원하는 변화들을 지정할 수 있고, commit() 메소드를 호출함으로써 그 변화들의 집합을 액티비티에 반영할 수 있습니다.

프레그먼트의 백스택에 트랜잭션을 적용하기 위해서는, commit()을 호출하기 전에 addToBackStack()을 호출해야 합니다. 프레그먼트의 백스택은 액티비티에 의해 관리되며, 사용자가 뒤로버튼을 눌렀을 때 이전 프레그먼트가 보이도록 해주는 등의 작업을 위해 필요합니다.

아래 예제는 이전 프레그먼트를 다음 프레그먼트로 대체하면서, 백스택을 이용해 이전 상태로 돌아갈 수 있도록 하는 방법을 보여줍니다:

// Create new fragment and transaction
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();

// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction
.replace(R.id.fragment_container, newFragment);
transaction
.addToBackStack(null);

// Commit the transaction
transaction
.commit();

위 예제에서 newFragment는, R.id.fragment_container가 가리키는 레이아웃 안에 있는 프레그먼트를 대체합니다. 그리고 addToBackStack()을 호출함으로써 트랜잭션을 프레그먼트 백스택에 저장하여, 사용자가 뒤로버튼을 눌렀을 때 이전 프레그먼트가 다시 보일 수 있게 됩니다.

FragmentTransaction변화를 추가하는 순서는 별로 중요하지 않지만 아래 2가지는 지켜져야 합니다.

  • commit() 메소드가 제일 나중에 호출되어야 합니다.
  • 동일한 부모뷰 안에 2개 이상의 프레그먼트를 추가하는 경우에는, 추가하는 순서에 따라 먼저 추가한 프레그먼트가 뷰 계층구조에서 상위에 위치하게 됩니다.

만약 프레그먼트를 제거하는 트랜잭션을 수행할 때 addToBackStack()을 호출하지 않고 commit()을 호출한다면, 해당 프레그먼트는 종료되며(destroyed) 다시 이전상태로 돌아갈 수 없습니다. 반면에 addToBackStack()을 호출했다면, 프레그먼트는 stopped상태가 되며 사용자에 의해 이전상태로 돌아갈 때 다시 resumed상태가 됩니다.

팁: 프레그먼트 트랜잭션에 의해 프레그먼트가 바뀔 때 애니메이션을 적용하기 위해 setTransition() 메소드를 사용할 수 있습니다.

commit() 메소드 호출 직후 해당 트랜잭션이 바로 적용되는 것은 아닙니다. 안드로이드 시스템은 UI의 변화를 액티비티의 UI 쓰레드("메인" 쓰레드)에서 수행하며, commit() 호출시 해당 트랜잭션에 의한 UI 변화를 메인 쓰레드의 작업 큐(queue)에 추가하기 때문에, 이미 다른 작업들이 대기하고 있다면 그 작업들이 먼저 수행됩니다. 하지만, 현재 UI 쓰레드에서 executePendingTransactions()를 호출함으로써 commit() 호출 직후 해당 트랜잭션이 적용되도록 할 수도 있습니다. 그러나 현재 쓰레드가 다른 쓰레드들과 의존적 상관관계가 있지 않다면, executePendingTransactions()는 별로 필요하지 않을 것입니다. 다시 말해서, 메인 쓰레드의 작업큐에 다른 UI 쓰레드에 의해 추가된 "UI의 변화"가 있을 경우, 반드시 이보다 먼저 프레그먼트 트랜잭션을 반영해야 한다면 executePendingTransactions()를 수행하면 됩니다. 하지만 아마도 이러한 경우는 거의 없을 것입니다.

주의사항: commit() 메소드는 액티비티의 상태가 저장되는 시점(사용자가 액티비티를 벗어날 때) 이전에만 호출될 수 있습니다. 만약 그 이후에 호출하면 예외가 발생됩니다. 왜냐하면, commit() 메소드 호출 후의 액티비티 상태가 저장될 수 없기 때문이며, 이를 무시하고서라도 commit을 하고 싶다면, commitAllowingStateLoss()를 호출하면 됩니다.


액티비티와 소통하기


비록 프레그먼트가 액티비티에 의존적이지 않은 독립적인 존재이고 여러 액티비티에 재사용될 수 있는 존재이긴 하더라도, 그를 포함하고 있는 액티비티와 직접적으로 소통할 수 있습니다.

특히, 프레그먼트는 getActivity()를 호출하여 액티비티 객체를 얻을 수 있고 그것을 통해 아래와 같이 액티비티의 뷰를 얻을 수도 있습니다:

View listView = getActivity().findViewById(R.id.list);

이와 마찬가지로, 액티비티도 FragmentManagerfindFragmentById()findFragmentByTag() 메소드를 이용하여 아래와 같이 프레그먼트 객체를 얻을 수 있습니다:

ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);


액티비티에 이벤트 콜백 생성하기

프레그먼트가 액티비티와 이벤트를 공유해야 하는 경우 좋은 방법은, 이벤트를 받을 프레그먼트에 콜백 인터페이스를 정의하고 액티비티에서 그것을 구현하도록 하는 것입니다. 액티비티가 구현한 콜백 메소드가 호출되면, 다른 프레그먼트에 관련 정보를 공유할 수도 있습니다.

예를 들어, 뉴스앱의 액티비티에 두 개의 프레그먼트를 보여줘야 하는데 하나(프레그먼트 A)는 뉴스 목록을 보여주고 다른 하나(프레그먼트 B)는 뉴스 내용을 보여줘야 한다면, 프레그먼트 A에서 선택된 뉴스 항목을 액티비티에게 알려줘야 하고, 액티비티는 그것을 프레그먼트 B에게 공유하여 그 뉴스의 내용을 보여줘야 할 것입니다. 아래 예제에서는 FragmentAOnArticleSelectedListener를 정의하고 있습니다:

public static class FragmentA extends ListFragment {
   
...
   
// Container Activity must implement this interface
   
public interface OnArticleSelectedListener {
       
public void onArticleSelected(Uri articleUri);
   
}
   
...
}

그리고 FragmentA객체를 포함하고 있는 액티비티는 FragmentA로부터 전달받은 이벤트 정보를 FragmentB로 전달하기 위해 OnArticleSelectedListeneronArticleSelected()를 오버라이드하여 구현합니다. 그리고나서 FragmentAonAttach() 콜백 메소드를 구현해야 하는데, 여기서 매개변수로 넘겨받은 액티비티를 OnArticleSelectedListener로 타입캐스팅하여 멤버변수로 저장합니다:

public static class FragmentA extends ListFragment {
   
OnArticleSelectedListener mListener;
   
...
   
@Override
   
public void onAttach(Activity activity) {
       
super.onAttach(activity);
       
try {
            mListener
= (OnArticleSelectedListener) activity;
       
} catch (ClassCastException e) {
           
throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");
       
}
   
}
   
...
}

만약 액티비티가 OnArticleSelectedListener를 구현하고 있지 않다면 ClassCastException 예외가 발생될 것이며, 제대로 구현했다면 mListener 멤버변수에 해당 인터페이스에 대한 구현 객체가 저장되어 이벤트 발생시 콜백 메소드를 호출함으로써 액티비티로 이벤트 정보를 전달할 수 있을 것입니다. 아래 예제에서는, ListFragment를 상속받아 FragmentA를 구현하고, 사용자가 리스트 아이템을 클릭했을 때 호출되는 onListItemClick()을 구현하여, 여기서 액티비티에게 이벤트 정보를 전달하기 위해 mListeneronArticleSelected()를 호출합니다:

public static class FragmentA extends ListFragment {
   
OnArticleSelectedListener mListener;
   
...
   
@Override
   
public void onListItemClick(ListView l, View v, int position, long id) {
       
// Append the clicked item's row ID with the content provider Uri
       
Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id);
       
// Send the event and Uri to the host activity
        mListener
.onArticleSelected(noteUri);
   
}
   
...
}

위의 예제에서 onListItemClick()id 매개변수는, 리스트 정보를 앱의 컨텐트 프로바이더로부터 얻어오는 경우, 선택된 항목의 id 정보를 전달해줍니다. 

컨텐트 프로바이더를 사용하는 방법에 대한 더 자세한 내용은 컨텐트 프로바이더에 대하여에서 학습하실 수 있습니다.


액션바에 항목 추가하기

프레그먼트에서는 onCreateOptionsMenu() 콜백 메소드를 구현함으로써 액티비티의 옵션 메뉴에 들어갈 항목들을 지정할 수 있습니다(액션바의 옵션 메뉴 버튼을 클릭했을 때 보여지는 메뉴). 하지만 프레그먼트가 옵션 메뉴를 사용한다는 것을 시스템에게 알려주기 위해 프레그먼트의 onCreate()에서 setHasOptionsMenu()을 호출해야만, 사용자가 옵션 메뉴 버튼을 클릭했을 때 onCreateOptionsMenu() 콜백 메소드가 호출됩니다.

프레그먼트에 의해 추가된 옵션 메뉴들은 기존에 액티비티 또는 다른 프레그먼트에 의해 추가된 옵션 메뉴들 아래로 추가됩니다. 그리고 프레그먼트에서는 옵션 메뉴가 선택되었을 때 호출되는 onOptionsItemSelected() 콜백 메소드도 오버라이드하여 구현할 수 있습니다.

registerForContextMenu()를 호출함으로써 프레그먼트 내의 특정 뷰가 컨텍스트 메뉴를 제공하도록 할 수 있습니다(컨텍스트 메뉴는 뷰를 롱클릭했을 때 뜨는 리스트 다이얼로그입니다). 사용자가 컨텍스트 메뉴를 띄울 때, 프레그먼트의 onCreateContextMenu() 콜백 메소드가 호출됩니다. 그리고 사용자가 컨텍스트 메뉴의 항목을 선택할 때, onContextItemSelected()가 호출됩니다.

메모: 프레그먼트에서 옵션 메뉴나 컨텍스트 메뉴에서 항목 선택시 실행할 작업을 구현해 놨다 하더라도, 액티비티에서 구현한 작업이 먼저 실행됩니다. 선택된 메뉴 항목에 대한 작업을 액티비티에서 다루고 있지 않을 경우에, 해당 이벤트 정보가 프레그먼트의 콜백 메소드로 전달됩니다. 액티비티의 onOptionsItemSelected()onContextItemSelected()boolean을 리턴하도록 되어 있는데, 이 값이 true면 메뉴 항목에 대한 처리를 한 것이고, false면 하지 않은 것입니다.

메뉴와 관련하여 더 자세한 내용은 메뉴액션바 문서에서 학습하실 수 있습니다.


프레그먼트의 생명주기 다루기


프레그먼트의 생명주기를 관리하는 것은 액티비티의 생명주기를 관리하는 것과 매우 유사합니다. 액티비티처럼 프레그먼트도 3가지의 상태를 갖습니다.

Resumed
프레그먼트가 액티비티에서 보여지는 상태입니다.

Paused
다른 액티비티가 전면(foreground)에 나오며 포커스를 가져갔지만 (resumed상태가 되었지만), 일부가 투명하거나 전체화면을 덮지 않으면서 여전히 이전 액티비티가 보이는 상태가 된다면, 이전 액티비티에서 보여지고 있던 프레그먼트의 상태는 paused상태가 됩니다.

Stopped
프레그먼트가 액티비티에서 보여지지 않는 상태입니다. 액티비티가 stopped상태로 되었거나, 프레그먼트가 액티비티에서 제거되었으나 백스택에 추가된 상태입니다. stopped상태의 프레그먼트는 여전히 살아있으며 멤버 변수 및 상태정보들이 시스템에 의해 유지됩니다. 하지만 사용자에게 보여지지 않는 상태이며, 액티비티가 종료되면 같이 종료됩니다.

액티비티에서처럼, 프레그먼트도 Bundle객체를 이용하여 상태를 저장 및 복구할 수 있습니다. 액티비티의 프로세스가 종료되었다가 다시 생성될 때 프로세스의 상태를 복구할 수 있다는 것입니다. 프레그먼트의 onSaveInstanceState()을 구현함으로써 상태를 저장할 수 있고, onCreate()onCreateView()onActivityCreated()에서 상태를 복구할 수 있습니다. 상태를 저장 및 복구하는 방법에 대해서는 액티비티 상태 저장하기에서 학습하실 수 있습니다.

액티비티 생명주기와 프레그먼트 생명주기의 가장 큰 차이점은 각각의 백스택에 저장되는 방식입니다. 액티비티는 stopped상태로 될 때 시스템에 의해 관리되는 액티비티 백스택에 위치하게 되며, 사용자가 뒤로버튼을 눌렀을 때 이전 액티비티가 재시작됩니다(자세한 내용은 태스크와 백스택에서 학습하실 수 있습니다). 하지만, 프레그먼트는 액티비티로부터 제거되는 트랜잭션이 수행되는 중에 addToBackStack()이 호출되어야만, 액티비티에 의해 관리되는 백스택에 저장되어 이전 프레그먼트를 재실행할 수 있습니다.

이외에, 프레그먼트의 생명주기를 관리하는 것은 액티비티의 생명주기를 관리하는 것과 매우 유사합니다. 따라서, 액티비티의 생명주기 관리하기에서 학습한 내용들을 프레그먼트에도 유사하게 적용할 수 있습니다. 하지만, 액티비티의 생명주기 흐름이 프레그먼트의 생명주기 흐름에 어떠한 영향을 끼치는지에 대해서는 충분히 이해할 필요가 있습니다.

주의사항: 프레그먼트에서 Context객체를 얻을때는 getActivity()를 사용할 수 있는데, 프레그먼트가 액티비티에 포함되어 있을때만(attached) 사용할 수 있다는 점에 주의해야 합니다. 액티비티가 아직 포함되지 않았거나, 프레그먼트가 액티비티로부터 제거된 후에 getActivity()를 호출하면 null이 리턴됩니다.


그림 3. 액티비티의 생명주기가 프레그먼트의 생명주기에 미치는 영향


액티비티의 생명주기와 연계하여 구현하기


액티비티의 생명주기는 그 안에 있는 프레그먼트들의 생명주기에 직접적인 영향을 줍니다. 그래서 프레그먼트의 생명주기 콜백 메소드는 액티비티의 생명주기 콜백 메소드와 매우 유사합니다. 예를 들어, 액티비티의 onPause()가 호출될 때, 그 액티비티 안에 있는 프레그먼트들의 onPause()들이 호출됩니다.

하지만 프레그먼트는 UI를 생성하거나 제거하는 것과 관련하여 액티비티와의 상호작용을 위해 몇가지 액티비티의 생명주기에는 없는 몇가지 콜백 메소드들을 갖습니다:

onAttach()
프레그먼트가 액티비티 안에 소속될 때 호출됩니다(매개변수로 액티비티 객체를 받습니다).

onCreateView()
프레그먼트의 뷰를 생성하기 위해 호출됩니다.

onActivityCreated()
액티비티의 onCreate()가 호출된 직후에 호출됩니다.

onDestroyView()
프레그먼트의 뷰가 제거될 때 호출됩니다.

onDetach()
프레그먼트가 액티비티로부터 떨어져 나올 때 호출됩니다.

위의 그림3은 액티비티의 생명주기에 영향을 받는 프레그먼트의 생명주기를 보여줍니다. 여기서 액티비티의 각 상태에 따라 프레그먼트의 어떤 콜백 메소드들이 호출되는지를 볼 수 있습니다. 예를 들어, 액티비티의 onCreate() 콜백 메소드가 호출되어 created상태에 있다면, 프레그먼트는 onActivityCreated() 콜백 메소드까지만 호출이 됩니다. 

일단 액티비티가 resumed상태가 되기만 하면, 동적으로 생성되는 프레그먼트들이 액티비티에 자유롭게 추가 또는 제거될 수 있습니다. 다시 말해서, 액티비티가 resumed상태에 있는 동안에만 프레그먼트의 생명주기가 독립적으로 변할 수 있다는 것입니다.

하지만 액티비티가 resumed상태에서 벗어나면, 프레그먼트는 다시 액티비티에 의해 액티비티 생명주기의 영향권으로 들어가게 됩니다. 


예제


본 문서에서 학습한 모든 것들을 포함하는 예제 코드를 소개합니다. 여기서는 2개의 프레그먼트로 구성된 하나의 액티비티를 구현하는 과정을 볼 수 있습니다. 그 2개의 프레그먼트 중 하나는 셰익스피어의 작품 목록을 보여주고, 나머지 하나는 선택된 작품의 요약된 내용을 보여줍니다. 본 예제에서는 화면의 가로 또는 세로 모드에 대하여 각 프레그먼트가 대응한 방법들도 볼 수 있습니다.

메인 액티비티는 아래 예제에서 보이는 바와 같이 일반적인 방식으로 레이아웃을 적용합니다:

@Override
protected void onCreate(Bundle savedInstanceState) {
   
super.onCreate(savedInstanceState);

    setContentView
(R.layout.fragment_layout);
}

적용된 레이아웃인 fragment_layout.xml은 아래와 같습니다:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   
android:orientation="horizontal"
   
android:layout_width="match_parent" android:layout_height="match_parent">

   
<fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
           
android:id="@+id/titles" android:layout_weight="1"
           
android:layout_width="0px" android:layout_height="match_parent" />

   
<FrameLayout android:id="@+id/details" android:layout_weight="1"
           
android:layout_width="0px" android:layout_height="match_parent"
           
android:background="?android:attr/detailsElementBackground" />

</LinearLayout>

위의 레이아웃을 사용하면, 액티비티가 레이아웃을 로드할 때, 시스템은 작품 목록을 보여주는 TitlesFragment를 생성하고, 작품 내용이 들어갈 영역인 FrameLayout을 내용 없이 영역만 잡도록 생성합니다. 그리고 사용자가 작품 목록에서 작품을 선택하면 FrameLayout 안에 해당 프레그먼트가 채워질 것입니다.

하지만, 좌우로 2개의 프레그먼트를 모두 보여주는 것은 화면이 세로 모드일 때 상당히 부자연스러울 것입니다. 그래서 위의 레이아웃은 가로 모드에서만 적용되도록 하기 위해 res/layout-land/fragment_layout.xml로 저장합니다.

그에 따라 세로 모드의 레이아웃은 아래 예제와 같이 작성하여 res/layout/fragment_layout.xml로 저장하면 됩니다:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   
android:layout_width="match_parent" android:layout_height="match_parent">
   
<fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
           
android:id="@+id/titles"
           
android:layout_width="match_parent" android:layout_height="match_parent" />
</FrameLayout>

위의 레이아웃은 TitlesFragment만 포함하고 있습니다. 이것은, 디바이스가 세로 모드일 때 작품 목록만 화면에 보여준다는 것을 의미합니다. 그리고 이 목록에서 사용자가 작품을 선택하면, 작품 내용을 보여주기 위해 프레그먼트를 생성하는 대신에 새 액티비티를 실행할 것입니다.

다음으로, 프레그먼트 클래스들이 어떻게 구현되어 있는지를 살펴볼 것입니다. 첫번째로 TitlesFragment는 셰익스피어 작품의 목록을 보여주는 프레그먼트로서, 리스트뷰의 역할을 수행하기 위해 ListFragment를 상속받습니다.

아래 코드를 살펴보면 사용자가 리스트의 항목을 클릭했을 때 상황에 따라 다음 동작이 2갈래로 분기되는 것을 알 수 있습니다. 화면에 2개의 레이아웃이 보여지고 있다면(FrameLayout이 있다면) 현재 액티비티에서 작품 내용도 보여주기 위해 프레그먼트를 생성하여 FrameLayout에 추가할 것이며, 그렇지 않다면(FrameLayout이 없다면) 새 액티비티를 실행하여 작품 내용을 보여주는 프레그먼트를 그 액티비티에 추가할 것입니다.

public static class TitlesFragment extends ListFragment {
   
boolean mDualPane;
   
int mCurCheckPosition = 0;

   
@Override
   
public void onActivityCreated(Bundle savedInstanceState) {
       
super.onActivityCreated(savedInstanceState);

       
// Populate list with our static array of titles.
        setListAdapter
(new ArrayAdapter<String>(getActivity(),
                android
.R.layout.simple_list_item_activated_1, Shakespeare.TITLES));

       
// Check to see if we have a frame in which to embed the details
       
// fragment directly in the containing UI.
       
View detailsFrame = getActivity().findViewById(R.id.details);
        mDualPane
= detailsFrame != null && detailsFrame.getVisibility() == View.VISIBLE;

       
if (savedInstanceState != null) {
           
// Restore last state for checked position.
            mCurCheckPosition
= savedInstanceState.getInt("curChoice", 0);
       
}

       
if (mDualPane) {
           
// In dual-pane mode, the list view highlights the selected item.
            getListView
().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
           
// Make sure our UI is in the correct state.
            showDetails
(mCurCheckPosition);
       
}
   
}

   
@Override
   
public void onSaveInstanceState(Bundle outState) {
       
super.onSaveInstanceState(outState);
        outState
.putInt("curChoice", mCurCheckPosition);
   
}

   
@Override
   
public void onListItemClick(ListView l, View v, int position, long id) {
        showDetails
(position);
   
}

   
/**
     * Helper function to show the details of a selected item, either by
     * displaying a fragment in-place in the current UI, or starting a
     * whole new activity in which it is displayed.
     */

   
void showDetails(int index) {
        mCurCheckPosition
= index;

       
if (mDualPane) {
           
// We can display everything in-place with fragments, so update
           
// the list to highlight the selected item and show the data.
            getListView
().setItemChecked(index, true);

           
// Check what fragment is currently shown, replace if needed.
           
DetailsFragment details = (DetailsFragment)
                    getFragmentManager
().findFragmentById(R.id.details);
           
if (details == null || details.getShownIndex() != index) {
               
// Make new fragment to show this selection.
                details
= DetailsFragment.newInstance(index);

               
// Execute a transaction, replacing any existing fragment
               
// with this one inside the frame.
               
FragmentTransaction ft = getFragmentManager().beginTransaction();
               
if (index == 0) {
                    ft
.replace(R.id.details, details);
               
} else {
                    ft
.replace(R.id.a_item, details);
               
}
                ft
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
                ft
.commit();
           
}

       
} else {
           
// Otherwise we need to launch a new activity to display
           
// the dialog fragment with selected text.
           
Intent intent = new Intent();
            intent
.setClass(getActivity(), DetailsActivity.class);
            intent
.putExtra("index", index);
            startActivity
(intent);
       
}
   
}
}

두번째로 DetailsFragmentTitlesFragment에서 선택된 작품의 요약된 내용을 보여주는 프레그먼트입니다:

public static class DetailsFragment extends Fragment {
   
/**
     * Create a new instance of DetailsFragment, initialized to
     * show the text at 'index'.
     */

   
public static DetailsFragment newInstance(int index) {
       
DetailsFragment f = new DetailsFragment();

       
// Supply index input as an argument.
       
Bundle args = new Bundle();
        args
.putInt("index", index);
        f
.setArguments(args);

       
return f;
   
}

   
public int getShownIndex() {
       
return getArguments().getInt("index", 0);
   
}

   
@Override
   
public View onCreateView(LayoutInflater inflater, ViewGroup container,
           
Bundle savedInstanceState) {
       
if (container == null) {
           
// We have different layouts, and in one of them this
           
// fragment's containing frame doesn't exist.  The fragment
           
// may still be created from its saved state, but there is
           
// no reason to try to create its view hierarchy because it
           
// won't be displayed.  Note this is not needed -- we could
           
// just run the code below, where we would create and return
           
// the view hierarchy; it would just never be used.
           
return null;
       
}

       
ScrollView scroller = new ScrollView(getActivity());
       
TextView text = new TextView(getActivity());
       
int padding = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
               
4, getActivity().getResources().getDisplayMetrics());
        text
.setPadding(padding, padding, padding, padding);
        scroller
.addView(text);
        text
.setText(Shakespeare.DIALOGUE[getShownIndex()]);
       
return scroller;
   
}
}

TitlesFragment 코드를 다시 살펴보면, 사용자가 리스트의 항목을 클릭했을 때, (DetailsFragment가 들어갈 자리인) R.id.details가 가리키는 뷰가 없다면 DetailsActivity를 실행하라고 되어 있습니다.

아래 코드가 DetailsActivity이며, 여기서는 단순히 DetailsFragment를 포함하고 있으며 화면이 세로 모드일 때 작품의 요약된 내용을 보여주도록 되어 있습니다:

public static class DetailsActivity extends Activity {

   
@Override
   
protected void onCreate(Bundle savedInstanceState) {
       
super.onCreate(savedInstanceState);

       
if (getResources().getConfiguration().orientation
               
== Configuration.ORIENTATION_LANDSCAPE) {
           
// If the screen is now in landscape mode, we can show the
           
// dialog in-line with the list so we don't need this activity.
            finish
();
           
return;
       
}

       
if (savedInstanceState == null) {
           
// During initial setup, plug in the details fragment.
           
DetailsFragment details = new DetailsFragment();
            details
.setArguments(getIntent().getExtras());
            getFragmentManager
().beginTransaction().add(android.R.id.content, details).commit();
       
}
   
}
}

위의 액티비티에서 화면이 가로 모드이면(Configuration.ORIENTATION_LANDSCAPE) 액티비티를 종료하는 부분에 주목하시기 바랍니다. 화면이 가로 모드이면 메인 액티비티에서 TitlesFragment 옆에 DetailsFragment를 보여주도록 되어 있기 때문에 DetailsActivity는 종료시키는 것입니다. 메인 액티비티를 보여줄 때 이미 가로 모드였다면 DetailsActivity를 실행할 일도 없었을 것입니다. 이러한 경우는 메인 액티비티를 보여줄 때는 세로 모드였다가, 항목을 선택하여 DetailsActivity를 실행하는 과정에서 onCreate()가 호출되기 전에 화면이 가로 모드로 바뀌는 경우라고 볼 수 있습니다.

프레그먼트와 관련하여 더 많은 샘플 코드를 확인하기 위해서는, SDK에 포함되어 있는 샘플 중 ApiDemos 를 참고하시기 바랍니다. 


Posted by 개발자 김태우
,

(원문: http://developer.android.com/guide/components/activities.html)

(위치: Develop > API Guides > App Components > Activities)

(전체 번역 글 목록으로 이동하기)


Activity에 대하여


액티비티(Activity)는 사용자에게 UI가 있는 화면을 제공하는 앱 컴포넌트입니다. 다시 말해서, 폰 다이얼러 화면, 카메라 촬영 화면, 이메일 쓰기 화면, 지도 보기 화면 등과 같이 사용자들이 뭔가 하기 위해 상호작용을 할 수 있는 화면을 제공한다는 것입니다. 각 액티비티는 하나의 윈도우에 UI를 그리며, 그 윈도우가 보통은 화면을 꽉 채우지만, 화면보다 작을수도 있고, 다른 윈도우의 위에 떠 있을 수도 있습니다.

앱은 보통 여러개의 액티비티로 이루어져 있고, 각 액티비티는 서로 느슨한 관계를 갖습니다. 일반적으로, 앱은 하나의 메인(main) 액티비티를 갖고, 그것은 사용자가 앱을 처음 실행했을때 보여지는 액티비티입니다. 각 액티비티는 다른 액티비티를 실행할 수 있습니다. 다른 액티비티가 실행되면 이전의 액티비티는 정지되지만(stopped), 시스템이 "백스택"이라 불리는 스택에 저장해뒀기 때문에 없어지지는 않습니다. 다시 말해서, 시스템은 새로운 액티비티를 시작하면 백스택에 담고 나서 사용자에게 보여줍니다. 백스택은 "후입선출"의 스택 메커니즘을 따르며, 사용자가 뒤로버튼을 누를 경우, 스택의 최상위(top)에 있는 현재 액티비티를 제거(pop and destroy)하고 이전의 액티비티를 시작합니다. 백스택에 대한 더 자세한 내용은 태스크와 백스택에서 학습하실 수 있습니다.

새로운 액티비티가 실행되면서 현재 액티비티가 정지하게 되면, 시스템은 생명주기의 콜백 메소드를 호출함으로써 액티비티의 상태가 변경되었음을 알려줍니다. 이러한 콜백 메소드들은 시스템이 액티비티를 생성하고, 보여주고, 멈추고, 제거하는 등의 상황에 호출되며, 적절히 오버라이드(override)함으로써 각각의 상황에 맞는 동작을 구현할 수 있습니다. 예를 들어, 액티비티는 정지되었을 때 네트웍이나 데이터베이스 관련 객체와 같이 덩치가 큰 객채들을 해제하는 것이 좋고, 액티비티가 다시 화면에 보여질 때 필요한 리소스들을 다시 가져와서 중지되었던 작업들을 다시 실행할 수 있습니다. 이렇게 액티비티가 생성되고 보여지고 멈추고 제거되고 하는 등의 상태변화는 모두 액티비티 생명주기의 일부입니다. 

여기서는 액티비티를 만들고 사용하는 기본적인 방법과 액티비티의 생명주기에 대한 내용, 그리고 다양한 콜백 메소드 구현 방법에 대해 학습할 것입니다.


액티비티 생성하기


액티비티를 생성하기 위해서는 Activity 클래스를 상속받거나, Activity 클래스의 서브클래스를 상속받아서, 생명주기에 따른 콜백 메소드들을 오버라이드하여 원하는대로 구현하면 됩니다. 콜백 메소드들 중 아래 2개의 메소드가 가장 중요합니다.

onCreate()
반드시 구현되어야 하는 콜백 메소드로서, 액티비티가 생성될 때 호출됩니다. 여기서 액티비티에 필요한 요소들을 초기화 및 멤버변수로 저장하고, setContentView()를 호출하여 액티비티의 레이아웃을 지정해야 합니다.

onPause()
사용자와 액티비티의 거리가 한단계 멀어지는 경우에 호출되는 콜백 메소드입니다. 달리 말하면, 액티비티가 화면에 보여지고 있는 상태와 완전히 정지되어 화면에서 사라진 상태의 중간 단계라고 볼 수 있습니다. 이 상태 이후에 액티비티가 종료될 수도 있기 때문에, 여기서는 액티비티가 다시 시작되었을 때 유지되어야 하는 값들이 있을 경우 저장하는 일을 합니다.

위의 두 콜백 메소드 외에도, 유연한 사용자 경험을 제공하고 예상치 못하게 액티비티가 멈추거나 종료되는 상황들에 대응하기 위하여 사용할 수 있는 콜백 메소드들이 더 있습니다. 모든 생명주기 콜백 메소드들에 대한 더 자세한 내용은 아래의 액티비티 생명주기 관리하기 부분에서 학습하실 수 있습니다.


사용자 인터페이스(UI) 구현하기

액티비티의 UI는 View클래스를 상속받은 뷰(view)들의 계층구조에 의해 제공됩니다. 각 뷰는 액티비티의 윈도우 안에서 일부 사각형 영역을 할당 받고, 사용자와 상호작용을 할 수 있습니다. 예를 들어, 액티비티 안에 버튼뷰가 있다면, 그 버튼뷰가 차지한 사각형 영역을 사용자가 터치할 경우 앱이 그에 따른 동작을 수행할 수 있습니다. 

안드로이드는 앱의 레이아웃을 쉽게 구성할 수 있도록 미리 만들어진 뷰들을 제공합니다. "위젯(Widgets)"은 Button, EditText, CheckBox, ImageView 등과 같이 사용자와 상호작용할 수 있는 뷰이고, "레이아웃(Layouts)"은 LinearLayout, GridView, RelativeLayout 등과 같이 자식뷰들을 갖는 뷰로서 ViewGroup 클래스를 상속받아 만들어졌습니다. 그리고 View클래스나 ViewGroup클래스, 또는 위젯이나 레이아웃 등을 상속받아 그것의 서브클래스를 만들어서 사용할 수도 있습니다. 

액티비티의 레이아웃을 정의하는 가장 일반적인 방법은 XML 레이아웃 파일을 만들고 앱의 리소스로 저장하는 것입니다. 이 방법은 앱의 동작을 책임지는 소스코드와 앱의 UI 디자인을 분리시켜 줍니다. 액티비티에서 레이아웃은 보통 setContentView()에 레이아웃의 리소스 ID를 넘겨서 지정하지만, 소스코드에서 생성된 뷰 객체를 setContentView()에 넘기기도 합니다. 

UI를 만드는 방법에 대하여 더 자세한 내용은 사용자 인터페이스 문서에서 학습하실 수 있습니다.


매니페스트에 액티비티 정의하기

시스템이 액티비티를 인식하기 위해서는 매니페스트 파일(AndroidManifest.xml)에 그 액티비티가 정의되어 있어야 합니다. 따라서 아래와 같이 매니페스트 파일의 <application>요소 안에 <activity>요소를 추가합니다.

<manifest ... >
 
<application ... >
     
<activity android:name=".ExampleActivity" />
      ...
 
</application ... >
  ...
</manifest >

<activity>요소에는 액티비티의 이름이나 아이콘, 테마, 스타일 등의 속성을 추가할 수 있습니다. android:name 속성은 필수이며 액티비티의 클래스명을 지정합니다. 이 값은 앱 출시 이후에는 되도록이면 바꾸지 않는 것이 좋습니다. 왜냐하면 바뀌기 전의 액티비티명을 갖는 명시적 인텐트에 의해 실행되는 경우가 만약에라도 있다면 에러가 발생할 것이기 때문입니다. 

<activity>요소 정의하기에서 더 자세한 내용을 학습하실 수 있습니다.


인텐트 필터 사용하기

<activity>요소는 다른 앱의 컴포넌트가 자신을 실행할 수 있도록 하기 위해 <intent-filter>요소를 포함할 수 있습니다.

안드로이드 SDK를 이용하여 새 앱 프로젝트를 생성할 때, 아래와 같이 "main" 액션과 "launcher" 카테고리를 갖는 인텐트 필터가 추가된 액티비티를 자동으로 생성할 수 있습니다.

<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>요소는 해당 액티비티가 앱의 진입점임을 나타내고, <category>요소는 시스템 런처의 앱 목록에 보여준다는 것을 의미합니다.

내 앱이 다른 앱에 의해 실행되는 것을 원치 않는다면, 인텐트 필터를 추가할 필요가 없으며, 명시적 인텐트를 통해서만 액티비티를 실행하도록 하면 됩니다. 그리고 위와 같이 "main" 액션과 "launcher" 카테고리를 갖는 인텐트 필터는 하나의 앱에 대하여 오직 하나의 액티비티에서만 가질 수 있습니다. 

반대로, 다른 앱으로부터나 내 앱 내에서 암묵적 인텐트로 액티비티를 실행하고자 한다면, 액티비티에 인텐트 필터를 추가해야 합니다. <intent-filter><action>은 필수사항이며, <category><data>는 선택사항입니다. 암묵적 인텐트로 어떤 액티비티가 실행될 것인가를 이러한 요소들로 정해줄 수 있는 것입니다.

액티비티가 인텐트에 어떻게 반응하는지에 대한 더 자세한 내용은 인텐트와 인텐트 필터에서 학습하실 수 있습니다.


액티비티 실행하기


어떤 액티비티를 실행할지에 대한 정보를 가지고 있는 인텐트를 startActivity()에 넘김으로써 액티비티를 실행할 수 있습니다. 인텐트는 명시적으로 실행할 액티비티를 지정할 수도 있고, 암묵적으로 액션타입을 지정하여 시스템이 조건에 부합하는 액티비티들을 찾도록 할 수도 있습니다. 또한 인텐트는 실행될 액티비티에서 필요로 하는 데이터들을 전달하는 역할도 합니다.

다른 앱과 상호작용하는 앱이 아니라면, 아래 예제와 같이 명시적으로 액티비티를 지정하는 명시적 인텐트만 사용해도 될 것입니다.

Intent intent = new Intent(this, SignInActivity.class);
startActivity
(intent);

하지만, 이메일을 쓰거나 메시지를 보내거나 SNS앱의 내 상태를 업데이트 하는 등의 일을 하려는데 내 앱에서는 해당 액티비티들을 구현하지 않았다면, 아래 예제와 같은 암묵적 인텐트를 이용해서 디바이스에 설치된 다른 앱의 해당 액티비티를 내 앱의 일부인 것처럼 실행할 수 있습니다. 이것이 인텐트의 매력적인 부분인데, 인텐트에 액션값을 지정하여 액티비티 실행요청을 하면 시스템이 조건에 부합하는 액티비티들을 찾아서, 1개이면 바로 실행하고 2개 이상이면 사용자가 선택할 수 있도록 보여줍니다. 

Intent intent = new Intent(Intent.ACTION_SEND);
intent
.putExtra(Intent.EXTRA_EMAIL, recipientArray);
startActivity
(intent);

위 예제의 인텐트는 이메일앱이나 메신저앱, SNS앱 등의 ACTION_SEND를 허용하는 앱들로 메시지를 전송하기 위해 만든 인텐트이며, EXTRA_EMAIL은 사용자가 이메일앱을 선택한 경우에 수신자 입력란(To.)에 미리 채워줄 수신자 이메일 주소들입니다. 위의 인텐트로 액티비티가 실행되었다가 할일을 모두 마치고 액티비티가 종료되면 바로 이전의 액티비티(새 액티비티를 실행했던 액티비티)가 다시 시작됩니다.


액티비티를 실행하여 결과값 얻기

때로는 액티비티를 실행하여 그로부터 어떤 결과값을 얻어야 하는 경우가 있습니다. 예를 들면, 내 사진 중 하나를 업로드하기 위해 갤러리앱에서 사진을 선택하는 경우가 있을 수 있겠죠. 이때는 액티비티 실행을 위해 startActivityForResult()를 실행하고, 실행된 액티비티에서 결과값 셋팅 후 종료되면, 이전 액티비티의 onActivityResult()에서 해당 결과값을 전달받아 관련 작업을 할 수가 있습니다.

아래 예제는, 내 액티비티가 주소록의 사람 목록 액티비티로부터 누군가의 정보를 가져와서 원하는 작업을 하는 코드 일부입니다.

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);
       
if (cursor.moveToFirst()) { // True if the cursor is not empty
           
int columnIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
           
String name = cursor.getString(columnIndex);
           
// Do something with the selected contact's name...
       
}
   
}
}

위의 예제에서는 onActivityResult()를 어떻게 구현할지에 대한 기본적인 로직을 보여줍니다. 첫째로 요청이 성공했는가, 즉 실행된 액티비티에서 정상적으로 결과값을 셋팅했는지를 확인하기 위해 resultCodeRESULT_OK인지를 비교합니다. 그리고 요청이 여러개일 수 있기 때문에 요청을 구분하기 위해 requestCodestartActivityForResult() 호출할 때 보냈던 값과 같은지를 비교합니다. 그리고나서, 매개변수로 전달받은 Intent객체(data)를 이용하여 선택된 주소록 사용자 정보를 구합니다.

ContentResolver는 사용자 정보를 쿼리하여 Cursor객체를 래턴해줍니다. 더 자세한 내용은 컨텐트 프로바이더에 대하여에서 학습하실 수 있습니다.

그리고 인텐트 사용 방법에 대한 더 자세한 내용은 인텐트와 인텐트 필터에서 학습하실 수 있습니다.


액티비티 종료하기


finish()를 호출함으로써 현재 액티비티를 종료할 수 있고, finishActivity()를 호출함으로써 startActivityForResult()로 호출했던 액티비티를 종료할 수 있습니다. 

메모: 보통은 위의 메소드들을 이용하여 액티비티를 종료할 필요가 없습니다. 시스템이 액티비티의 생명주기를 관리해주기 때문이며, 액티비티를 강제로 종료하는게 오히려 사용자에게 좋지 않은 영향을 미칠 수도 있기 때문입니다. 따라서, 액티비티가 다음 액티비티를 실행하면서 자신은 스택에 아예 남지 않기를 원하는 경우에만 액티비티를 명시적으로 종료하도록 합니다.


액티비티 생명주기 관리하기


액티비티의 콜백 메소드들을 구현함으로써 생명주기를 관리하는 것은 안드로이드 앱을 만드는데에 필수적인 부분입니다. 액티비티의 생명주기는 다른 액티비티들과의 관계, 그리고 그들의 태스크와 백스택에 직접적으로 영향을 받습니다. 

액티비티의 상태에는 아래의 세가지가 있습니다.

Resumed
액티비티가 전면(foreground)에 있고, 사용자가 인터페이스를 사용할 수 있도록 포커스를 가지고 있는 상태입니다. ("running" 상태라고도 합니다.)

Paused
다른 액티비티가 resumed 상태가 되었으나 여전히 이전 액티비티가 일부 보이고 있는 상태입니다. 다시 말해서, 이전 액티비티 위에 다음 액티비티가 보여지고 있지만, 반투명하거나 화면의 일부만 덮고 있는 상태입니다. paused 상태의 액티비티는 resumed 상태와 마찬가지로 메모리에 살아 있으며 여전히 윈도우 매니저와 연결되어 있습니다. 하지만 메모리가 부족한데 다른데에서 끌어쓸 자원이 없다면 시스템에 의해 종료될 수도 있는 상태입니다. 

Stopped
다른 액티비티가 resumed 상태가 되면서 이전 액티비티가 화면에서 완전히 사라져서 후면(background)으로 넘어간 상태입니다. stopped 상태의 액티비티도 paused 상태와 마찬가지로 메모리에 살아는 있으나 윈도우 매니저와의 연결은 끊어진 상태입니다. 따라서 메모리가 부족할 때 시스템에 의해서 종료될 수 있습니다.

만약 액티비티가 pausedstopped 상태가 되면, 시스템은 finish()메소드를 호출하거나, 바로 프로세스를 종료함으로써 액티비티 객체를 메모리에서 날려버릴 수 있습니다. 그럴 경우 액티비티를 다시 실행하면, 당연히도 다시 생성하게 됩니다.


생명주기 콜백 메소드 구현하기

액티비티의 상태들이 변경될 때, 콜백 메소드들이 호출됩니다. Activity클래스를 상속받아 내 액티비티를 만들 때, 모든 콜백 메소드들을 오버라이드(override)하여 구현할 수 있습니다. 아래 예제에는 액티비티의 주요 콜백 메소드들이 포함되어 있습니다.

public class ExampleActivity extends Activity {
   
@Override
   
public void onCreate(Bundle savedInstanceState) {
       
super.onCreate(savedInstanceState);
       
// The activity is being created.
   
}
   
@Override
   
protected void onStart() {
       
super.onStart();
       
// The activity is about to become visible.
   
}
   
@Override
   
protected void onResume() {
       
super.onResume();
       
// The activity has become visible (it is now "resumed").
   
}
   
@Override
   
protected void onPause() {
       
super.onPause();
       
// Another activity is taking focus (this activity is about to be "paused").
   
}
   
@Override
   
protected void onStop() {
       
super.onStop();
       
// The activity is no longer visible (it is now "stopped")
   
}
   
@Override
   
protected void onDestroy() {
       
super.onDestroy();
       
// The activity is about to be destroyed.
   
}
}

메모: 위와 같이 콜백 메소드를 구현할 때는 항상 부모 클래스의 메소드인 super.onXXX()를 호출해줘야 합니다. 

종합적으로 생각해보면, 이러한 콜백 메소드들은 액티비티의 전체 생명주기를 정의합니다. 콜백 메소드들을 구현함으로써 생명주기 안에 있는 세가지 중첩된 반복구간(nested loops)을 관리할 수 있습니다. 그 세가지는 아래와 같습니다.

  • 전체 구간(entire lifetime)onCreate()에서 시작하여 onDestroy()로 끝납니다. onCreate()에서 레이아웃 정의와 같은 액티비티 전체에 대한 준비 작업을 하고, onDestroy()에서 리소스들을 해제합니다. 예를 들어 액티비티에서 네트웍을 통해 필요한 데이터를 받아와야 한다면, 쓰레드를 onCreate()에서 생성하여 멤버 변수로 가지고 있다가 onDestroy()에서 해제하면 될 것입니다.

  • 보이는 구간(visible lifetime)onStart()에서 시작하여 onStop()으로 끝납니다. 이때 사용자는 액티비티를 화면에서 볼 수 있으며 상호작용(interact)할 수 있습니다. 만약 다른 액티비티가 실행되면서 현재 액티비티의 onStop()이 호출되었다면 그 액티비티는 화면에 보이지 않을 것입니다. onStart()와 onStop() 사이에서는 액티비티가 사용자에게 보여지는 동안 필요한 리소스들을 잘 가지고 있어야 합니다. 예를 들면, 브로드캐스트 리시버를 등록하는 작업은 보통 onStart()에서 하며, onStop()에서 등록해제를 합니다. 이것은 브로드캐스트를 받아서 하는 일이 보통 화면 UI를 변경하는 일이기 때문에 보이는 구간에서만 등록되어 있도록 하는 것입니다. 전체 구간 안에서 보이는 구간은 여러번 있을 수 있습니다. 다시 말해서, onCreate()와 onDestroy() 사이에서 onStart()와 onStop()이 여러번 호출될 수 있다는 것입니다.

  • 전면 구간(foreground lifetime)onResume()에서 시작하여 onPause()로 끝납니다. 이때 액티비티는 가장 전면에, 즉 사용자와 가장 가까운 곳에 있고 사용자와 상호작용할 수 있도록 포커스를 갖습니다. onPause()는 디바이스가 슬립모드로 들어가는 경우(화면이 꺼지는 경우)와 다이얼로그가 뜨는 경우 등에 호출됩니다. 이렇듯 액티비티는 전면에 나섰다가 빠지는 경우가 자주 발생하기 때문에, 콜백 메소드에서 시간이 오래 걸리는 작업은 되도록 피해야 합니다.

아래의 그림1은 위의 세가지 구간과 상태들간의 이동흐름을 나타냅니다. 사각형은 상태가 변할때 호출되는 콜백 메소드를 표현합니다.


그림 1. 액티비티 생명주기


아래의 표1에서도 생명주기를 표현하는데, 각 콜백 메소드에 대하여 더 자세히 설명하며, 콜백 메소드가 호출된 후 (다른 콜백 메소드가 호출되기 전) 시스템에 의해 종료될 수 있는지 여부와 다음으로 호출될 콜백 메소드가 무엇인지를 알려줍니다.


표 1. 액티비티 콜백 메소드에 대한 설명

 콜백메소드

 설명

 콜백메소드
 호출후 종료
 가능여부

 다음으로
 호출될
 콜백메소드

 onCreate()

 액티비티가 생성될때 호출되며, 뷰를 생성하거나 리스트에 데이터를 바인드하는 등의 구현을 여기에 하도록 합니다. 매개변수로 Bundle객체를 받는데, 종료될때 액티비티의 상태를 저장했다면(액티비티의 상태 저장하기 참고) 해당 Bundle객체를 통해 그 상태를 복구할 수 있습니다.
onCreate() 다음으로는 항상 onStart()가 호출됩니다.

 불가능

 onStart()

 - onRestart()

 액티비티가 정지되었다가 다시 시작되기 직전에 호출됩니다. 
onRestart() 다음으로는 항상 onStart()가 호출됩니다.

 불가능

 onStart()

 - onStart()

 액티비티가 사용자에게 보여지기 전에 호출됩니다. 
 이어서 액티비티가 전면에 나서며 포커스를 받을때 onResume()이 호출되며, 그렇지 않고 바로 가려지면 onStop()이 호출됩니다.

 불가능

 onResume()
 또는
 onStop()

 - - onResume()

 액티비티가 사용자와 상호작용을 할 수 있는 상태가 되기 직전에 호출됩니다. 이때 액티비티는 백스택의 최상단에 있고, 사용자는 액티비티에 뭔가를 할 수 있는 상태가 됩니다.
 onResume() 다음으로는 항상 onPause()가 호출됩니다.

 불가능

 onPause()

 - - onPause()

 시스템이 다른 액티비티를 resume상태로 만들때 호출됩니다. 여기서는 보통 데이터를 저장하거나 애니메이션을 정지하거나 CPU 사용량을 줄이기 위해 정지시켜도 될만한 객체들의 동작을 정지시키는 등의 구현을 합니다. 다만 onPause()가 완료된 후에야 다른 액티비티가 resume상태가 될 수 있기 때문에 시간이 오래 걸리는 작업은 피해야 합니다.
 이어서 액티비티가 다시 resume상태가 된다면 onResume()이 호출되고, 아예 화면에서 보이지 않게 되면 onStop()이 호출됩니다.

 가능

 onResume()
 또는
 onStop()

 - onStop()

 액티비티가 더이상 사용자에게 보여지지 않을때 호출됩니다. 이러한 경우는 보통 액티비티가 종료되는 상황이거나, 다른 액티비티가 resume상태가 되면서 이전 액티비티를 완전히 가려버리는 상황입니다. 
 이어서 액티비티가 다시 사용자에게 보여지는 상태가 되는 경우 onRestart()가 호출되고, 액티비티가 종료되고 백스택에서도 빠지는 경우 onDestroy()가 호출됩니다.

 가능 

 onRestart()
 또는
 onDestroy()

 onDestroy()

 액티비티가 종료될 때 호출되며, 생명주기에서 마지막으로 호출되는 콜백 메소드입니다. finish()가 호출되었거나, 시스템이 다른 작업을 위한 메모리를 확보하기 위해 임의로 종료시키는 경우에 호출되는데, 이 두가지 경우는 isFinishing()을 통해 구별할 수 있습니다. finish()가 호출된 경우 isFinishing()은 true를 리턴합니다.

 가능

 없음


"콜백 메소드 호출후 종료 가능여부"는 해당 메소드가 리턴된 후 다른 코드의 실행이 없는 상태에서 시스템이 액티비티를 종료할 수 있는지 여부를 나타내며, onPause(), onStop(), onDestroy() 이렇게 세개만이 "가능"합니다. 이 중 onPause()가 가장 먼저 호출되는 메소드이고 여기서 시스템에 의해 종료되면 onStop()onDestroy()가 호출되지 않을 수도 있기 때문에, onPause()에서 중요한 데이터들은 저장해야 합니다. 하지만 onPause()에서 어떤 일을 할지에 대해서는 신중히 결정해야 하는데, 그것은 onPause()에서 시간이 오래 걸리는 작업으로 프로시져를 붙들고 있으면 다음 액티비티의 실행이 늦어질 수 있기 때문입니다.

"콜백 메소드 호출후 종료 가능여부"가 "불가능"인 콜백 메소드들이 호출된 후 다른 메소드가 호출되기 전까지는, 액티비티를 실행중인 프로세스가 시스템의 종료 명령으로부터 보호됩니다. 따라서, 액티비티는 onPause()가 호출된 후부터 다시 onResume()이 호출되기 전까지는 시스템에 의해 종료될 수 있으며, 반대로 onResume()이 호출된 후부터 onPause()가 호출되기 전까지는 종료될 수 없습니다. 

메모: 표1에서 "콜백 메소드 호출후 종료 가능여부"가 "불가능"인 메소드가 호출된 상태라 하더라도 아주 긴급한 상황에서는 시스템에 의해 액티비티가 종료될 수도 있습니다. 액티비티가 종료되는 상황에 대한 더 자세한 내용은 프로세스와 쓰레드에서 학습하실 수 있습니다.


액티비티의 상태 저장하기

액티비티 생명주기 관리하기에서 설명한 바와 같이 액티비티는 paused나 stopped상태에서도 메모리에 액티비티의 상태를 나타내는 데이터들을 가지고 있습니다. 따라서, 다시 resume상태가 되면 paused나 stopped상태가 되기 전과 같은 상태가 되는 것입니다.

하지만 시스템이 메모리 확보를 위해 액티비티를 종료시키는 경우에는 그 액티비티를 다시 실행할 때 이전 상태가 메모리에 남아있지 않기 때문에 복구할 수 없습니다. 이러한 경우, 액티비티가 종료되기 전 호출되는 콜백 메소드인 onSaveInstanceState()에서 유지해야하는 데이터들을 저장해야 액티비티가 다시 생성될 때 저장된 데이터들을 전달받아 이전 상태를 복구할 수 있습니다.

onSaveInstanceState()에서 매개변수로 Bundle객체를 전달받는데, 이 객체의 putString(), putInt() 등의 메소드를 이용하여 이름-값 쌍의 데이터를 저장할 수 있습니다. 그러면 액티비티가 종료되었다가 다시 생성될 때 onCreate()onRestoreInstanceState()에서 매개변수로 받는 Bundle객체에 저장된 데이터들을 꺼내서 액티비티의 이전 상태를 복구할 수 있는 것입니다. 액티비티를 처음 생성하는 경우와 같이 복구할 데이터가 없는 경우에는 Bundle객체가 null로 옵니다.


그림 2. 액티비티가 이전 상태를 복구하는 두가지 경우를 보여줍니다. 하나는 액티비티가 종료되었다가 다시 생성될 때 저장된 데이터를 이용하여 복구하는 경우이고, 다른 하나는 액티비티가 stopped상태가 되었다가 다시 resume상태가 될 때 메모리에 데이터들이 그대로 살아있기 때문에 자연스럽게 이전 상태가 복구된 경우입니다.

메모: 액티비티가 종료되기 전에 onSaveInstanceState()의 호출이 반드시 보장되는 것은 아닙니다. 사용자가 명시적으로 뒤로버튼을 누르는 경우와 같이 액티비티의 이전 상태를 복구할 필요가 없는 경우에는 onSaveInstanceState()가 호출되지 않습니다. 만약 시스템이 onSaveInstanceState()를 호출한다면, 그것은 아마도 onStop()이나 onPause()보다 먼저 호출될 것입니다.


onSaveInstanceState()를 오버라이드하여 추가적인 구현을 하지 않더라도, 기본적으로 몇가지 복구되는 것들이 있습니다. Activity클래스의 onSaveInstanceState()에서는 액티비티의 모든 뷰의 onSaveInstanceState()를 호출하도록 되어 있습니다. 즉, 뷰가 이전 상태를 복구하도록 구현되어 있다면 그 뷰는 액티비티가 종료 및 재생성 될 때 이전 상태로 복구된다는 것입니다. 안드로이드 프레임웍의 거의 모든 위젯들은 이 onSaveInstanceState()가 적절히 구현되어 있습니다. 예를 들면, EditText 위젯은 사용자로부터 입력받은 문자열을 저장하고, CheckBox 위젯은 체크여부를 저장합니다. 다만, 이러한 뷰 및 위젯들을 레이아웃 xml파일에 정의할 때 android:id를 지정해야만 상태를 저장 및 복구하며, android:id를 지정하지 않으면 이전 상태가 복구되지 않습니다.

뷰나 위젯이 이전 상태를 저장 및 복구하지 않도록 하기 위해서는 android:saveEnabled"false"로 선언하거나, setSaveEnabled()메소드를 호출하면 됩니다. 보통은 굳이 이렇게 설정할 필요가 없으나, 액티비티의 UI를 뭔가 다르게 복구되도록 구현하고 싶다면 이용할 수도 있겠죠.

비록 뷰나 위젯이 기본적으로 이전 상태를 복구할 수 있다 하더라도, 원하는 모든게 복구될 수는 없으므로 결국 onSaveInstanceState()를 오버라이드해야 하는 경우가 생깁니다. 예를 들어, 내 액티비티를 만들면서 추가한 멤버변수에 대하여, 액티비티가 실행되는 동안 그 값이 변한다면, 액티비티가 종료 및 재생성될때 그 값을 저장 및 복구해야할 것입니다.

위에서 언급한 바와 같이 onSaveInstanceState()onRestoreInstanceState()는 뷰나 위젯의 상태 복구와 같이 기본적인 기능이 구현되어 있기 때문에, 그것들을 오버라이드할 경우 추가적인 구현부분 전에 반드시 super.onSaveInstanceState()super.onRestoreInstanceState()를 호출해줘야 합니다.

메모: onSaveInstanceState()는 호출이 보장되지 않기 때문에, 즉 호출되지 않는 경우도 있을 수 있기 때문에 액티비티 UI의 상태를 저장하는 용도로만 사용하도록 합니다. 그리고 반드시 저장해야하는 중요 데이터는 onPause()에서 저장하도록 합니다.

액티비티의 이전 상태 저장 및 복구에 대하여 가장 손쉬운 테스트 방법은, 디바이스의 오리엔테이션을 가로(landscape), 세로(portrait)로 번갈아서 변경해보는 것입니다. 이렇게 디바이스의 오리엔테이션이 변경될 경우, 기본적으로 시스템은 액티비티를 종료 및 재생성합니다. 가로 및 세로에 대하여 서로 다른 레이아웃이 적용될 수도 있기 때문이죠. 사용자들은 디바이스의 오리엔테이션을 수시로 변경할 수 있기 때문에 액티비티의 상태를 유지하도록 조치하는 것은 상당히 중요합니다.


디바이스의 상태가 변경될 때

스크린 오리엔테이션이나 키보드의 사용가능 여부, 언어 등의 디바이스 상태는 앱이 실행중일때 변경될 수도 있습니다. 이러한 경우 안드로이드 시스템은 액티비티를 종료 및 재실행하며, onDestroy()onCreate()가 연이어 호출됩니다. 이것은 디바이스의 상태가 변경될 경우 그에 맞는 리소스를 적용하기 위해서입니다. 가로일때와 세로일때 서로 다른 레이아웃 xml파일을 적용해야 하는 경우가 그 예입니다.

내 앱이 스크린 오리엔테이션의 변화에 적절히 대응하도록 개발되었다면, 액티비티의 생명주기상에서 일어날 수 있는 다른 불청객 이벤트에 대해서도 대응할 수 있을 것입니다.

위에서 계속 언급했던바와 같이 액티비티의 종료 및 재생성시 상태를 유지하는 가장 좋은 방법은, onSaveInstanceState()에서 상태를 저장하고, onRestoreInstanceState() 또는 onCreate()에서 상태를 복구하는 것입니다.

실행중에 디바이스의 상태가 변경될 때 그것을 다루는 방법들은 실행 중 상태 저장 및 복구하기에서 학습하실 수 있습니다.


액티비티들 조직화하기

어떤 액티비티가 다른 액티비티를 실행하는 경우, 그 둘 모두 상태의 변화가 발생합니다. 이전 액티비티는 paused 및 stopped상태가 되고(완전히 가려지지 않고 일부가 보인다면 stopped상태가 되지 않습니다), 다음 액티비티는 생성됩니다. 이때, 다음 액티비티가 생성되기 전까지 이전 액티비티가 완전히 정지(stopped)되지 않는다는 것을 기억해야 합니다. 

액티비티의 생명주기 콜백 메소드가 호출되는 순서는 잘 정의되어 있습니다. 아래 예제는 액티비티A가 액티비티B를 실행하는 경우 콜백 메소드가 호출되는 순서를 보여줍니다.

  1. 액티비티A의 onPause()가 호출됩니다.
  2. 액티비티B의 onCreate(), onStart(), onResume()이 차례로 호출됩니다. (이제 액티비티B가 포커스를 가지고 있습니다.)
  3. 액티비티A가 더이상 화면에 보이지 않는다면 onStop()이 호출됩니다.

위와 같은 콜백 메소드의 호출 순서를 잘 숙지하고, 액티비티들 사이의 상태 변화시 서로 공유되는 데이터들을 처리할 때 주의해야 합니다. 예를 들어 액티비티A가 정지될때 데이터를 DB에 저장하고, 액티비티B가 생성될때 그 데이터를 DB에서 불러와야 하는 경우, 액티비티A의 onStop()에서가 아니라 onPause()에서 데이터를 저장해야 한다는 것을 알 수 있습니다.


Posted by 개발자 김태우
,