2016년 9월 30일 금요일

안드로이드 Adapter View의 작동원리

RecyclerView가 등장한 이후로, ListView, GridView는 잘 이용되지 않제 됬지만 ListView를 이해하고 있으면 RecyclerView도 쉽게 이해할 수 있기때문에, Adapter View를 먼저 정리해보았다.

Adapter View?

  • 화면이 작은 모바일 장비에서, 다양한 정보를 한번에 표현하기 위해 이용하는 View들
  • 많은 정보를 효과적으로 처리하기 위해, View에 직접 정보를 주입하지 않고, Adapter라는 중간 매개체를 이용하기 때문에 붙여진 이름이다.
  • AdapterView는 ViewGroup을 상속받으므로, 내부적으로 많은 뷰들을 담을 수 있다.
  • 대표적인 AdapterView의 서브 클래스 : ListView, GridView, Spinner, Gallery

Adapter?

  • 데이터의 원본을 받아서 관리하고, 어댑터뷰가 출력할 수 있는 형태로 데이터를 제공하는 중간 객체
  • ListAdapter, SpinnerAdapter 인터페이스를 구현한 BaseAdapter 추상클래스의 서브클래스들을 이용해 다양한 데이터들을 다룰 수 있고, 원한다면 BaseAdapter를 직접 상속받아서 구현할 수도 있다.
    • ArrayAdapter : T 타입의 배열 데이터를 이용한 Adapter. List<T>T[] 타입 둘다 가능하다.(T[] 타입일 경우 데이터의 변경이 불가능하다.)
    • CursorAdapter : Database의 Cursor를 이용한 Adapter.
    • SimplAdapter : XML파일의 정적인 데이터를 이용한 Adapter.
  • 제공받은 원본을 가공하여 여러 어댑터뷰와 연결하는 기능을 제공한다.
    • 어댑터는 결국 받은 원본을 화면에 어떻게 보여줄지 정해주는 객체.
  • 어댑터와 연결된 원본 데이터가 변경되면, notifyDataSetChanged 메서드를 호출하여 원본이 변경되었다고 어댑터뷰에 알려주어 화면이 다시 그려지도록 해야한다.
    • 즉, 어댑터뷰를 변경하고 싶으면 원본을 변경하고 화면을 다시 그려야 한다.

동작원리

AdapterView
  • 어댑터뷰는 화면 드로잉에 필요한 정보를 어댑터에게 요청하게 되고, 어댑터는 자신이 가지고 있는 데이터를 가지고 요청받은 정보를 어댑터뷰에 리턴한다.
  • 다음의 메서드는 BaseAdapter를 상속받게 되면 반드시 구현해야 하는 메서드로, 어댑터뷰에서 화면 드로잉을 할때 어댑터에게 정보를 요청하는 메서드이다.
    • int getCount() : 화면에 표시해야 하는 데이터의 갯수 반환
    • T getItem(int position) : 인자로 받은 위치의 데이터 반환
    • long getItemId(int position) : 인자로 받은 위치의 데이터 id 구분자 반환
    • View getView(int position, View convertView, ViewGroup parent) : 인자로 받은 위치의 데이터가 화면에 표시될 뷰 반환
  • getCount 메서드로부터 반환된 데이터의 갯수만큼 나머지 메서드들이 호출되며, 화면을 그린다.

getView에 대하여

  • 원본의 각 아이템들이 어댑터뷰에 어떻게 보일지 뷰를 그려서 반환하는 메서드이다.
  • 화면이 그려져야할 때 어댑터뷰로부터 호출된다.
  • 기존에 화면에 그렸던 뷰가 존재하지 않으면 convertView는 null값이 넘어오며, 이전에 화면에 그려졌다가 보이지 않게 된 뷰가 존재한다면 null이 아닌 값이 넘어온다.
    • 4.0 이전에 convertView는 무작위 순서로 넘어왔으나, 4.0 이후에는 화면에서 사라지는 순서대로 넘어오는 듯하다.
    • 이 Recycling 기법으로, 어댑터뷰는 화면에 표시할 최소한의 뷰만을 생성하게 된다.

