Android i redukcja objętości kodu

“ Boilerplate” to kod, który nie wnosi do programu niczego użytecznego, a którego obecność jest wymagana do jego działania. Przez niego kod aplikacji puchnie, co potrafi utrudnić lub spowolnić pracę nad nim. Aplikacje na system Android pisane są najczęściej w języku Java, co samo w sobie cechuje się dużą objętością kodu. Jednak ostatnimi czasy zdążyły pojawić się rozwiązania na pozbycie się jego zbędnego nadmiaru. Kilka z nich zamierzam opisać w tym artykule.

Java 8

Java 8 ma znaczący wpływ na redukcję kodu, ponieważ wprowadza wyrażenia lambda i referencje metod. Wyrażenia lambda zastępują tworzenie instancji interfejsu funkcyjnego, czyli takiego z jedną metodą, skracając jednocześnie kod o nic nie wnoszącą treść. Ponadto, referencje metod pozwalają skrócić ten kod nawet bardziej poprzez proste odniesienie się do konkretnej, istniejącej metody.

// standardowy kod
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onUserClick();
}
});
// kod z wykorzystaniem lambda
view.setOnClickListener(v -> onUserClick());
// kod z wykorzystaniem referencji do metody
view.setOnClickListener(this::onUserClick);

Pomimo tego, że Java 8 jest z nami już od 2014 roku, nadal są problemy z jej oficjalnym wsparciem dla systemu Android. Do niedawna Java 8 była wspierana tylko od API 24 (7.0 Nougat) wzwyż przy wykorzystaniu eksperymentalnego toolchaina o nazwie Jack. Na szczęście powstały już rozwiązania tego problemu.

Retrolambda

Popularna biblioteka, która backportuje wspomniane funkcjonalności języka Java 8 do starszych wersji poprzez wykorzystanie anonimowych klas w procesie kompilacji. Umożliwia korzystanie z takich cech języka jak wyrażenia lambda, referencje metod i domyślne metody interfejsu.

Default toolchain

Rozwiązanie podobne do tego z Retrolambda, zostało również oficjalnie wprowadzone przez Google, które wbudowało tę funkcjonalność w domyślny toolchain. Stało się to niestety dość niedawno, bo wraz z wersją 3.0 Android Studio. Wykorzystanie części cech języka Java 8 staje się możliwe po dodaniu poniższego kodu do pliku build.gradle modułu aplikacji:

android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

Należy się jeszcze upewnić, że Android Studio i gradle w głównym pliku build.gradle są w wersji przynajmniej 3.0.

Podmiana istniejącego kodu

Aby szybko wyszukać kod, który można zamienić na wyrażenia lambda wystarczy z poziomu Android Studio kliknąć “Analyze” > “Inspect Code”, a w oknie wynikowym pod kategorią Java 8 zaznaczyć wynik odpowiadający lambdzie, po czym kliknąć “Replace with lambda”.

Extras

Dodatkowo po przejściu na Java 8 można wykorzystać statyczne i domyślne metody interfejsu, które również mogą zaoszczędzić trochę kodu, szczególnie w przypadkach, gdzie chcemy dodać metodę do interfejsu, który jest implementowany w wielu miejscach naszej aplikacji.

public interface Apple {

public default int getColor() {
return Color.RED;
}

}

Część funkcjonalności Java 8 takich jak Stream API nadal nie będzie dostępna w najbliższym czasie, ale o dość rozbudowanej alternatywie Stream API — RxJava napiszę w kolejnym artykule.

Kotlin

Alternatywa dla tych, których nie przekonuje Java 8. Kod napisany w Kotlinie często jest krótszy od analogicznego napisanego w języku Java. Od Android Studio 3.0, Kotlin jest oficjalnie wspieranym językiem w tym IDE, a dzięki wbudowanym narzędziom, klasy można konwertować swobodnie między obydwoma językami.

Poniżej przykłady tego samego kodu w obu językach:

// Java 7
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
...
}
});
// Java 8
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(v -> {
...
});
// Kotlin
val button = findViewById(R.id.button) as Button
button.setOnClickListener {
...
}

Możliwości Kotlina na tym się dopiero zaczynają i polecam zapoznanie się z większą częścią tego języka. Na całkowitym przejściu można zyskać nie tylko na ilości znaków, ale też na samej jakości kodu ze względu na udogodnienia przykładowo eliminujące występowanie NullPointerException.

Binding

Do dalszego skracania kodu można wykorzystać binding, który służy najczęściej do powiązania elementu interfejsu aplikacji do odpowiedniej zmiennej. Powstało już kilka rozwiązań, które pozwalają uwolnić się od nagminnego używania findViewById(); dla każdego z widoków.

Butterknife

Jedną z najpopularniejszych bibliotek na pewno jest Butterknife, która wywołuje findViewById() za nas w generowanym na bieżąco kodzie. Jej użycie sprowadza się do dodania adnotacji z identyfikatorami widoków do odpowiadających im zmiennych i wywołania funkcji ButterKnife.bind(). ButterKnife ma znacznie więcej możliwości jak np. przypisywanie View.OnClickListener nawet do kilku widoków jednocześnie i nie tylko.

