Android Architecture Components: WorkManager

Jest to cykl artykułów poświęcony komponentom architektury Androida. Wszystkie artykuły znajdziesz na tej stronie.

Wykonywanie zadań w tle jest częstą praktyką wśród aplikacji na system Android. Najczęściej jest to synchronizacja z serwerem w celu wysłania lub odebrania danych, na przykład pobranie nowych artykułów. Częstym błędem wśród początkujących programistów jest wykonywanie długotrwałych operacji na wątku głównym aplikacji. Takie zadania powinno się wykonywać w osobnym wątku. Od początku Androida programiście musieli borykać się z problemem w jaki sposób wykonać daną rzecz w jak najlepszy sposób. Na szczęście Google przedstawiło potężne narzędzie do tego typu zadań. Czym jest <strong]]>WorkManager i w jaki sposób wykonywać proste zadania w tle? W tym artykule właśnie tego się dowiesz.</strong]]>

Praca w tle

Praca w tle to dowolne zadanie związane z aplikacją, które nie jest wykonywane w głównym wątku aplikacji UI. Operacje te mogą być długotrwałe oraz wykorzystujące sporo zasobów systemowych. Aby wydłużyć czas pracy baterii i zapewnić lepszą obsługę powstały pewne ograniczenia. Od wersji Marshmallow zespół Androida zaczął bardziej koncentrować się na optymalizacji baterii. Zespół ustalić limity w tle. Limity te obejmują:

Sporo tego? Prawda. Do tej pory, aby wykonywać prace w tle, programiści nie tylko musieli wiedzieć o tych optymalizacjach baterii, ale także musieli wybierać między wieloma sposobami wdrożenia pracy. Podejmowanie decyzji, które narzędzia użyć do wykonywania pracy w tle, wymaga od programisty jasnego zrozumienia, co chce osiągnąć i pod jakimi ograniczeniami.

Android Architecture Components: WorkManager

WorkManager jest zalecanym rozwiązaniem do wykonywania zadań w tle, biorąc pod uwagę wszystkie limity narzucone przez system operacyjny. Jeśli chcesz zagwarantować, że zadanie zostanie uruchomione, nawet jeśli jest odłożone, powinieneś użyć WorkManager. Ten interfejs API umożliwia planowanie zadań jednorazowych lub powtarzających się. Oprócz tego możesz zastosować ograniczenia wykonania. Na przykład, gdy urządzenie jest bezczynne lub ładuje się. WorkManager należy do kategorii gwarantowanego wykonania. To znaczy, że respektuje funkcje zarządzania energią, więc jeśli zadanie ma zostać uruchomione o określonej godzinie, a urządzenie jest w tym czasie w stanie Doze to WorkManager spróbuje uruchomić to zadanie po wyłaczeniu trybu Doze. Tryb Doze Jeśli chcesz uruchomić zadanie w dokładnie określonym czasie, który wyzwala akcje lub wymaga interakcji z użytkownikiem i nie może być odroczone, użyj AlarmManagera. Kiedy warto korzystać z narzędzia od Google? Oto kilka przykładów użycia:

  • Synchronizowanie danych z serwerem.
  • Zapisywanie danych w bazie danych.
  • Wysyłanie logów na serwer.
  • Przesyłanie plików na serwer.
  • Wykonywanie kosztownych operacji na danych (obróbka zdjęć, filmów).

Skoro już wiemy czym jest biblioteka WorkManager rozłóżmy ją teraz na częśći pierwsze.

Uzupełnij wiedzę o: Tryby uruchamiania aktywności w Androidzie

Omówienie klas WorkManagera

Głównymi klasami, które zawiera biblioteka WorkManager są:

  • WorkManager — jest to główna klasa, której używasz do kolejkowania żądań WorkRequests.
  • Worker — określa jakie zadanie należy wykonać.
  • WorkRequest — klasa podstawowa do określania parametrów pracy, które powinny być zakolejkowane. Istnieją dwa główne typy żądań pracy:. OneTimeWorkRequest — wykonuje zadanie tylko jeden raz. PeriodicWorkRequest — wykonuje zadanie okresowo.
  • WorkInfo — jeżeli potrzebujesz znać status zadania, zapytaj o to WorkManagera. Zwróci Ci obiekt LiveData zawierający jeden lub więcej obiektów WorkInfo.

Kodujemy!

