안드로이드 개발자 사이트 번역


주위에서 안드로이드 개발을 시작하시려는 분들이 질문을 많이 하십니다.

"안드로이드 책 좀 추천해 줘."

그럴 때마다 제 대답은 비슷했습니다.

"안드로이드 개발자 사이트를 보는 게 제일 좋을걸요."

이런 대답을 몇 번 하다보니, 문득 사이트를 번역해야겠다고 마음 먹게 되었습니다. (개발자 사이트 + 스택오버플로)의 조합이라면 능히 험한 안드로이드 세상을 헤쳐 나갈 수 있을 것이라 믿어 의심치 않습니다. 

본 글은 계속 업데이트 될 것입니다. 아직은 많이 부족하죠. 계속 번역해서 아래의 글 목록을 채워나갈 것입니다. 아직 번역되지 않은 글 중에 번역했으면 좋겠다고 생각되는 글이 있다면 댓글로 의견 주세요. 

많은 분들께 도움이 되었으면 하는 바람입니다. 


/**
 * 글 목록
 */

API 가이드 extends 개발(Develop) {

    안드로이드 입문(Introduction to Android) {

        앱의 기본 개념(Application Fundamentals)

        기기 호환성(Device Compatibility)

        시스템 퍼미션(System Permissions)

    }

    앱 컴포넌트 개요(App Components) {

        인텐트와 인텐트 필터(Intents and Intent Filters)

        액티비티에 대하여(Activities) {

            프레그먼트(Fragments)

            로더(Loaders)

            태스크와 백스택(Tasks and Back Stack)

        }

        서비스에 대하여(Services) {

            서비스 바인드하기(Bound Services)

            안드로이드 인터페이스 정의 언어(AIDL)

        }

        컨텐트 프로바이더에 대하여(Content Providers) {

            컨텐트 프로바이더 기초(Content Provider Basics)

        }

        앱 위젯(App Widgets)

        프로세스와 쓰레드(Processes and Threads)

    }

}


Posted by 개발자 김태우
,

(원문: http://developer.android.com/guide/components/processes-and-threads.html)

(위치: Develop > API Guides > App Components
> Processes and Threads
)

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


프로세스와 쓰레드


어떤 앱 컴포넌트가 해당 앱에 대하여 처음 시작되는 경우, 즉 그 앱에 (실행 중인) 다른 앱 컴포넌트가 없는 경우에, 안드로이드 시스템은 리눅스 프로세스를 새로 생성하여 실행하고 쓰레드를 만들어서 앱 컴포넌트를 실행시켜 줍니다. 기본적으로, 동일한 앱의 컴포넌트들은 동일한 프로세스 및 ("메인" 쓰레드라고 불리는) 쓰레드에서 실행됩니다. 앱의 다른 컴포넌트가 이미 존재하여 프로세스가 이미 생성되었다면, 또 다른 앱 컴포넌트가 실행될 때 그 프로세스에서 실행되며 쓰레드도 같습니다. 하지만 기본적인 동작이 그러할 뿐, 내 앱의 컴포넌트들을 서로 다른 프로세스에서 실행되도록 할 수 있으며, 쓰레드도 얼마든지 새로 생성하여 실행할 수 있습니다. 

본 문서에서는 안드로이드 앱에서 프로세스와 쓰레드가 어떻게 동작하는지에 대하여 설명합니다. 


프로세스

기본적으로 앱의 모든 컴포넌트들은 동일한 프로세스에서 실행되며, 대부분의 경우 이를 변경할 필요가 없습니다. 하지만 특정 앱 컴포넌트를 다른 프로세스에서 실행시켜야 할 필요가 있는 경우, 매니페스트 파일에서 그 앱 컴포넌트가 실행될 프로세스를 지정하도록 할 수 있습니다. 

매니페스트 파일에서 앱 컴포넌트들을 선언할 때, <activity>, <service>, <receiver>, <provider> 요소를 사용하는데, 이들은 android:process 속성을 통해 해당 컴포넌트가 실행될 프로세스를 지정할 수 있습니다. 이 속성을 지정함으로써, 각 컴포넌트들이 서로 다른 프로세스에서 실행되도록 할 수도 있고, 대체로 같은 프로세스에서 실행하되 일부만 다른 프로세스에서 실행되도록 할 수도 있으며, 심지어는 서로 다른 앱인데 같은 프로세스에서 실행되도록 할 수도 있습니다. 하지만 서로 다른 앱이 같은 프로세스에서 실행되려면, 해당 앱들이 동일한 리눅스 유저 ID를 공유해야하고, 동일한 인증서(certificates)로 서명되어(signed) 있어야 합니다. 

<application> 요소에서도 android:process 속성을 지정할 수 있는데, 이는 해당 앱의 모든 컴포넌트들에 대한 기본값이 됩니다. 

안드로이드는 메모리가 부족하거나 다른 앱의 컴포넌트가 먼저 메모리를 필요로 하는 경우 프로세스를 종료시킬 수도 있습니다. 그러면 결과적으로 그 프로세스에서 실행 중이던 앱 컴포넌트들도 모두 종료됩니다. 그리고 해당 앱 컴포넌트들이 다시 실행될 때 프로세스도 다시 시작됩니다. 

안드로이드 시스템은 어떤 프로세스를 종료시킬지 결정할 때, 그 프로세스가 사용자에게 얼마나 중요한지를 판단합니다. 예를 들어, 더 이상 화면에 보이지 않는 액티비티를 실행중인 프로세스가, 화면에 보이고 있는 액티비티를 실행중인 프로세스보다 더 우선적으로 종료되는 것입니다. 따라서 어떤 프로세스를 종료시킬지 결정하는 기준은, 프로세스에서 실행중인 앱 컴포넌트들의 상태에 달려있다고 볼 수 있습니다. 그에 대한 자세한 내용은 이어지는 섹션에서 설명합니다. 


프로세스의 생명주기

안드로이드 시스템은 앱의 프로세스를 가능한 한 유지하려고 노력합니다. 하지만 시간이 지날수록 새로운 프로세스나 더 중요한 프로세스를 실행하기 위해 메모리가 필요해지고, 그 때문에 오래된 프로세스는 종료시켜야 할 필요가 생깁니다. 시스템은, 프로세스들 중에 어떤 것을 살리고 어떤 것을 종료시킬지를 결정하기 위해서, 프로세스들을 해당 프로세스에서 실행 중인 컴포넌트들의 상태에 기반한 "중요도 계층"으로 나눕니다. 그리고 부족한 시스템 자원을 채우기 위해서 가장 중요도가 낮은 프로세스들을 먼저 종료시키고, 또 필요하면 그 다음으로 중요도가 낮은 프로세스들을 종료시킵니다. 

중요도 계층은 아래의 5가지로 구분됩니다. 중요도가 가장 높은 것부터 설명합니다. 

1. 전방에 있는 프로세스(Foreground process)

사용자가 현재 뭔가 하고 있는 프로세스입니다. 아래 조건들 중 하나 이상을 만족한다면 그 프로세스는 전방(foreground)에 있는 것으로 간주합니다.

  • 사용자와 상호작용하고 있는 Activity(onResume()이 호출된 Activity)를 실행중인 프로세스
  • 사용자와 상호작용하고 있는 Activity에 바인드된 Service를 실행 중인 프로세스
  • "전방에 있는" Service(startForeground()가 호출된 Service)를 실행 중인 프로세스
  • onCreate(), onStart(), onDestroy()와 같은 생명주기 콜백 메소드가 호출되어 리턴되기 전까지의 Service를 실행 중인 프로세스
  • onReceive() 메소드가 호출되어 리턴되기 전까지의 BroadcastReceiver를 실행 중인 프로세스

일반적으로 전방(foreground)에는 한번에 많은 프로세스가 존재하지 않습니다. 그리고 이러한 프로세스는, 가용 메모리가 너무 적어서 더이상 아무것도 실행할 수 없을 때에만 종료될 수 있습니다. 

2. 보여지고 있는 프로세스(Visible process)

전방에 있는 컴포넌트는 없지만, 사용자에게 보여지는 컴포넌트가 실행 중인 프로세스입니다. 아래 조건 중 하나를 만족하면 사용자에게 보여지는 것으로 간주합니다:

  • 전방에 있지는 않지만 여전히 화면에 보이는 Activity(onPause()가 호출된 Activity)를 실행 중인 프로세스. 예를 들어, 전방에 있는 Activity에서 다이얼로그를 실행한 경우, 다이얼로그가 전방으로 오고, Activity는 전방에서 벗어나 사용자와 상호작용할 수는 없지만 화면에는 보이는 상태가 됩니다. 
  • "보이는" Activity에 바인드 된 Service를 실행 중인 프로세스

"보이는" 프로세스들도 매우 중요하기 때문에, 전방에 있는 프로세스 외에 다른 상태의 프로세스가 없을 때에만 종료시킬 수 있습니다.

3. 서비스 프로세스(Service process)

startService()가 호출되어 Service를 실행 중인 프로세스지만, 위의 전방에 있는 프로세스나 보여지고 있는 프로세스가 아닌 경우에 서비스 프로세스로 구분합니다. 비록 사용자의 눈에 뭔가가 보이는 것은 아니지만, 후방(background)에서 음악을 재생한다거나, 네트워크를 통해 데이터를 다운로드 하는 등의 일을 합니다. 전방에 있는 프로세스와 보여지고 있는 프로세스 외에 다른 상태의 프로세스가 없을 때에만 종료시킬 수 있습니다. 

4. 후방에 있는 프로세스(Background process)

사용자에게 보여지지 않는 Activity(onStop()이 호출된 Activity)를 실행 중인 프로세스입니다. 이러한 프로세스들은 사용자에게 직접적인 영향을 미치지 못하며, 전방에 있는 프로세스나 보여지고 있는 프로세스나 서비스 프로세스를 실행할 때 메모리 복구를 위해 종료될 수 있습니다. 보통 후방에 있는 프로세스는 많이 존재할 수 있기 때문에, 시스템은 최근 사용 목록(least recently used list, LRU list)을 참조하여, 최근에 사용된 Activity를 가장 나중에 종료시킵니다. Activity가 생명주기 콜백 메소드들을 제대로 구현하여, Activity 종료시 현재 상태를 저장하고, 재생성시 현재 상태를 복구하고 있다면, Activity가 종료되고 나서 사용자가 뒤로 버튼을 눌러 다시 돌아왔을 때에 Activity가 재생성 되면서 이전 상태로 복구되기 때문에 사용자에게 비정상적인 경험을 주지는 않습니다. Activity에서 현재 상태를 저장하고 복구하는 방법에 대해서는 Activity 상태 저장하기에서 학습하실 수 있습니다. 

5. 텅 빈 프로세스(Empty process)

활성화된 앱 컴포넌트가 하나도 없는 프로세스입니다. 이러한 프로세스가 존재하는 이유는, 프로세스 자체를 캐싱(caching)함으로써 앱 컴포넌트를 다시 실행해야할 때 프로세스를 새로 생성하지 않기 위함입니다. 시스템은 메모리가 부족한 경우뿐만 아니라, 프로세스 캐시와 커널 캐시 간의 균형을 맞추기 위해 이러한 프로세스들을 종료시키기도 합니다. 

안드로이드 시스템은 프로세스가 실행 중인 앱 컴포넌트들의 상태에 기반하여, 해당 프로세스를 위의 5개 계층의 조건에 부합하는 계층 중 가장 상위 계층으로 지정합니다. 예를 들어, 프로세스가 Service와 보이는(visible) Activity를 실행 중이라면, 그 프로세스는 서비스 프로세스가 아니라 보여지고 있는 프로세스입니다. 

그리고, 프로세스의 계층은 프로세스 간의 의존성에 의해 결정될 수도 있습니다. 어떤 프로세스 B가 다른 프로세스 A에 의존적인 상황이라면, A는 B보다 낮은 계층일 수 없습니다. 예를 들면, 프로세스 A의 컨텐트 프로바이더가 프로세스 B의 클라이언트에게 데이터를 제공하고 있는 상황이거나, 또는 프로세스 A의 Service가 프로세스 B의 컴포넌트에 바인드 되어 있는 상황인 경우, 프로세스 A의 계층은 프로세스 B의 계층 이상이 되어야 합니다. 

Service를 실행 중인 프로세스가 후방에 있는 Activity를 실행 중인 프로세스보다 중요도 계층이 높기 때문에, Activity에서 시간이 오래 걸리는(long-running) 작업을 비동기로 실행시키기 위하여, 쓰레드 보다는 Service를 이용하는 것이 좋습니다. 예를 들어, 어떤 Activity에서 사진을 업로드 해야하는 경우에 Service를 생성하여 사용하면 그 Activity에서 빠져나가더라도 "서비스 프로세스"가 되지만, 쓰레드를 생성하여 사용했다면 Activity에서 빠져나가는 순간 "후방에 있는 프로세스"가 되기 때문에 더 중요도가 낮아지게 되는 것입니다. 그리고 이와 같은 이유로, 브로드캐스트 리시버의 경우에도 시간이 오래 걸리는(time-consuming) 작업은 쓰레드보다 Service를 사용하는 것이 좋습니다. 


쓰레드

앱이 실행되면, 시스템은 "메인 쓰레드"를 생성합니다. 메인 쓰레드는 버튼 같은 사용자 인터페이스의 이벤트 관련 동작 및 화면에 뷰를 그려주는 동작 등을 담당하며, android.widgetandroid.view 패키지에 포함된(안드로이드 UI 툴킷에 포함된) 뷰들과의 상호작용을 담당합니다. 그래서 메인 쓰레드를 UI 쓰레드라고 부르기도 합니다. 

시스템은 각 컴포넌트에 대하여 별도의 쓰레드를 생성하지 않습니다. 동일한 프로세스 내에서 실행되는 모든 컴포넌트들은 하나의 UI 쓰레드에서 실행됩니다. 따라서, onKeyDown() 같은 이벤트 관련 콜백 메소드나 앱 컴포넌트의 생명주기 콜백 메소드 등의 시스템 콜백 메소드는 항상 UI 쓰레드에서 실행됩니다. 

예를 들어, 사용자가 버튼을 터치하면, UI 쓰레드가 터치 이벤트를 버튼에게 보내서 눌려진(pressed) 상태로 변경시킨 후, 새로 그리기(invalidate) 요청을 이벤트 큐에 추가합니다. 그리고 적당한 시점에 UI 쓰레드가 다시 해당 요청을 꺼내서(dequeues) 버튼에게 새로 그리라고 알려주는 것입니다. 

사용자와의 상호작용과 관련된 콜백 메소드에서 많은 일을 해야하는 경우, UI 쓰레드에서만 처리하게 되면 퍼포먼스가 낮아질 수 있습니다. 특히, 네트워크 관련 작업이나 데이터베이스에 질의하는 등의 시간이 오래 걸리는 작업은, 그 시간 동안 전체 UI를 멈춰(block) 버립니다. 이렇게 UI 쓰레드가 멈추면, 어떠한 이벤트도 발생하지 못하고, 화면에 뷰를 새로 그리지도 못하게 되며, 이런 상태로 약 5초 정도 지나면 응답대기(application not responding, ANR) 다이얼로그를 출력하게 됩니다. 이것은 사용자로 하여금 짜증을 유발시킬 수 있고 앱을 삭제하게 만드는 원인이 될 수도 있습니다. 

게다가, 안드로이드 UI 툴킷은 멀티 쓰레드에 안전하지(thread-safe) 않습니다.  따라서 UI 관련 갱신 작업을 워커 쓰레드에서 하면 안됩니다. 간단히 정리하자면, UI 관련 작업과 관련하여 아래 2가지 규칙을 지켜야 합니다.

1. UI 쓰레드를 멈추면(block) 안됩니다.

2. UI 쓰레드가 아닌 쓰레드에서 안드로이드 UI 툴킷에 접근하면 안됩니다.


워커(Worker) 쓰레드

위에서 설명한 바와 같이, UI 쓰레드만 사용하는 경우 쓰레드가 멈추면 앱이 아무런 반응을 할 수 없는 상태가 됩니다. 따라서 시간이 걸리는 작업을 해야하는 경우에는 별도의 쓰레드(후방(background) 쓰레드 또는 워커(worker) 쓰레드)를 생성해야 합니다. 

아래 코드는, 클릭이 발생했을 때 별도의 쓰레드에서 이미지를 다운로드하여 ImageView에 보여주는 것을 나타냅니다:

public void onClick(View v) {
   
new Thread(new Runnable() {
       
public void run() {
           
Bitmap b = loadImageFromNetwork("http://example.com/image.png");
            mImageView
.setImageBitmap(b);
       
}
   
}).start();
}

위의 코드는 네트워크 관련 작업을 처리하기 위해 쓰레드를 생성하고 있기 때문에 제대로 동작할 것처럼 보입니다. 하지만, 위에서 설명한 2가지 규칙 중 하나인 "UI 쓰레드가 아닌 쓰레드에서 안드로이드 UI 툴킷에 접근하면 안된다"는 규칙에 어긋난 코드이기 때문에, ImageView에 이미지를 셋팅하는 작업은 UI 쓰레드에서 하도록 수정해야 합니다. 

워커 쓰레드에서 UI 쓰레드를 실행하는 방법은 아래와 같이 몇가지가 있습니다:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)

