본문 바로가기

Android

Dagger setup with WorkManager

목표

WorkManager를 사용하기 위해서 결정하고 제일 먼저 해야할 일은 Worker에 의존성 주입을 설정하는 일이다.

우리가 해야할 백그라운드 작업은

  • 내일 할 일을 불러올 수 있어야하고,
  • 알람을 등록할 수 있어야한다.

그러므로 TaskRepositoryAlarmUtils의 주입이 필요하다.

Worker에 객체를 주입하기 위해서 필요한 설정을 알아보자.

Dependencies

우선 WorkManager를 사용하기 위해서는 dependencies 추가가 필요하다

// WorkManager
implementation "androidx.work:work-runtime:2.2.0"

이제 WorkManager, Worker, WorkRequest에 대한 정보들을 사용할 수 있다.

Dagger에게 WorkerFactory를 이용해서 인스턴스 생성 방법을 학습시킨다.

WorkManager는 getInstance() 메소드를 통해 인스턴스를 받아와서 사용한다. 즉 우리가 생성자에 관여할 수 없다.

 

Worker의 경우 Injection에 사용하기 위해 WorkerFactory라는 클래스를 지원하고 있다.

WorkManager에게 ParentWorkerFactory 클래스를 사용해서 어떻게 커스텀 Worker를 생성할 수 있는지 알려주는 용도이다.

기억해야할 포인트는 Factory는 Parent와 Child 2가지라는 점이다.

  • ChildWorkerFactory는 각 커스텀 Worker를 생성할때 사용되는 Factory들을 대표하는 인터페이스다.
  • ParentWorkerFactory는 WorkManager에게 Worker의 생성을 학습시키기 위해 사용되는 팩토리이다.

Module 연결하기.

 

CustomWorker의 ChildWorkerFactory

// CustomWorker
public class TestWorker extends Worker {
    // Instances that we want to injected.
    TaskRepository taskRepository;
    AlarmUtils alarmUtils;

    public TaskWorker(Context context, WorkerParameters workerParams, TaskRepository taskRepository, AlarmUtils alarmUtils) {
        super(context, workerParams);
        this.taskRepository = taskRepository;
        this.alarmUtils = alarmUtils;
    }

    @Override
    public Result doWork() {
        // background job

        // taskRepository 사용해서 Task 꺼내오고
        // alarmUtils 사용해서 알람 등록시킨다.

    }

    /* TestWorker를 생성하는 Factory pattern
       ChildWorkerFactory 인터페이스를 사용한다. */
    public static class Factory implements ChildWorkerFactory {
        private final Provider<AlarmUtils> alarmUtilsProvider;
        private final Provider<TaskRepository> taskRepositoryProvider;

        @Inject
        public Factory(Provider<AlarmUtils> alarmUtilsProvider, Provider<TaskRepository> taskRepositoryProvider) {
            this.alarmUtilsProvider = alarmUtilsProvider;
            this.taskRepositoryProvider = taksRepositoryProvider;
        }

        @Override
        public ListenableWorker create(Context appContext, WorkerParamters workerParameters) {
            return new TestWorker(appContext, workerParamters, taskRepositoryProvider.get(), alarmUtilsProvider.get());
        }
    }
}

이제 TestWorker.Factory는 TestWorker를 생성할 때 주입받은 객체를 추가한 생성자를 사용해서 TestWorker 인스턴스를 생성할 수 있게 되었다.

 

위 WorkerBindingModule을 보면 메소드의 반환형이 ChildWorkerFactory 인터페이스이다.

여러 개의 커스텀 Worker를 하나의 인터페이스형으로 관리하는 것이다.

 

ParentWorkerFactory

 

이제 WorkManager에게 알려줄 ParentWorkerFactory를 구현하면 된다.
ParentWorkerFactory는 Worker클래스의 클래스이름을 key값으로 가지고 Worker를 value값으로 가지는 Map을 사용한다.

// ParentWorkerFactory
// Map< Provider<클래스이름> , Provider<ChjildWorkerFactory>>
// 즉 ChildWorkerFactory를 관리하고 사용해서 Worker를 생성하는 역할을 담당하게 된다.
public class DaggerWorkerFactory extends WorkerFactory {
    private final Map<Class<? extends ListenableWorker>, Provider<ChildWorkerFactory> workersFactories;