public class ButterFragment extends Fragment {
@BindView(R.id.button1) Button button1;
@BindView(R.id.button2) Button button2;
private Unbinder unbinder;

@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment, container, false);
unbinder = ButterKnife.bind(this, view);
return view;
}

@OnClick({R.id.button1, R.id.button2}) public void onButtonClick(View view) {
...
}

@Override public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
}

Unbinder zwracany z ButterKnife.bind() ułatwia zarządzanie pamięcią, ponieważ umożliwia szybkie uwolnienie wykorzystanych referencji. Należy z tego korzystać mimo, że nie ma takiego wymogu.

Data Binding Library

Tutaj także istnieje alternatywa od Google, a mianowicie Data Binding. Rozwiązanie to polega na bardziej rozbudowanych layoutach, do których przeniesiona zostaje część logiki aplikacji. W tych layoutach można wykorzystywać proste instrukcje warunkowe, metody, wyrażenia lambda i referencje do metod. To pozwala na umieszczenie prostych warunków w layoucie bez potrzeby pisania ich w kodzie, co zwiększa czytelność samego kodu. Zalecane jest umieszczanie tylko tej części kodu, która odpowiada za wygląd danego widoku i należy powstrzymać się od umieszczania skomplikowanych warunków i obliczeń w layoutach, ponieważ trudniej jest to później debugować.

Aby skorzystać z Data Binding wystarczy dodać do pliku build.gradle poniższy kod:

android {
....
dataBinding {
enabled = true
}
}

Ponieważ Data Binding generuje kod na podstawie layoutów ze zmienioną strukturą, je też trzeba dostosować:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="member" type="com.example.Member"/>
</data>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{member.name}"
android:textAllCaps="@{member.getAge > 15}"/>
</layout>

Layout używany do tej pory musi znajdować się teraz w <layout/> wraz z <data/>, gdzie powinny znaleźć się zmienne używane w tym layoucie. W tym przykładzie jest tylko TextView wyświetlające nazwę użytkownika, wyświetlając ją CAPSem dla użytkowników z wiekiem powyżej 15.

Do wykorzystania powyższego layoutu w klasie MainActivity wystarczy wywołać binding:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
binding.setMember(member);
}

Po wywołaniu metody setMember() widoki, które zależne są od tego obiektu zostają zaktualizowane o nowe dane. Czyli w tym przykładzie jest to tylko TextView.

Jak widać powyższe rozwiązania redukują objętość kodu aplikacji poprzez przeniesienie jego zbędnej części do wygenerowanych klas na podstawie podanych przez nas parametrów. Wykorzystując te rozwiązania pozostawiamy w kodzie tylko to, co nas interesuje, bez powtarzalnych bloków przypisywania zmiennych i prostych warunków kontrolujących wygląd interfejsu użytkownika.

Generowanie kodu

Generowanie kodu używane między innymi w bibliotekach bindujących można wykorzystać na własny sposób. To co możemy wygenerować zależy głównie od potrzeb i wyobraźni. Od klas POJO, przez Buildery, aż po Singletony. Przykładowo dla kilku klas POJO możemy wygenerować odpowiednie dla nich Buildery. Sprowadza się to do dodania procesora adnotacji, który na podstawie adnotacji wygeneruje potrzebne nam klasy.

Generowanie kodu to dość obszerny temat. Po krótce polega na utworzeniu 2 modułów. Jeden z nich powinien zawierać definicje adnotacji używanych w projekcie, a drugi sam ich procesor. Moduł z adnotacjami należy wpiąć zarówno do modułu procesora jak i reszty aplikacji. Procesor do aplikacji trzeba wpiąć poprzez annotationProcessor . Dodatkowo cały moduł procesora powinien być biblioteką Android i zawierać klasę rozszerzającą AbstractProcessor. W tej klasie możemy uzyskać informacje na temat klas i pól z adnotacjami, które nas interesują i na ich podstawie wygenerować potrzebne pliki.

JavaPoet

Biblioteka, która w prosty sposób pozwala nam wygenerować obiekty JavaFile. Z poziomu procesora adnotacji można je zapisać jako pliki do folderu z generowanym kodem źródłowym naszej aplikacji, bądź do innego wybranego folderu.

Generowanie klas pozwala nam odizolować powtarzalny lub przewidywalny kod od tego, nad którym pracują na codzień programiści. Tym bardziej, że domyślnie jest on zapisywany w specjalnym folderze z wygenerowanym kodem aplikacji. Wystarczy tylko upewnić się, że generowany kod nie zawiera błędów i można skupić się na reszcie aplikacji.

Podsumowanie

Opisane rozwiązania ukazują sposoby ograniczania kodu tylko do jego najważniejszej części poprzez zarówno czystą redukcję znaków jak i separowanie prostego, powtarzalnego kodu. Rozwiązania te pochodzą z własnych doświadczeń i na pewno nie wyczerpują tematu. Dlatego jeśli znacie jakieś inne ciekawe rozwiązania tego typu to proszę o kontakt.