따라서, 위의 잘못된 코드는 View.post(Runnable) 메소드를 이용하여 아래 코드와 같이 수정할 수 있습니다:

public void onClick(View v) {
   
new Thread(new Runnable() {
       
public void run() {
           
final Bitmap bitmap =
                    loadImageFromNetwork
("http://example.com/image.png");
            mImageView
.post(new Runnable() {
               
public void run() {
                    mImageView
.setImageBitmap(bitmap);
               
}
           
});
       
}
   
}).start();
}

이제 이 구현은 멀티 쓰레드에 안전(thread-safe)합니다. 네트워크 관련 작업은 별도의 쓰레드를 생성하여 실행하고, ImageView에 이미지를 셋팅하는 작업은 UI 쓰레드에서 실행합니다. 

하지만 복잡도가 증가하면, 코드도 복잡해지고 유지하기 어려워질 수 있습니다. 그리고 UI 쓰레드와 워커 쓰레드 간에 메시지를 전달해야 한다면 Handler를 사용할 수 있으며, AsyncTask를 사용하여 UI와의 상호작용을 위한 워커 쓰레드의 실행을 단순화할 수도 있습니다. 


AsyncTask 사용하기

AsyncTask는 비동기로 작업을 수행하고 나서 UI를 갱신하는 경우에 사용합니다. AsyncTask를 사용하면, 워커 쓰레드에서 시간이 오래 걸릴 수 있는 작업을 수행하고 그 결과를 UI 쓰레드로 보내주는데, 이때 쓰레드를 직접 다루지 않아도 되고, 핸들러(handler)도 따로 사용할 필요가 없습니다.

AsyncTask를 사용하기 위해서는, AsyncTask 클래스를 확장하는 서브클래스를 만들어서 doInBackground() 메소드를 구현해야 합니다. 그리고 onPostExecute() 메소드를 구현함으로써 doInBackground()의 결과를 전달 받아서 UI를 멀티쓰레드에 안전하게(thread-safe) 갱신할 수 있습니다. 마지막으로 AsyncTask 서브클래스에 대한 인스턴스의 execute() 메소드를 호출함으로써 실행할 수 있습니다. 

아래 예제 코드는, 위에서 쓰레드를 사용했던 예제에 대하여 AsyncTask를 사용하는 방식으로 수정한 것입니다:

public void onClick(View v) {
   
new DownloadImageTask().execute("http://example.com/image.png");
}

private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
   
/** The system calls this to perform work in a worker thread and
      * delivers it the parameters given to AsyncTask.execute() */

   
protected Bitmap doInBackground(String... urls) {
       
return loadImageFromNetwork(urls[0]);
   
}

   
/** The system calls this to perform work in the UI thread and delivers
      * the result from doInBackground() */

   
protected void onPostExecute(Bitmap result) {
        mImageView
.setImageBitmap(result);
   
}
}
이제 워커 쓰레드에서 수행할 작업과 UI 쓰레드에서 수행할 작업이 분리되었기 때문에, UI는 안전해졌고 코드는 단순해졌습니다.

