2018년 11월 27일 화요일

[안드로이드] Paging Library 적용기

Paging Library 적용기

Paging Library?

  • 개념 설명
  • Android Jetpack / Architectiure Component에 추가된 라이브러리로 데이터 페이징을 위한 구조를 제공

적용하기

  • 예시에서는 ItemKeyedDataSource를 이용하여, LiveData와 연동되는 페이징을 구현하였다.

1. Paging Library 추가

  • AndroidX
dependencies {
    def paging_version = "2.1.0-beta01"

    implementation "androidx.paging:paging-runtime:$paging_version" // use -ktx for Kotlin

    // alternatively - without Android dependencies for testing
    testImplementation "androidx.paging:paging-common:$paging_version" // use -ktx for Kotlin

    // optional - RxJava support
    implementation "androidx.paging:paging-rxjava2:$paging_version" // use -ktx for Kotlin
}
  • AndroidX 이전
dependencies {
    def paging_version = "1.0.0"

    implementation "android.arch.paging:runtime:$paging_version"

    // alternatively - without Android dependencies for testing
    testImplementation "android.arch.paging:common:$paging_version"

    // optional - RxJava support
    implementation "android.arch.paging:rxjava2:$paging_version"
}

2. DataSource 선택 / 구현하기

  • 데이터를 어디서, 어떻게 가져올 지 정의하는 단계
  • 다음의 3가지 인터페이스 중, 페이징 성격에 맞는 하나를 선택한다.
    • PageKeyDataSource : 데이터가 다음, 이전 키를 포함하고 있을 때 (ex. nextPageToken 값이 존재할 때 등)
    • ItemKeyedDataSource : N번째 데이터로, N-1 / N+1의 데이터를 가져올 때(ex. 날짜별 정렬, 정렬된 ID 등)
    • PositionalDataSource : 특정 위치의 데이터를 가져올 때(ex. 100번쨰 위치에서 10개의 데이터를 가져올 때)
  • 각 인터페이스의 추상메서드를 구현하는 클래스를 생성한다. (인터페이스별로 추상메서드가 각각 다름)
    • PageKeyDataSource
      • loadAfter : 추가할(스크롤을 아래로 내릴 때) 데이터를 로드하여, 파라미터로 받은 콜백의 onResult 메서드 호출
      • loadBefore : 이전(스크롤을 위로 올릴 때) 데이터를 로드하여, 파라미터로 받은 콜백의 onResult 메서드 호출
      • loadInitial : 최초 데이터를 로드하여, 파라미터로 받은 콜백의 onResult 메서드 호출
    • ItemKeyedDataSource
      • PageKeyDataSource의 3가지 메서드(메서드 이름은 같지만 파라미터는 조금씩 다름)
      • getKey : 파라미터로 받은 아이템의 키값 반환
    • PositionalDataSource
      • loadInitial : 최초 데이터를 로드하여, 파라미터로 받은 콜백의 onResult 메서드 호출
      • loadRange : 특정 범위의 데이터를 로드하여, 파라미터로 받은 콜백의 onResult 메서드 호출
  • 해당 데이터소스를 생성하는 Factory 클래스를 생성한다.
  • ex (예제에서는 DateTime의 getMillis()를 키로 사용)
public class DailyDataSource extends ItemKeyedDataSource<Long, DailyItem> {
    // 팩토리 클래스
    public static class Factory extends DataSource.Factory<Long, DailyItem> {
        @Override
        public DataSource<Long, DailyItem> create() {
            return new DailyDataSource();
        }
    }

    private DailyDataSource() {}

    @Override
    public void loadInitial(@NonNull LoadInitialParams<Long> params, @NonNull LoadInitialCallback<DailyItem> callback) {
        callback.onResult(fetchAfter(new DateTime(params.requestedInitialKey), params.requestedLoadSize));
    }

    @Override
    public void loadAfter(@NonNull LoadParams<Long> params, @NonNull LoadCallback<DailyItem> callback) {
        callback.onResult(fetchAfter(new DateTime(params.key).minusDays(1), params.requestedLoadSize));
    }

    @Override
    public void loadBefore(@NonNull LoadParams<Long> params, @NonNull LoadCallback<DailyItem> callback) {
        LocalDate keyDate = new LocalDate(params.key);
        if (keyDate.isBefore(LocalDate.now())) {
            callback.onResult(fetchBefore(new DateTime(params.key).plusDays(1), params.requestedLoadSize));
        }
    }

    @NonNull
    @Override
    public Long getKey(@NonNull DailyItem item) {
        return item.getDateTime().getMillis();
    }
    
    private List<DailyItem> fetchBefore(DateTime key, int howManyDays) {
        List<DailyItem> dailyItemList = new ArrayList<>();
        // 데이터 로드
        return dailyItemList;
    }
    
    private List<DailyItem> fetchAfter(DateTime key, int howManyDays) {
        List<DailyItem> dailyItemList = new ArrayList<>();
        // 데이터 로드
        return dailyItemList;
    }
}

