Wstrzykiwanie zależności na Androidzie

Wstrzykiwanie zależności to technika powszechnie używana w programowaniu i dobrze nadająca się do programowania na Androida. Postępując zgodnie z zasadami DI, tworzysz podstawy dobrej architektury aplikacji.

Wdrożenie wstrzykiwania zależności przynosi takie korzyści:

  • Możliwość ponownego wykorzystania kodu
  • Łatwość refaktoryzacji
  • Łatwość testowania

Podstawy wstrzykiwania zależności

Zanim omówimy wstrzykiwanie zależności na urządzeniach z Androidem, znajdziesz na tej stronie ogólne informacje o tym, jak działa wstrzykiwanie zależności.

Co to jest wstrzykiwanie zależności?

Zajęcia często wymagają odwołań do innych klas. Na przykład klasa Car może wymagać odwołania do klasy Engine. Te wymagane klasy są nazywane zależnościami, a w tym przykładzie klasa Car zależy od tego, czy istnieje instancja klasy Engine, która ma zostać uruchomiona.

Klasa może pobrać potrzebny obiekt na 3 sposoby:

  1. Klasa tworzy zależność, której potrzebuje. W podanym wyżej przykładzie Car utworzy i zainicjuje własną instancję Engine.
  2. Pobierz gdzie indziej W ten sposób działają niektóre interfejsy API Androida, takie jak pobieranie Context i getSystemService().
  3. Podaj go jako parametr. Aplikacja może udostępniać te zależności podczas tworzenia klasy lub przekazywać je do funkcji, które wymagają danej zależności. W powyższym przykładzie konstruktor Car otrzymałby jako parametr Engine.

Trzecia opcja to wstrzykiwanie zależności. W tym podejściu zależności klas są udostępniane, a nie odczytywane przez instancję klasy.

Oto przykład. Bez wstrzykiwania zależności reprezentowanie w kodzie Car, który tworzy własną zależność Engine, wygląda tak:

Kotlin

class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

Java

class Car {

    private Engine engine = new Engine();

    public void start() {
        engine.start();
    }
}


class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}
Klasa samochodu bez wstrzykiwania zależności

To nie jest przykład wstrzykiwania zależności, ponieważ klasa Car konstruuje własną klasę Engine. Może to stanowić problem, ponieważ:

  • Car i Engine są ściśle sprzężone – wystąpienie obiektu Car używa jednego typu obiektu Engine i nie można łatwo używać podklas ani alternatywnych implementacji. Gdyby obiekt Car skonstruował własny obiekt Engine, konieczne byłoby utworzenie 2 typów obiektu Car, zamiast ponownego używania tego samego obiektu Car w wyszukiwarkach typu Gas i Electric.

  • Twarda zależność od Engine utrudnia testowanie. Funkcja Car używa prawdziwego wystąpienia Engine, co uniemożliwia użycie podwójnej wartości testowej do modyfikowania Engine w różnych przypadkach testowych.

Jak wygląda kod z wstrzykiwaniem zależności? Zamiast każde wystąpienie Car konstruujące przy zainicjowaniu własny obiekt Engine otrzymuje obiekt Engine jako parametr w swoim konstruktorze:

Kotlin

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

Java

class Car {

    private final Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}


class MyApp {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car(engine);
        car.start();
    }
}
Klasa samochodu korzystająca z wstrzykiwania zależności

Funkcja main używa funkcji Car. Ponieważ funkcja Car zależy od parametru Engine, aplikacja tworzy instancję Engine, a potem używa jej do utworzenia instancji Car. Zalety tego podejścia opartego na danych:

  • Możliwość wielokrotnego korzystania z aplikacji Car. Do usługi Car możesz przekazywać różne implementacje kodu Engine. Możesz na przykład zdefiniować nową podklasę klasy Engine o nazwie ElectricEngine, której ma używać usługa Car. Jeśli używasz DI, musisz tylko przekazać w instancji zaktualizowanej podklasy ElectricEngine, a Car będzie nadal działać bez wprowadzania zmian.

  • Łatwe testowanie aplikacji Car. Możesz zdać egzaminy podwójne, aby sprawdzić w różnych sytuacjach. Możesz na przykład utworzyć testowy duplikat elementu Engine o nazwie FakeEngine i skonfigurować go na potrzeby różnych testów.

Istnieją 2 główne sposoby wstrzykiwania zależności na Androidzie:

  • Constructor Injection (Wstrzykiwanie konstruktora). w sposób opisany powyżej. Przekazujesz zależności klasy do jej konstruktora.

  • Wstrzyknięcie pola (lub Setter Injection). Niektóre klasy platformy Androida, takie jak działania i fragmenty, są tworzone przez system, dlatego wstrzykiwanie konstruktora nie jest możliwe. W przypadku wstrzykiwania pól instancje są tworzone po utworzeniu klasy. Kod będzie wyglądał tak:

Kotlin

class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

Java

class Car {

    private Engine engine;

    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.setEngine(new Engine());
        car.start();
    }
}

Automatyczne wstrzykiwanie zależności