AsyncTask에 대한 자세한 내용은 클래스 문서를 참조해야겠지만, 아래와 같이 간단히 요약할 수 있습니다:

  • 매개변수, 프로그레스(progress) 변수, 비동기 처리에 대한 결과값에 대하여 제너릭(generic)을 이용해서 클래스 타입을 지정할 수 있습니다. 
  • AsyncTask를 실행하면 워커 쓰레드에서 doInBackground() 메소드를 자동적으로 실행해줍니다.
  • onPreExecute(), onPostExecute(), onProgressUpdate() 메소드들은 UI 쓰레드에서 실행됩니다.
  • doInBackground()에서 리턴하는 값은 onPostExecute()로 전달됩니다.
  • doInBackground()에서 원하는 시점에 publishProgress()를 호출하면, UI 쓰레드에서 onProgressUpdate()가 실행됩니다.
  • 실행된 태스크는 때(any time)와 장소(any thread)를 가리지 않고 취소시킬 수 있습니다. 

주의사항: 워커 쓰레드를 사용하는 경우 만날 수 있는 또 다른 문제는, 실행 중에 상태가 바뀔 때(예를 들면 디바이스가 가로 또는 세로 모드로 변경되는 경우) 액티비티가 재시작이 되면서 워커 쓰레드가 종료될 수도 있다는 것입니다. 따라서 태스크를 사용하는 경우, 액티비티가 종료될 때 태스크를 취소했다가 액티비티가 다시 시작될 때 태스크를 다시 실행하도록 해야 합니다. Shelves 샘플앱의 소스 코드를 참고하실 수 있습니다. 


멀티쓰레드에 안전한(Thread-safe) 메소드

경우에 따라서는 메소드가 둘 이상의 쓰레드에서 호출될 수도 있기 때문에, 메소드를 구현할 때는 멀티쓰레드에 안전하게(thread-safe) 구현해야 합니다. 

이것은, 액티비티에 바인드 된 서비스의 메소드처럼 원격으로 호출되는 메소드의 경우에 특히 더 그렇습니다. IBinder 인터페이스를 구현한 클래스의 메소드들을 IBinder 구현 객체가 실행되고 있는 동일한 프로세스에서 호출하는 경우, 해당 메소드들은 호출하는 쪽의 쓰레드에서 실행됩니다. 하지만 다른 프로세스에서 호출하는 경우에는, IBinder 구현 객체가 실행되고 있는 프로세스의 쓰레드 풀에서(UI 쓰레드는 제외) 시스템에 의해 선택된 쓰레드에서 해당 메소드들이 실행됩니다. 예를 들어, onBind() 메소드는 서비스가 속한 프로세스의 UI 쓰레드에서 실행되지만, onBind()에서 리턴해주는 IBinder 객체의 메소드들은 (UI 쓰레드가 아닌) 시스템이 선택한 쓰레드에서 실행되는 것입니다. 그리고, 서비스는 둘 이상의 클라이언트에게 바인드 될 수 있기 때문에, IBinder 객체의 메소드들은 둘 이상의 서로 다른 쓰레드에서 실행될 수 있습니다. 따라서 IBinder의 구현 클래스는 멀티 쓰레드에 안전(thread-safe)해야 합니다. 

이와 비슷하게, 컨텐트 프로바이더도 둘 이상의 프로세스로부터 데이터 요청을 받을 수 있습니다. 비록 ContentResolverContentProvider 클래스는 프로세스간 통신에 대한 부분을 숨겨놨지만(캡슐화), ContentProvider의 query(), insert(), delete(), update(), getType()과 같은 메소드들은 ContentProvider가 속한 프로세스의 쓰레드 풀에서 선택된 쓰레드(UI 쓰레드 제외)에서 실행됩니다. 이러한 메소드들도 둘 이상의 쓰레드에서 호출될 수 있기 때문에, 반드시 멀티 쓰레드에 안전(thread-safe)하게 구현되어야 합니다. 


프로세스간 통신(IPC)

안드로이드는 원격 프로시저 호출(RPC) 방식을 사용하여 프로세스간 통신(IPC)을 지원합니다. 이는 원격에서(다른 프로세스에서) 실행되는 액티비티 또는 다른 앱 컴포넌트에서의 호출을 의미하며, 그 호출에 대한 리턴값을 호출한 쪽에 전달해 줄 수 있습니다. IPC를 할 때는 메소드 호출과 그에 따른 데이터들을 OS가 이해할 수 있도록 분해하고, 호출하는 쪽의 프로세스로부터 전송하고, 호출받는 쪽의 프로세스에서 수신되어 재조립 및 재지정의 과정을 거치게 됩니다. 그리고 리턴값을 반대 방향으로 전송합니다. 안드로이드에는 이미 이러한 IPC에 대한 모든 코드가 구현되어 있기 때문에, 내 앱에서는 RPC 인터페이스에 대한 정의 및 구현에만 집중하면 됩니다. 

IPC를 수행하기 위해서는, 내 앱에서 bindService() 메소드를 호출함으로써 서비스를 바인드해야 합니다. 이에 대한 자세한 내용은 서비스에 대하여 문서에서 학습하실 수 있습니다. 


Posted by 개발자 김태우
,

