nowe funkcjonalności Android problem

Nowe funkcjonalności – jak rzuciłem się na głęboką wodę i prawie utonąłem

Polecane

Cześć, dzisiaj będzie nieco bardziej technicznie. Jednak nie martw się, jeśli nie jesteś obeznany w programowaniu na Androida. W końcu tytułowe nowe funkcjonalności dla wszystkich stanowią pewne wyzwanie. Pod koniec znajdziesz podsumowanie, jak mi poszło, stanowiące swego rodzaju TL;DR. Opiszę, dlaczego warto traktować z dystansem wszelakie nowe rozwiązania i używać ich w produkcyjnych aplikacjach, ale dopiero po pewnym czasie od premiery.

Nowe funkcjonalności, czyli początkowe założenia

W ramach samorozwoju chciałem napisać aplikację, która wyświetli widok z poziomu serwisu Androida, używając do tego WindowManager. Dokładnie w taki sam sposób działają dymki czatów z Messengera i pływająca ikona z Yanosika.

Nie przegap
Gry na HMS – jak wyglądają możliwości AppGallery i innych sklepów?
gry hms huawei call of duty pubg fortnite apex legends mobile
Kontynuujemy przygodę z Huawei Mobile Services. Jest to niejako nasza misja, w której chcemy pokazać, jak wygląda życie bez Usług Google. W ostatnim artykule dotyczącym aplikacji bankowych słusznie zauważyliście w komentarzach, że widać zmiany oraz rosnące zainteresowanie AppGallery. W takim razie w kolejnym kroku pojawiają się gracze. Czy gry w sklepie Huawei są godne uwagi? […]

Zakładane działanie było dość proste. Aplikacja miała umożliwiać wyłączenie ekranów OLED bez wstrzymywania aktualnie działającej aplikacji. Cel? Pomoc na przykład przy słuchaniu YouTube bez premium.

nowe funkcjonalności android
Finalny efekt

Jako że był to projekt pisany głównie dla zabawy, to zdecydowałem się na wykorzystanie wszystkiego, czego kiedykolwiek używałem lub chciałbym się nauczyć:

Jeśli te nazwy wypisane wyżej nic ci nie mówią, to możesz wykorzystać produktywnie czas kwarantanny i rozpocząć naukę programowania na platformę Android. Na pewno pomocne okaże się nasze forum, gdzie nasi specjaliści zapewniają wsparcie zarówno początkującym, jak i bardziej zaawansowanym programistom. Z pewnych źródeł wiem też, że w przyszłym tygodniu forum bardzo się odmieni i być może stanie się nawet polskim Stack Overflow, ale nie mówcie nikomu, bo to tajemnica.

Co mnie zaskoczyło?

Największą nowością dla mnie zdecydowanie było dynamic features. Wcześniej, gdy ktoś stawiał w swojej aplikacji na modularyzację, to wszystkie moduły były dołączone do głównego modułu (najczęściej o nazwie app). Innymi słowy, moduł app był zależny od wszystkich innych modułów.

nowe funkcjonalności diagram

W przypadku dynamic features zależności te zostają odwrócone i można stworzyć bardzo fajną architekturę. App nie ma bezpośredniego dostępu do kodu znajdującego się w modułach funkcjonalności. Możemy go traktować jako kontener przechowujący kod wspólny dla wszystkich modułów (jakieś kolory, style kontrolek itp.).

Dynamiczne moduły mają jeszcze jedną ciekawą właściwość. Dzielimy je bowiem na dwa typy:

  • Obecne w momencie instalacji
  • Instalowane w trakcie działania aplikacji

W pierwszym przypadku sprawa jest prosta. Kod z takiego modułu nie jest co prawda widoczny z poziomu modułu app, lecz jest fizycznie obecny na urządzeniu. Zupełnie inaczej ma się sprawa w drugim przypadku. Tutaj kod jest pobierany dopiero na żądanie. Tak więc musimy zawsze sprawdzać, czy dany moduł jest w tej chwili zainstalowany. Jeśli nie, to musimy zażądać jego pobranie.

Nawigacja, a nowe funkcjonalności

