Manuelle Abhängigkeitsinjektion

Bei der empfohlenen App-Architektur von Android ist es empfehlenswert, Code in Klassen aufteilen, um von der Trennung von Belangen zu profitieren. wobei jede Klasse der Hierarchie eine einzelne definierte Verantwortung hat. Das führt zu mehr kleineren Klassen, die miteinander verbunden werden müssen. Abhängigkeiten des jeweils anderen zu erfüllen.

<ph type="x-smartling-placeholder">
</ph> Android-Apps bestehen normalerweise aus vielen Klassen, von denen einige
    voneinander abhängig sind.
Abbildung 1: Ein Modell der App einer Android-App Grafik

Die Abhängigkeiten zwischen den Klassen können als Grafik dargestellt werden, in der jede Klasse class ist mit den Klassen verbunden, von denen sie abhängig ist. Die Darstellung all Ihrer Die Klassen und ihre Abhängigkeiten bilden den Anwendungsgraph. In Abbildung 1 sehen Sie eine Abstraktion des Anwendungsdiagramms. Wenn Klasse A (ViewModel) von Klasse B (Repository) abhängt, gibt es eine Linie, die von A nach B zeigt, die diese Abhängigkeit darstellt.

Die Abhängigkeitsinjektion hilft beim Erstellen dieser Verbindungen und ermöglicht es Ihnen, Implementierungen für Tests. Beim Testen einer ViewModel der von einem Repository abhängig ist, können Sie Repository mit Fälschungen oder Modellen, um die verschiedenen Fälle zu testen.

Grundlagen der manuellen Abhängigkeitsinjektion

In diesem Abschnitt wird beschrieben, wie die manuelle Abhängigkeitsinjektion bei einem echten Android-Gerät App-Szenarios zu erstellen. Es wird ein iterativer Ansatz erläutert, wie Sie beginnen können, mit Abhängigkeitsinjektion in Ihre App. Der Ansatz verbessert sich, bis einen Punkt, der dem von Dagger automatisch generierten, von dir. Weitere Informationen zu Dagger finden Sie unter Dagger-Grundlagen.

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

Bei der Erläuterung des Anmeldevorgangs für eine typische Android-App zeigt der LoginActivity hängt von LoginViewModel ab, das wiederum von UserRepository abhängt. Dann hängt UserRepository von einem UserLocalDataSource und einem UserRemoteDataSource, das wiederum von einem Retrofit abhängig ist .

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

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 erstellen möchten, von LoginViewModel an einem anderen Teil des Codes.

  2. Abhängigkeiten müssen der Reihe nach deklariert werden. Sie müssen eine Instanz UserRepository vor LoginViewModel, um sie zu erstellen.

  3. Es ist schwierig, Objekte wiederzuverwenden. Wenn Sie UserRepository wiederverwenden möchten mehrere Funktionen nutzen, müsstest du dafür sorgen, Singleton-Muster. Das Singleton-Muster erschwert das Testen, da alle Tests den gleichen Singleton-Instanz verwendet.

Abhängigkeiten mit einem Container verwalten

Um das Problem der Wiederverwendung von Objekten zu lösen, können Sie Ihre eigenen Objekte erstellen dependencies container, mit der Sie Abhängigkeiten abrufen. Alle Instanzen von diesem Container bereitgestellt werden, können öffentlich sein. Da Sie in diesem Beispiel nur eine Instanz von UserRepository ist, können Sie deren Abhängigkeiten mit der die Möglichkeit, sie in Zukunft zu veröffentlichen, falls sie zur Verfügung gestellt werden müssen:

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 zentralen Ort platziert werden, den alle Aktivitäten nutzen können: das Klasse Application. Benutzerdefinierten 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 Rufen Sie den gemeinsamen Wert der Instanz UserRepository ab:

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);
    }
}

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