getView최적화1 - View Holder 패턴

  • 뷰 전개(inflating)은 매우 비싼 연산이므로, getView에서 findViewById 등, 뷰 전개하는 연산을 반복적으로 하는 것은 퍼포먼스를 떨어뜨린다.
  • View Holder 패턴은 convertView를 처음 생성할 때, 미리 뷰를 전개하여 View의 태그에 참조객체를 저장하는 방법이다.
    • 뷰 전개를 view 생성시에 딱 한번만 하므로, 퍼포먼스가 많이 향상된다.
// 전개된 뷰의 참조값을 저장할 객체
private class ViewHolder {
    private TextView textName;
    private TextView textType;
}

// 어댑터의 getView 메서드
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;

 if(convertView == null) {
     // convertView가 null이면 Holder 객체를 생성하고
        // 생성한 Holder 객체에 inflating 한 뷰의 참조값을 저장
        holder = new ViewHolder();

        convertView = inflater.inflate(R.layout.layout_list_item, parent, false);
        holder.textName = (TextView) convertView.findViewById(R.id.text_item_name);
        holder.textType = (TextView) convertView.findViewById(R.id.text_item_type);
        
        // View의 태그에 Holder 객체를 저장
        convertView.setTag(holder);
    } else {
     // convertView가 null이 아니면 뷰를 생성할때 태그에 저장했던 Holder 객체가 존재
        // 이 Holder 객체는 자신을 inflating한 참조값(다시 전개할 필요가 없다.)
        holder = (ViewHolder) convertView.getTag();
    }

 // 속성값 변경
    ParcelableModel parcelableModel = getItem(position);
    holder.textName.setText(parcelableModel.name);
    holder.textType.setText(Integer.toString(parcelableModel.type));
    return convertView;
}

getView최적화2 - 지연 로딩(Lazy Loading)

  • 항목중에 네트워크로부터 받아와야 하는 파일이 있을 경우에, 네트워크 작업을 getView에서 진행하게 되면 getView가 리턴되지 않아서 어댑터뷰의 화면이 그려지지 않아 화면이 멈추게 된다.
  • 이런 경우, AsyncTask 등의 방법을 이용해 작업을 비동기로 처리한다. 비동기 처리시 뷰를 바인딩하여, 작업이 완료된 후 자동적으로 뷰의 속성을 변경되도록 구현한다.

2016년 9월 29일 목요일

[안드로이드] 7.0(누가) Quick Settings 타일 API 적용 제목 수정

Quick Settings?

  • 안드로이드 상단을 스와이프하면 보이는 스위치 버튼들
  • 기존 안드로이드 버전에서는 커스텀 버튼을 만들 수 없었다.(제조사에서 만든 버튼 제외)
  • 7.0부터는 일반 앱에서 제공하는 커스텀 버튼을 추가할 수 있다.
  • 이 버튼은 앱을 시작하기 위한 용도가 아니고, 앱을 켜지 않고도 빠르게 on/off해야하는 기능을 추가하는 용도로 사용되어야 한다.
  • Quick Settings를 구현하기 위해서는 7.0(API 24)에서 추가된 Tile API를 이용해야 한다.
Tile

Quick Settings 구현

TileService 추가

  • Service클래스의 하위 클래스로 Quick Setting을 구현하기 위한 컴포넌트 클래스
  • 매니페스트 파일에 TileService를 등록해야 한다.
    • android:label 속성값은 타일 버튼의 설명이 들어간다. 코드로 변경가능하다.
    • android:icon 속성값은 타일 버튼의 아이콘이 들어간다. 코드로 변경가능하다. (아이콘은 안드로이드 정책에 따라 반드시 흰색이어야 한다. off, 비활성 표시를 위해 알파를 이용한다)
    • android.permission.BIND_QUICK_SETTINGS_TILE 퍼미션을 꼭 추가해주어야 한다.
    • 시스템이 TileService를 인식하려면 TileService.ACTION_QS_TILE 액션을 인텐트필터로 등록해야 한다.