3. PagedList 생성

  • 데이터 갱신을 어떤 방식으로 관찰할 지 정의하는 단계
  • PagedList를 얻기 위해 Builder 클래스가 존재하며, 다음의 2가지 PagedListBuilder 중 맞는 것을 선택한다.
    • LivePagedListBuilder : LiveData를 이용하여 데이터를 관찰. 이를 이용하면 LiveData<PagedList< T >> 객체를 받을 수 있다.
    • RxPagedListBuilder : RxJava2를 이용하여 데이터 관찰. 이를 이용하면 Flowable<PagedList< T >> / Observable<PagedList< T >> 객체를 받을 수 있다.
  • 생성시에, DataSource.Factory 인스턴스를 넘겨주어야 한다.
  • 데이터를 불러야 하는 곳에서 build 메서드를 호출한다.
  • ex (예제에서는 LivePagedListBuilder 이용)
public class DailyViewModel extends AndroidViewModel {
    ...
    private LivePagedListBuilder<Long, DailyItem> pagedListBuilder;
    private DataSource<Long, DailyItem> dataSource;

    public DailyViewModel(@NonNull Application application) {
        super(application);
        PagedList.Config config = new PagedList.Config.Builder()
                .setInitialLoadSizeHint(5)
                .setPrefetchDistance(4)
                .setPageSize(5)
                .build();
        DataSource.Factory dataSourceFactory = new DailyDataSource.Factory();
        pagedListBuilder = new LivePagedListBuilder<>(dataSourceFactory, config);
        ...
    }
    // 특정 위치(key)로 이동시에 호출
    public LiveData<PagedList<DailyItem>> load(DateTime key) {
        LiveData<PagedList<DailyItem>> dailyList = pagedListBuilder.setInitialLoadKey(key.getMillis()).build();
        ...
    }
    
    // 현재 DataSource가 갱신되었을 때 호출
    public void invalidate() {
        dataSource.invalidate();
    }
    
    ... ViewModel 추가 로직
}

4. PagedListAdapter 구현

  • 전달받은 PagedList 객체를 이용하여, 데이터의 변경이 있는지 여부를 확인하여 UI(RecyclerView)를 구성하는 단계
  • PagedListAdapter를 상속받는 클래스를 구현해야 한다.
    • PagedListAdapter에서 데이터의 변경 여부를 확인하기 위해, 내부적으로 DiffUtil이 사용되므로 생성자에서 콜백을 구현해서 넘겨주어야 한다..
    • PagedListAdapter에서 PagedList를 사용하도록 래핑되어 있다.
  • 기존 ReyclerVIew.Adapter의 구현체와 같이, onCreateVIewHolder / onBindViewHolder 메서드를 구현해야 한다.
  • ex.
public class DailyAdapter extends PagedListAdapter<DailyItem, DailyViewHolder> {
    ...

    public DailyAdapter() {
        super(DIFF_CALLBACK);
    }

    @NonNull
    @Override
    public DailyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        ...
    }

    @Override
    public void onBindViewHolder(@NonNull DailyViewHolder holder, int position) {
        ...
    }

    private static DiffUtil.ItemCallback<DailyItem> DIFF_CALLBACK = new DiffUtil.ItemCallback<DailyItem>() {
        @Override
        public boolean areItemsTheSame(DailyItem oldItem, DailyItem newItem) {
            return oldItem.getDateTime().equals(newItem.getDateTime());
        }

        @Override
        public boolean areContentsTheSame(DailyItem oldItem, DailyItem newItem) {
            return oldItem.equals(newItem);
        }
    };
}

5. 어댑터 연결 & 데이터 바인딩

  • RecyclerView에 만들어진 어댑터를 연동하고, PagedListBuilder의 결과를 구독하여, 어댑터에 갱신된 데이터를 넣어준다.
  • ex
public class DailyFragment extends BaseFragment {
    private DailyViewModel dailyViewModel;
    private DailyAdapter adapter;
    private RecyclerView recyclerView;
    
    @Override
    public View onCreateView(LayoutInflator inflator, ViewGroup container, Bundle savedInstanceState) {
        ...
        initRecyclerView();
        initViewModel();
        ...
    }
    
    private void initRecyclerView() {
        adapter = new DailyAdapter();
        recyclerView.setAdapter(adapter);
        ...
    }
    
    private void initViewModel() {
        dailyViewModel = ViewModelProviders.of(this).get(DailyViewModel.class);
        dailyViewModel.load(new DateTime()).observe(this, (dailyList) -> {
            // 얻은 PagedList를 어댑터에 전달
            // submitList() 메서드는 PagedListAdapter에 구현되어 있는 메서드로, 내부적으로 화면갱신을 해주기 때문에 notifyXXX 메서드를 호출하지 않아도 된다.
            adapter.submitList(dailyList);
        });
    }
}

주관적 후기

장점

  • 깔끔하게 구조화되어 있어서, 코드가 간결해지고 리팩토링이 쉬워졌다.
  • 페이징을 직접 구현하기 위해 고민해야 하는 사항들(ex 스크롤처리, prefetch 등)을 할 필요가 없어져서 핵심 로직에만 집중할 수 있다.

단점

  • 모든 라이브러리가 그렇듯이, 라이브러리에 대한 많은 학습이 필요할 것 같다. 생각보다 많은 기능들이 있고, 여러 사용케이스들을 모두 만족하려면, 경험해보지 못한 많은 이슈가 있을 것 같다.

2018년 7월 16일 월요일

[안드로이드] 안드로이드 P Priority Buckets

Exploring Android P : Priority Buckets