(원문: http://developer.android.com/guide/topics/appwidgets/index.html)

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

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


앱 위젯


앱 위젯은 (런처의 홈 스크린처럼) 다른 앱에 들어가며 주기적으로 업데이트 될 수 있는 내 앱의 독립적인 작은 뷰를 말합니다. 이러한 앱 위젯은 앱 위젯 프로바이더를 이용하여 만들 수 있습니다. 그리고, 다른 앱의 위젯을 담을 수 있는 앱 컴포넌트를 앱 위젯 호스트라고 합니다. 아래의 이미지는 국내 유명 음악 앱의 위젯입니다.

본 문서에서는 앱 위젯 프로바이더를 이용하여 앱 위젯을 만드는 방법에 대해 학습할 것입니다. 그리고 앱 위젯 호스트(AppWidgetHost)를 만드는 방법에 대해서는 앱 위젯 호스트 문서에서 학습하실 수 있습니다. 


기본 개념

앱 위젯을 만들기 위해서는 아래의 몇가지가 필요합니다:

AppWidgetProviderInfo 객체

앱 위젯의 레이아웃, 업데이트 시기와 같은 메타정보와 AppWidgetProvider 클래스에 대한 정보를 가지고 있는 객체이며, XML 파일로 정의해야 합니다.

AppWidgetProvider 클래스 구현

앱 위젯에 대한 인터페이스들을 구현하는 클래스입니다. BroadcastReceiver 클래스를 확장한 클래스로서 브로드캐스트 이벤트들을 받아서 처리합니다. 앱 위젯이 업데이트되거나, 활성화 및 비활성화되거나, 삭제될때 브로드캐스트 이벤트를 받습니다. 따라서 이 클래스를 확장하여 내 앱의 위젯이 해당 이벤트들을 받았을때 어떻게 동작할지를 구현할 수 있습니다.

View 레이아웃

앱 위젯의 레이아웃을 XML 파일로 정의해야 합니다. 

그리고, 앱 위젯에 대한 설정 액티비티를 만들 수 있습니다. 이것은 선택적으로 적용 가능한 액티비티로서, 사용자가 앱 위젯을 화면에 추가할 때 실행되어 앱 위젯에 대한 설정을 할 수 있습니다. 

이어지는 섹션들에서는 위의 컴포넌트들 각각에 대하여 설명합니다.


매니페스트 파일에서 앱 위젯 선언하기

첫번째로, 내 앱의 AndroidManifest.xml 파일에 AppWidgetProvider 클래스를 선언합니다. 예제 코드:

<receiver android:name="ExampleAppWidgetProvider" >
   
<intent-filter>
       
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
   
</intent-filter>
   
<meta-data android:name="android.appwidget.provider"
               
android:resource="@xml/example_appwidget_info" />
</receiver>

<receiver> 요소의 android:name에는 앱 위젯에 대한 AppWidgetProvider 클래스를 지정해줘야 합니다. 

<intent-filter> 요소는 <action>을 포함해야하고, 이 <action>은 android:name 속성을 가지고 있어야 합니다. 위 예제는, ExampleAppWidgetProviderandroid.appwidget.action.APPWIDGET_UPDATE 액션을 갖는 인텐트에 대하여 동작한다는 것을 의미합니다. 이 액션(action)은 앱 개발자가 매니페스트 파일에 선언해야 하는 유일한 액션이며, 다른 브로드캐스트들은 AppWidgetManager에 의해 자동적으로 AppWidgetProvider에 전달됩니다. 

<meta-data> 요소는 AppWidgetProviderInfo에 대한 정보를 지정하며, 아래 속성들을 필요로 합니다:

  • android:name - 메타데이터의 이름을 지정하는 것으로서, AppWidgetProviderInfo에 대한 데이터임을 나타내기 위해 android.appwidget.provider를 사용합니다.
  • android:resource - AppWidgetProviderInfo에 대한 리소스 파일을 지정합니다.


AppWidgetProviderInfo 메타정보 추가하기

AppWidgetProviderInfo는 앱 위젯의 본질에 대하여 정의합니다. 예를 들어, 앱 위젯의 최소 크기, 레이아웃, 업데이트 빈도, 그리고 (선택적으로) 위젯 생성시점에 실행할 설정 액티비티를 정의합니다. 이것은 하나의 <appwidget-provider> 요소를 갖는 XML 파일이며 res/xml/ 경로상에 저장됩니다.

예제 코드:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
   
android:minWidth="40dp"
   
android:minHeight="40dp"
   
android:updatePeriodMillis="86400000"
   
android:previewImage="@drawable/preview"
   
android:initialLayout="@layout/example_appwidget"
   
android:configure="com.example.android.ExampleAppWidgetConfigure"
   
android:resizeMode="horizontal|vertical"
   
android:widgetCategory="home_screen|keyguard"
   
android:initialKeyguardLayout="@layout/example_keyguard">
</appwidget-provider>

<appwidget-provider> 요소의 각 속성들에 대하여 설명합니다:

  • minWidthminHeight 속성은 앱 위젯의 (기본적인, 최초의) 최소 크기를 지정합니다. 기본 홈 화면은 앱 위젯을 윈도우에 배치할 때 그리드 형태의 셀들에 기반하여 배치합니다. 만약 앱 위젯의 minWidth나 minHeight가 셀의 크기와 일치하지 않는다면, 근처의 셀이 앱 위젯의 영역으로 사용됩니다. 
    메모: 앱 위젯이 여러 디바이스에 호환되려면, 앱 위젯의 최소 크기가 4x4를 넘어가지 않도록 해야합니다.
  • minResizeWidthminResizeHeight 속성은 앱 위젯의 (절대적인) 최소 크기를 지정합니다. 이 값들은 앱 위젯이 너무 작아져서 내용들이 잘 보이지 않거나 사용하기 어려울 정도가 되지 않도록 지정해두는 최소값입니다. 이 값들을 이용하면 (사용자에 의해) 앱 위젯이 minWidth와 minHeight에 지정된 크기보다 더 작게 리사이즈 될 수 있습니다. (이 속성들은 안드로이드 3.1에서 소개되었습니다.)
  • updatePeriodMillis 속성은 AppWidgetProvider의 onUpdate() 콜백 메소드가 호출되는 주기를 정의합니다. 하지만 실제 업데이트 시기가 정확히 지정된 시간을 따른다고 보장할 수는 없습니다. 그리고 가능하다면 배터리의 사용량을 줄이기 위해서 한시간에 한번 이상은 업데이트를 하지 않는 것이 좋습니다. 이는 설정에서 사용자가 직접 지정하도록  할 수 있습니다. 어떤 사용자는 15분마다 업데이트 되는 주식 시세 표시 위젯을 원할 수도 있고, 또 어떤 사용자는 하루에 4번만 업데이트 되는 위젯을 원할 수도 있기 때문입니다. 
    메모: 만약 updatePeriodMillis에 지정한 시간이 지나 업데이트할 시간이 되었을 때 디바이스가 슬립 상태(화면이 꺼져 있는 상태)라면, 디바이스는 업데이트를 수행하기 위해 화면을 깨웁니다. 만약 한시간에 한번 이상 업데이틀 하지 않도록 했다면, 배터리 소모에 큰 악영향은 없겠지만, 자주 업데이트를 해야 하거나, 디바이스가 슬립 상태일 때는 업데이트를 하지 않아야 한다면, 알람(alarm)을 기반으로 개발하는 것이 나을 것입니다. 그러기 위해서는 일단 updatePeriodMillis 값을 0으로 지정하여 주기적 업데이트를 비활성화 한 후, AlarmManager를 이용하여 AppWidgetProvider가 받을 인텐트로 알람을 셋팅하면 됩니다. 알람 타입을 ELAPSED_REALTIME 또는 RTC로 지정하면 디바이스가 깨어날때만 알람이 전달됩니다.
  • initialLayout 속성은 앱 위젯의 레이아웃을 정의하는 xml 리소스 파일을 가리킵니다.
  • configure 속성은 사용자가 앱 위젯을 추가할 때 위젯 관련된 값들을 설정하기 위해 실행되는 액티비티를 정의합니다. 이것은 선택적인 사항이며, 아래의 앱 위젯의 설정 액티비티 만들기에서 자세히 설명합니다.
  • previewImage 속성은 앱 위젯이 어떤 모습일지 위젯을 고를 때 보여주는 이미지를 정의하며, 이 값이 없으면 앱 아이콘으로 대체됩니다. 이 속성은 안드로이드 3.0 버전부터 추가되었으며, 자세한 내용은 미리보기 이미지 설정하기에서 학습할 수 있습니다.
  • autoAdvanceViewId 속성은 위젯의 호스트에 의해 자동으로 변형되는 (auto-scaled) 뷰에 대한 ID를 지정합니다. (안드로이드 3.0 버전부터 추가되었습니다.)
  • resizeMode 속성은 위젯이 사용자에 의해 리사이즈 되는 것과 관련된 값을 지정하며, "horizontal"이나 "vertical"이나 "none"이 들어갈 수 있습니다. 위젯을 누르고 있으면 리사이즈 핸들이 나타나게 되고, 좌우(horizontal) 또는 상하(vertical)로 드래그하여 크기를 조정할 수 있습니다. 상하좌우 모두 리사이즈 되도록 하기 위해서는 "horizontal|vertical" 로 지정하면 됩니다. (안드로이드 3.0 버전부터 추가되었습니다.)
  • minResizeHeight 속성은 위젯이 리사이즈 될 수 있는 최소 높이를 지정합니다(dp단위로 지정). 이 값이 minHeight보다 크거나, resizeMode에 "vertical" 설정이 되어 있지 않으면 리사이즈 되지 않습니다. (안드로이드 4.0 버전부터 추가되었습니다.)
  • minResizeWidth 속성은 위젯이 리사이즈 될 수 있는 최소 너비를 지정합니다. 이 값이 minWidth보다 크거나, resizeMode에 "horizontal" 설정이 되어 있지 않으면 리사이즈 되지 않습니다. (안드로이드 4.0 버전부터 추가되었습니다.)
  • widgetCategory 속성은 앱 위젯을 홈화면에 노출할지("home_screen"), 잠금화면에 노출할지("keyguard"), 또는 둘 다 노출할지("home_screen|keyguard")를 선언합니다. 둘 다 노출되는 위젯은 위젯 클래스 작성시 디자인 가이드라인을 준수해야 하며, 자세한 내용은 잠금화면에 앱 위젯 보이기에서 학습할 수 있습니다. 이 속성의 기본값은 "home_screen"입니다. (안드로이드 4.2 버전부터 추가되었습니다. 안드로이드 5.0 버전에서 deprecated 된 것으로 보입니다.)
  • initialKeyguardLayout 속성은 잠금화면에 보여줄 앱 위젯의 레이아웃 xml 파일을 가리킵니다. 이것은 android:initialLayout과 같은 방식으로 동작합니다. 즉, 앱 위젯이 초기화될 때 보여지며 업데이트 될 수 있는 레이아웃입니다. (잠금화면용 레이아웃을 따로 지정할 수 있다는 것인데, 안드로이드 4.2 버전부터 추가되었습니다. 안드로이드 5.0 버전에서 deprecated 된 것으로 보입니다.)


앱 위젯 레이아웃 만들기

앱 위젯을 만들기 위해서는 res/layout 디렉토리에 xml 레이아웃 파일이 정의되어 있어야 하며, 아래 나열된 뷰 객체들을 사용할 수 있습니다. 

앱의 레이아웃을 만드는데 익숙하다면 앱 위젯의 레이아웃을 만드는 것도 어렵지 않습니다. 다만, 앱 위젯의 레이아웃은 RemoteViews를 기반으로 한다는 데에 주의해야 하며, 이는 모든 종류의 뷰나 레이아웃 클래스를 지원하지는 않는다는 것을 의미합니다. 

RemoteViews 객체는 아래의 레이아웃 클래스들을 지원하고:

FrameLayout
LinearLayout
RelativeLayout
GridLayout

아래의 뷰 클래스들을 지원합니다:

AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper

위에 나열된 클래스들 이외의 클래스들은 지원되지 않으며, 심지어 위의 클래스들을 상속받아 만든 커스텀뷰도 지원되지 않습니다. 

RemoteViews는 ViewStub 도 지원하는데, 이것은 처음에는 보이지 않고 크기가 없는 상태였다가 나중에 필요시 런타임에 생성되는 (lazy inflate) 뷰입니다. 


앱 위젯에 여백 추가하기

위젯은 일반적으로 스크린 경계선까지 꽉 채워져서는 안되고 다른 위젯과 겹쳐서도 안되기 때문에, 위젯의 모든 경계 부분에는 여백(margin)이 있어야 합니다. 

안드로이드 4.0 버전부터는 앱 위젯이 홈 화면의 다른 위젯이나 앱 아이콘들과 더 잘 구분되어 보이도록 하기 위해, 위젯의 경계 부분과 위젯이 들어가는 영역의 경계 부분 사이에 공간(padding, 이하 패딩으로 지칭)을 자동으로 만들어 줍니다. 따라서 이와 같은 유용한 기능을 이용하기 위해서는 앱의 매니페스트 파일의 targetSdkVersion14 이상으로 선언해야 합니다. 

sdk 버전이 14 미만(안드로이드 4.0버전 미만)일 때는 자체적으로 정의한 패딩을 지정하고, 14 이상(안드로이드 4.0버전 이상)일 때는 자동으로 만들어진 패딩 이외에 패딩이 더 추가되지 않도록 하기 위해서 아래와 같이 하면 됩니다.

1. 앱의 매니페스트 파일의 targetSdkVersion14 또는 그 이상으로 선언합니다.

2. 아래와 같이 레이아웃 파일을 작성합니다. android:padding@dimen/widget_margin을 넣어줍니다. 

<FrameLayout
 
android:layout_width="match_parent"
 
android:layout_height="match_parent"
 
android:padding="@dimen/widget_margin">

 
<LinearLayout
   
android:layout_width="match_parent"
   
android:layout_height="match_parent"
   
android:orientation="horizontal"
   
android:background="@drawable/my_widget_background">
    …
 
</LinearLayout>

</FrameLayout>

3. 두 개의 디멘션(dimension) 리소스를 생성하는데, 하나는 안드로이드 4.0버전 미만에서 자체 패딩을 주기 위해 res/values 에 생성하고, 다른 하나는 안드로이드 4.0버전 이상에서 자체 패딩을 주지 않기 위해 res/values-v14 에 생성합니다:

res/values/dimens.xml:

<dimen name="widget_margin">8dp</dimen>

res/values-v14/dimens.xml:

<dimen name="widget_margin">0dp</dimen>


다른 방법으로는, 위젯의 배경을 나인패치(nine-patch) 이미지로 설정하되 안드로이드 4.0버전 미만과 그 이상을 위한 이미지를 따로 준비하면 됩니다. 


AppWidgetProvider 클래스 사용하기

AppWidgetProvider 클래스는 앱 위젯의 브로드캐스트들을 편리하게 다루기 위해 BroadcastReceiver 클래스를 상속받아 구현된 클래스입니다. AppWidgetProvider는 앱 위젯이 업데이트되거나, 삭제되거나, 활성화/비활성화될 때와 같이 위젯과 관련된 이벤트가 발생할 때 그에 대한 브로드캐스트를 받으며, 호출되는 콜백 메소드들은 아래와 같습니다:

onUpdate()

이 메소드는 AppWidgetProviderInfo에 정의된 updatePeriodMillis 만큼의 시간간격으로 주기적으로 호출되는 콜백 메소드입니다. 이 메소드는 또한 사용자가 위젯을 화면에 추가할때도 호출되며, 필요하다면 여기서 뷰에 이벤트 리스너를 달거나 서비스(Service component)를 시작하는 작업을 해야 합니다. 하지만 만약 앱 위젯에 대한 설정 액티비티를 선언했다면, 사용자가 위젯을 화면에 추가하는 시점에는 이 메소드가 호출되지 않으며, 위젯이 화면에 추가된 후에야 다시 주기적으로 호출됩니다. 따라서 설정 액티비티가 선언되어 있다면, 뷰에 이벤트 리스너를 달거나 하는 등의 초기화 작업을, 설정 액티비티에서 설정이 완료되는 시점에 하도록 해야 합니다. (앱 위젯의 설정 액티비티 만들기 참고.)

onAppWidgetOptionsChanged()

이 메소드는 위젯이 화면에 추가되어 처음 보여지거나 리사이즈될 때마다 호출되는 콜백 메소드이며, 위젯의 크기에 따라 내용을 변경해야 하는 경우 사용합니다. getAppWidgetOptions() 메소드를 호출하면 번들(Bundle)을 리턴해 주는데 여기에서 아래의 값들을 얻을 수 있습니다:

    • OPTION_APPWIDGET_MIN_WIDTH - 위젯의 현재 너비에 대한 작은 쪽 경계의 크기. dp 단위.
    • OPTION_APPWIDGET_MIN_HEIGHT - 위젯의 현재 높이에 대한 작은 쪽 경계의 크기. dp 단위.
    • OPTION_APPWIDGET_MAX_WIDTH - 위젯의 현재 너비에 대한 큰 쪽 경계의 크기. dp 단위.
    • OPTION_APPWIDGET_MAX_HEIGHT - 위젯의 현재 높이에 대한 큰 쪽 경계의 크기. dp 단위. 

이 메소드는 API 레벨 16(안드로이드 4.1버전)부터 추가되었습니다. 따라서 이 메소드를 구현하고자 한다면, 하위 버전의 디바이스에서 이 메소드가 호출되지 않는다는 점에 주의해야 합니다. 

onDeleted(Context, int[])

이 메소드는 앱 위젯이 앱 위젯 호스트에서 삭제될 때 호출되는 콜백 메소드입니다.

onEnabled(Context)

이 메소드는 앱 위젯의 인스턴스가 처음 생성될 때 호출되는 콜백 메소드입니다. 예를 들어, 사용자가 어떤 하나의 앱에 대하여 2개의 앱 위젯을 화면에 추가할 경우, 이 메소드는 첫번째 위젯이 추가될 때만 호출됩니다. 위젯에 대한 데이터베이스를 생성하거나, 해당 앱에 대한 모든 위젯에 대하여 한번만 실행되기를 바라는 부분이 있다면, 여기에 구현하면 됩니다. 

onDisabled(Context)

이 메소드는 앱 위젯 호스트에서 모든 위젯이 삭제되는 시점, 즉 마지막으로 남아있던 위젯이 삭제될 때 호출되는 콜백 메소드입니다. 여기서는 onEnabled(Context)에서 했던 일들을 정리해야(clean up) 합니다(데이터베이스 삭제 등).

onReceive(Context, Intent)

이 메소드는 BroadcastReceiver 클래스의 기본적인 콜백 메소드로서, AppWidgetProvider 클래스에서 적당한 시점에 위의 콜백 메소드들이 호출되도록 잘 구현되어 있기 때문에, 보통의 경우에는 구현할 필요가 없고, 위의 콜백 메소드들로 충분하지 않을 경우에만 구현하면 됩니다. 

AppWidgetProvider의 가장 중요한 콜백 메소드는 onUpdate() 입니다. 왜냐하면 (설정 액티비티가 선언되어 있지 않다면) 앱 위젯이 호스트에 추가될 때 호출되는 메소드이며, 여기서 위젯의 초기화 작업 및 변경사항 적용을 해야하기 때문입니다. 만약 위젯이 사용자와 상호작용을 해야 한다면, 이 콜백 메소드에서 이벤트 리스너를 등록해야 합니다. 그리고 만약 앱 위젯이 파일이나 데이터베이스를 생성하지 않고, 그 외에도 위젯이 삭제될 때 정리해야할(clean up) 무언가가 없다면, AppWidgetProvider 개발시 onUpdate()만 구현하면 됩니다. 아래의 예제 코드는, 눌렀을 때 액티비티를 실행하는 버튼으로 구성된 AppWidgetProvider의 구현 클래스입니다: 

public class ExampleAppWidgetProvider extends AppWidgetProvider {

   
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
       
final int N = appWidgetIds.length;

       
// Perform this loop procedure for each App Widget that belongs to this provider
       
for (int i=0; i<N; i++) {
           
int appWidgetId = appWidgetIds[i];

           
// Create an Intent to launch ExampleActivity
           
Intent intent = new Intent(context, ExampleActivity.class);
           
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);

           
// Get the layout for the App Widget and attach an on-click listener
           
// to the button
           
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
            views
.setOnClickPendingIntent(R.id.button, pendingIntent);

           
// Tell the AppWidgetManager to perform an update on the current app widget
            appWidgetManager
.updateAppWidget(appWidgetId, views);
       
}
   
}
}
위의 AppWidgetProvider는 onUpdate() 콜백 메소드만 구현하고 있으며, 버튼 눌렀을 때 액티비티가 실행되도록 하기 위하여 setOnClickPendingIntent(int, PendingIntent)를 호출합니다. 그리고 화면에 ExampleAppWidgetProvider에 대한 위젯이 2개 이상 추가될 수 있기 때문에, 반복문을 통해 appWidgetIds의 모든 값에 대하여, 즉 모든 위젯에 대하여 업데이트 되는 내용을 적용해야 합니다. 그리고 위젯이 2개 이상일 경우에도 onUpdate()는 모든 위젯에 대하여 같은 시기에 호출됩니다. 예를 들어서 updatePeriodMillis가 2시간으로 정의되어 있고, 첫번째 위젯을 추가한 후 1시간 후에 두번째 위젯을 추가한 경우, onUpdate()는 첫번째 위젯이 추가된 시점을 기준으로 2시간 간격으로 호출되는 것입니다.  
메모: AppWidgetProvider는 BroadcastReceiver 클래스를 확장하여 만든 클래스이기 때문에, (BroadcastReceiver의 생명주기에 따라) 콜백 메소드가 실행된 후에 내 앱의 프로세스가 멈출 수도 있습니다. 그리고 앱 위젯의 셋업 과정이 (웹 요청 등의 원인으로) 오래 걸리는 경우에는 앱의 응답 지연 문제(Application Not Responding, ANR)를 방지하기 위하여, onUpdate() 메소드 안에서 서비스(Service)를 실행해 주는 것이 좋습니다. 