Wiemy już czym jest WorkManager oraz z czego się składa. Teraz przyjrzymy się przykładowi, aby jeszcze bardziej zrozumieć to narzędzie. Poniżej znajduje się przykład, w którym wykonujemy zdanie w tle, a dokładniej wstrzymujemy pracę programu na 3 sekundy, a następnie wyświetlamy powiadomienie.

public class SyncWorker extends Worker {
    private static final String LOG_TAG = "TAG";
    public SyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }
    @NonNull
    @Override
    public Result doWork() {
        try {
            Thread.sleep ( 3000 );
            showNotification();
            Log.d (LOG_TAG, "SUCCESS!" );
            return Result.success();
        } catch (InterruptedException e) {
            Log.e (LOG_TAG, "FAILURE: " + e);
            return Result.failure();
        }
    }
    private void showNotification() {
        NotificationManager notificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel("default", "Default", NotificationManager.IMPORTANCE_DEFAULT);
            notificationManager.createNotificationChannel(channel);
        }
        NotificationCompat.Builder notification = new NotificationCompat.Builder(getApplicationContext(), "default")
                .setContentTitle("Sync Articles")
                .setContentText("Update finished successfully!")
                .setSmallIcon(R.mipmap.ic_launcher);
        notificationManager.notify(1, notification.build());
    }
}
public class WorkManagerBasicActivity extends AppCompatActivity {
    private static final String LOG_TAG = "TAG";
    private TextView text;
    private Button run;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.workmanager_basic_activity);
        init();
        run.setOnClickListener(view -> runTaskOnce() );
    }
    private void runTaskOnce() {
        OneTimeWorkRequest simpleRequest = new OneTimeWorkRequest.Builder(SyncWorker.class).build();
        WorkManager.getInstance().enqueue(simpleRequest);
        Log.d (LOG_TAG, "runTaskOnce" );
    }
    private void init() {
        run = findViewById(R.id.run);
        text = findViewById(R.id.text);
    }
}

Pamiętaj, aby do pliku gradle dodać wpis:

implementation "android.arch.work:work-runtime:1.0.0-alpha04"

Klasę SyncWorker rozszerzamy o Worker i zastępuje metodę doWork(). W tej metodzie właśnie umieszczamy zadanie, które chcemy wykonać w tle. Metoda zwraca typ, który określa wynik pracy. Istnieją 3 rodzaje wyników, które możesz zwrócić:

  • Result.SUCCESS — praca zakończona pomyślnie.
  • Result.RETRY — praca nie powiodła się i WorkManager powinien spróbować ponownie.
  • Result.FAILURE — praca nie powiodła się i nie ma potrzeby ponownej próby.

W klasie aktywności — WorkManagerBasicActivity mamy przycisk. Po naciśnięciu tego przycisku jest tworzony nowy WorkRequest. Jak już wspomniałem mamy do wyboru dwa typy obiektu WorkRequest, które można utworzyć, OneTimeWorkRequest i PeriodicWorkRequest. Tutaj został przedstawiony sposób stworzenia instancji obiektu OneTimeWorkRequest, używając klasy konstruktora. Następnie musimy przekazać ten obiekt do WorkManager za pomocą metody enqueue(). Za każdym razem kiedy wciśniemy przycisk zadanie zostanie wykonane.

Sprawdzanie statusu pracy

WorkManager umożliwia sprawdzanie statusu pracy. Zwraca obiekt LiveData, Jeśli nie znasz LiveData przeczytaj ten wpis. Jak to zrobić? Nic trudnego. Musimy odwołać się do WorkManagera i pobrać stan naszego zadania. Po uzyskaniu identyfikatora można pobrać informacje z WorkInfo. Kod jest dość prosty i intuicyjny:

private void runTaskOnce() {
        text.setText(null);
        OneTimeWorkRequest simpleRequest = new OneTimeWorkRequest.Builder(SyncWorker.class).build();
        WorkManager.getInstance().enqueue(simpleRequest);
        WorkManager.getInstance().getWorkInfoByIdLiveData(simpleRequest.getId()).observe(this, new Observer<WorkInfo>() {
            @Override
            public void onChanged(WorkInfo workInfo) {
                if (workInfo != null) {
                    text.append("SimpleWorkRequest: " + workInfo.getState().name() + "\n");
                }
            }
        });
        Log.d (LOG_TAG, "runTaskOnce" );
    }

WorkManager statusKomunikacja z Workerem

Na temat komunikacji między aktywnością a serwisem pisałem obszernie. Teraz zobaczmy, jak możesz wysyłać dane do i z SyncWorker. Na początku zobacz jak przesyłać dane do SyncWorker.