Wenn LoginViewModel an mehr Stellen in der Anwendung benötigt wird, hat ein zentraler Ort, an dem Sie Instanzen von LoginViewModel erstellen, Sinn. Sie können die Erstellung von LoginViewModel in den Container verschieben und Folgendes angeben: dieses Typs mit einer Factory erstellen. 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);
    }
}

Sie können die LoginViewModelFactory in die AppContainer aufnehmen und die LoginActivity verbraucht sie:

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, aber es gibt noch einige zu berücksichtigende Herausforderungen:

  1. Sie müssen AppContainer selbst verwalten und Instanzen für alle erstellen von Hand.

  2. Es gibt immer noch viel Boilerplate-Code. Sie müssen Fabriken oder -Parameter manuell hinzufügen, 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 für das Projekt. Wenn Ihre App größer wird und Sie verschiedene entstehen noch mehr Probleme:

  1. Bei unterschiedlichen Abläufen kann es sinnvoll sein, Objekte nur in der den Umfang dieses Ablaufs. Wenn Sie beispielsweise LoginUserData (das könnte aus dem Nutzernamen und dem Passwort bestehen, die nur bei der Anmeldung verwendet werden. um Daten aus einem alten Anmeldevorgang eines anderen Nutzers beizubehalten. Sie möchten eine neue für jeden neuen Ablauf. Dazu erstellen Sie FlowContainer -Objekten innerhalb der AppContainer, wie im nächsten Codebeispiel gezeigt.

  2. Die Optimierung des Anwendungsdiagramms und der Flusscontainer kann ebenfalls schwierig sein. Denken Sie daran, nicht benötigte Instanzen zu löschen, je nachdem, in dem Sie sich befinden.

Angenommen, Sie haben einen Anmeldevorgang, der aus einer Aktivität besteht (LoginActivity) und mehrere Fragmente (LoginUsernameFragment und LoginPasswordFragment) enthalten. Für diese Datenansichten gilt Folgendes:

  1. Greifen Sie auf dieselbe LoginUserData-Instanz zu, die bis zum Anmeldevorgang abgeschlossen.

  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 wird erstellt, wenn der Anmeldevorgang beginnt, und wird aus dem Arbeitsspeicher entfernt, wenn der Vorgang endet.

Fügen Sie dem Beispielcode ein LoginContainer hinzu. Sie möchten in der Lage sein, mehrere Instanzen von LoginContainer in der App zu erstellen. Singleton, machen Sie daraus eine Klasse mit den Abhängigkeiten, AppContainer.

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;
}

Sobald Sie einen für einen Ablauf spezifischen Container haben, müssen Sie entscheiden, wann Sie einen Container erstellen möchten und löschen Sie die Containerinstanz. Da der Anmeldevorgang in der Eine Aktivität (LoginActivity), die den Lebenszyklus verwaltet des 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 das LoginContainer von AppContainer und verwenden die freigegebene Instanz LoginUserData.

In diesem Fall haben Sie es mit der Lebenszykluslogik der Ansicht zu tun, Beobachtungen des Lebenszyklus sinnvoll.

Fazit

Die Abhängigkeitsinjektion ist eine gute Technik, um skalierbare und testbare Android-Apps. Mit Containern können Sie Instanzen von Klassen in verschiedenen und als zentraler Ort zum Erstellen von Instanzen Ihrer App Klassen mithilfe von Factorys.

Wenn Ihre Anwendung größer wird, werden Sie feststellen, Boilerplate-Code (z. B. Fabriken), der fehleranfällig sein kann. Außerdem müssen Sie Umfang und Lebenszyklus der Container selbst verwalten, Verwerfen von Containern, die nicht mehr benötigt werden, um Arbeitsspeicher freizugeben. Dies kann zu kleinen Fehlern und Speicherlecks in Ihrer App führen.

Im Abschnitt Dagger führen Sie folgende Schritte aus: erfahren Sie, wie Sie mit Dagger diesen Prozess automatisieren und denselben Code generieren können, die Sie sonst per Hand geschrieben hätten.