앱 위젯의 브로드캐스트 인텐트 받기

AppWidgetProvider는 브로드캐스트 인텐트의 액션값을 신경쓰지 않고 지정된 콜백 메소드들만 구현해 주면 되는 편리한 클래스입니다. 하지만 만약 브로드캐스트 인텐트를 직접 처리하고자 한다면, BroadcastReceiver의 확장 클래스를 만들거나 AppWidgetProvider의 onReceive(Context, Intent) 메소드를 오버라이드한 후 브로드캐스트 인텐트의 액션값에 대한 처리를 해주면 됩니다. 그 액션값들은 아래와 같습니다:

  • ACTION_APPWIDGET_UPDATE
  • ACTION_APPWIDGET_DELETED
  • ACTION_APPWIDGET_ENABLED
  • ACTION_APPWIDGET_DISABLED
  • ACTION_APPWIDGET_OPTIONS_CHANGED


앱 위젯의 설정 액티비티 만들기

사용자가 앱 위젯을 추가할 때 설정 액티비티를 보여줄 수 있습니다. 이 액티비티는 앱 위젯이 생성되는 시점에 앱 위젯 호스트에 의해 자동적으로 실행되며, 위젯의 색상이나 크기, 위젯 갱신 주기 등의 설정을 할 수 있습니다. 

설정 액티비티는 다른 액티비티들과 마찬가지로 매니페스트 파일에 정의되어야 하며, 앱 위젯 호스트에서 ACTION_APPWIDGET_CONFIGURE 액션값을 가진 암묵적 인텐트로 실행하기 때문에, 아래 예제 코드와 같이 해당 액션값에 대한 인텐트 필터를 추가해줘야 합니다:

<activity android:name=".ExampleAppWidgetConfigure">
   
<intent-filter>
       
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
   
</intent-filter>
</activity>

그리고 설정 액티비티는 AppWidgetProviderInfo XML파일의 android:configure 에도 선언되어야 합니다(위의 AppWidgetProviderInfo 메타정보 추가하기 참고). 예제 코드는 아래와 같습니다:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    ...
   
android:configure="com.example.android.ExampleAppWidgetConfigure"
    ...
>
</appwidget-provider>

액티비티의 이름은 패키지명을 포함한 전체 이름으로 선언해야 합니다. 왜냐하면 이는 내 앱의 범위 밖에서 참조되는 값이기 때문입니다. 

설정 액티비티를 만들기 위한 사전 작업은 위의 내용이 다입니다. 이제 설정 액티비티를 구현하면 되는데, 이때 기억해야할 중요한 내용이 두 개 있습니다:

  • 앱 위젯 호스트는 설정 액티비티를 실행할 때 startActivityForResult()를 호출하며, 설정 액티비티는 종료되기 전에 결과 인텐트를 셋팅해줘야 합니다. 결과 인텐트에는 앱 위젯 ID가 포함되어 있어야 하는데, 이는 설정 화면이 실행될 때 받는 인텐트에 EXTRA_APPWIDGET_ID로 담겨 있습니다. 그리고 결과 인텐트에도 같은 키값으로 저장하면 됩니다. 
  • 설정 액티비티가 실행된 경우에는, 앱 위젯이 생성될 때, 시스템이 ACTION_APPWIDGET_UPDATE 을 보내지 않기 때문에 onUpdate() 메소드가 호출되지 않습니다. 따라서, 설정 액티비티에서 그 역할을 해줘야 합니다. 위젯이 처음 생성되는 시점에 설정 액티비티에서  AppWidgetManager를 이용하여 업데이트 요청을 해야 합니다. 하지만 그 이후부터는 onUpdate()가 다시 호출됩니다. 즉, 앱 위젯이 생성되는 처음 1회만 onUpdate()가 스킵되는 것입니다. 

이어지는 내용은 설정 화면에서 위젯에 대하여 설정한 결과들을 위젯 호스트에게 어떻게 다시 전달해 주는지에 대한 내용입니다.