이번 구글 IO에서는 안드로이드 최신 버전인 P에 대한 새로운 내용이 많이 발표되었습니다. Priority Buckets은 발표된 내용 중 하나로, 앱들에게 우선순위를 부여하여 배터리를 관리하는 업데이트 입니다. 앱이 얼마나 최근에 실행되었는지, 얼마나 자주 실행되는지의 기준으로 각 앱에 우선순위를 부여하는 것입니다. 이 아티클에서는 이 버킷이 무엇이고, 이에따라 앱의 동작에 어떤 영향이 있는지를 간략하게 살펴볼 예정입니다.
이 Priority Buckets는 앱들간에 더 스마트하게 주어진 자원을 분배하도록 고안되었습니다. 만약, 우리가 어떤 주기적으로 사용하는 앱을 사용하려는 중에, 자주 사용하지도 않는 앱이 백그라운드 동작을 시도하려고 하면, 우리는 현재 작업을 하는 동안, 그 백그라운드 작업을 꺼버리고 싶지 않을까요? 다른 예시로는, 우리가 자주쓰는 앱이 메세지를 받는 그때에, 자주 쓰이지도 않는 앱의 High Priority 메세지를 받는다면, 자주 쓰이지 않는 앱의 메세지를 차단시키는 것은 매우 좋은 방법일 수 있습니다. 이러한 목적들을 위해, 앱을 특정 버킷에 넣어 그룹화하는 Priority Buckets 개념이 고안되었습니다. 다음의 5가지 버킷이 존재하며, 해당 버킷에는 다른 동작규칙이 있습니다. 모든 앱들은 반드시 5가지의 버킷 중 하나에 속하게 됩니다.
  • Active : 현재 유저가 사용중인 앱이 속한 버킷입니다. 이는 앱이 포그라운드이거나, 시스템 레벨 프로세스가 실행중이거나, 혹은 어떤 방식으로든 그 프로세스와 상호작용중인 것입니다. 만약, 앱에 런처 엑티비티가 없으면, 이 앱은 절대 이 버킷에 속할 수 없습니다.
  • Working Set : 앱이 지금은 사용중이지 않지만, 거의 매일 사용되다시피 자주 실행되는 앱이 속한 버킷입니다.
  • Frequent : 앱이 지금은 사용중이지 않지만, 일주일중에 가끔 실행되는 앱이이 속한 버킷입니다. 예를들어, 어떤 앱을 일주일에 3번정도 사용한다면, 그 앱은 이 버킷에 속하게 됩니다.
  • Rare : 디바이스에서 앱이 거의 사용되지 않을 때 이 버킷에 속하게 됩니다.
  • Never : 앱이 설치는 되었으나, 한번도 실행된 적이 없을 때 속하는 버킷입니다.


앱이 특정 버킷에 속하게 되면, 영원히 해당 버킷에 머무르게 되는 것은 아닙니다. 이 버킷들은 시스템이 앱의 활동에 따라 동적으로 채워질 것입니다. 즉, 한번도 사용된 적 없었던 앱이 꾸준히 사용빈도가 늘어난다면, 버킷이 변경될 것입니다. 디바이스가 머신러닝을 사용할 수 있다면, 머신러닝을 활용하여 앱의 사용빈도를 결정하고, 이 예측에 따라 버킷을 결정할 것입니다. 만약 머신러닝을 사용할 수 없는 디바이스라면, 얼마나 최근에 실행되어 있는지를 기준으로 앱을 정렬하여 버킷을 구성할 것입니다. 사용성에 따라서, 이 지표를 카운트하는 다음의 몇몇 방법들이 있습니다.
  • 사용자의 앱 실행
  • 사용자의 Notification 반응성 - Notification이 보여졌지만, 사용자에 의해 바로 닫힌다면 그 앱의 버킷에 영향을 미치지 않습니다.
그렇다면, 앱이 각 버킷에 속하게 되는 것이, 동작에 어떤 영향을 미치는 걸까요? 다음의 표를 통해, 시스템이 각 버킷에 제한하는 사항들을 확인할 수 있습니다.