<service android:name=".TestTileService"
            android:label="Test Label"
            android:icon="@drawable/ic_toastdrive"
            android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
            <intent-filter>
                <action android:name="android.service.quicksettings.action.QS_TILE" />
            </intent-filter>
        </service>
  • 시스템은 모든 앱의 매니페스트를 읽어서, ACTION_QS_TILE 액션을 추가한 TileService를 타일 리스트에 추가한다.(아직은 사용자의 입력을 받지 못한다.)
Tile
  • 타일 리스트에 있는 타일을 QuickSetting 영역으로 드래그하면, 이때부터 타일버튼으로 사용자의 입력을 받을 수 있다.
Tile

Tile 버튼으로 사용자의 입력 받기

  • TileService는 버튼의 상태에 따라 콜백을 받는 다음 메서드들이 정의되어 있다.
    • onTileAdded() : 타일이 QuickSetting 영역에 추가되었을 때 호출
    • onTileRemoved() : 타일이 QuickSetting 영역에서 제거되었을 때 호출
    • onStartListening() : 타일이 리스닝 상태로 들어갔을 때(QuickSetting 영역이 보일때) 호출
    • onStopListening() : 타일이 리스닝 상태에서 나왔을 때(QuickSetting 영역이 가려질때) 호출
    • onClick() : QuickSetting 영역에서 타일이 눌렸을 때 호출
  • onTileAdded/Removed, onStart/StopListening 메서드는 QuickSetting 영역과 타일리스트의 경계에서 여러번 호출될 수 있으니 유의하도록 한다.
  • 일반적으로 onClick()메서드를 이용해 사용자와 인터렉션 한다.
public class TestTileService extends TileService {
    boolean isListening = false;
    ...

    @Override
    public void onClick() {
        Tile tile = getQsTile();
        int tileState = tile.getState();
        if (isListening && tileState != Tile.STATE_UNAVAILABLE) {
            tile.setState(tileState == Tile.STATE_ACTIVE? Tile.STATE_INACTIVE : Tile.STATE_ACTIVE);
            tile.updateTile();
        }
    }
    ...
}
  • 앱의 생명주기와는 상관없이, Tile 객체는 상태를 보존한다. 그러나, 시스템이 재부팅되면 상태보존이 되지 않는다.(실장비에서 확인이 필요)

Tile 버튼을 활용하기 위한 TileService의 기능들

다이얼로그 보여주기

  • TitleService 클래스의 showDialog() 메서드를 호출한다. 인자로는 보여줄 다이얼로그 객체를 넘겨준다.
  • 이 메서드를 호출하면, Quick Setting 영역은 닫히고, 다이얼로그가 호출된다.
  • 다이얼로그는 반드시 Theme.AppCompat 테마 / 하위테마를 사용해야 한다.
public class TestTileService extends TileService {
    boolean isListening = false;
    ...

    @Override
    public void onClick() {
        Tile tile = getQsTile();
        int tileState = tile.getState();
        if (isListening && tileState != Tile.STATE_UNAVAILABLE) {
            tile.setState(tileState == Tile.STATE_ACTIVE? Tile.STATE_INACTIVE : Tile.STATE_ACTIVE);
            tile.updateTile();
            if (tileState == Tile.STATE_ACTIVE) {
                // 보여줄 다이얼로그 생성
                AlertDialog dialog = new AlertDialog.Builder(this, R.style.MaterialBaseTheme_AlertDialog)
                        .setTitle("Test Title")
                        .setMessage("Test Message")
                        .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                Toast.makeText(AppApplication.sContext, "OK", Toast.LENGTH_SHORT).show();
                            }
                        }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                Toast.makeText(AppApplication.sContext, "Cancel", Toast.LENGTH_SHORT).show();
                            }
                        }).create();
                // 다이얼로그 호출
                showDialog(dialog);
            }
        }
    }
    ...
}

