(원문: 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 개발자 김태우
,