Manuelle Abhängigkeitsinjektion

Für die von Android empfohlene App-Architektur wird empfohlen, Ihren Code in Klassen zu unterteilen, um von der Trennung der Probleme zu profitieren. Dabei handelt es sich um ein Prinzip, bei dem jede Klasse der Hierarchie eine einzelne definierte Verantwortlichkeit hat. Dies führt zu mehr, kleineren Klassen, die miteinander verbunden sein müssen, um die Abhängigkeiten der anderen zu erfüllen.

Android-Apps bestehen normalerweise aus vielen Klassen und einige sind voneinander abhängig.
Abbildung 1: Modell der Anwendungsgrafik einer Android-App

Die Abhängigkeiten zwischen den Klassen können als Graph dargestellt werden, in dem jede Klasse mit den Klassen verbunden ist, von denen sie abhängig ist. Die Darstellung all Ihrer Klassen und ihrer Abhängigkeiten besteht im Anwendungsdiagramm. In Abbildung 1 sehen Sie eine Abstraktion der Anwendungsgrafik. Wenn Klasse A (ViewModel) von Klasse B (Repository) abhängt, gibt es eine Linie, die von A nach B zeigt und diese Abhängigkeit darstellt.

Mit Abhängigkeitsinjektionen können diese Verbindungen hergestellt werden und Sie können Implementierungen zum Testen austauschen. Wenn Sie beispielsweise ein ViewModel testen, das von einem Repository abhängt, können Sie verschiedene Implementierungen von Repository entweder mit Fälschungen oder Simulationen übergeben, um die verschiedenen Fälle zu testen.

Grundlagen der manuellen Abhängigkeitsinjektion

In diesem Abschnitt wird beschrieben, wie Sie die manuelle Abhängigkeitsinjektion in einem echten Szenario einer Android-App anwenden. Dabei wird ein iterativer Ansatz zur Verwendung der Abhängigkeitsinjektion in Ihrer App erläutert. Der Ansatz wird verbessert, bis ein Punkt erreicht wird, der dem von Dagger automatisch für Sie erzeugten Punkt sehr ähnlich ist. Weitere Informationen zu Dagger finden Sie unter Dagger-Grundlagen.

Ein Ablauf ist eine Gruppe von Bildschirmen in Ihrer App, die einer Funktion entsprechen. Anmeldung, Registrierung und Bezahlvorgang sind Beispiele für Abläufe.

Wenn der Anmeldevorgang für eine typische Android-App abgedeckt wird, hängt LoginActivity von LoginViewModel ab, die wiederum von UserRepository abhängt. Dann hängt UserRepository von UserLocalDataSource und UserRemoteDataSource ab, die wiederum von einem Retrofit-Dienst abhängen.

LoginActivity ist der Einstiegspunkt für den Anmeldevorgang und der Nutzer interagiert mit der Aktivität. Daher muss LoginActivity den LoginViewModel mit allen zugehörigen Abhängigkeiten erstellen.

Die Klassen Repository und DataSource des Ablaufs sehen so aus:

Kotlin

class UserRepository(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

class UserLocalDataSource { ... }
class UserRemoteDataSource(
    private val loginService: LoginRetrofitService
) { ... }

Java

class UserLocalDataSource {
    public UserLocalDataSource() { }
    ...
}

class UserRemoteDataSource {

    private final Retrofit retrofit;

    public UserRemoteDataSource(Retrofit retrofit) {
        this.retrofit = retrofit;
    }

    ...
}

class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }

    ...
}

So sieht LoginActivity aus:

Kotlin

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // In order to satisfy the dependencies of LoginViewModel, you have to also
        // satisfy the dependencies of all of its dependencies recursively.
        // First, create retrofit which is the dependency of UserRemoteDataSource
        val retrofit = Retrofit.Builder()
            .baseUrl("https://example.com")
            .build()
            .create(LoginService::class.java)

        // Then, satisfy the dependencies of UserRepository
        val remoteDataSource = UserRemoteDataSource(retrofit)
        val localDataSource = UserLocalDataSource()

        // Now you can create an instance of UserRepository that LoginViewModel needs
        val userRepository = UserRepository(localDataSource, remoteDataSource)

        // Lastly, create an instance of LoginViewModel with userRepository
        loginViewModel = LoginViewModel(userRepository)
    }
}

Java

public class MainActivity extends Activity {