    @Inject
    public DaggerWorkerFactory(Map<Class<? extends ListenableWorker>, Provider<ChildWorkerFactory>> workersFactories) {
        super();
        this.workersFactories = workersFactories;
    }

    @Nullable
    @Override
    public ListenableWorker createWorker(Context, appContext, String workerClassName, WorkerParameters workerParams) {
        Provider<ChildWorkerFactory> factoryProvider = getWorkerFactoryProviderByKey(workerFactories, workerClassName);

        return factoryProvider.get().create(appContext, workerParams);
    }

    public static Provider<ChildWorkerFactory> getWorkerFactoryProviderByKey(Map<Class<? extends ListenableWorker>, Provider<ChildWorkerFactory>> map, String key) {
        for (Map.Entry<Class<? extends ListenableWorker>, Provider<ChildWorkerFactory>> entry : map.entrySet()) {
            if (Objects.equals(key, entry.getKey().getName())) {
                return enrty.getValue();
            }
        }
        return null;
    }
}

ParentWorkerFactory의 구현은 위와 같이 된다.

 

이제 WorkManager에게 구현한 ParentWorkerFactory를 사용하라고 알려주기만 하면 된다.

Configuration config = new Configuration.Builder()
    .setWorkerFactory(parentWorkerFactory)
    .build();
WorkManager.initialize(this, config);

WorkManager가 등록한 ParentWorkerFactory를 사용한다.
ParentWorkerFactory는 클래스 이름을 키 값으로 각 커스텀 Worker를 초기화하는 ChildWorkerFactory를 사용할 수 있게 된다.

 

실제 PeriodicWorkRequest를 사용해서 값 출력해보기.

인젝션 설정을 마치고 WorkManager가 얼마나 지연되는지 확인하기 위해서 가지고 있는 2가지 디바이스(킷캣, 오레오)에서 각각 PeriodicWorkRequest를 사용해서 실행 값을 출력해보았다.

1. KitKat에서 실행했을 때

킷캣 버전의 경우 Doze모드가 없으므로

매우 일관되게 지연없이 일정한 인터벌 간격으로 실행되는 것을 알 수 있었다.
중간에 Device Reboot를 했지만 백그라운드 작업이 계속 진행되는 것을 확인하였다.

2. Oreo에서 실행했을 때

오레오 버전의 경우 배터리 효율을 위한 standby나 doze 모드가 활용되는 버전이다.
그래서 스마트폰을 사용하던 시간 대에는 비교적 지연없이 인터벌마다 실행이 되었지만.
수면시간 동안 오랜시간 스마트폰을 사용하지 않아서 배터리 효율성을 우선하는 모드가 되었을 경우 지연이 발생하였다.

 

Summary

Dagger에게 Worker에 Injection할 수 있도록 필요한 세팅에 대해서 알아보았다.

 

전체적인 과정은 ViewModel에 인젝션하는 과정과 비슷하다. 우리는 Worker 생성자에 대한 권한을 갖고 있지 않으므로 Factory라는 우회적인 방법을 사용해서 추가 파마리터를 수용해서 인스턴스를 생성하도록 하는 방법이다.

 

그 과정에서 구글은 WorkerFactory라는 클래스를 지원하고 있으며 WorkerFactory를 상속한 클래스는 각 Worker의 Factory를 Map형태로 관리한다.

 

즉, Dagger가 원하는 클래스 이름(key)을 말하면 해당 클래스의 인스턴스를 생성하는 Factory를 제공한다.

 

 

그리고 인터벌을 설정해서 PeriodicWorkRequest를 사용해서 Task를 추가시키는 작업을 해보았다.
Oreo 버전에서 지연이 되는 것을 직접 확인할 수 있었고, 우리의 조건을 부합하기 위해서는 AlarmManager를 사용해야만 Specific Runtime을 보장할 수 있을 것 같다.

 

참고자료

https://www.droidcon.com/media-detail?video=328597877