Skip to content

Commit 1a46810

Browse files
committed
workmanager post added
1 parent 236d6ea commit 1a46810

File tree

4 files changed

+281
-0
lines changed

4 files changed

+281
-0
lines changed

_drafts/2019-07-15-workmanager.md

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
---
2+
layout: post
3+
title: "WorkManager"
4+
date: 2019-07-15
5+
categories: ["Background"]
6+
image: background/workmanager
7+
github: background/tree/master/workmanager
8+
description: "Background threading"
9+
keywords: "workmanager, workrequest, worker, jobscheduler, jobdispatcher, jobservice, job, jobinfo, constraints, alarmmanager, broadcastreceiver, async, task, background, threading, deferrable, chain, enqueue, schedule, android, programowanie, programming"
10+
---
11+
12+
## Charakterystyka
13+
`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.
14+
15+
## Implementacja
16+
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.
17+
18+
{% highlight kotlin %}
19+
class WorkManagerActivity : AppCompatActivity() {
20+
21+
private lateinit var workId : UUID //store id in case to manage work
22+
23+
override fun onCreate(savedInstanceState: Bundle?) {
24+
super.onCreate(savedInstanceState)
25+
setContentView(R.layout.activity_workmanager)
26+
27+
button.setOnClickListener {
28+
startSimpleWork()
29+
observeWorkStatus()
30+
}
31+
}
32+
33+
private fun startSimpleWork() {
34+
val workRequest = createWorkRequest()
35+
WorkManager.getInstance().enqueue(workRequest)
36+
workId = workRequest.id
37+
}
38+
39+
private fun createWorkRequest() : WorkRequest {
40+
//create input data
41+
val inputData = workDataOf(Pair("INPUT_KEY", "some_input"))
42+
43+
//create constraint conditions
44+
val constraint = Constraints.Builder()
45+
.setRequiredNetworkType(NetworkType.CONNECTED)
46+
.setRequiresBatteryNotLow(true)
47+
.setRequiresStorageNotLow(true)
48+
.setRequiresCharging(true)
49+
.build()
50+
51+
//init WorkRequest using WorkRequest class or some Builder for single or periodic work
52+
val workRequest = OneTimeWorkRequestBuilder<CustomWorker>()
53+
.setInputData(inputData)
54+
.setConstraints(constraint)
55+
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS) //retry work conditions
56+
.setInitialDelay(3, TimeUnit.SECONDS)
57+
.build()
58+
59+
return workRequest
60+
}
61+
62+
private fun observeWorkStatus() {
63+
WorkManager.getInstance().getWorkInfoByIdLiveData(workId)
64+
.observe(this, Observer { workInfo ->
65+
//do some action based on workInfo state
66+
})
67+
}
68+
}
69+
70+
class CustomWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
71+
72+
//do some custom work, e.g. upload or sync something
73+
override fun doWork(): Result {
74+
val input = inputData.getString("INPUT_KEY") //get the input
75+
//do some work
76+
val result = "some_work_result" //retrieve the result
77+
val output = workDataOf(Pair("OUTPUT_KEY", result)) //create the output
78+
79+
//return the success, failure or retry result based on situations
80+
return Result.success(output)
81+
}
82+
}
83+
{% endhighlight %}
84+
85+
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`.
86+
87+
{% highlight kotlin %}
88+
class WorkManagerChainActivity : AppCompatActivity() {
89+
90+
override fun onCreate(savedInstanceState: Bundle?) {
91+
super.onCreate(savedInstanceState)
92+
setContentView(R.layout.activity_workmanager_chain)
93+
94+
button.setOnClickListener {
95+
startChainedWork()
96+
}
97+
}
98+
99+
private fun startChainedWork() {
100+
WorkManager.getInstance()
101+
.beginWith(Arrays.asList(createPartOneWorkRequest(), createPartTwoWorkRequest()))
102+
.then(createFinalWorkRequest())
103+
.enqueue()
104+
}
105+
106+
private fun createPartOneWorkRequest() : OneTimeWorkRequest {
107+
val inputData = workDataOf(Pair("SIZE", "small"))
108+
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
109+
110+
return OneTimeWorkRequestBuilder<PartWorker>()
111+
.setInputData(inputData)
112+
.setConstraints(constraints)
113+
.build()
114+
}
115+
116+
private fun createPartTwoWorkRequest() : OneTimeWorkRequest {
117+
val inputData = workDataOf(Pair("COLOR", "red"))
118+
val constraints = Constraints.Builder().setRequiresStorageNotLow(true).build()
119+
120+
return OneTimeWorkRequestBuilder<PartWorker>()
121+
.setInputData(inputData)
122+
.setConstraints(constraints)
123+
.build()
124+
}
125+
126+
private fun createFinalWorkRequest() : OneTimeWorkRequest {
127+
return OneTimeWorkRequestBuilder<FinalWorker>()
128+
.setInputMerger(ArrayCreatingInputMerger::class.java) //merge all values from every keys
129+
.setConstraints(Constraints.Builder().setRequiresBatteryNotLow(true).build())
130+
.build()
131+
}
132+
}
133+
134+
class PartWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
135+
136+
override fun doWork(): Result {
137+
val color = inputData.getString("COLOR")
138+
val size = inputData.getString("SIZE")
139+
140+
//do some work
141+
142+
if(color != null) {
143+
val output = workDataOf(Pair("RESULT", "colored to $color"))
144+
return Result.success(output)
145+
}
146+
else if(size != null) {
147+
val output = workDataOf(Pair("RESULT", "resize to $size"))
148+
return Result.success(output)
149+
}
150+
else {
151+
return Result.failure()
152+
}
153+
}
154+
}
155+
156+
class FinalWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
157+
158+
override fun doWork(): Result {
159+
//get merged input from pre workers
160+
//it contains all values in this key from every pre Worker
161+
val input = inputData.getString("RESULT") //so it should be: colored to red, resize to small
162+
//do something with input and return result
163+
return Result.success()
164+
}
165+
}
166+
{% endhighlight %}
167+
168+
## JobScheduler
169+
`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`.
170+
171+
{% highlight kotlin %}
172+
class JobScheduler : AppCompatActivity() {
173+
174+
lateinit var jobScheduler: JobScheduler //works min for API 21
175+
lateinit var jobDispatcher: FirebaseJobDispatcher //alernative for lower API
176+
177+
override fun onCreate(savedInstanceState: Bundle?) {
178+
super.onCreate(savedInstanceState)
179+
setContentView(R.layout.activity_jobscheduler)
180+
181+
jobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
182+
jobDispatcher = FirebaseJobDispatcher(GooglePlayDriver(this))
183+
184+
button.setOnClickListener {
185+
//call schedule method from JobScheduler or JobDispatcher
186+
val resultCode = jobScheduler.schedule(createJobInfo())
187+
if(resultCode == JobScheduler.RESULT_SUCCESS) {
188+
//job is scheduled
189+
}
190+
}
191+
192+
//call cancel method from JobScheduler or JobDispatcher to cancel pending Job on some event
193+
}
194+
195+
//customized and schedule this JobInfo to JobScheduler
196+
private fun createJobInfo() : JobInfo {
197+
val componentName = ComponentName(this, CustomJobService::class.java)
198+
return JobInfo.Builder(1, componentName)
199+
.setMinimumLatency(1000)
200+
.setOverrideDeadline(3000)
201+
.setRequiresCharging(true)
202+
.build()
203+
}
204+
205+
//customized and schedule this Job for JobDispatcher
206+
private fun createJob() : Job {
207+
return dispatcher.newJobBuilder()
208+
.setService(CustomJobService::class.java)
209+
.setConstraints(Constraint.DEVICE_CHARGING)
210+
.setTrigger(Trigger.executionWindow(1, 3))
211+
.setTag("TAG")
212+
.build()
213+
}
214+
}
215+
216+
class CustomJobService : JobService() {
217+
218+
//note that JobService is running on main thread like standard Service
219+
private val uiHandler = Handler(Looper.getMainLooper())
220+
private val executor = Executors.newSingleThreadExecutor()
221+
222+
override fun onStartJob(params: JobParameters?): Boolean {
223+
//reached when job starts running
224+
executor.execute {
225+
//do some background job
226+
jobFinished(params, false) //call to inform that job is finished
227+
//post some result on UI if needed
228+
}
229+
return false //is work still in progress?
230+
}
231+
232+
override fun onStopJob(params: JobParameters?): Boolean {
233+
//reached when job has stopped - manual or auto
234+
jobFinished(params, false)
235+
return false //should work be retried?
236+
}
237+
}
238+
{% endhighlight %}
239+
240+
## AlarmManager
241+
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.
242+
243+
{% highlight kotlin %}
244+
class AlarmManagerActivity : AppCompatActivity() {
245+
246+
val alarmManager : AlarmManager by lazy { getSystemService(ALARM_SERVICE) as AlarmManager }
247+
248+
override fun onCreate(savedInstanceState: Bundle?) {
249+
super.onCreate(savedInstanceState)
250+
setContentView(R.layout.activity_alarmmanager)
251+
252+
button.setOnClickListener {
253+
startAlarm()
254+
}
255+
}
256+
257+
private fun startAlarm() {
258+
//prepare Intent and PendingIntent (e.g. send alarm to BroadcastReceiver)
259+
val intent = Intent(this, AlarmBroadcastReceiver::class.java)
260+
val pendingIntent = PendingIntent.getBroadcast(this, 100, intent, 0)
261+
262+
//schedule alarm with specific trigger time and PendingIntent
263+
//use specific method based on OS version in addition
264+
val time = System.currentTimeMillis() + 3000
265+
val type = RTC_WAKEUP
266+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
267+
alarmManager.setExactAndAllowWhileIdle(type, time, pendingIntent)
268+
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
269+
alarmManager.setExact(type, time, pendingIntent)
270+
else
271+
alarmManager.set(type, time, pendingIntent)
272+
}
273+
}
274+
275+
class AlarmBroadcastReceiver : BroadcastReceiver() {
276+
277+
override fun onReceive(context: Context, intent: Intent) {
278+
//do some action when Broadcast receive Intent from AlarmManager
279+
}
280+
}
281+
{% endhighlight %}
497 KB
Loading
17.6 KB
Loading
49.6 KB
Loading

0 commit comments

Comments
 (0)