이 제약사항들은 디바이스가 배터리 전원을 사용할 때에만 적용되기는 하지만, 이러한 상황들을 인지하고, 앱이 원하는 시나리오대로 동작하도록 최적화해야 합니다. 자세히 살펴보면, 먼저 Active 버킷에서는 제한사항이 없으므로, 크게 신경쓸 부분이 없습니다. Working Set 버킷에서는 약간의 제약사항이 추가됩니다. 이 경우에 모든 실행중인 모든 Job은 최대 2시간까지 지연될 수 있고, 모든 알람 또한 최대 6분까지 지연될 수 있습니다. Frequent 버킷에 속한 앱은 모든 Job이 최대 8시간까지 지연될 수 있고, 알람도 최대 30분까지 지연될 수 있습니다. 그리고, High priority FCM을 하루에 10개만 받을 수 있도록 제한이 추가됩니다. Rare 버킷에 속한 앱은 모든 Job 최대 24시간이 지연될 수 있고, 알람 또한 최대 2시간이나 지연될 수 있습니다. High priority FCM도 하루에 5개로 제한되며, 네트워크 제한이 추가되어 최대 24시간이나 지연될 수 있습니다.
Note : 만약 앱이 다른 패키지로 분리되어 있다면, 각각 다른 버킷에 속할 수 있습니다. 해당 패키지들 간에 같은 행동이 될 것이리고 장담하지 마세요. 다른 결과가 발생할 수 있습니다.
앱이 속한 현재 버킷을 알고 싶다면, UsageStatsManager클래스의 getAppStandbyBucket() 메서드를 이용하세요. 앱은 각 상황에 맞게 동작하도록 구현되어야 하지만, 이 매서드는 타이밍이 중요한 상황일 때 유용할 수 있습니다. 이를 염두에 두고, 앱을 다른 버킷으로 이동시키려는 시도는 하지않아야 합니다. 디바이스 제조사별로 Priority Buckets의 배정 기준이 달라질 수 있기 때문에, 특정한 결정기준으로 앱이 버킷에 속할것이라는 장담은 할 수 없습니다. 그리고, 버킷을 이동시키기 위해 사용자의 액션을 얻으려는 메세지를 강요해서도 안됩니다. 예를 들어, 의미없는 우선순위가 높은 노티피케이션을 이용하여 사용자의 액션을 얻으려 하는 생각은 좋지 않습니다. 단, 해당 노티피케이션이 사용자에게 가치를 제공한다면 그것은 얼마든지 좋은 사례입니다. 버킷레벨을 변경하기 위해 시스템을 어뷰징하지 마세요.

원문

https://medium.com/google-developer-experts/exploring-android-p-priority-buckets-d34d12059d36

2018년 6월 20일 수요일

[안드로이드] WorkManager에 관하여

Services. The life with/without. And WorkManager.

백그라운드를 다루는 안드로이드의 새 버전들의 출시로 인해, 백그라운드 다루기는 이전보다 더 복잡해졌습니다. 그래서 구글은 JetPack의 일부로, 이러한 백그라운드 다루는 작업을 도와주기 위해 WorkManager를 출시했습니다.
WorkManager가 무엇인지 배우기 전에,이것이 왜 만들어지게 되었고, 왜 필요한지를 알 필요가 있습니다. 어떤 컴포넌트 / 라이브러리든지 그 내부에서 어떤일이 일어나고 있고, 왜 그것을 사용해야 하는지 정확히 아는 개발자가 정말 좋은 개발자입니다.
이 글은 3개의 파트로 구성됩니다.
  1. 메모리 기본
  2. 현재 존재하는 백그라운드 방법
  3. WorkManager
먼저, 백그라운드에 관련된 것을 시작하기 전에, 우리는 안드로이드 프로세스의 메모리 관리의 기본에 대해 이해할 필요가 있습니다. 바로 이것이 첫번째 파트가 될 것입니다.

1. 메모리 기본

아주 오래전에, 안드로이드 커널은 리눅스 커널을 기반으로 해서 개발되었습니다. 안드로이드 커널과 모든 리눅스 커널들의 가장 핵심이 되는 차이점은 바로 스왑공간(Swap Space)가 없다는 것입니다.
리눅스의 스왑공간은 램이 꽉 찼을 때 사용됩니다. 시스템은 더 많은 메모리 리소스를 필요로 하지만 램이 꽉 찼을 때, 메모리상의 비활성 페이지를 스왑공간으로 이동시킵니다. 스왑공간은 램의 용량이 작은 디바이스에 도움이 되긴 하지만, 램의 용량을 늘리는것을 대체할 수는 없습니다.스왑공간은 램보다도 엑세스타임이 느린 하드 드라이브에 위치하기 때문입니다.


안드로이드에서는, 스왑공간 같은 것은 없습니다. 시스템의 메모리가 다 소진되었을 때, OOM 킬러를 이용해 프로세스를 강제종료 시켜버립니다. OOM 킬러는 Visible 상태와 소모된 메모리의 양에 기반하여 프로세시를 정리하여 여유 메모리를 확보합니다.
모든 프로세스는 엑티비티 매니저가 부여한 자신의 oom_adj 점수를 가지고 있습니다. 이 점수는 애플리케이션의 상태(Foreground, Background, Background Service 등등...)의 조합입니다. 다음은 모든 oom_adj값을 보여주고 있습니다.
# Define the oom_adj values for the classes of processes that can be
# killed by the kernel.  These are used in ActivityManagerService.
    setprop ro.FOREGROUND_APP_ADJ 0
    setprop ro.VISIBLE_APP_ADJ 1
    setprop ro.SECONDARY_SERVER_ADJ 2
    setprop ro.BACKUP_APP_ADJ 2
    setprop ro.HOME_APP_ADJ 4
    setprop ro.HIDDEN_APP_MIN_ADJ 7
    setprop ro.CONTENT_PROVIDER_ADJ 14
    setprop ro.EMPTY_APP_ADJ 15
프로세스의 oom_adj의 값이 클수록 커널의 OOM 킬러에게 정리당하기 쉽습니다. OOM킬러는 현재 사용가능한 여유메모리 크기와 oom_adj 임계값을 기반으로 구성한 규칙을 사용합니다. 즉, OOM 킬러의 조건은 다음과 같습니다.
여유 메모리의 공간이 X 보다 작을 때, oom_adj값이 Y보다 큰 프로세스를 정리하라!




