2022년 10월 10일 월요일

WorkManager 백그라운드 태스크 다루기

WorkManager 백그라운드 태스크 다루기

WorkManager 백그라운드 태스크 다루기

Android 에서 백그라운드 서비스, 브로드캐스트등을을 하기위해서 Thread, Async, JobScheduler,알람매니져등이 있지만 최근에는 WorkManager를 이용하기를 권장한다.

WorkManager는 주로 백단 에서 작업해야하는 것들에 대해 사용하는데, 앱이 종료되거나 다시시작되더  WorkManager가 작업을 다시 시작해주기 때문에 안정적인 서비스 개시가 가능하도록 해준다.

그렇다고 모든 서비스, 쓰레드에 WorkManager를 쓰라는건 아니고 , 즉각실행해야 될거는 coroutines(또는 rxjava등) , 정확한 시간에 가동되야 하는거는 AlarmManager, 기기가 다시 시작되어도 실행되어야 할 백그라운드 작업등은 WorkManager를 쓰라는 얘기다.

WorkerManager 는 API레벨과 앱상태같은 요건에 근거해서 내부적으로 적절한 방법으로 백그라운작업을 어떻게 할지 자동으로 선택하다.

https://developer.android.com/guide/background

WorkerManger에는 다음과 같은 개념이 있다.

  • Worker : Abstract클래스로서 이 클래스를 확장받은 일할놈(?)이 어떤일을 할지를 구체적으로 기술한다. 자신의 일이 잘 끝났다 못끝냈다는 Success, Failure, Retry 3개의 값을 반환함으로서 workmanager가 전반적인 work를 관리할수 있도록 한다.
  • WorkRequest : 작업할놈이 결정되었다면 이 클래스를 통해서 작업요청을 해야하는데, OneTimeWorkrequest, PeriodicWorkRequest가 있다.
  • WorkerManager : WorkRequest를 받은 Worker가 를 큐에 넣거나 빼는 등의 전체 work 에 대한 작업상태를 모니터링 한다.
  1. 앱이 백그라운드에서 log 를 출력하기.

https://github.com/sugoigroup/android_workmanager_example/commit/d9662cc3e3fa38c01079a7bac25003b5b42fd46f

dependencies {
...

    def work_version = "2.5.0"
    implementation "androidx.work:work-runtime:$work_version"

}

build.gradle (Module:… .app)에 workerManager 의존성추가

  1. 샘플워커 클래스를 만들고, workRequest->workerManager에 등록

https://github.com/sugoigroup/android_workmanager_example/commit/d9662cc3e3fa38c01079a7bac25003b5b42fd46f

---UploadWorker.java (일할놈)를 만들어서 등록  

import android.util.Log;

import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

public class UpladWorker extends Worker {
    private int count = 0;

    public UpladWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @Override
    public Result doWork() {

        while (count < 10) {
            count++;
            try {
                Log.i("worker", "now background" + count);
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return Result.success();

    }
}

  

---workerRequest -> workerManager에 worker  


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 충전중일때만 worker 가 실행되도록 제한을 건다.
        Constraints constraints = new Constraints.Builder()
                .setRequiresCharging(true)
                .build();

        // 이번 한번만 일하도록 한다.
        OneTimeWorkRequest oneTimeWorkRequest =  new OneTimeWorkRequest.Builder(UpladWorker.class)
                .setConstraints(constraints)
                .build();

        // 매니져에게 일할거라고 등록한다.
        WorkManager.getInstance(this).enqueue(oneTimeWorkRequest);
    }

I/worker: now background1

I/worker: now background2

I/worker: now background3

I/worker: now background4 --> 이 시점에서 Home 버튼을눌러 앱을 보이지않는 백그라운드상태로 전환했다.

I/worker: now background5 --> 백그라운드상태이지만 워커는 계속움직여 로그가 찍힌다.

I/worker: now background6

I/worker: now background7

I/worker: now background8

I/worker: now background9

I/worker: now background10

  1. 백그라운드 상테에서 알림창으로 통지가 오는지 확인해보자.

https://github.com/sugoigroup/android_workmanager_example/commit/3dbb0496cf0427cbdb6c710987f8efb3a1fa1d4e

–worker (일할놈)이 로그찍는일에서 통지하는일로 역활을 바꾸어 보자.


public class UpladWorker extends Worker {
    private int count = 0;