설정 액티비티에서 앱 위젯 업데이트하기

설정 액티비티가 실행되는 경우에는, 설정이 완료되었을 때 설정 액티비티에서 직접 위젯을 업데이트 해줘야 합니다. 이때 AppWidgetManager를 사용합니다. 

위젯을 업데이트하고 설정 액티비티를 종료하는 일련의 과정은 아래와 같습니다:

1. 설정 액티비티를 실행하는 인텐트로부터 앱 위젯 ID를 전달 받습니다:

Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null) {
    mAppWidgetId
= extras.getInt(
           
AppWidgetManager.EXTRA_APPWIDGET_ID,
           
AppWidgetManager.INVALID_APPWIDGET_ID);
}

2. 설정 액티비티 내에서 위젯 관련 설정이 진행됩니다.

3. 설정이 완료되었다면, AppWidgetManager의 인스턴스를 가져옵니다:

AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);

4. 아래와 같이 앱 위젯을 업데이트 합니다:

RemoteViews views = new RemoteViews(context.getPackageName(),
R
.layout.example_appwidget);
appWidgetManager
.updateAppWidget(mAppWidgetId, views);

5. 결과 인텐트를 만들어서 셋팅하고 설정 액티비티를 종료합니다:

    Intent resultValue = new Intent();
    resultValue
    .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
    setResult
    (RESULT_OK, resultValue);
    finish
    ();

팁: 설정이 완료되지 않고 설정 액티비티에서 벗어나는 경우에는 결과값에 RESULT_CANCELED를 셋팅합니다. 그러면 앱 위젯 호스트는 설정이 취소되었다는 결과를 받게 되고 앱 위젯은 화면에 추가되지 않을 것입니다. 

SDK에 포함되어 있는 샘플 프로젝트 중 하나인 ApiDemos에서 ExampleAppWidgetConfigure.java 파일을 참고하시기 바랍니다.


미리보기 이미지 설정하기

안드로이드 3.0 버전부터 앱 위젯이 어떻게 생겼는지를 나타내는 미리보기 이미지를 설정할 수 있으며, 이는 위젯 선택 화면에서 보여집니다. 미리보기 이미지를 따로 지정하지 않는 경우에는 앱의 아이콘이 그 역할을 대신합니다. 

예제 코드는 아래와 같습니다:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
  ...
 
android:previewImage="@drawable/preview">
</appwidget-provider>

앱 위젯의 미리보기 이미지를 쉽게 생성해주기 위해 안드로이드 에뮬레이터에는 "Widget Preview"라는 앱이 포함되어 있습니다. 이 앱을 실행한 후 위젯을 선택하여 이미지를 생성할 수 있습니다. 


잠금화면에 앱 위젯 보이기

안드로이드 4.2버전부터 위젯을 잠금화면에도 추가할 수 있게 되었습니다. 앱 위젯이 잠금화면에도 추가될 수 있도록 하기 위해서는, AppWidgetProviderInfo를 정의하는 XML 파일에 android:widgetCategory를 선언해줘야 합니다. 여기에 들어갈 수 있는 값으로는 "home_screen"과 "keyguard"가 있고, 하나 또는 두개 모두 들어갈 수 있으며, 잠금화면에 들어가려면 "keyguard"가 포함되어 있어야 합니다. 

( * 역자주: 잠금화면에 앱 위젯을 추가하는 기능은 안드로이드 5.0버전에서 deprecated 된 것으로 보입니다. 4.2~4.4버전에서 잠금화면에 위젯을 추가하는 방법은, 잠금화면의 상단영역을 좌에서 우로 스와잎하면 위젯을 추가할 수 있는 플러스(+) 버튼이 나오는데, 5.0버전 이상에서는 해당 기능이 동작하지 않습니다. 그리고 설정 > 개인설정 > 보안 > 화면보안 섹션에 위젯 사용에 대한 항목이 있는데, 5.0버전 이상에서는 해당 항목이 보이지 않습니다. 5.0버전 이상에서는 노티피케이션바 영역에 들어가는 뷰 영역이 잠금화면에도 보여지기 때문에, 위젯을 대체할 수 있다고 판단하여 deprecated 된 것으로 보입니다. )

기본적으로 위젯은 홈 화면에 들어가는 것이었기 때문에, android:widgetCategory의 기본값은 "home_screen"입니다. 따라서 위젯이 홈 화면에 들어갈 수 있고 또 잠금화면에도 들어갈 수 있게 하려면, 아래와 같이 "keyguard"를 추가하면 됩니다:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
   ...
   
android:widgetCategory="keyguard|home_screen">
</appwidget-provider>

위젯을 잠금화면과 홈화면에 모두 보여주고자 하는데 각각의 위젯에 다른 레이아웃을 적용하고 싶다면, 위젯이 추가되는 시점에 위젯 카테고리(widgetCategory)를 확인하여 각각에 맞는 레이아웃을 적용하면 됩니다. getAppWidgetOptions()를 호출하면 Bundle 객체를 리턴해 주는데, 여기에 OPTION_APPWIDGET_HOST_CATEGORY를 키로 하는 값이 WIDGET_CATEGORY_HOME_SCREEN 또는 WIDGET_CATEGORY_KEYGUARD 입니다. 이 값은 위젯이 속해 있는 곳의 위젯 호스트에 의해서 결정되는 값입니다. 그리고 아래 예제 코드에서와 같이 AppWidgetProvider에서 값을 확인할 수 있습니다:

AppWidgetManager appWidgetManager;
int widgetId;
Bundle myOptions = appWidgetManager.getAppWidgetOptions (widgetId);

// Get the value of OPTION_APPWIDGET_HOST_CATEGORY
int category = myOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, -1);

// If the value is WIDGET_CATEGORY_KEYGUARD, it's a lockscreen widget
boolean isKeyguard = category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;

위젯 카테고리 값이 확인되었다면 이제 그에 따라 레이아웃을 다르게 적용할 수도 있는 것입니다:

int baseLayout = isKeyguard ? R.layout.keyguard_widget_layout : R.layout.widget_layout;

그리고 잠금화면에 보여질 위젯에 대한 레이아웃을 android:initialKeyguardLayout에 지정해야 합니다. 이는 잠금화면용 레이아웃이라는 것만 빼면 나머지는 android:initialLayout과 동일합니다. 위젯이 초기화되기까지 시간이 다소 걸린다면 그 동안에 보여줄 레이아웃이 여기에 들어갑니다. 


크기 조정 가이드라인

잠금화면에 추가된 위젯에 대해서는 minWidth, minHeight, minResizeWidth, minResizeHeight 속성이 무시됩니다. 홈화면에 추가된 위젯에 대해서는 여전히 적용되는 속성이지만, 잠금화면에 추가된 위젯에 대해서만 무시되는 것입니다. 

잠금화면 위젯의 너비(width)는 항상 제공되는 영역을 꽉 채웁니다. 그리고 잠금화면 위젯의 높이(height)는 아래의 규칙들을 따릅니다:

  • 위젯이 위아래로 리사이즈 가능하다고 정의되어 있지 않은 경우(android:resizeMode에 vertical이 포함되어 있지 않은 경우), 위젯의 높이는 항상 "작은" 상태가 됩니다. 
    - 세로 모드(portrait mode)의 폰에서, "작은" 상태란 잠금해제 UI가 보여지는 영역을 제외한 나머지 영역을 의미합니다. 
    - 태블릿 또는 가로 모드(landscape mode)의 폰에서, "작은" 상태란 디바이스에 따라 다르게 정의될 수 있습니다. 
  • 위젯이 위아래로 리사이즈 가능하다고 정의되어 있는 경우, 세로 모드의 폰에서는 위젯의 높이가 "작은" 상태가 되고, 그 외의 모든 경우에는 가능한 공간을 꽉 채웁니다. 


앱 위젯에 목록 보이기

안드로이드 3.0 버전부터 앱 위젯에 목록(collections)을 보여줄 수 있게 되었습니다. 컨텐트 프로바이더와 같은 것들로부터 데이터를 받아와서 그 목록을 보여주기 위해 RemoteViewsService 를 사용할 수 있습니다. RemoteViewsService로부터 제공받는 데이터들을 보여주기 위해서 아래의 뷰들 중 하나를 사용할 수 있으며, 아래의 뷰들을 이제 "콜랙션 뷰(collection views)"라고 지칭하도록 하겠습니다. 

ListView

하나의 행에 하나의 항목이 있고, 위아래로 스크롤되는 뷰입니다. Gmail 앱의 위젯이 그 예입니다.

GridView

하나의 행에 하나 이상의 항목이 있을 수 있고, 위아래로 스크롤되는 뷰입니다. 북마크 앱의 위젯이 그 예입니다.

StackView

카드가 겹쳐져 있는 형태이며 위아래로 스와잎하면 가장 위의 카드가 뒤로 넘어가는 방식의 뷰입니다. 유튜브나 도서 앱의 위젯이 그 예입니다. 

AdapterViewFlipper

2개 이상의 뷰들을 ViewAnimator로 애니메이션하는 방식의 뷰이며, 한번에 하나의 뷰만이 화면에 보여집니다. 

위에서 언급한 바와 같이, 이러한 콜렉션 뷰들은 어댑터(Adapter)를 이용하여 다른 곳으로부터 데이터를 제공 받아서 보여줍니다. 어댑터는 데이터셋과 그에 대응되는 뷰 사이에서 중간자 역할을 하며 그 둘을 연결시켜 줍니다. 콜렉션 뷰들이 어댑터를 기반으로 해야하기 때문에, 안드로이드 프레임웍에는 위젯에서도 이러한 방식이 적용될 수 있도록 별도의 모듈이 포함되어 있어야 합니다. 기존의, 앱 내에 콜렉션 뷰를 보여줄 때의 방식은 RemoteViews를 사용하는 것이 아니기 때문에, 그에 대한 방식이 따로 있어야 하는 것입니다. 따라서 앱 위젯에서는 Adapter 대신에 Adapter 인터페이스를 래핑(wrapping)한 RemoteViewsFactory를 사용합니다. 동작하는 방식은 기존에 Adapter를 사용하던 방식과 유사합니다. RemoteViewsFactory가 항목의 위치를 받아서 그에 해당하는 아이템을 찾고 그 데이터를 RemoteViews 객체에 적용하여 화면에 보여주는 것입니다. 따라서 앱 위젯에 콜렉션 뷰를 사용하기 위해서는 RemoteViewsServiceRemoteViewsFactory를 구현해야 합니다. 