즉, 가장 중요한 내용은, 앱이 메모리를 적게 소비할수록, 프로세스가 정리되지 않고 중요한 내용을 다룰 기회가 더 많아진다는 것입니다. 그리고, 두번째로 중요한 내용은 애플리케이션의 상태에 대해 이해하는 것입니다. 그래서, 앱이 백그라운드에 진입했을 때에도 뭔가를 지속적으로 하고싶으면, 서비스 컴포넌트를 사용해야 합니다.
  • 서비스는 UI를 제공하지 않고, 백그라운드에서 오래 걸리는 동작을 수행할 수 있도록 하는 앱 컴포넌트입니다.
다음과 같이 서비스를 사용해야 했던 몇가지 이유가 있습니다.
  1. 시스템에게 이 프로세스가 오래 걸리는 작업이 있음을 알려주고, 그에 맞는 oom_adj 점수를 얻도록 하기 위함입니다.
  2. 안드로이드 애플리케이션의 4대 진입점 중 하나입니다.
  3. 서비스를 별개의 프로세스에서 실행시킬 수 있습니다.
그러나!! 서비스를 사용하는 것의 단점이 있습니다. 프로세스가 계속 실행되고 있는 것이므로 배터리를 엄청나게 소비합니다.
아마 모든 개발자들이 아무런 제한없이 백그라운드에서, 원하는 모든 동작들을 수행했을 것입니다. 그래서, 구글은 마시멜로에서 처음 도즈(Doze)모드를 도입하고 누가에서 더 발전시켰습니다.
만약 도즈모드에 익숙하지 않다면, 반드시 익숙해지길 권합니다. 유저가 디바이스의 스크린을 끄고나면, 도즈모드가 시작되어 네트워크 통신, Sync, GPS, 알람, 와이파이 스캔 등을 비활성화 시켜버립니다. 이 도즈모드는 사용자가 스크린을 켜거나, 디바이스를 충전기에 연결할 때까지 유지됩니다. 중요하지 않은 일을 수행하는 앱의 개수를 줄임으로써 디바이스의 배터리를 절약하도록 합니다.
구글은 도즈모드를 통해 시작한 백그라운드 제한을 오레오에서 더더욱 강화했습니다. API 26으로 타게팅된 앱이 백그라운드 서비스 생성을 허가받지 않은 채로 startService() 메서드를 호출하려고 하면 IllegalStateException 예외를 던집니다.
혹자는 API 26으로 타게팅을 안하면 된다고 생각할 수도 있지만, 구글에서 다음과 같은 정책을 발표했습니다. (정책확인) 요약하자면 다음과 같습니다.
  • 2018년 8월 : 새로 출시되는 앱들은 반드시 API 26(Oreo 8.0) 이상
  • 2018년 11월 : 기존 앱들도 API 26(Oreo 8.0) 이상
  • 2019년 이후 : 매년 targetSdkVersion 요구사항이 향상될 것입니다. 안드로이드가 매년 새로운 버전을 낼 때마다, 모든 앱들은 해당 API 레벨 이상을 타겟팅해야 합니다.
이를 통해 알 수 있는 것은....
백그라운드에서 오랜시간 동작할 수 있다는 서비스의 기본목적을 수행하는 것이 허용되지 않게 되었다. 그러므로, 백그라운드 작업을 위해 더 이상 서비스를 사용하지 않게 될 것이다.
서비스를 대체하여 백그라운드에서 동작을 수행하려면, job API를 사용해야 합니다.

2. 현재 존재하는 백그라운드 방법

약간의 KB를 다운로드하는 간단한 네트워크 예제를 살펴봅시다.
가장 직접적인 방법(그리고 부정확한...)은 해당 동작을 수행하는 별도의 스레드를 가지는 것입니다.
int threads = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(threads);
executor.submit(myWork);
어떤 유저가 매우 느린 3G환경을 가지고 있고, 엘리베이터를 타면서 로그인 동작을 수행했다고 생각해 봅시다. 그리고, 네트워크 작업 중에 유저가 전화를 받게됩니다.
OkHttp의 기본 타임아웃은 꽤 큰 편입니다.
  • connectTimeout = 10_000;
  • readTimeout = 10_000;
  • writeTimeout = 10_000;
게다가, 기본적으로 네트워크의 재시도 회수는 3번입니다.
그러므로, 가장 최악의 경우 90초 (30 * 3)가 걸리게 됩니다.
자 그러면 다음의 문제를 생각해 봅시다.

로그인은 성공했을까요???

앱이 백그라운드에 있는 동안, 우리는 모릅니다. 우리가 위에서 살펴봤듯이, 프로세스가 계속 살아있어서 로그인 요청에 대한 응답을 받아서 유저정보를 잘 저장했을지를 보장할 수 없습니다. 혹, 사용자의 전화가 오프라인 상태가 되어 인터넷 연결이 끊길 수도 있습니다
유저 입장에서, "나는 정보를 입력하고 버튼을 눌렀으니 로그인이 잘 되었을 꺼야" 라고 생각할 수 있습니다. 그렇지만 만약에 로그인이 되어있지 않다면, 유저는 앱의 안좋은 UX를 탓하게 될 것입니다. 그러나, 이는 UX의 문제가 아니라 기술적인 문제입니다.
그래서 JobScheduler가 이 상황을 해결하기 위해 등장했습니다.
ComponentName service = new ComponentName(this, MyJobService.class);
JobScheduler mJobScheduler = (JobScheduler)getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo.Builder builder = new JobInfo.Builder(jobId, serviceComponent)
 .setRequiredNetworkType(jobInfoNetworkType)
 .setRequiresCharging(false)
 .setRequiresDeviceIdle(false)
 .setExtras(extras).build();