    public UpladWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @Override
    public Result doWork() {

        while (count < 10) {
            count++;
            try {
               // Log.i("worker", "now background" + count);
                showNotification("worker", "now background" + count);
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return Result.success();

    }

    private void showNotification(String task, String desc) {
        NotificationManager manager = (NotificationManager) getApplicationContext().getSystemService(
                Context.NOTIFICATION_SERVICE);
        String channelId = "my_channel";
        String channelName = "my_name";
        // Oreo 부터는 노티에 알림하려면 채널이 있어야 한다.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new
                    NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT);
            manager.createNotificationChannel(channel);
        }
        NotificationCompat.Builder builder =
                new NotificationCompat.Builder(getApplicationContext(), channelId)
                        .setContentTitle(task)
                        .setContentText(desc)
                        .setSmallIcon(R.mipmap.ic_launcher);
        manager.notify(1, builder.build());
    }
}

핸폰 알림창에 3초마다 알림이 갱신된다…

  1. workManager에게 특정태그로 요청된 workRequest를 통해 중지명령을 내려서 worker가 명령이후에는 작업하지않도록 해보자.

일단 간단하게 hello textview 를 클릭하면 클릭한 시점부터 worker가 일을 안하도록 하자.

https://github.com/sugoigroup/android_workmanager_example/commit/b44eaa1059d44fa718d089053c63f2ad81005fe6

-------------
UploadWorker.java

    public Result doWork() {

        while (count < 10) {
            if (isStopped()) {
                continue;
            }
            count++;
            try {
               // Log.i("worker", "now background" + count);
                showNotification("worker", "now background" + count);
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return Result.success();

    }
-------------------
MainActivity.java

        // 충전중일때만 worker 가 실행되도록 제한을 건다.
        Constraints constraints = new Constraints.Builder()
                .setRequiresCharging(true)
                .build();

        // 이번 한번만 일하도록 한다.
        OneTimeWorkRequest oneTimeWorkRequest =  new OneTimeWorkRequest.Builder(UpladWorker.class)
                .setConstraints(constraints)
                .addTag(CANCEL_ME)
                .build();

        // 매니져에게 일할거라고 등록한다.
        WorkManager.getInstance(this).enqueue(oneTimeWorkRequest);

Hello world라는 텍스트뷰를 클릭하면 알림창이 더이상 안온다…

  1. WorkerManager는 각 workRequest에 대해 모두 기록하고 중간에 멈춰질때 다시 복귀하도록 관리하고 있다. 따라서 이번 예제에서 setRequiresCharging(work의 일하는 제약을 충전중일경우에만) 이라고 한정지었기 때문에 충전중이 아닌상태로 앱을 시작했다가 , 충전중상태로 다시 앱을 재가동하면 충전중이 아닐때의 work 까지 총 두개의 work 가 일하게 된다.
    때문에 이를 막기 위해서 beginUniqueWork라는 함수로 workManager를 가동하면 KEEP, REPLACE, APPEND 라는 3개의 옵션을 통해 같은 이름을 가진 작업 queue가 있을때, 나중에 들어온 같은 이름의 작업을 무시할건지, 중간에 교체할건지, 순서대로 진행할건지를 결정할수 있도록 해준다

https://github.com/sugoigroup/android_workmanager_example/commit/4bd3b10667b4f09879a317f124d68066b44c1f3e


   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);

       TextView tv = (TextView) findViewById(R.id.hello);
       tv.setOnClickListener(e -> {
           startQueue();
       });
       startQueue();
   }

   private void startQueue() {

       // 충전중일때만 worker 가 실행되도록 제한을 건다.
       Constraints constraints = new Constraints.Builder()
               .setRequiresCharging(true)
               .build();

       // 이번 한번만 일하도록 한다.
       final OneTimeWorkRequest  oneTimeWorkRequest =  new OneTimeWorkRequest.Builder(UpladWorker.class)
               .setConstraints(constraints)
               .addTag(CANCEL_ME)
               .build();

       // 매니져에게 일할거라고 등록한다.
       //
       WorkManager.getInstance(this)
               .beginUniqueWork("iamUnique", ExistingWorkPolicy.APPEND_OR_REPLACE, oneTimeWorkRequest)
               .enqueue();
   }

같은 WorkRequest를 A ,실행중에 B 실행 에 대해

KEEP : B는 무시된다.(단 A가 끝난상태면 B는 진행한다.)
REPLACE : B로 새롭게 시작한다.(A가 어디까지 진행된지 상관없다)

APPEND : A가끝날때 까지 기다렸다가 B를 진행한다.

APPEND OR REPLACE : A가진행중이면 B를 뒤에 추가하고 시작하되, A가 실패로 끝난상태거나 취소가 이루어졌다면 B로 새롭게 시작한다.

hello world text 뷰를 클릭하면서 위의 네가지를 바꾸어 보면 알림창에 해당 옵션에 따라서 적절하게 queue가 진행될거다. 

