WorkManager

Background

  |   12 min czytania

Charakterystyka

WorkManager dostarcza prosty interfejs umożliwiający zarządzanie zadaniami wykonywanymi w tle, których realizacja może być odroczona w czasie i ograniczona wymaganiami stanu systemu. Gwarantuje ukończenie pracy niezależnie od cyklu życia aplikacji czy systemu dzięki lokalnej bazie danych, która zarządza informacjami i statusami zadań z kolejki. Automatycznie wykrywa spełnienie warunków wstępnych stanu systemu dokonując rozpoczęcia, wstrzymania lub wznowienia zadania co pozwala na optymalne zarządzanie użycia baterii czy transmisji danych. Realizuje prace w tle stosując najlepsze praktyki oraz zachowuje zgodność z ograniczeniami dla różnych wersji systemu. Ponadto umożliwia zaplanowanie nie tylko zadań jednorazowanych, ale także cyklicznych oraz złożonych łańcuchów zadań (sekwencyjncyh i asynchronicznych). WorkManager nie jest całkowicie nową implementacją lecz wykorzystuje funckjonalność samodzielnych mechanizmów takich jak m.in. JobScheduler, JobDispatcher, AlarmManager czy Executor, których użycie zależne jest od wersji systemu, stanu cyklu życia aplikacji i czasu wykonania.

Implementacja

Zadania są kolejkowane na instancji WorkManager poprzez przekazanie do metody enqueue obiektu typu WorkRequest odpowiedzialnego za stworzenie kompletnego zadania. W skład WorkRequest wchodzą m.in. zadanie (Worker), ograniczenia (Constraints), dane wejściowe czy kryteria wznowienia, powtarzalności i opóznienia pracy. Klasa Worker w metodzie doWork definiuje wykonywaną pracę oraz zwraca rezultat zadania.

class WorkManagerActivity : AppCompatActivity() {

    private lateinit var workId : UUID //store id in case to manage work

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_workmanager)

        button.setOnClickListener {
            startSimpleWork()
            observeWorkStatus()
        }
    }

    private fun startSimpleWork() {
        val workRequest = createWorkRequest()
        WorkManager.getInstance().enqueue(workRequest)
        workId = workRequest.id
    }

    private fun createWorkRequest() : WorkRequest {
        //create input data
        val inputData = workDataOf(Pair("INPUT_KEY", "some_input"))

        //create constraint conditions
        val constraint = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .setRequiresBatteryNotLow(true)
            .setRequiresStorageNotLow(true)
            .setRequiresCharging(true)
            .build()

        //init WorkRequest using WorkRequest class or some Builder for single or periodic work
        val workRequest = OneTimeWorkRequestBuilder<CustomWorker>()
            .setInputData(inputData)
            .setConstraints(constraint)
            .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS) //retry work conditions
            .setInitialDelay(3, TimeUnit.SECONDS)
            .build()

        return workRequest
    }
	
	private fun observeWorkStatus() {
        WorkManager.getInstance().getWorkInfoByIdLiveData(workId)
            .observe(this, Observer { workInfo ->
                //do some action based on workInfo state
        })
    }
}

class CustomWorker(context: Context, params: WorkerParameters) : Worker(context, params) {

    //do some custom work, e.g. upload or sync something
    override fun doWork(): Result {
        val input = inputData.getString("INPUT_KEY") //get the input
        //do some work
        val result = "some_work_result" //retrieve the result
        val output = workDataOf(Pair("OUTPUT_KEY", result)) //create the output

        //return the success, failure or retry result based on situations
        return Result.success(output)
    }
}

Jeśli zadanie składa się z mniejszych procesów, które powinny zostać wykonane sekwencyjne lub asynchronicznie, a rezultat kolejnego zależy od poprzedniego wówczas należy wykorzystać mechanizm łańcucha zadań. Metoda budowniczego beginWith pozwala na asynchroniczne wywolanie grupy zadań początkowych natomiast then na sekwencyjne wywołanie kolejnych kroków. Łączenie argumentów wejściowych z grupy zadań poprzedzających możliwe jest dzięki InputMerger.

class WorkManagerChainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_workmanager_chain)

        button.setOnClickListener {
            startChainedWork()
        }
    }

    private fun startChainedWork() {
        WorkManager.getInstance()
            .beginWith(Arrays.asList(createPartOneWorkRequest(), createPartTwoWorkRequest()))
            .then(createFinalWorkRequest())
            .enqueue()
    }

    private fun createPartOneWorkRequest() : OneTimeWorkRequest {
        val inputData = workDataOf(Pair("SIZE", "small"))
        val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()

        return OneTimeWorkRequestBuilder<PartWorker>()
            .setInputData(inputData)
            .setConstraints(constraints)
            .build()
    }

    private fun createPartTwoWorkRequest() : OneTimeWorkRequest {
        val inputData = workDataOf(Pair("COLOR", "red"))
        val constraints = Constraints.Builder().setRequiresStorageNotLow(true).build()

        return OneTimeWorkRequestBuilder<PartWorker>()
            .setInputData(inputData)
            .setConstraints(constraints)
            .build()
    }

    private fun createFinalWorkRequest() : OneTimeWorkRequest {
        return OneTimeWorkRequestBuilder<FinalWorker>()
            .setInputMerger(ArrayCreatingInputMerger::class.java) //merge all values from every keys
            .setConstraints(Constraints.Builder().setRequiresBatteryNotLow(true).build())
            .build()
    }
}

class PartWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    
    override fun doWork(): Result {
        val color = inputData.getString("COLOR")
        val size = inputData.getString("SIZE")
        
        //do some work
        
        if(color != null) {
            val output = workDataOf(Pair("RESULT", "colored to $color"))
            return Result.success(output)
        }
        else if(size != null) {
            val output = workDataOf(Pair("RESULT", "resize to $size"))
            return Result.success(output)
        }
        else {
            return Result.failure()
        }
    }
}

class FinalWorker(context: Context, params: WorkerParameters) : Worker(context, params) {

    override fun doWork(): Result {
        //get merged input from pre workers
        //it contains all values in this key from every pre Worker
        val input = inputData.getString("RESULT") //so it should be: colored to red, resize to small
        //do something with input and return result
        return Result.success()
    }
}

JobScheduler

JobScheduler pozwala na planowanie zadań w tle gwarantując ich ukończenie w nieokreślonej przyszłości. Wykonanie zadania jest uzależnione od warunków stanu systemu. Może się zdarzyć, że rozpoczęcie pracy będzie natychmiastowe lub pozostanie w oczekiwaniu na korzystny stan systemu umożliwiający podjęcie działania zgodnie z wymaganiami wstępnymi. Optymalizuje użycie baterii i pamięci w stosunku do innych kosztownych rozwiązań realizacji pracy w tle takich jak np. Service, AlarmManager czy BroadcastReceiver. Usługa JobScheduler jest jednak dostępna od wersji systemu Android L. Alernatywnym rozwiązaniem może być wykorzystanie FirebaseJobDispatcher (działa także poniżej Android L). Obie usługi działają w oparciu o JobService, którego zadaniem jest obsługa zdarzeń rozpoczęcia i zatrzymania pracy. Warunki wstępne deklarowane są w obiektach JobInfo lub Job, a zaplanowanie zadania odbywa się przy pomocy metody schedule.

class JobSchedulerActivity : AppCompatActivity() {

    lateinit var jobScheduler: JobScheduler //works min for API 21
    lateinit var jobDispatcher: FirebaseJobDispatcher //alernative for lower API

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_jobscheduler)

        jobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
        jobDispatcher = FirebaseJobDispatcher(GooglePlayDriver(this))

        button.setOnClickListener {
            //call schedule method from JobScheduler or JobDispatcher
            val resultCode = jobScheduler.schedule(createJobInfo())
            if(resultCode == JobScheduler.RESULT_SUCCESS) {
                //job is scheduled
            }
        }
		
        //call cancel method from JobScheduler or JobDispatcher to cancel pending Job on some event
    }

    //customized and schedule this JobInfo to JobScheduler
    private fun createJobInfo() : JobInfo {
        val componentName = ComponentName(this, CustomJobService::class.java)
        return JobInfo.Builder(1, componentName)
            .setMinimumLatency(1000)
            .setOverrideDeadline(3000)
            .setRequiresCharging(true)
            .build()
    }
	
	//customized and schedule this Job for JobDispatcher
	private fun createJob() : Job {
        return dispatcher.newJobBuilder()
            .setService(CustomJobService::class.java)
            .setConstraints(Constraint.DEVICE_CHARGING)
            .setTrigger(Trigger.executionWindow(1, 3))
            .setTag("TAG")
            .build()
    }
}

class CustomJobService : JobService() {

    //note that JobService is running on main thread like standard Service
    private val uiHandler = Handler(Looper.getMainLooper())
    private val executor = Executors.newSingleThreadExecutor()

    override fun onStartJob(params: JobParameters?): Boolean {
        //reached when job starts running
        executor.execute {
            //do some background job
            jobFinished(params, false) //call to inform that job is finished
            //post some result on UI if needed
        }
        return false //is work still in progress?
    }

    override fun onStopJob(params: JobParameters?): Boolean {
        //reached when job has stopped - manual or auto
        jobFinished(params, false)
        return false //should work be retried?
    }
}

AlarmManager

Zadaniem AlarmManager jest zapewnienie dostępu do usług alarmowych systemu, dzięki czemu możliwe jest zaplanowanie uruchomienia zadania w wybranym momencie w przyszłości. Gdy alarm się włącza wówczas przechwytywana jest zarejestrowana intencja (PendintIntent) i delegowana do miejsca docelowego, gdzie jest wykonywana (np. przez BroadcastReceiver). Emisja alarmu może być jednorazowa lub cykliczna i działa niezależnie od cyklu życia aplikacji.

class AlarmManagerActivity : AppCompatActivity() {

    val alarmManager : AlarmManager by lazy { getSystemService(ALARM_SERVICE) as AlarmManager }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_alarmmanager)

        button.setOnClickListener {
            startAlarm()
        }
    }

    private fun startAlarm() {
        //prepare Intent and PendingIntent (e.g. send alarm to BroadcastReceiver)
        val intent = Intent(this, AlarmBroadcastReceiver::class.java)
        val pendingIntent = PendingIntent.getBroadcast(this, 100, intent, 0)

        //schedule alarm with specific trigger time and PendingIntent
        //use specific method based on OS version in addition 
        val time = System.currentTimeMillis() + 3000
        val type = RTC_WAKEUP
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
            alarmManager.setExactAndAllowWhileIdle(type, time, pendingIntent)
        else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
            alarmManager.setExact(type, time, pendingIntent)
        else
            alarmManager.set(type, time, pendingIntent)
    }
}

class AlarmBroadcastReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        //do some action when Broadcast receive Intent from AlarmManager
    }
}