RemoteViewsService는 RemoteViewsFactory 인터페이스의 구현체인 어댑터를 가지고 있으며, 그 어댑터가 각 위치에 해당하는 RemoteViews 객체를 리턴해 줍니다. RemoteViewsFactory는 (ListView나 GridView와 같은) 콜렉션 뷰와 콜렉션 뷰에 보여줘야할 데이터 셋을 중간에서 연결해 주는 어댑터에 대한 인터페이스입니다. StackView Widget 샘플앱을 살펴보면, 아래와 같은 형태로 구현하고 있음을 알 수 있습니다:

public class StackWidgetService extends RemoteViewsService {
   
@Override
   
public RemoteViewsFactory onGetViewFactory(Intent intent) {
       
return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
   
}
}

class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {

//... include adapter-like methods here. See the StackView Widget sample.

}


샘플 앱

본 문서에 있는 예제 코드들은 StackView Widget 샘플앱으로부터 가져온 것들입니다:


위의 샘플 앱 위젯은 "0!"부터 "9!"까지의 문자를 보여주는 10개의 뷰의 스택으로 구성되어 있으며, 아래와 같이 동작합니다:

  • 사용자는 가장 앞에 있는 뷰를 아래로 플링(fling)하여 다음 뷰를 앞으로 가져오거나, 위로 플링하여 이전 뷰를 앞으로 가져올 수 있으며, 이는 StackView의 기본적인 동작입니다. 
  • 사용자가 뷰를 건드리지 않더라도, 슬라이드쇼처럼 적당한 시간을 주기로 자동으로 다음뷰로 넘어가도록 할 수 있습니다. 이는 res/xml/stackwidgetinfo.xml 파일에 android:autoAdvanceViewId="@id/stack_view"와 같이 선언하면 되며, @id/stack_view는 위젯 레이아웃의 StackView의 ID입니다.  
  • 사용자가 뷰를 클릭하면 "Touched view n" 이라는 토스트 메시지가 출력되며, 여기서 n은 터치된 뷰의 위치값을 나타냅니다. 이와 관련하여 더 자세한 내용은 각 항목에 기능 추가하기에서 학습하실 수 있습니다. 


목록을 보이게 하는 앱 위젯 구현하기

앱 위젯에서 목록을 보여주기 위해서는, (위에서 학습한 바와 같이) 앱 위젯을 구현하는데 필요한 일반적인 단계에 더하여, 아래에서 설명하는 추가적인 단계들을 진행해야 합니다. 


목록 구현을 위한 매니페스트 파일 수정사항

매니페스트 파일에서 앱 위젯 선언하기 섹션에서 학습한 내용에 더하여, 앱 위젯이 RemoteViewsService를 사용하여 목록을 보여주도록 하기 위해서는, 매니페스트 파일에 서비스(service)를 BIND_REMOTEVIEWS 퍼미션과 함께 선언해줘야 합니다. 이것은 다른 앱이 내 앱 위젯의 데이터에 접근하는 것을 막아줍니다. 예제 코드는 아래와 같습니다:

<service android:name="MyWidgetService"
...
android:permission="android.permission.BIND_REMOTEVIEWS" />

위 코드에서 MyWidgetService는 RemoteViewsService의 서브 클래스입니다. 


목록 구현을 위한 앱 위젯의 레이아웃

목록을 보여주고자 하는 앱 위젯의 레이아웃에는 콜렉션 뷰 중의 하나가 포함되어 있어야 합니다. 콜렉션 뷰에는 ListView, GridView, StackView, AdapterViewFlipper가 있습니다. 아래 예제 코드는 StackView Widget 샘플widget_layout.xml 파일입니다:

<?xml version="1.0" encoding="utf-8"?>

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   
android:layout_width="match_parent"
   
android:layout_height="match_parent">
   
<StackView xmlns:android="http://schemas.android.com/apk/res/android"
       
android:id="@+id/stack_view"
       
android:layout_width="match_parent"
       
android:layout_height="match_parent"
       
android:gravity="center"
       
android:loopViews="true" />
   
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
       
android:id="@+id/empty_view"
       
android:layout_width="match_parent"
       
android:layout_height="match_parent"
       
android:gravity="center"
       
android:background="@drawable/widget_item_background"
       
android:textColor="#ffffff"
       
android:textStyle="bold"
       
android:text="@string/empty_view_text"
       
android:textSize="20sp" />
</FrameLayout>

위 코드에서 empty_view는 stack_view가 빈(empty) 상태일 때 텍스트("This is the empty view")를 보여주는 뷰입니다. 

위의 레이아웃 파일은 앱 위젯 자체에 대한 레이아웃이며, StackView의 각각의 항목을 나타내는 레이아웃 파일도 필요합니다. StackView Widget 샘플widget_item.xml 파일이 그것입니다. 


목록 구현을 위한 AppWidgetProvider 클래스 구현

일반적인 앱 위젯과 마찬가지로, 목록을 보여주는 경우에도 AppWidgetProvider의 서브 클래스를 구성하는 대부분의 코드는 onUpdate() 메소드입니다. 일반적인 경우와의 가장 큰 차이점은 setRemoteAdapter() 메소드를 호출한다는 것입니다. 이것은 콜렉션 뷰가 데이터를 어디에서 가져오는지를 말해줍니다. RemoteViewsService는 내부적으로 RemoteViewsFactory의 구현체를 사용하여 콜렉션 뷰 각각의 위치에 해당 데이터를 연결해주는 어댑터의 역할을 해줍니다. setRemoteAdapter(int viewId, Intent intent) 에서 viewId는 콜렉션 뷰의 ID이고, intent는 RemoteViewsService의 구현체를 가리키는 인텐트입니다. ( * 역자주: setRemoteAdapter(int appWidgetId, int viewId, Intent intent) 는 API Level 14에서 deprecated 되었기 때문에, minSdk가 14이상이라면 아래 예제 코드에서 for 루프문을 제거할 수 있습니다. )

아래 예제 코드는 StackView Widget 샘플StackWidgetProvider.java의 onUpdate() 메소드 부분이며, 여기서 목록을 보여주기 위해 어댑터를 셋팅하는 부분을 볼 수 있습니다:

public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
   
// update each of the app widgets with the remote adapter
   
for (int i = 0; i < appWidgetIds.length; ++i) {
       
       
// Set up the intent that starts the StackViewService, which will
       
// provide the views for this collection.
       
Intent intent = new Intent(context, StackWidgetService.class);
       
// Add the app widget ID to the intent extras.
        intent
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
        intent
.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
       
// Instantiate the RemoteViews object for the app widget layout.
       
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
       
// Set up the RemoteViews object to use a RemoteViews adapter.
       
// This adapter connects
       
// to a RemoteViewsService  through the specified intent.
       
// This is how you populate the data.
        rv
.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);
       
       
// The empty view is displayed when the collection has no items.
       
// It should be in the same layout used to instantiate the RemoteViews
       
// object above.
        rv
.setEmptyView(R.id.stack_view, R.id.empty_view);

       
//
       
// Do additional processing specific to this app widget...
       
//
       
        appWidgetManager
.updateAppWidget(appWidgetIds[i], rv);  
   
}
   
super.onUpdate(context, appWidgetManager, appWidgetIds);
}


RemoveViewsService 클래스

위에서 설명한 바와 같이, RemoteViewsService의 서브 클래스는 내부적으로 RemoteViewsFactory의 구현체를 이용하여 목록의 각 항목에 대응되는 RemoteViews에 해당 데이터를 주입합니다. 

1. RemoteViewsService의 서브 클래스를 구현합니다. RemoteViewsService는 어댑터를 이용하여 RemoteViews에 데이터를 주입합니다. 

2. RemoteViewsService의 서브 클래스에는 RemoteViewsFactory 인터페이스의 구현체를 리턴해 주는 부분을 구현해야 합니다. RemoteViewsFactory는 (ListView, GridView 등의) 콜렉션 뷰와 그에 대응되는 데이터들을 연결시켜 주는 어댑터 인터페이스로서, Adapter 인터페이스의 랩퍼(wrapper) 인터페이스입니다.

다시 말해서, RemoteViewsService의 서브 클래스를 구현할 때 가장 중요한 일은 RemoteViewsFactory 인터페이스의 구현체를 리턴해 주는 것입니다. 


RemoteViewsFactory 인터페이스

RemoteViewsFactory 인터페이스를 구현한 클래스는 앱 위젯에 데이터를 제공해 줍니다. 즉, 위젯의 콜렉션 뷰의 각 아이템에 대한 레이아웃 XML 파일에 해당 데이터를 주입할 수 있게 해줍니다. 데이터는 데이터베이스로부터 얻어온 커서(cursor) 객체도 되고, 간단한 배열(array)도 될 수 있습니다. StackView Widget 샘플에서 데이터는 WidgetItem 객체를 담는 ArrayList 였습니다. RemoteViewsFactory는 위젯의 콜렉션 뷰에 데이터를 주입해 주는 어댑터인 것입니다. 

RemoteViewsFactory 인터페이스를 구현할 때, onCreate()getViewAt() 메소드를 구현하는 것이 중요합니다. 

시스템은 RemoteViewsFactory의 구현체가 처음 생성되는 시점에 onCreate()를 호출해 주며, 여기에서 데이터를 가져오는 것과 관련된 일들을 해야 합니다. 예를 들어, StackView Widget 샘플에서는 onCreate()에서 ArrayList에 WidgetItem 객체들을 담는 일을 합니다. 그러면 위젯이 화면에 보여질 때, 시스템이 이 ArrayList에서 현재 위치에 해당하는 WidgetItem 객체를 가져와서 그 안에 셋팅되어 있는 텍스트를 뷰에 보여주는 것입니다. 

아래 예제 코드는 StackView Widget 샘플RemoteViewsFactory 구현체의 일부로서, onCreate()에 주목하시기 바랍니다:

class StackRemoteViewsFactory implements
RemoteViewsService.RemoteViewsFactory {
   
private static final int mCount = 10;
   
private List<WidgetItem> mWidgetItems = new ArrayList<WidgetItem>();
   
private Context mContext;
   
private int mAppWidgetId;

   
public StackRemoteViewsFactory(Context context, Intent intent) {
        mContext
= context;
        mAppWidgetId
= intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
               
AppWidgetManager.INVALID_APPWIDGET_ID);
   
}

   
public void onCreate() {
       
// In onCreate() you setup any connections / cursors to your data source. Heavy lifting,
       
// for example downloading or creating content etc, should be deferred to onDataSetChanged()
       
// or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
       
for (int i = 0; i < mCount; i++) {
            mWidgetItems
.add(new WidgetItem(i + "!"));
       
}
       
...
   
}
...