  1. 서로 다른 workRequest를 순차적으로 일시키자.

https://github.com/sugoigroup/android_workmanager_example/commit/255afd504af3c17417a39f4f00801e408f896c26



------
LogWorker.java
public class LogWorker  extends Worker {

    public LogWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @Override
    public Result doWork() {
         Log.i("worker", "Wow All Done!");
        return Result.success();
    }
}

-------
MainActivity.java

        // 이번 한번만 일하도록 한다.
        final OneTimeWorkRequest  oneTimeWorkRequest =  new OneTimeWorkRequest.Builder(UpladWorker.class)
                .setConstraints(constraints)
                .addTag(CANCEL_ME)
                .build();

        // 이번 한번만 일하도록 한다.
        final OneTimeWorkRequest  logWorkerRequest =  new OneTimeWorkRequest.Builder(LogWorker.class)
                .setConstraints(constraints)
                .addTag(CANCEL_ME)
                .build();

        // 매니져에게 일할거라고 등록한다.
        // then 으로 순차적인 workRequest를 실행할수 있다.
        WorkManager.getInstance(this)
                .beginUniqueWork("iamUnique", ExistingWorkPolicy.APPEND_OR_REPLACE, oneTimeWorkRequest)
                .then(logWorkerRequest)
                .enqueue();
    }

  1. workRequest(일요청)에 자료를 넣고(input), 일이 끝나면 처리한 결과(output)을 받아서 textview에 뿌려보자.

백그라운드 쓰레드에서 작동하는 workManager 에 데이터를 전달하고 이를 다시 받아서 UI쓰레드에서 ui를 변경하는것은 참 귀찮은 작업인데, 화면변경은 livedata 를 통해서 전달 할수 있어서 간편하다. 이밖에도 Future 인터페이스를 통해 해당 workRequest 작업이 끝나는 시점에만 특정한 로직을 실행한다든지 할수도있다.

https://github.com/sugoigroup/android_workmanager_example/commit/b97b03e7743c6d86001a7df8a821a80bd115325f


------
LogWorker.java
public class LogWorker  extends Worker {

    public LogWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @Override
    public Result doWork() {

        String detector = getInputData().getString(KEY_DETECTOR);

        Log.i("worker",   detector + " detector is founding a key!");

        //내보낼 값을 정하자
        Data outputData = new Data.Builder()
            .putString(FOUND_OUT_KEY, "elzzup")
            .build();
        return Result.success(outputData);
}

-------
MainActivity.java

        private void startQueue() {

        // 충전중일때만 worker 가 실행되도록 제한을 건다.
        Constraints constraints = new Constraints.Builder()
                .setRequiresCharging(true)
                .build();

        // 이번 한번만 일하도록 한다.
        final OneTimeWorkRequest  oneTimeWorkRequest =  new OneTimeWorkRequest.Builder(UpladWorker.class)
                .setConstraints(constraints)
                .addTag(CANCEL_ME)
                .build();

        //보낼값을 정하자.
        Data whoIsTheDetector = new Data.Builder().putString(KEY_DETECTOR, "kim").build();

        // 이번 한번만 일하도록 한다.
        final OneTimeWorkRequest  logWorkerRequest =  new OneTimeWorkRequest.Builder(LogWorker.class)
                .setConstraints(constraints)
                .setInputData(whoIsTheDetector)
                .addTag(CANCEL_ME)
                .build();

        // 매니져에게 일할거라고 등록한다.
        // then 으로 순차적인 workRequest를 실행할수 있다.
        WorkManager.getInstance(this)
                .beginUniqueWork("iamUnique", ExistingWorkPolicy.APPEND_OR_REPLACE, oneTimeWorkRequest)
                .then(logWorkerRequest)
                .enqueue();

        // 결과를
        foundKeyByWorkerGuy(logWorkerRequest.getId());
    }

    private void foundKeyByWorkerGuy(UUID workerUUID) {
        LiveData<WorkInfo> lf = WorkManager.getInstance(this).getWorkInfoByIdLiveData(workerUUID);

        lf.observe(this, workInfo -> {
            if (workInfo.getOutputData().getString(FOUND_OUT_KEY) != null) {
                tv.setText("The Key is  " + workInfo.getOutputData().getString(FOUND_OUT_KEY));
            } else {
                tv.setText("Finding the key");
            }
        });
    }

workManager 는 백그라운드 서비스용도로 사용되기 위한 기능이므로써 쓰레드비슷한 형식으로 자겁해서 UI에 변화를 줘야되는 즉, 화면이 떠있는 상태로 foreground작업에는 쓰레드나 coroutine을 사용하는게 맞다.

0 comments:

댓글 쓰기