public class SyncWorker extends Worker {
    public static final String EXTRA_TITLE = "EXTRA_TITLE";
    public static final String EXTRA_TEXT = "EXTRA_TEXT";
    public static final String EXTRA_OUTPUT_MESSAGE = "output_message";
    private static final String LOG_TAG = "TAG";
    public SyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }
    @NonNull
    @Override
    public Result doWork() {
        String title = getInputData().getString(EXTRA_TITLE );
        String text = getInputData().getString(EXTRA_TEXT);
        try {
            // do something...
            Thread.sleep ( 3000 );
            showNotification(title, text);
            Log.d (LOG_TAG, "SUCCESS!" );
            return Result.success();
        } catch (InterruptedException e) {
            Log.e (LOG_TAG, "FAILURE: " + e);
            return Result.failure();
        }
    }
    private void showNotification(String title, String text) {
        NotificationManager notificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel("default", "Default", NotificationManager.IMPORTANCE_DEFAULT);
            notificationManager.createNotificationChannel(channel);
        }
        NotificationCompat.Builder notification = new NotificationCompat.Builder(getApplicationContext(), "default")
                .setContentTitle(title)
                .setContentText(text)
                .setSmallIcon(R.mipmap.ic_launcher);
        notificationManager.notify(1, notification.build());
    }
}
public class WorkManagerBasicActivity extends AppCompatActivity {
    private static final String LOG_TAG = "TAG";
    private TextView text;
    private Button run;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.workmanager_basic_activity);
        init();
        run.setOnClickListener(view -> runTaskOnce() );
    }
    private void runTaskOnce() {
        text.setText(null);
        Data data = new Data.Builder()
                .putString(SyncWorker.EXTRA_TITLE, "Sync Articles")
                .putString(SyncWorker.EXTRA_TEXT, "Update finished successfully!")
                .build();
        OneTimeWorkRequest simpleRequest = new OneTimeWorkRequest.Builder(SyncWorker.class)
                .setInputData(data)
                .build();
        WorkManager.getInstance().enqueue(simpleRequest);
        WorkManager.getInstance().getWorkInfoByIdLiveData(simpleRequest.getId()).observe(this, new Observer<WorkInfo>() {
            @Override
            public void onChanged(WorkInfo workInfo) {
                if (workInfo != null) {
                    text.append("SimpleWorkRequest: " + workInfo.getState().name() + "\n");
                }
            }
        });
        Log.d (LOG_TAG, "runTaskOnce" );
    }
    private void init() {
        run = findViewById(R.id.run);
        text = findViewById(R.id.text);
    }
}

W klasie SyncWorker definiujemy 3 stałe łańcuchowe (przydadzą się nam tez póżniej). Dane odbieramy za pomocą metod getInputData().getString(). W aktywności tworzymy obiekt Data i w nim umieszczamy dane. Następnie te informacje dołączamy do OneTimeWorkRequestOneTimeWorkRequest.Builder udostępnia metodę o nazwie setInputData, która przyjmuje obiekt Data. A jak w drugą stronę? Proces jest prawie taki sam. W klasie SyncWorker trzeba stworzyć instancję danych i dodać ją do wyniku zwracanego przez metodę doWork(). Nasza metoda doWork() będzie wyglądać tak:

@NonNull
   @Override
   public Result doWork() {
       String title = getInputData().getString(EXTRA_TITLE );
       String text = getInputData().getString(EXTRA_TEXT);
       Data data = new Data.Builder()
               .putString(EXTRA_OUTPUT_MESSAGE, "Message from SyncWorker!")
               .build();
       try {
           // do something...
           Thread.sleep ( 3000 );
           showNotification(title, text);
           Log.d (LOG_TAG, "SUCCESS!" );
           return Result.success(data);
       } catch (InterruptedException e) {
           Log.e (LOG_TAG, "FAILURE: " + e);
           return Result.failure(data);
       }
   }

a w aktywności, metoda runTaskOnce() wygląda tak::