    private LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // In order to satisfy the dependencies of LoginViewModel, you have to also
        // satisfy the dependencies of all of its dependencies recursively.
        // First, create retrofit which is the dependency of UserRemoteDataSource
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService.class);

        // Then, satisfy the dependencies of UserRepository
        UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit);
        UserLocalDataSource localDataSource = new UserLocalDataSource();

        // Now you can create an instance of UserRepository that LoginViewModel needs
        UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);

        // Lastly, create an instance of LoginViewModel with userRepository
        loginViewModel = new LoginViewModel(userRepository);
    }
}

Bei diesem Ansatz gibt es Probleme:

  1. Es gibt eine Menge Boilerplate-Code. Wenn Sie eine weitere Instanz von LoginViewModel an einem anderen Teil des Codes erstellen möchten, wäre Codeduplikate vorhanden.

  2. Abhängigkeiten müssen in der richtigen Reihenfolge deklariert werden. Sie müssen UserRepository vor LoginViewModel instanziieren, um sie zu erstellen.

  3. Es ist schwierig, Objekte wiederzuverwenden. Wenn Sie UserRepository für mehrere Features wiederverwenden möchten, müssen Sie dem Singleton-Muster folgen. Das Singleton-Muster erschwert die Tests, da alle Tests dieselbe Singleton-Instanz verwenden.

Abhängigkeiten mit einem Container verwalten

Um das Problem der Wiederverwendung von Objekten zu lösen, können Sie eine eigene Abhängigkeiten-Container-Klasse erstellen, mit der Sie Abhängigkeiten abrufen. Alle von diesem Container bereitgestellten Instanzen können öffentlich sein. Da Sie in diesem Beispiel nur eine Instanz von UserRepository benötigen, können Sie die zugehörigen Abhängigkeiten als privat festlegen und sie bei Bedarf in Zukunft öffentlich machen:

Kotlin

// Container of objects shared across the whole app
class AppContainer {

    // Since you want to expose userRepository out of the container, you need to satisfy
    // its dependencies as you did before
    private val retrofit = Retrofit.Builder()
                            .baseUrl("https://example.com")
                            .build()
                            .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    // userRepository is not private; it'll be exposed
    val userRepository = UserRepository(localDataSource, remoteDataSource)
}

Java

// Container of objects shared across the whole app
public class AppContainer {

    // Since you want to expose userRepository out of the container, you need to satisfy
    // its dependencies as you did before
    private Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://example.com")
            .build()
            .create(LoginService.class);

    private UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit);
    private UserLocalDataSource localDataSource = new UserLocalDataSource();

    // userRepository is not private; it'll be exposed
    public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);
}

Da diese Abhängigkeiten in der gesamten Anwendung verwendet werden, müssen sie an einem gemeinsamen Ort platziert werden, den alle Aktivitäten verwenden können: die Klasse Application. Erstellen Sie eine benutzerdefinierte Application-Klasse, die eine AppContainer-Instanz enthält.

Kotlin

// Custom Application class that needs to be specified
// in the AndroidManifest.xml file
class MyApplication : Application() {

    // Instance of AppContainer that will be used by all the Activities of the app
    val appContainer = AppContainer()
}

Java

// Custom Application class that needs to be specified
// in the AndroidManifest.xml file
public class MyApplication extends Application {

    // Instance of AppContainer that will be used by all the Activities of the app
    public AppContainer appContainer = new AppContainer();
}

Jetzt können Sie die Instanz von AppContainer aus der Anwendung abrufen und die freigegebene UserRepository-Instanz abrufen:

Kotlin

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Gets userRepository from the instance of AppContainer in Application
        val appContainer = (application as MyApplication).appContainer
        loginViewModel = LoginViewModel(appContainer.userRepository)
    }
}

Java

public class MainActivity extends Activity {

    private LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Gets userRepository from the instance of AppContainer in Application
        AppContainer appContainer = ((MyApplication) getApplication()).appContainer;
        loginViewModel = new LoginViewModel(appContainer.userRepository);
    }
}

Auf diese Weise haben Sie kein Singleton-UserRepository. Stattdessen haben Sie eine AppContainer, die für alle Aktivitäten verwendet wird, die Objekte aus dem Diagramm enthält und Instanzen dieser Objekte erstellt, die andere Klassen nutzen können.

Wenn LoginViewModel an mehr Stellen in der Anwendung benötigt wird, ist es sinnvoll, einen zentralen Ort zu haben, an dem Sie Instanzen von LoginViewModel erstellen. Sie können die Erstellung von LoginViewModel in den Container verschieben und neue Objekte dieses Typs mit einer Factory bereitstellen. Der Code für ein LoginViewModelFactory sieht so aus:

Kotlin

// Definition of a Factory interface with a function to create objects of a type
interface Factory<T> {
    fun create(): T
}

// Factory for LoginViewModel.
// Since LoginViewModel depends on UserRepository, in order to create instances of
// LoginViewModel, you need an instance of UserRepository that you pass as a parameter.
class LoginViewModelFactory(private val userRepository: UserRepository) : Factory {
    override fun create(): LoginViewModel {
        return LoginViewModel(userRepository)
    }
}

Java

// Definition of a Factory interface with a function to create objects of a type
public interface Factory<T> {
    T create();
}

// Factory for LoginViewModel.
// Since LoginViewModel depends on UserRepository, in order to create instances of
// LoginViewModel, you need an instance of UserRepository that you pass as a parameter.
class LoginViewModelFactory implements Factory {

    private final UserRepository userRepository;

    public LoginViewModelFactory(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public LoginViewModel create() {
        return new LoginViewModel(userRepository);
    }
}

Du kannst LoginViewModelFactory in die AppContainer aufnehmen und dafür sorgen, dass die LoginActivity sie verwendet:

Kotlin

// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory
class AppContainer {
    ...
    val userRepository = UserRepository(localDataSource, remoteDataSource)

    val loginViewModelFactory = LoginViewModelFactory(userRepository)
}

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Gets LoginViewModelFactory from the application instance of AppContainer
        // to create a new LoginViewModel instance
        val appContainer = (application as MyApplication).appContainer
        loginViewModel = appContainer.loginViewModelFactory.create()
    }
}

Java

// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory
public class AppContainer {
    ...

    public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);

    public LoginViewModelFactory loginViewModelFactory = new LoginViewModelFactory(userRepository);
}

public class MainActivity extends Activity {

    private LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Gets LoginViewModelFactory from the application instance of AppContainer
        // to create a new LoginViewModel instance
        AppContainer appContainer = ((MyApplication) getApplication()).appContainer;
        loginViewModel = appContainer.loginViewModelFactory.create();
    }
}

Dieser Ansatz ist besser als der vorherige, es gibt jedoch noch einige Herausforderungen:

  1. Sie müssen AppContainer selbst verwalten und Instanzen für alle Abhängigkeiten manuell erstellen.

  2. Es gibt immer noch jede Menge Boilerplate-Code. Sie müssen Factorys oder Parameter manuell erstellen, je nachdem, ob Sie ein Objekt wiederverwenden möchten oder nicht.

Abhängigkeiten in Anwendungsabläufen verwalten

AppContainer wird kompliziert, wenn Sie mehr Funktionen in das Projekt einbinden möchten. Wenn Ihre Anwendung größer wird und Sie mit der Einführung verschiedener Feature-Abläufe beginnen, treten noch mehr Probleme auf:

  1. Wenn Sie verschiedene Abläufe haben, kann es sinnvoll sein, dass Objekte einfach im Bereich dieses Ablaufs leben. Wenn Sie beispielsweise LoginUserData erstellen, das aus dem Nutzernamen und dem Passwort bestehen kann, die nur für den Anmeldevorgang verwendet werden, möchten Sie keine Daten aus einem alten Anmeldevorgang eines anderen Nutzers beibehalten. Für jeden neuen Ablauf benötigen Sie eine neue Instanz. Dazu erstellen Sie FlowContainer-Objekte innerhalb des AppContainer, wie im nächsten Codebeispiel gezeigt.

  2. Auch die Optimierung der Anwendungsgrafik und der Flusscontainer kann schwierig sein. Denken Sie daran, je nach aktuellem Ablauf nicht benötigte Instanzen zu löschen.

Angenommen, Ihr Anmeldevorgang besteht aus einer Aktivität (LoginActivity) und mehreren Fragmenten (LoginUsernameFragment und LoginPasswordFragment). Diese Ansichten sollen Folgendes ermöglichen:

  1. Rufen Sie dieselbe LoginUserData-Instanz auf, die bis zum Abschluss des Anmeldevorgangs freigegeben werden muss.

  2. Erstellen Sie eine neue Instanz von LoginUserData, wenn der Ablauf neu gestartet wird.

Dazu können Sie einen Container für den Anmeldevorgang verwenden. Dieser Container muss zu Beginn des Anmeldevorgangs erstellt und nach Ende des Ablaufs aus dem Arbeitsspeicher entfernt werden.