엑티비티 시작하기

  • TileService 클래스의 startActivityAndCollapse() 메서드를 호출한다. 인자로는 엑티비티를 호출하기 위한 인텐트를 넘겨준다.
  • 이 메서드를 호출하면 Quick Setting 영역은 닫히고, 엑티비티가 실행된다.
  • 만약, 기존 앱이 실행중이라면 인자로 넘겨주는 Intent의 엑티비티 스택관련 플래그에 영향을 받는다.
public class TestTileService extends TileService {
    boolean isListening = false;
    ...

    @Override
    public void onClick() {
        Tile tile = getQsTile();
        int tileState = tile.getState();
        if (isListening && tileState != Tile.STATE_UNAVAILABLE) {
            tile.setState(tileState == Tile.STATE_ACTIVE? Tile.STATE_INACTIVE : Tile.STATE_ACTIVE);
            tile.updateTile();
            if (tileState == Tile.STATE_ACTIVE) {
                // 호출할 인텐트 생성
                Intent intent = new Intent(AppApplication.sContext, WebViewActivity.class);
                intent.putExtra(WebViewActivity.ARG_URL, "http://developer.android.com");
                // 엑티비티 호출
                startActivityAndCollapse(intent);
            }
        }
    }
    ...
}

잠금화면에서의 동작

  • 잠금화면 상태에서는 잠금화면에 가려서 위의 다이얼로그 / 엑티비티 호출은 보이지 않는다.(잠금화면에 가려졌을 뿐, 정상적으로 동작한다.)
  • 현재 상태가 잠금화면 상태인지를 확인하려면 다음 메서드를 이용한다.
    • isLocked() : 현재 상태가 잠금화면일 경우 true 반환
    • isSecure() : 현재 상태가 보안중일 경우 true 반환(true 인 경우에는 당연히 잠금화면 상태이므로 isLocked()는 true를 반환)
  • TileService 의 unlockAndRun() 메서드를 이용하면 잠금화면을 해제하고 동작을 수행한다. 인자로는 unlock을 수행한 후에 수행될 내용을 구현한 Runnable 객체이다.
    • unlockAndRun() 메서드가 Quick Setting 영역을 닫지는 않는다. 그래서 Quick Setting 영역이 그대로 보이며, 그 뒤에서 동작이 수행된다.
    • 패턴이 걸려있는 경우, unlockAndRun() 메서드가 호출되면 패턴해제화면이 노출된다. 패턴을 풀어야 Runnable 객체를 수행한다.(지문은 실 장비에서 테스트가 필요하지만, 동일한 프로세스일 것으로 추정)
    • 패턴해제화면에서 해제를 취소한 경우 Runnable 객체의 동작이 수행되지 않는다.
public class TestTileService extends TileService {
    boolean isListening = false;
    ...

    @Override
    public void onClick() {
        Tile tile = getQsTile();
        int tileState = tile.getState();
        if (isListening && tileState != Tile.STATE_UNAVAILABLE) {
            tile.setState(tileState == Tile.STATE_ACTIVE? Tile.STATE_INACTIVE : Tile.STATE_ACTIVE);
            tile.updateTile();
            if (tileState == Tile.STATE_ACTIVE) {
                unlockAndRun(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(AppApplication.sContext, "unlock and run Test", Toast.LENGTH_SHORT).show();
                    }
                });
            }
        }
    }
    ...
}

2016년 9월 28일 수요일

[안드로이드] 7.0(누가) 백그라운드 최적화

안드로이드에서 7.0버전을 출시하면서 백그라운드를 최적화하기 위해 다음의 2가지 사항을 변경했다.
  • 매니페스트 파일에 등록된 CONNECTIVITY_ACTION(android.net.conn.CONNECTIVITY_CHANGE) 브로드캐스트 수신을 무시한다.(앱이 백그라운드에서 이 브로드캐스트를 더이상 받지 못한다.) 단, Context.registerReceiver()메서드를 이용해 메인스레드에서 등록된 리시버는 앱이 실행중일 때, 똑같이 CONNECTIVITY_ACTION 브로드캐스트를 받을 수 있다.
  • 앱은 더이상 ACTION_NEW_PICTUREACTION_NEW_VIDEO 브로드캐스트를 보내거나 받을 수 없다. 이는 targetSdkVersion에 상관없이 모든 앱에 영향을 끼친다.