Tak jak wspomniałem, moduł app nie ma dostępu do kodu z innych modułów. Tutaj pojawia się pierwszy problem, ponieważ ciężko nawigować do czegoś, czego nie ma. Przeszukując internet, natrafiłem na dwa najpopularniejsze rozwiązania:

  • Refleksja
  • Deep linki

Oba rozwiązania jakoś do mnie nie przemawiały. Miałem wrażenie, że jest to bardziej hack niż faktyczne podejście do tematu. Na szczęście natrafiłem na rozszerzenie jetpackowego Navigation Component, które wspiera nawigowanie do dynamicznych modułów. Teraz jest on częścią produkcyjnej biblioteki (tyle że w wersji alfa). Gdy ją znalazłem, była trzymana jako osobne repozytorium, które trzeba było dodawać do gradle. Niemniej jednak z braku lepszych alternatyw postanowiłem użyć rozwiązania od Google, bo przecież co by mogło pójść nie tak? 🙂

Wstrzykiwanie zależności

Zakładanym działaniem, do jakiego dążyłem, była możliwość współdzielenia tych samych instancji z modułu app pomiędzy wszystkimi dynamicznymi modułami. Czyli jeśli w app miałbym klasę PermissionsHelper, to chciałbym mieć możliwość wstrzyknięcia dokładnie tej samej instancji w module funkcjonalnosc1, funkcjonalnosc2 i tak dalej.

W momencie planowania projektu było to dla mnie niemałą zagwostką. Mogłem mieć globalny komponent w app, który byłby używany w pozostałych dynamicznych modułach. Brzmi dobrze – w końcu wszystkie moduły widzą app i mogą się podpiąć do grafu. No ale właśnie, w jaki sposób w app zdefiniować metodę inject dla klasy, o której nic nie wiemy?

Kolejną myślą było tworzenie sub komponentu dla każdego dynamicznego modułu. Mają one przecież odwróconą zależność, więc nie powinno być problemu. Będą zależne od głównego komponentu zdefiniowanego w app, do którego przecież mamy dostęp. Jednak takie podejście również nie zadziała. W celu stworzenia subkomponentu musimy dodać do głównego metodę, która zwróci ten właśnie subkomponent oraz przyjmie wszystkie jego moduły, bez bezparametrowego konstruktora, jako argument tej metody tworzącej. Tak więc to podejście odpada, ponieważ z poziomu app nie będziemy mieli dostępu do subkomponentów zdefiniowanych w dynamicznych modułach.

Na szczęście znalazłem rozwiązanie moich wszystkich problemów. W Daggerze istnieje coś takiego jak Component.Factory. Pozwala to na jednostronną zależność komponentów. Co najlepsze, twój komponent może zależeć zarówno od innego komponentu, jak i sub komponentu.

Tak na przykład wygląda komponent modułu, który zawiera serwis, odpowiedzialny za wyświetlanie czarnej powłoki w mojej aplikacji:

@FeatureScope
@Component(modules = [OverlayModule::class], dependencies = [AppComponent::class])
interface OverlayComponent {

    fun inject(service: OverlayService)

    @Component.Factory
    interface Factory {
        fun create(component: AppComponent): OverlayComponent
    }
}

Jak możecie zauważyć, mój komponent zależy wyłącznie od AppComponent. Jednak równie dobrze możecie stworzyć komponent zależący od sub komponentu – na przykład w celu podpięcia fragmentów:

@FeatureScope
@Component(
    modules = [SettingsModule::class],
    dependencies = [ActivityComponent::class]
)
interface SettingsComponent {

    fun inject(fragment: SettingsFragment)

    @Component.Factory
    interface Factory {
        fun create(component: ActivityComponent): SettingsComponent
    }
}

Nowe funkcjonalności to betonowe buty?

O ile początkowo jakoś szło mi to androidowe pływanie, tak co jakiś czas zauważałem, że mam na sobie betonowe buty w postaci zbyt wielu nowości w jednym projekcie. Właśnie od tego momentu będę w większości tylko narzekał. Najbardziej żałuję chyba użycia Kts gradle. Co prawda ma on jedną, ogromną zaletę. Mianowicie można bardzo czytelnie zorganizować sobie zależności, bazując na kotlinowych objectach:

object Coroutines {
        object Versions {
            const val coroutines = "1.3.2"
        }

        const val core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
        const val android =
            "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}"
        const val test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.coroutines}"
    }

I potem użyć tego w ten sposób:

implementation(Libraries.Coroutines.core)
implementation(Libraries.Coroutines.android)
testImplementation(Libraries.Coroutines.test)

Lecz to na tyle, jeśli chodzi o zalety. Największą wadą jest niski poziom wsparcia. Jeśli masz jakiś problem z konfiguracją swojego projektu, to wszelkie fragmenty kodu, jakie znajdziesz w internecie, będą napisane w Groovym. Sam będziesz musiał sobie poradzić z portowaniem tego na Kotlina. Nie chodzi nawet o samą składnię, bo tego można się nauczyć dość szybko. Najgorsze jest to, że niektóre funkcje z pluginów przyjmują całkiem inne argumenty i musisz się przekopywać przez tomy dokumentacji.

nowe funkcjonalności

Kolejną sprawą jest brak wsparcia IDE w oficjalnej wersji Android Studio -opcja Project Structure jest dla ciebie bezużyteczna. Nie wiem, jak to wygląda obecnie, ale jeszcze 3 miesiące temu ta funkcjonalność nie działała nawet w wersji Preview.

Nowe funkcjonalności to dziwne błędy

Kolejną rzeczą, która mnie dotknęła, są dziwne i losowe błędy w budowaniu projektu. Nigdy nie używałem opcji Invalidates Caches / Restart tak często, jak przy tym projekcie, nie wspominając już o Clean Project. Zaznaczam, że to nie są błędy spowodowane błędną konfiguracją lub po prostu moim błędem. Wszystkie przechodziły po zrobieniu Clean Project lub Invalidates Caches / Restart i nie występowały przy każdej próbie budowania. Niżej wrzucę kilka przykładów, które uwieczniłem na zrzutach ekranu.

Po wpisaniu kodów tych błędów można znaleźć kilka rozwiązań, ale żadne nie działają w tym konkretnym przypadku. Pozostaje więc tylko się przyzwyczaić jak do świecącego Check Engine w samochodzie.

nowe funkcjonalności

Brak pełnej kompatybilności pomiędzy różnymi narzędziami

Google to ogromna firma, która zatrudnia około 100 tys. pracowników. Zapewne osoby pracujące nad różnymi bibliotekami Androida nie mają ze sobą styczności. To są jedynie moje dywagacje, lecz można dojść do takich wniosków, gdy próbuje się połączyć dynamic features z data binding oraz navigation component.

Zacznijmy od data binding. Jest to dość stare narzędzie – pamiętam, że pierwszy raz użyłem tego w okolicach 2015. Jednym z jego większych problemów jest brak pełnego wsparcia dla Kotlina. Nie możemy na przykład użyć extension method jako wartość atrybutu kontrolki UI.
Dodatkowo sam procesor adnotacji nie do końca radzi sobie z projektem wielomodułowym. Nie możemy zadeklarować wspólnego BindingAdaptera w app w taki sposób, aby był widoczny w pozostałych modułach. Jeśli chcemy skorzystać z jakiegoś BindingAdaptera, musi on znajdować się w tym samym module, co widok, w którym chcemy go użyć. Oznacza to kilkukrotne kopiowanie klas. Co prawda można wyeliminować powielanie kodu (przykład niżej), lecz nadal będziemy musieli posiadać kilka klas, które robią to samo.

@BindingAdapter("imageUrl")
fun ImageView.loadImage(imageUrl: String?) {
    Glide.with(this)
        .load(imageUrl)
        .into(this)
}
import com.test.app.bindingAdapters.loadImage

@BindingAdapter("imageUrl")
fun ImageView.loadImage(imageUrl: String?) = loadImage(imageUrl)

O ile pewne zgrzyty w DataBinding jestem w stanie zrozumieć, ponieważ obecna wersja na pewno nie była pisana pod kątem współpracy z dynamic features, o tyle czuję się rozczarowany dziwnym działaniem dynamic navigation component, które było przecież projektowane do wsparcia dynamic features.