mJobScheduler.schedule(jobInfo);
JobScheduler는 시작된 Job을 스케쥴링합니다. 적땅한 시간이 되면, 시스템은 MyJobService를 시작하고, onStartJob() 메서드 안의 내용들을 수행할 것입니다. 꽤 좋은 이론이지만, JobScheduler는 최소 API21 이상에서만 사용할 수 있습니다. 게다가... API 21, 22에서의 JobScheduler는 버그덩어리 컴포넌트 였습니다. 즉, JobScheduler를 제대로 사용하려면 최소 API 레벨은 23 이여야 한다는 것입니다.
만약 API21, 22에서도 제대로 사용하려면 JobDispatcher를 사용할 수 있습니다.
Job myJob = firebaseJobDispatcher.newJobBuilder()
 .setService(SmartService.class)
 .setTag(SmartService.LOCATION_SMART_JOB)
 .setReplaceCurrent(false)
 .setConstraints(ON_ANY_NETWORK)
 .build();
firebaseJobDispatcher.mustSchedule(myJob);
그러나.... 이것은 구글플레이서비스에 의존성을 가지고 있습니다. 그러므로, 아마존기반의 디바이스, 중극 제조사에서는 사용할 수 없는 방법입니다. 그러므로 JobDispatcher도 좋은 선택은 아닙니다. 그러면, 오레오 이전 디바이스에서, 기존 서비스의 이점을을 취하려면 어떻게 해야 할까요??
바로 서포트 라이브러리의 JobIntentService를 이용하면 됩니다. 이 API는 기존의 인텐트서비스를 job으로 실행할 수 있습니다. 그러나, 이것도 오레오에서 가능한 빨리 호출을 수행하는데 도움이 되지 않습니다. 그러므로, 다시 원점으로 돌아갑니다. 현재 실행중인 기기의 안드로이드 버전에 따른 관리, 백그라운드에서 작업을 실행하고, 적절한 스케쥴러에게 작업 등록 등...

3. WorkManager

위의 내용을 종합했을 때, 디바이스의 안드로이드 버전 / 구글플레이서비스의 여부에 따라 다른 해결책들이 존재합니다. 그러므로, 여러분은 이걸 상황에 맞게 모두 구현해야 한다는 고민에 빠지게 될 것입니다. 안드로이드 팀은 이러한 불만사항을 수용하여 Google I/O에서 WorkManager라는 해결책을 제시했습니다.
WorkManager는 시스템 기반의 백그라운드 프로세싱 API를 제공하여, 구현을 단순화하도록 도와줍니다. 이 클래스는 앱이 포그라운드에 없어도, 백그라운드 Job에서 작업들이 수행되도록 합니다. JobScheduler나 Firebase의 JobDispatcher를 사용할 수 있다면 사용하고, 앱이 포그라운드라면 그 프로세스에서 가급적 처리할 수 있도록 합니다. 개발자가 백그라운드 처리과정을 깊게 신경쓰지 않아도 되도록 간단하게 래핑해놓았습니다.
WorkManager 라이브러리는 몇개의 컴포넌트를 가지고 있습니다.
  • WorkManager : 인수 / 제약조건을 가진 작업을 받아서, 큐에 추가합니다.
  • Worker : 백그라운드 스레드에서 동작하는 doWork() 메서드 하나만 가지고 있습니다. 모든 백그라운드 태스크들은 이 메서드 안에서 수행되어야 합니다. 가능한한 심플하도록 구성해야 합니다.
  • WorkRequest : Worker가 어떤 인수와 제약조건(ex : 인터넷, 충전 등)와 함께 큐에 추가되어야할지를 명시하는 역할입니다.
  • WorkResult : 성공, 실패, 재시도
  • Data : Worker로 주고받는 영구적인 키-벨류 쌍의 값 입니다.
먼저, Worker 클래스를 상속받아 새로운 클래스를 만들고 doWork()메서드를 구현합니다.
public class LocationUploadWorker extends Worker {
    ...
     //Upload last passed location to the server
    public WorkerResult doWork() {
        ServerReport serverReport = new ServerReport(getInputData().getDouble(LOCATION_LONG, 0),
                getInputData().getDouble(LOCATION_LAT, 0), getInputData().getLong(LOCATION_TIME,
                0));
        FirebaseDatabase database = FirebaseDatabase.getInstance();
        DatabaseReference myRef =
                database.getReference("WorkerReport v" + android.os.Build.VERSION.SDK_INT);
        myRef.push().setValue(serverReport);
        return WorkerResult.SUCCESS;
    }
}
다음으로, 생성한 Worker를 실행하기 위해 WorkManager를 호출합니다.
Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType
            .CONNECTED).build();
Data inputData = new Data.Builder()
            .putDouble(LocationUploadWorker.LOCATION_LAT, location.getLatitude())
            .putDouble(LocationUploadWorker.LOCATION_LONG, location.getLongitude())
            .putLong(LocationUploadWorker.LOCATION_TIME, location.getTime())
            .build();