Fügen wir dem Beispielcode LoginContainer hinzu. Sie möchten mehrere Instanzen von LoginContainer in der Anwendung erstellen können. Anstatt sie zu einer Singleton-Instanz zu machen, sollten Sie sie zu einer Klasse mit den Abhängigkeiten machen, die der Anmeldevorgang aus der AppContainer benötigt.

Kotlin

class LoginContainer(val userRepository: UserRepository) {

    val loginData = LoginUserData()

    val loginViewModelFactory = LoginViewModelFactory(userRepository)
}

// AppContainer contains LoginContainer now
class AppContainer {
    ...
    val userRepository = UserRepository(localDataSource, remoteDataSource)

    // LoginContainer will be null when the user is NOT in the login flow
    var loginContainer: LoginContainer? = null
}

Java

// Container with Login-specific dependencies
class LoginContainer {

    private final UserRepository userRepository;

    public LoginContainer(UserRepository userRepository) {
        this.userRepository = userRepository;
        loginViewModelFactory = new LoginViewModelFactory(userRepository);
    }

    public LoginUserData loginData = new LoginUserData();

    public LoginViewModelFactory loginViewModelFactory;
}

// AppContainer contains LoginContainer now
public class AppContainer {
    ...
    public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);

    // LoginContainer will be null when the user is NOT in the login flow
    public LoginContainer loginContainer;
}

Wenn Sie einen Container speziell für einen Ablauf haben, müssen Sie entscheiden, wann die Containerinstanz erstellt und gelöscht werden soll. Da Ihr Anmeldevorgang in einer Aktivität abgeschlossen ist (LoginActivity), verwaltet die Aktivität den Lebenszyklus dieses Containers. LoginActivity kann die Instanz in onCreate() erstellen und in onDestroy() löschen.

Kotlin

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel
    private lateinit var loginData: LoginUserData
    private lateinit var appContainer: AppContainer


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        appContainer = (application as MyApplication).appContainer

        // Login flow has started. Populate loginContainer in AppContainer
        appContainer.loginContainer = LoginContainer(appContainer.userRepository)

        loginViewModel = appContainer.loginContainer.loginViewModelFactory.create()
        loginData = appContainer.loginContainer.loginData
    }

    override fun onDestroy() {
        // Login flow is finishing
        // Removing the instance of loginContainer in the AppContainer
        appContainer.loginContainer = null
        super.onDestroy()
    }
}

Java

public class LoginActivity extends Activity {

    private LoginViewModel loginViewModel;
    private LoginData loginData;
    private AppContainer appContainer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        appContainer = ((MyApplication) getApplication()).appContainer;

        // Login flow has started. Populate loginContainer in AppContainer
        appContainer.loginContainer = new LoginContainer(appContainer.userRepository);

        loginViewModel = appContainer.loginContainer.loginViewModelFactory.create();
        loginData = appContainer.loginContainer.loginData;
    }

    @Override
    protected void onDestroy() {
        // Login flow is finishing
        // Removing the instance of loginContainer in the AppContainer
        appContainer.loginContainer = null;

        super.onDestroy();
    }
}

Wie LoginActivity können Anmeldefragmente auf LoginContainer von AppContainer zugreifen und die freigegebene LoginUserData-Instanz verwenden.

Da Sie sich in diesem Fall mit der Logik des Ansichtslebenszyklus befassen, ist die Lebenszyklusbeobachtung sinnvoll.

Fazit

Die Abhängigkeitsinjektion ist eine gute Methode zum Erstellen skalierbarer und testbarer Android-Apps. Verwenden Sie Container, um Instanzen von Klassen in verschiedenen Teilen Ihrer Anwendung freizugeben und um Instanzen von Klassen mit Factorys zu erstellen.

Wenn Ihre Anwendung größer wird, stellen Sie fest, dass Sie viel Boilerplate-Code schreiben (z. B. Fabriken), was fehleranfällig sein kann. Außerdem müssen Sie Umfang und Lebenszyklus der Container selbst verwalten und nicht mehr benötigte Container optimieren und verwerfen, um Arbeitsspeicher freizugeben. Dies kann zu kleinen Fehlern und Speicherlecks in Ihrer App führen.

Im Dagger-Abschnitt erfahren Sie, wie Sie diesen Prozess mit Dagger automatisieren und denselben Code generieren können, den Sie andernfalls von Hand geschrieben hätten.