Parę wskazówek dla profesjonalnych rozwiązań

avatar icon blog author
Mikołaj Kąkol
Mogłoby się wydawać, że dokumentacja Androida jest naprawdę dobra. Większość klas jest dogłębnie opisana, poza tym mamy do dyspozycji zestawy treningowe oraz API Guide, który opisuje wszystkie warte poznania komponenty i koncepty systemu. Niestety według mnie dokumentacja ta ma bardzo niefortunne przykłady użycia owych komponentów.

W zasadzie można by je w większości opisać tak:

public class ExampleActivity extends Activity implement OnComponentCallback {

    @Override
    protected void onCreate() {
        super.onCreate();
        // TU PRZYKŁAD UŻYCIA KOMPONENTU
    }

    @Override
    public void onVeryImportantCallback(VeryData soMuch) {
        // TU PRZYKŁAD JAK WRZUCIĆ TE DANE DO NP. ADAPTERA
    }
}

Programowanie na Androida nie jest łatwe, szczególnie dla osób początkujących. To niestety powoduje, że wiele osób widzi w tych przykładach szybkie rozwiązania problemów, które mają do zrobienia. Często chętnie korzysta się z opcji kopiuj-wklej, dopisując kolejne dziesiątki, a nawet setki linijek kodu do klasy ExampleActivity. Nasza klasa po chwili nasłuchuje na broadcastach, odpowiada za ściąganie danych z API, łączy się z Google API oraz wyświetla punkty na mapie! @1@$#%! Pewnie każdy z nas napisał raz, a może dwa razy takie coś i do dziś źle wspomina ten okres programowania. Pewnie każdy z was widział takie cuda co mają ponad 1000 linii oraz 20 deklaracji zmiennych, aż pięść sama się zaciskała.

Niestety jest to wynik tego, że dokumentacja nie wspomina o architekturze rozwiązań. Ze świecą można tam szukać przykładów, a nawet zachęt do rozbijania swoich klas na mniejsze. A wręcz przeciwnie, co krok można przeczytać, że tworzenie nowych instancji to zło, ponieważ potem te obiekty będą musiały zostać zebrane przez Garbage Collector, a im większa ilość obiektów tym dłużej będzie to robił. Wszędzie znajdują się tylko informacje o tym jak bardzo ważna jest wydajność i jakie sztuczki robić, aby ją uzyskać.

Jednak, czy aby właśnie to mieli na myśli autorzy pisząc dokumentację? Według mnie - nie. Po prostu developerzy zbyt chętnie korzystają z gotowych rozwiązań, nie myśląc o architekturze rozwiązania. Prawdą jest natomiast, że w dokumentacji nie znajdziemy zbyt wiele przykładów, być może dlatego, gdyż nikt w Mountain View nie wymyślił na tyle dobrego rozwiązania, które warte byłoby przedstawienia szerszemu gronu. Wynika to z tego, że programowanie na tej platformie jest wyjątkowo niewdzięczne, gdy musimy obsługiwać obroty ekranu, przechodzenie w tło, ubijanie aplikacji przez system. To powoduje, że nagle sample code przestaje działać.

W tym wpisie nie wskażę, jak wygląda idealne rozwiązanie, gdyż takie nie istnieje. Jednak chciałbym podać parę wskazówek, które pomogą wam pisać lepsze aplikacje.

Nie używaj klasy AsyncTask

Nie używaj klasy AsyncTask, a przynajmniej nie używaj ich wewnątrz klas obsługujących UI. AsyncTaski miały być tym sposobem, który powoduje, że w łatwy sposób możemy wykonać jakieś operacje w tle, a zarazem powiadamiać UI o progresie zadania oraz jego zakończeniu. Tak naprawdę wbrew mojej bardzo negatywnej opinii jest to super klasa. Problem leży w developerach (oraz dokumentacji), którzy korzystają z nich wewnątrz np. Activity.

W dokumentacji tej klasy znajdziemy taki przykład kodu:

private class DownloadFilesTask extends AsyncTask {
    protected Long doInBackground(URL... urls) {
        int count = urls.length;
        long totalSize = 0;
        for (int i = 0; i < count; i++) {
            totalSize += Downloader.downloadFile(urls[i]);
            publishProgress((int) ((i / (float) count) * 100));
            // Escape early if cancel() is called
            if (isCancelled()) break;
        }
        return totalSize;
    }

    protected void onProgressUpdate(Integer... progress) {
        setProgressPercent(progress[0]);
    }