OneTimeWorkRequest uploadWork = new OneTimeWorkRequest.Builder(LocationUploadWorker.class)
            .setConstraints(constraints).setInputData(inputData).build();
WorkManager.getInstance().enqueue(uploadWork);
WorkManager가 남은 과정들을 모두 처리할 것입니다. WorkManager는 가장 최적의 스케쥴을 선택하여 작업을 큐에 추가할 것입니다. 또한, 작업의 인수, 세부사항, 변경되는 상태를 저장할 것입니다. 심지어, LiveData를 이용하여 작업상태를 구독할 수도 있습니다.
WorkManager.getInstance().getStatusById(locationWork.getId()).observe(this,
        workStatus -> {
    if(workStatus!=null && workStatus.getState().isFinished()){
         ...
    }
});
WorkManager 라이브러리의 구조는 다음의 그림을 참고할 수 있습니다.


  1. 백그라운드로 수행할 작업을 Worker의 doWork() 메서드를 구현하여 정의합니다.
    2.생성한 Worker 인스턴스를 가지고 WorkRequest를 생성합니다. 필요하다면WorkRequest를 생성할 때 DataConstraints를 추가합니다.
  2. WorkManager의 enqueue() 메서드를 이용해 생성한 WorkRequest를 큐에 추가합니다. 이 떄, 내부적으로 Room에 작업을 저장합니다.
  3. 내부적으로 적절한 스케쥴러(JobScheduler, JobDispatcher, Executor, AlarmManager)중 하나를 골라 doWork() 메서드를 호출합니다.
  4. 작업의 결과는 LiveData를 이용해 구독할 수 있고, 해당 작업의 산출물은 Data로 이용할 수 있습니다.
이것은 어디까지나 기본동작으로, 여러 추가기능들이 있습니다. 이 추가기능을 따로 써도 되고, 조합해서도 쓸 수 있습니다.
  • 주기적으로 실행하기
Constraints constraints = new Constraints.Builder().setRequiredNetworkType
        (NetworkType.CONNECTED).build();
PeriodicWorkRequest locationWork = new PeriodicWorkRequest.Builder(LocationWork
        .class, 15, TimeUnit.MINUTES).addTag(LocationWork.TAG)
        .setConstraints(constraints).build();
WorkManager.getInstance().enqueue(locationWork);
  • 두개 이상의 job을 순차적으로 실행하기
WorkManager.getInstance(this)
        .beginWith(Work.from(LocationWork.class))
        .then(Work.from(LocationUploadWorker.class))
        .enqueue();
  • 두개 이상의 job을 병렬로 실행하기
WorkManager.getInstance(this).enqueue(Work.from(LocationWork.class,  LocationUploadWorker.class));
  • Note : 주기적 job과 일회성 job을 함께 조합할 수 없습니다.

이 외에도 WorkManager를 이용해 할 수 있는 많은 상황들이 있습니다. Work 취소하기, 결합하기, 연결하기, 한 Work의 Data를 다른 Work로 합치기 등등... 이 다른 기능들을 더 확인하려면 문서를 찾아보길 권합니다.

원문

https://medium.com/google-developer-experts/services-the-life-with-without-and-worker-6933111d62a6

2018년 6월 5일 화요일

[안드로이드] Android JetPack

Use Android Jetpack to Accelerate Your App Development

이 포스트에서, 안드로이드 앱 개발을 더 쉽도록 도와주는 차세대의 컴포넌트, 도구, 구조적 가이드라인을 포함하는 JetPack을 소개하려고 한다.
JetPack은 새로운 안드로이드의 기능들이 하위호환성을 유지할 수 있도록 도와주는 Support Library로부터 고안되었다. 그리고, 앱의 라이프사이클과 관련된 데이터 처리, 수시로 변하는 데이터 처리의 복잡성을 쉽게 처리할 수 있도록 디자인된 아키텍쳐 컴포넌트를 소개했다. 아키텍쳐 컴포넌트를 작년에 발표한 이래로 많은 앱들이 이 컴포넌트를 사용하기 시작했다. LinkedIn, Zillow, iHeartRadio 같은 회사들은 더 적은 버그, 더 쉬워진 테스트, 앱의 순수 기능에만 집중할 수 있는 시간 등의 이점을 보았다.

What is Android JetPack

Android JetPack은 좋은 앱을 만들기 위한 컴포넌트, 도구, 가이드라인의 모음 이다. JetPack은 기존의 서포트 라이브러리, 아키텍쳐 컴포넌트들을 모두 가져와서 4개의 카테고리로 재배치했다.