W poprzednim przykładzie udało Ci się samodzielnie utworzyć i udostępnić zależności różnych klas oraz zarządzać nimi bez konieczności korzystania z biblioteki. Jest to tzw. ręczne wstrzykiwanie zależności lub ręczne wstrzykiwanie zależności. W przykładzie Car występuje tylko jedna zależność, ale większa liczba zależności i klas może utrudniać ręczne wstrzykiwanie zależności. Ręczne wstrzykiwanie zależności również wiąże się z kilkoma problemami:

  • W przypadku dużych aplikacji prawidłowe podłączenie wszystkich zależności może wymagać obszernego kodu. W architekturze wielowarstwowej, aby utworzyć obiekt dla górnej warstwy, trzeba podać wszystkie zależności znajdujących się pod nią warstw. Konkretny przykład: do zbudowania prawdziwego samochodu mogą być potrzebne silnik, skrzynia biegów, podwozia i inne części. Silnik z kolei potrzebuje cylindrów i świeczek zapłonowych.

  • Jeśli nie możesz utworzyć zależności przed ich przekazaniem – na przykład w przypadku korzystania z leniwego inicjowania lub określania zakresu obiektów w przepływach aplikacji – musisz utworzyć i obsługiwać kontener (lub wykres zależności), który będzie zarządzać czasem trwania zależności w pamięci.

Istnieją biblioteki, które rozwiązują ten problem, automatyzując proces tworzenia i udostępniania zależności. Można je podzielić na 2 kategorie:

  • Rozwiązania oparte na odczuciach, które łączą zależności w czasie działania.

  • Rozwiązania statyczne, które generują kod łączący zależności podczas kompilacji.

Dagger to obsługiwana przez Google popularna biblioteka do wstrzykiwania zależności w językach Java, Kotlin i Android. Dagger ułatwia korzystanie z DI w aplikacji, tworząc wykres zależności i nim zarządzając. Zapewnia w pełni statyczne i czasowe kompilacje rozwiązania problemów z rozwojem i wydajnością rozwiązań opartych na odczuciach, takich jak Guice.

Alternatywy dla wstrzykiwania zależności

Alternatywą dla wstrzykiwania zależności jest użycie lokalizatora usług. Wzorzec projektu lokalizatora usług poprawia również oddzielenie klas od konkretnych zależności. Tworzysz klasę o nazwie lokalizator usług, która tworzy i przechowuje zależności, a następnie udostępnia te zależności na żądanie.

Kotlin

object ServiceLocator {
    fun getEngine(): Engine = Engine()
}

class Car {
    private val engine = ServiceLocator.getEngine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

Java

class ServiceLocator {

    private static ServiceLocator instance = null;

    private ServiceLocator() {}

    public static ServiceLocator getInstance() {
        if (instance == null) {
            synchronized(ServiceLocator.class) {
                instance = new ServiceLocator();
            }
        }
        return instance;
    }

    public Engine getEngine() {
        return new Engine();
    }
}

class Car {

    private Engine engine = ServiceLocator.getInstance().getEngine();

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}

Wzorzec lokalizatora usług różni się od wstrzykiwania zależności w sposobie korzystania z elementów. Wzorzec lokalizatora usług daje klasom kontrolę i prośby o wstrzykiwanie obiektów. Dzięki wstrzykiwaniu zależności aplikacja ma kontrolę i aktywnie wprowadza wymagane obiekty.

W porównaniu z wstrzykiwaniem zależności:

  • Gromadzenie zależności wymaganych przez lokalizator usług utrudnia testowanie kodu, ponieważ wszystkie testy muszą współdziałać z tym samym globalnym lokalizatorem usług.

  • Zależności są kodowane w implementacji klas, a nie na powierzchni interfejsu API. Dlatego trudniej jest określić, czego dana klasa potrzebuje z zewnątrz. W związku z tym zmiany w Car lub zależności dostępne w lokalizatorze usług mogą powodować błędy w środowisku wykonawczym lub testów, ponieważ odwołania do nich nie łamią.

  • Zarządzanie czasem trwania obiektów jest trudniejsze, jeśli chcesz uwzględnić coś innego niż okres użytkowania całej aplikacji.

Korzystanie z Hilt w aplikacji na Androida

Hilt to zalecana biblioteka Jetpack do wstrzykiwania zależności na Androidzie. Hilt definiuje standardowy sposób wykonywania operacji typu DI w aplikacji, udostępniając kontenery dla każdej klasy Androida w projekcie i automatycznie zarządzając ich cyklami życia.

Hilt działa w oparciu o popularną bibliotekę DI Dagger, aby korzystać z poprawności czasu kompilacji, wydajności środowiska wykonawczego, skalowalności i obsługi Android Studio zapewnianej przez Dagger.

Więcej informacji o Hilt znajdziesz w artykule o wstrzykiwaniu zależności za pomocą Hilt.

Podsumowanie

Wstrzykiwanie zależności zapewnia aplikacji takie korzyści:

  • Ponowne wykorzystanie klas i rozłączenie zależności: łatwiejsza zamiana implementacji zależności. Ponowne wykorzystanie kodu jest łatwiejsze dzięki odwróceniu kontroli – klasy nie kontrolują już sposobu tworzenia zależności, ale działają z dowolną konfiguracją.

  • Łatwość refaktoryzacji: zależności stają się możliwym do zweryfikowania częścią interfejsu API, więc można je sprawdzić w czasie tworzenia obiektu lub kompilacji, zamiast być ukryte jako szczegóły implementacji.

  • Łatwość testowania: klasa nie zarządza swoimi zależnościami, więc podczas testowania możesz ją przetestować w różnych implementacjach, aby przetestować różne przypadki.

Aby w pełni poznać korzyści wstrzykiwania zależności, wypróbuj tę funkcję ręcznie w aplikacji, zgodnie z opisem w sekcji Ręczne wstrzykiwanie zależności.

Dodatkowe materiały

Więcej informacji o wstrzykiwaniu zależności znajdziesz w dodatkowych materiałach poniżej.

Próbki