    protected void onPostExecute(Long result) {
        showDialog("Downloaded " + result + " bytes");
    }
}

Ten przykład wręcz krzyczy, że jak potrzebujesz czegoś ściągnąć wykonaj mnie w Activity, a wtedy w łatwy sposób obsłużysz progres oraz zakończenie ściąganie. Niestety, wystarczymy, że obrócimy ekran, a żadnego progresu już nie zobaczymy. Poprawnym zaś rozwiązaniem z użyciem AsyncTask do pobrania danych opierać powinna się o dodatkowe dwie klasy. Jedna z nich powinna odpowiadać za trzymanie stanu, co wysyłamy i jaki jest progres każdego z elementów, a druga powinna przy użyciu obserwatora pozwalać nasłuchiwać na zmiany. W ten sposób napiszemy kod, który będzie można łatwo przetestować, ponieważ jest on niezależny od UI.

Nie używaj Loaderów

Problemów z nimi jest co nie miara, a powodów by je używać nie ma żadnych. Klasy te według mnie nie rozwiązują żadnych problemów, w rzeczywistych aplikacjach.

Odsuń logikę biznesową od UI

Android jako framework jest bardzo nieprzyjemny w testowaniu. Dlatego trzeba starać się jak najmniej pracować ściśle z nim. W idealnym przypadku powinniśmy móc napisać całą logikę biznesową, bez ani jednej linijki zaczynającej się od:

import android.xxxx;

Tak napisany kod będzie można bardzo łatwo i szybko(!) testować. A czym mniej kodu w naszych Activty, Fragment, View, tym mniej błędów w nich. Temat jest rozległy, mam nadzieję, że będę miał jeszcze okazję o tym więcej napisać. Dla dociekliwych parę słów kluczowych, dzięki którym można znaleźć wiele ciekawych wpisów: MVVM, MVP, #qualitymatters.

Nie twórz obiektów, wymagaj ich

Gdy już będziesz przesuwać logikę biznesową to unikaj sytuacji, w których samemu tworzysz klasy. Jeżeli piszesz klasę odpowiadającą za komunikację z API, nie twórz w niej klienta HTTP, wymagaj aby został on Tobie podany. Inversion of Control (IoC) nazywane też Dependency Injection (DI), to wzorzec, które mówi, aby zależności były nam dostarczane. Najprostszym rozwiązaniem jest podanie ich w konstruktorze, zaś według mnie najlepszym to skorzystanie z biblioteki dagger.

Nie wymyślaj koła

Wiele poważnych problemów w Androidzie została rozwiązana całkiem fachowo. Chociażby pobieranie obrazków, ich cachowanie, obsługa REST API - do tego wszystkiego są gotowe biblioteki, z których warto korzystać.

Wzorzec Signelton jest fe

Singletony mają to do siebie, że powodują błędy w testach, a skoro powodują błędy tam, to pewnie też spowodują wiele błędów w aplikacji. Gdy uruchamiamy pojedynczo testy wszystko działa, gdy uruchamiamy wszystkie jednocześnie coś się psuje. Jest tak zwykle, gdy jakiś test zmienił stan obiektu, a inny test się tego nie spodziewał.

Według mnie w Androidzie przy zastosowaniu odpowiednio Dependency Injection w naszej całej aplikacji powinien istnieć tylko jeden Singleton inicjowany najpewniej w klasie dziedziczącej po Application. To miejsce powinno być początkiem dostępu do wszystkich innych klas.

Nawet wówczas ta właściwość nie powinno być typu final, lepiej w takim wypadku będzie jeżeli pozwolimy sobie je inicjować wielokrotnie. Dzięki takiemu podejściu napisanie wylogowania polega na usunięciu tej właściwości i stworzeniu nowej. Nie będziemy musimy się martwić, że przez przypadek gdzieś w cache'u siedzą dane innej osoby, albo co gorsza zwrócimy je nieuprawnionej osobie. Tak samo rozwiążemy problem czyszczenia dowolnego zasobu, który jest swego rodzaju Singletonem. Pozwoli to również zachować hermetyczność testów.

O architekturze w Androidzie można by napisać książkę, a nawet wtedy tematu byśmy nie wyczerpali. Prawdopodobnie też byłaby to słaba książka. Mam nadzieję, że te parę wskazówek wam się przyda. Może następnym razem opiszę rozwiązania stosowane przez nas nieco dokładniej. Dajcie znać w komentarzach co myślicie!