JetPack 라이브러리는 안드로이브 플랫폼의 일부로 포함되지 않고(unbundled) 별개로 제공되는 라이브러리 이다. 즉, 각 컴포넌트를 필요한 시점에 맞추어 적욧할 수 있다는 의미다. JetPack에 새로운 기능이 추가되면 손쉽게 앱에 반영하여 배포할 수 있다. JetPack으로 변경되면서,기존/신규 컴포넌트들이 androidx.*로 이동되었다. 해당 내용은 다음의 포스트를 참고한다.
또한, 서포트 라이브러리의 취지대로, JetPack의 컴포넌트도 특정 버전에 종속되지 않도록 만들어졌기 때문에 다양한 안드로이드 버전의 플랫폼에서 실행할 수 있다.
게다가, JetPack은 관심의 분리 / 테스트 가능을 중점적으로 하는 현대 설계기법과 코틀린 통합과 같은 생산성 관련 내용들도 고려하여 제작되었다. 이는 더 적은 코드로 견고하고 고품질의 앱을 쉽게 만들도록 도와준다. JetPack의 컴포넌트들은 유기적으로 동작할 수 있도록 구현되었지만, 개발자는 굳이 모두를 사용할 필요 없이 필요한 부분만 사용할 수도 있다. 그러므로 이미 구현된 모든 부분을 고칠 필요 없이, 필요한 부분에만 JetPack 컴포넌트를 순차적으로 교체할 수도 있다.
우리는 다음과 같은 피드백을 받았기 때문에, JetPack의 이러한 특성이 얼마나 중요한지 알고 있다.
우리 앱은 MVVM으로 코드 구조 변경을 고려하고 있었습니다. 아키텍쳐 컴포넌트는 이 고민을 쉽게 해결하고 구현할 수 있도록 도와주었습니다. 그리고, 테스트하기도 훨씬 수월해졌습니다. ViewModel만 유닛테스트를 수행하면 되기 때문에 코드가 전보다 훨씬 견고해졌습니다.

What's New

다음의 5가지 새로운 컴포넌트가 JetPack에 곧 추가될 것이다.
  • WorkManager alpha release
  • Navigation alpha release
  • Paging stable release
  • Slices alpha release
  • Android KTX (Kotlin Extensions) alpha release

WorkManager

WorkManager 컴포넌트는 백그라운드 제약상황에서 실행이 보장되어야 하는 백그라운드 동작을 구현하기 위한 멋진 해결방법을 제공하는 라이브러리 이다. 이 라이브러리는 다음과 같은 특징을 가지고 있다.
  • 단순함
  • 최근 스타일의 API
  • Google Play Service의 유무에 상관없이 동작할 수 있음
  • work의 그래프를 생성하여 순서를 지정할 수 있음
  • work의 상태를 조회할 수 있음
    자세한 내용은 공식문서에서 확인할 수 있다.

Navigation

엑티비티는 시스템에서 제공하는 앱 UI의 진입점이지만, 화면간에 데이터를 공유할떄나, 화면전환에서 유연성이 떨어지는 점 때문에 이상적인 아키텍쳐라고 볼 수 없었다. 그래서 하나의 엑티비티만을 사용하는 것을 기본 아키텍쳐로 하여 앱을 만들도록 하는 Navagation 컴포넌트가 도입되었다. 프레그먼트의 지원을 통해 Lifecycle, ViewModel 같은 아키텍쳐 컴포넌트의 모든 이점을 얻을 수 있으며, Navigation 컴포넌트가 FragmentTransition의 복잡함을 처리할 수 있다. 게다가, Navigation컴포넌트는 올바른 Up / Back 동작을 자동적으로 빌드되도록, Deep 링크를 지원하도록, NavigationDrawer / BottomNavigation 같은 UI위젯에서도 적절하게 네비게이셔닝이 되도록 도와준다. 안드로이드 스튜디오 3.2에서는 네비게이션 에디터를 추가하여 네비게이션 속성들을 보고 관리할 수 있도록 추가될 예정이다.


자세한 내용은 공식문서에서 확인할 수 있다.

Paging

앱에 표시되는 데이터는 양이 많고, 로드하는 동작은 비용이 크기 때문에, 다운로드나 생성, 한번에 너무 많이 보여주는 동작을 피해야한다. Paging 컴포넌트는 RecyclerView에서 많은 양의 데이터를 로드하고 나타내는 작업을 빠르게 수행할 수 있도록 도와준다. 페이징 된 데이터를 로컬 / 네트워크 / 둘다 에서 불러올 수 있으며, 어떻게 컨텐츠를 불러올지 정의할 수 있다. Room, LiveData, RxJava와 함께 사용될 수 있다.
자세한 내용은 공식문서에서 확인할 수 있다.

Slice

JetPack에 Slice 컴포넌트가 추가되었다. "Slice"는 특정 내용을 다른 앱에서 보여주는 개념이다. 다음의 예시는 구글 어시스턴트의 검색결과로 어시스턴트 UI안에 내용물을 렌더링한다.


자세한 내용은 공식문서에서 확인할 수 있다.

Android KTX

JetPack의 많은 목표중에 하나는 코틀린 언어의 이점을 이용해 앱 구현의 생산성을 높이는 것이다. Android KTX는 코틀린코드를 다음과 같이 변경할 수 있도록 도와준다.
view.viewTreeObserver.addOnPreDrawListener(
  object : ViewTreeObserver.OnPreDrawListener {
    override fun onPreDraw(): Boolean {
      viewTreeObserver.removeOnPreDrawListener(this)
      actionToBeTriggered()
      return true
    }
});
더 간결하게 줄일 수 있다.
view.doOnPreDraw { actionToBeTriggered() }

이는 단지 JetPack을 이용하여 코틀린을 지원하는 시작일 뿐이다. 우리의 목표는 JetPack을 이용해 코틀린 개발자가 좋은 앱을 만드는 것이다.
자세한 내용은 공식문서에서 확인할 수 있다.

원문

https://android-developers.googleblog.com/2018/05/use-android-jetpack-to-accelerate-your.html