nowe funkcjonalności

W czym problem? Biblioteka świetnie radzi sobie z nawigacją do klas znajdujących się w innych modułach. Możemy posiadać tylko jeden navigation graph w app i zadeklarować w nim wszystkie fragmenty i aktywności, nawet jeśli znajdują się w innych modułach. Problem zaczyna się, gdy nasze widoki przyjmują jakieś argumenty.
Nie zbudujemy naszego projektu, jeśli argument widoku znajduje się w module niewidocznym dla app. Co ciekawe nasz plik xml przejdzie pomyślnie kompilację. Proces budowania wysypuje się dopiero przy kompilacji wygenerowanych klas. Dzieje się tak dlatego, że klasy parametrów nie są używane przez refleksję (jak w przypadku fragmentów i aktywności), lecz jako normalne klasy, których fizycznie nie ma w app. To oczywiście prowadzi do błędów kompilacji.

Wnioski na przyszłość

W tym wypadku nie przejmowałem się jakoś bardzo, ponieważ pisałem ten projekt bardziej dla zabawy. Podstawowym błędem było wrzucenie wielu nowości jednocześnie. Na przykład przy błędach budowania ciężko stwierdzić, czy są one spowodowane użyciem dynamic features, data binding czy może Kts Gradle.

Z racji tego, że większość rzeczy jest dość nowa, to ciężko liczyć na wsparcie społeczności. Większość problemów trzeba rozwiązywać samemu lub zakładać nowe wątki na Stack Overflow, które w moim przypadku pozostają bez odpowiedzi już od kilku miesięcy. Dlatego w komercyjnych projektach, gdzie liczy się stabilność działania, radziłbym stopniować wprowadzanie nowości.

nowe funkcjonalności

Nie mniej cieszę się, że przeżyłem tę przygodę. Mam ogromną satysfakcję, że robiłem coś, czego nie próbowało przede mną zbyt wielu programistów. Zostawiłem też po sobie wkład w postaci kilku odpowiedzi na Stack Overflow oraz kilku zgłoszonych błędach w systemie raportowania błędów od Google.

Samo zgłaszanie błędów to też ciekawa sprawa. Jeśli zgłaszasz coś specyficznego, możesz mieć pewność, że programiści z Google poproszą cię o wrzucenie projektu, na którym mogą ten błąd reprodukować. Jednak cała dyskusja jest publiczna. Jeśli nie chcesz udostępniać swojego kodu każdemu, musisz specjalnie przygotować nowy projekt, w którym wystąpi ten sam błąd. To oczywiście wiąże się z dodatkowym nakładem pracy. Mimo wszystko warto to robić, ponieważ dzięki temu programiści będą mogli naprawić błąd w nadchodzących wydaniach.

Mam nadzieję, że za kilka miesięcy będzie można używać takiej wybuchowej kombinacji bez obawy o stabilność projektu.

Tl;dr, czyli nowe funkcjonalności w skrócie

Świadomy zagrożeń postanowiłem wrzucić do projektu kilka nowych, nie do końca sprawdzonych bibliotek. Musiałem zmierzyć się z wieloma błędami, przekopać tomy dokumentacji oraz zgodzić się na wiele kompromisów, które maskowały te błędy. Dlatego nie radziłbym takiego podejścia, gdy tworzycie aplikacje komercyjne dla zewnętrznych klientów.

Na szczęście udało się ukończyć aplikację, o czym możecie się przekonać sami. Na dziś to już tyle lub raczej aż tyle. Jak wam się podoba taka forma artykułów? Wolicie takie konkretne, techniczne tematy czy bardziej abstrakcyjne podejście? Napiszcie koniecznie w komentarzu.

Jeśli nadal jesteście głodni wiedzy, to zachęcam do przeczytania ostatniego tekstu Tomka o HMS. Od siebie dodam, że tekst mnie trochę zainspirował i w najbliższym czasie opiszę proces migracji mojej aplikacji właśnie do HMS. Dobra, strasznie się dziś rozgadałem, więc do zobaczenia już niebawem!






Przewiń stronę, by przeczytać kolejny wpis
x