안드로이드 7.0을 대응해야 한다면, 반드시 위의 두가지 사항을 확인해서 대응해 주어야 한다. 애응하지 않으면 어떤 부작용이 있을지 알 수 없다.

CONNECTIVITY_ACTION 제한

targetSdkVersion을 24로 설정한 앱의 경우, 매니페스트에 설정한 CONNECTIVITY_ACTION 브로드캐스트를 더 이상 받지 못하게 된다. 이 상황은 백그라운드에서 CONNECTIVITY_ACTION 처리를 하고있는 앱이라면, 반드시 확인해봐야 한다.
Note : 위에서도 언급했지만, 앱이 실행중일 때에는 Context의 registerReceiver() 메서드를 이용해 CONNECTIVITY_ACTION 수신 리시버를 등록하고, 계속 브로드캐스트 받을 수 있다.

네트워크 Job 스케쥴링

안드로이드 5.1 이상

안드로이드 5.1(API 21) 버전에서 새로 추가된 JobScheduler 관련 API를 이용해 처리할 수 있다. JobInfo.builder 로 jobInfo인스턴스를 생성할 때, setRequiredNetworkType() 메서드를 이용해 네트워크 타입을 지정하고 생성한 후에 그 인스턴스를 스케쥴에 등록한다. 그러면, 스케쥴러가 해당 네트워크 타입이 되었을 때, Job을 수행한다. 다음 예제코드는 네트워크 타입이 WIFI 일때의 JobInfo를 등록하는 내용이다.
public static final int MY_BACKGROUND_JOB = 0;
...
public static void scheduleJob(Context context) {
JobScheduler js =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo job = new JobInfo.Builder(
MY_BACKGROUND_JOB,
new ComponentName(context, MyJobService.class))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
.build();
js.schedule(job);
}
위의 예제에서 네트워크 타입이 UNMETERED(무과금 -> WIFI) 가 되면, JobInfo를 만들때 등록했던 JobService 클래스의 onStartJob()메서드로 콜백된다. (JobService 클래스 참고)

안드로이드 5.1 미만

안드로이드 5.1(API 21)버전 미만에서는 JobScheduler 클래스를 사용할 수 없으므로, 구글플레이 서비스를 이용해 구현된 GcmNetworkManager 클래스를 이용해야 한다. GcmNetworkManager는 Task 추상클래스의 하위 클래스 객체를 스케쥴링하게된다. Task 클래스 하위 인스턴스는 다음과 같이 스케쥴링한다.
     OneoffTask myTask = new OneoffTask.Builder()
.setService(MyGcmTaskService.class)
.setRequiredNetwork(NETWORK_STATE_UNMETERED)
.setTag("test-upload")
.build();
GcmNetworkManager.getInstance(this).schedule(myTask);
해당 태스크의 순서가 되면, GcmTaskService 클래스의 onRunTask() 메서드로 콜백된다. (GcmTaskService 클래스 참고) 단, GcmTaskService 클래스가 콜백을 받으려면 다음과 같이 매니페스트에 등록되어야 한다.
     <service android:name=".MyUploadService"
android:exported="true"
<!-- 다른 코드로부터 이 서비스가 실행되지 않도록 하기 위해서 추가 -->
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
<!-- 이 서비스가 콜백을 받기 위해 추가 -->
<intent-filter>
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
</intent-filter>
</service>

앱이 실행중일 때, Network Connectivity 모니터링하기

앱 실행중에 브로드캐스트 리시버로 CONNECTIVITY_ACTION을 수신하는 방법 외에도, ConnectivityManager 클래스의 registerNetworkCallback()메서드를 이용해, 해당 상태에서의 콜백을 받을 수 있다. (이 메서드는 API 21에서 추가되었다.)
NetworkRequest.Builder 를 이용하여 NetworkRequest 인스턴스를 생성한 후에, 그 인스턴스를 registerNetworkCallback()메서드의 첫번째 인자로 넘기고, 두번째 인자로는 첫번째 인자에 명시된 상태로 네트워크가 변할 때, 실행될 콜백 메서드를 넘긴다. 앱이 종료될 때, unregisterNetworkCallback()메서드를 호출해 주어야 한다.