private void runTaskOnce() {
        text.setText(null);
        Data data = new Data.Builder()
                .putString(SyncWorker.EXTRA_TITLE, "Sync Articles")
                .putString(SyncWorker.EXTRA_TEXT, "Update finished successfully!")
                .build();
        OneTimeWorkRequest simpleRequest = new OneTimeWorkRequest.Builder(SyncWorker.class).setInputData(data).build();
        WorkManager.getInstance().enqueue(simpleRequest);
        WorkManager.getInstance().getWorkInfoByIdLiveData(simpleRequest.getId()).observe(this, new Observer<WorkInfo>() {
            @Override
            public void onChanged(WorkInfo workInfo) {
                if (workInfo != null) {
                    text.append("SimpleWorkRequest: " + workInfo.getState().name() + "\n");
                }
                if (workInfo != null && workInfo.getState().isFinished()) {
                    String message = workInfo.getOutputData().getString(SyncWorker.EXTRA_OUTPUT_MESSAGE);
                    text.append("SimpleWorkRequest (Data): " + message);
                }
            }
        });
        Log.d (LOG_TAG, "runTaskOnce" );
    }

Warunki pracy

Możesz dodać wiele warunków do swojego zadania, np. Wykonywać tylko pracę, gdy urządzenie ładuje się i jest podłączone do sieci WI-FI. Wystarczy użyć funkcji setContrainsts. Wcześniej za pomocą Constraints musisz zbudować swoje warunki.

Constraints constraints = new Constraints.Builder()
          .setRequiresCharging(true)
          .setRequiredNetworkType(NetworkType.UNMETERED)
          .build();
OneTimeWorkRequest simpleRequest = new OneTimeWorkRequest.Builder(SyncWorker.class)
          .setInputData(data)
          .setConstraints(constraints)
          .build();

Jeżeli podłączysz telefon do ładowarki i do internetu to powiadomienie pokaże się. Odłączając urządzenie od ładowania nie pojawi się powiadomienie. Możesz sprawdzić status pracy – jest tylko kolejkowa. Po podłączeniu kabla nie otrzymasz natychmiast powiadomienia. WorkManager uruchamia żądania WorkRequest między określonymi oknami czasowymi. Zatem Twoje zadanie może nie zostać uruchomione natychmiast po spełnieniu warunków, ale ostatecznie zostanie uruchomione. Nie martw się! WorkManager waitUzupełnij wiedze o: Ikony FontAwesome w Androidzie

Anulowanie pracy

Przychodzi czas, że też musisz anulować zadania, które zostały dodane do kolejki lub są wykonywane. Na to też pozwala WorkManager. Aby anulować zadanie musisz najpierw pobrać id zdania, a następnie wywołać WorkManagera i anulować zadanie.

UUID workId = simpleRequest.getId();
...
WorkManager.getInstance().cancelWorkById(workId);

WorkManager postara się, aby anulować zadanie. Czasem jest to niemożliwe — zadanie może już być uruchomione lub zakończone, gdy spróbujesz je zniszczyć. WorkManager zapewnia również metody anulowania wszystkich zadań w unikalnej sekwencji pracy lub z określonym znacznikiem.

Tagowanie pracy

Do każdego zgłoszonego zlecenia możesz przypisać tag. Załóżmy, że chcesz anulować wszystkie zadania, które ładują obrazy wybrane przez użytkownika, po prostu nadaj wszystkim ten sam znacznik i wywołaj cancelWorkByTag(). Aby nadać tag musisz skorzystać z metody addTag()

OneTimeWorkRequest simpleRequest = new OneTimeWorkRequest.Builder(SyncWorker.class)
           .setInputData(data)
           .setConstraints(constraints)
           .addTag("image")
           .build();
...
WorkManager.getInstance().cancelAllWorkByTag("image");

Cykliczna praca

Jeśli chcesz zaplanować pracę uruchamianą okresowo, użyj PeriodicWorkRequestPeriodicWorkRequest zapewnia dokładnie te same metody, które zapewnia OneTimeWorkRequest, wymaga tylko 2 parametrów w swoim konstruktorze. Oba określają czas wewnętrzny, w którym żądanie powinno być uruchamiane. Pierwszy to wartość rzeczywista, a drugi to wartość TimeUnit.

PeriodicWorkRequest periodicWorkRequest = new PeriodicWorkRequest
        .Builder(SyncWorker.class, 1, TimeUnit.HOURS)
        .build();
WorkManager.getInstance().enqueue(periodicWorkRequest);

Powyższy przykład przestawia, że dane zadanie będzie wykonywane co 1 godzinę. Oczywiście to nie koniec możliwości WorkManagera 🙂

Co dalej?

  • Polub stronę MYENV na Facebooku oraz śledź mnie na Twitterze
  • Zachęcam do komentowania i pisania propozycji tematów, o których chcesz przeczytać
  • Poleć ten wpis za pomocą poniższych przycisków. Będę Ci za to bardzo wdzięczny 🙂
  • Życzę Ci miłego dnia i miłego kodowania 🙂