RemoteViewsFactory의 getViewAt(int position) 메소드는 position에 해당하는 데이터가 주입된 RemoteViews 객체를 리턴해 줍니다. 예제 코드는 아래와 같습니다:

public RemoteViews getViewAt(int position) {
   
   
// Construct a remote views item based on the app widget item XML file,
   
// and set the text based on the position.
   
RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
    rv
.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);

   
...
   
// Return the remote views object.
   
return rv;
}


각 항목에 기능 추가하기

위에서는 위젯의 콜렉션 뷰에 데이터를 어떻게 바인드할까에 대하여 설명했습니다. 그렇다면 콜렉션 뷰의 각 아이템들의 동작 방식은 어떻게 구현할 수 있을까요? 

위의 AppWidgetProvider 클래스 사용하기 섹션에서 설명한 바와 같이, 버튼을 클릭했을 때 액티비티를 실행하는 등의 동작을 하기 위해서 setOnClickPendingIntent() 메소드를 사용하는 것이 일반적입니다. 하지만 이러한 방식은 콜렉션 뷰의 각 아이템에 대해서는 적용할 수 없으며, 대신에 setOnClickFillInIntent() 메소드를 사용합니다. 콜렉션 뷰 자체에 대해서는 setPendingIntentTemplate() 메소드를 호출하여 콜렉션 뷰를 클릭했을 때 어떤 동작을 하게되는지에 대해서 정의하고, 콜렉션 뷰의 아이템 뷰에 대해서는 setOnClickFillInIntent() 메소드를 호출하여 위치값(position) 등의 데이터를 전달합니다. 

이번 섹션에서도 콜렉션 뷰의 아이템들의 동작을 추가하는 방식을 설명하기 위하여 StackView Widget 샘플의 코드를 인용합니다. 사용자가 위젯의 맨 앞의 뷰를 클릭하면, "Touched view n"이라는 토스트 메시지가 출력됩니다(n에는 터치한 뷰의 position값이 들어갑니다). 동작하는 흐름을 간단히 살펴보면 아래와 같습니다: 

  • (AppWidgetProvider의 서브클래스인) StackWidgetProviderTOAST_ACTION이라는 액션값을 갖는 펜딩 인텐트를 생성합니다.  
  • 사용자가 위젯을 터치하면, TOAST_ACTION이 브로드캐스트 됩니다.
  • StackWidgetProvider의 onReceive()에 해당 브로드캐스트 인텐트가 전달되고, 토스트 메시지를 출력합니다. 터치한 뷰의 position값은 RemoteViewsFactory의 getViewAt()에서 setOnClickFillInIntent()를 통해 전달된 값입니다. 


setPendingIntentTemplate() 호출하기

StackWidgetProvider는 펜딩 인텐트를 셋팅합니다. 하지만 콜렉션 뷰의 아이템 뷰들에 대하여 각각의 펜딩 인텐트를 셋팅할 수는 없기 때문에, 콜렉션 뷰 전체에 대해서는 펜딩 인텐트 템플릿을 셋팅하고(setPendingIntentTemplate() 호출), 아이템 뷰 각각에 대해서는 데이터 전달용 인텐트(fill-in intent)를 셋팅합니다(setOnClickFillInIntent() 호출). 

아래 예제 코드에서는, 사용자가 뷰를 터치하면 TOAST_ACTION으로 브로드캐스트하고, 그것을 onReceive()에서 받아서, 터치된 뷰에 대한 토스트 메시지를 출력해 주고 있습니다:

public class StackWidgetProvider extends AppWidgetProvider {
   
public static final String TOAST_ACTION = "com.example.android.stackwidget.TOAST_ACTION";
   
public static final String EXTRA_ITEM = "com.example.android.stackwidget.EXTRA_ITEM";

   
...

   
// Called when the BroadcastReceiver receives an Intent broadcast.
   
// Checks to see whether the intent's action is TOAST_ACTION. If it is, the app widget
   
// displays a Toast message for the current item.
   
@Override
   
public void onReceive(Context context, Intent intent) {
       
AppWidgetManager mgr = AppWidgetManager.getInstance(context);
       
if (intent.getAction().equals(TOAST_ACTION)) {
           
int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
               
AppWidgetManager.INVALID_APPWIDGET_ID);
           
int viewIndex = intent.getIntExtra(EXTRA_ITEM, 0);
           
Toast.makeText(context, "Touched view " + viewIndex, Toast.LENGTH_SHORT).show();
       
}
       
super.onReceive(context, intent);
   
}
   
   
@Override
   
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
       
// update each of the app widgets with the remote adapter
       
for (int i = 0; i < appWidgetIds.length; ++i) {
   
           
// Sets up the intent that points to the StackViewService that will
           
// provide the views for this collection.
           
Intent intent = new Intent(context, StackWidgetService.class);
            intent
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
           
// When intents are compared, the extras are ignored, so we need to embed the extras
           
// into the data so that the extras will not be ignored.
            intent
.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
           
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
            rv
.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);
   
           
// The empty view is displayed when the collection has no items. It should be a sibling
           
// of the collection view.
            rv
.setEmptyView(R.id.stack_view, R.id.empty_view);

           
// This section makes it possible for items to have individualized behavior.
           
// It does this by setting up a pending intent template. Individuals items of a collection
           
// cannot set up their own pending intents. Instead, the collection as a whole sets
           
// up a pending intent template, and the individual items set a fillInIntent
           
// to create unique behavior on an item-by-item basis.
           
Intent toastIntent = new Intent(context, StackWidgetProvider.class);
           
// Set the action for the intent.
           
// When the user touches a particular view, it will have the effect of
           
// broadcasting TOAST_ACTION.
            toastIntent
.setAction(StackWidgetProvider.TOAST_ACTION);
            toastIntent
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
            intent
.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
           
PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent,
               
PendingIntent.FLAG_UPDATE_CURRENT);
            rv
.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent);
           
            appWidgetManager
.updateAppWidget(appWidgetIds[i], rv);
       
}
   
super.onUpdate(context, appWidgetManager, appWidgetIds);
   
}
}


setOnClickFillInIntent() 호출하기

RemoteViewsFactory에서는 콜렉션 뷰의 각 아이템에 데이터 전달용 인텐트(fill-in intent)를 셋팅해야 합니다. 이것은 어느 아이템이 클릭되었는지에 대한 정보를 전달해 주는 것입니다. 데이터 전달용 인텐트에 담긴 데이터는, 아이템이 클릭되어 브로드캐스트 하는 시점에 최종적으로 전달되는 인텐트에 추가됩니다. 

public class StackWidgetService extends RemoteViewsService {
   
@Override
   
public RemoteViewsFactory onGetViewFactory(Intent intent) {
       
return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
   
}
}

class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
   
private static final int mCount = 10;
   
private List<WidgetItem> mWidgetItems = new ArrayList<WidgetItem>();
   
private Context mContext;
   
private int mAppWidgetId;

   
public StackRemoteViewsFactory(Context context, Intent intent) {
        mContext
= context;
        mAppWidgetId
= intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
               
AppWidgetManager.INVALID_APPWIDGET_ID);
   
}

   
// Initialize the data set.
       
public void onCreate() {
           
// In onCreate() you set up any connections / cursors to your data source. Heavy lifting,
           
// for example downloading or creating content etc, should be deferred to onDataSetChanged()
           
// or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
           
for (int i = 0; i < mCount; i++) {
                mWidgetItems
.add(new WidgetItem(i + "!"));
           
}
           
...
       
}
       
...
   
       
// Given the position (index) of a WidgetItem in the array, use the item's text value in
       
// combination with the app widget item XML file to construct a RemoteViews object.
       
public RemoteViews getViewAt(int position) {
           
// position will always range from 0 to getCount() - 1.
   
           
// Construct a RemoteViews item based on the app widget item XML file, and set the
           
// text based on the position.
           
RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
            rv
.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);
   
           
// Next, set a fill-intent, which will be used to fill in the pending intent template
           
// that is set on the collection view in StackWidgetProvider.
           
Bundle extras = new Bundle();
            extras
.putInt(StackWidgetProvider.EXTRA_ITEM, position);
           
Intent fillInIntent = new Intent();
            fillInIntent
.putExtras(extras);
           
// Make it possible to distinguish the individual on-click
           
// action of a given item
            rv
.setOnClickFillInIntent(R.id.widget_item, fillInIntent);
       
           
...
       
           
// Return the RemoteViews object.
           
return rv;
       
}
   
...
   
}


목록 데이터의 최신화

아래 그림은 위젯의 목록을 최신화할때 실행되는 메소드들의 흐름을 나타내며, RemoteViewsFactory와 어떻게 상호작용을 하는지 보여줍니다:



위젯에 목록을 보여주는 이유 중 하나는 어떤 최신 컨텐츠 일부를 보여주기 위해서일 것입니다. 예를 들어, Gmail 앱의 위젯은 받은 편지함의 최근 편지 몇개를 보여줍니다. 그러기 위해서는, RemoteViewsFactory가 새로 받은 편지를 보여주기 위해 콜렉션 뷰를 갱신할 필요가 있으며, 이를 위해 AppWidgetManager의 notifyAppWidgetViewDataChanged() 메소드를 호출해야 합니다. 그러면 RemoteViewsFactory의 onDataChanged() 콜백 메소드가 호출되고, 여기서 최신 데이터를 가져오는 작업을 할 수 있습니다. onDataChanged()는 동기적으로 동작합니다. 즉, onDataChanged()에서 데이터 가져오는 일이 끝나야 그 데이터를 아이템 뷰에 적용하는(fetch data) 메소드가 호출되는 것입니다. onDataChanged()getViewAt()에서는 시간이 꽤 오래 걸리는 작업들(processing-intensive operations)이 실행될 수 있을 것입니다. 따라서 그럴 때는 RemoteViewsFactory의 getLoadingView()를 구현함으로써 실행 중에 로딩뷰가 보여지도록 할 수 있습니다. 


Posted by 개발자 김태우
,