NEW_PICTURE / NEW_VIDEO 제한

안드로이드 7.0에서는 ACTION_NEW_PICTUREACTION_NEW_VIDEO 브로드캐스트를 주고받을 수 없도록 변경되었다. 이러한 현상은, 어떤 애플리케이션이 새로운 이미지나 비디오를 프로세싱할 때, 이 액션을 등록한 모든 리시버에게 브로드캐스트 되어 많은 앱들이 깨어나야만 했다.

새로운 JobInfo 메서드

안드로이드 7.0에서는 Content URI의 변화를 연결하기 위해 JobInfo API를 확장하여 다음의 메서드들을 추가하였다.
  • JobInfo.TriggerContentUri() : content URI의 변화를 감지하기 위해 요구되는 파라미터들을 캡슐화한다.
  • JobInfo.Builder.addTriggerContentUri() : TriggerContentUri 객체를 JobInfo로 전달한다. ContentObserver는 캡슐화된 content URI를 모니터링한다. 만약 Job과 관련된 TriggerContentUri가 여러개 있으면, 시스템은 한개의 URI가 변하더라도 콜백을 전달한다. 어떤 URI의 하위 컨텐트의 변화를 감지하고 싶으면, TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS 플래그를 추가하면 된다. 이 플래그는, ContentResolver.registerContentObserver()메서드의 매개변수인 notifyForDescendants와 대응하는 역할을 한다.
Note : TriggerContentUri() 메서드는 setPeriodic()setPersisted() 메서드와 같이 사용할 수 없다. 컨텐트의 변경을 계속 감지하려면, JobService가 콜백을 끝내기 전에 새로운 JobInfo 인스턴스를 만들어 스케쥴링한다.
다음 예제는 MEDIA_URI가 변경될떄를 감지하기 위해 스케쥴링하는 코드이다.
public static final int MY_BACKGROUND_JOB = 0;
...
public static void scheduleJob(Context context) {
JobScheduler js =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo.Builder builder = new JobInfo.Builder(
MY_BACKGROUND_JOB,
new ComponentName(context, MediaContentJob.class));
builder.addTriggerContentUri(
new JobInfo.TriggerContentUri(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
js.schedule(builder.build());
}

새로운 JobParameter 메서드

안드로이드 7.0에서는 JobParameter를 확장하여, Job 콜백이 왔을 때 해당 컨텐트의 권한이나 URI같은 유용한 정보를 볼 수 있도록 확장되었다.
  • Uri[] getTriggeredContentUris() : 이 Job에서 연결된 URI를 배열로 반환한다. 만약 연결된 URI가 없다면 null을 리턴한다.
  • String[] getTriggeredContentAuthorities() : 이 Job에서 연결된 URI들의 권한ㅇ르 배열로 반환한다. 반환된 결과가 null이 아니라면, getTriggeredContentUris() 메서드를 사용하면 된다.
다음 예제는 변경된 URI의 권한과 URI를 JobService.onStartJob() 메서드에서 받아서 기록하는 코드이다.
@Override
public boolean onStartJob(JobParameters params) {
StringBuilder sb = new StringBuilder();
sb.append("Media content has changed:\n");
if (params.getTriggeredContentAuthorities() != null) {
sb.append("Authorities: ");
boolean first = true;
for (String auth :
params.getTriggeredContentAuthorities()) {
if (first) {
first = false;
} else {
sb.append(", ");
}
sb.append(auth);
}
if (params.getTriggeredContentUris() != null) {
for (Uri uri : params.getTriggeredContentUris()) {
sb.append("\n");
sb.append(uri);
}
}
} else {
sb.append("(No content)");
}
Log.i(TAG, sb.toString());
return true;
}