החדרת תלות ידנית

ארכיטקטורת האפליקציות המומלצת ב-Android מעודדת חלוקה את הקוד למחלקות, כדי שתוכלו להפיק תועלת מהפרדת סוגיות, שבהם לכל מחלקה בהיררכיה יש אחריות מוגדרת אחת. כך נוצרים יותר כיתות קטנות יותר שצריכים להיות מחוברים אחד לשני כדי למלא את יחסי התלות אחד של השני.

אפליקציות ל-Android מורכבות בדרך כלל ממחלקות רבות, ומחלקן
    תלויים זה בזה.
איור 1. מודל של אפליקציה ל-Android תרשים

יחסי התלות בין המחלקות יכולים להיות מיוצגים בגרף, שבו כל הכיתה מקושרת לכיתות שבהן היא תלויה. שמייצג את כל המחלקות ויחסי התלות שלהם יוצרים את תרשים האפליקציות. באיור 1 ניתן לראות פשטה של תרשים האפליקציה. כשסיווג א' (ViewModel) תלוי בכיתה ב' (Repository), יש אירועים קו שמצביע מ-A ל-B שמייצג את התלות הזו.

החדרת תלות עוזרת ליצור את החיבורים האלה ומאפשרת לך להחליף יישומים לבדיקה. לדוגמה, כשבודקים ViewModel שתלויות במאגר, אפשר להעביר יישומים שונים של Repository עם זיופים או חיקויים כדי לבדוק את המקרים השונים.

עקרונות בסיסיים של החדרת תלות ידנית

הקטע הזה מסביר איך להחיל הזרקת תלות ידנית במכשיר Android אמיתי בתרחיש אחר באפליקציה. הוא מתאר גישה של ניסיון חוזר לגבי האופן שבו תתחילו באמצעות החדרת תלות באפליקציה שלך. הגישה תשתפר עד נקודה שדומה מאוד למה ש-Dagger היה יוצר באופן אוטומטי את/ה. למידע נוסף על Dagger, אפשר לקרוא את העקרונות הבסיסיים של Dagger.

תהליך נחשב כקבוצת מסכים באפליקציה שתואמים . התחברות, הרשמה ותשלום הם דוגמאות לתהליכים.

כשסוגרים תהליך התחברות באפליקציה אופיינית ל-Android, LoginActivity תלוי בעמודה LoginViewModel, שתלויה בתור UserRepository. ואז UserRepository תלוי ב-UserLocalDataSource UserRemoteDataSource, שבתורו תלוי ב-Retrofit לאחר השיפור.

LoginActivity הוא נקודת הכניסה לתהליך ההתחברות והמשתמש מקיים אינטראקציה עם הפעילות. לכן, LoginActivity צריך ליצור את LoginViewModel עם כל יחסי התלות שלו.

המחלקות Repository ו-DataSource של הזרימה נראות כך:

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

    ...
}

כך נראה LoginActivity:

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

יש בעיות בגישה הזו:

  1. יש הרבה קוד סטנדרטי. אם רוצים ליצור מכונה נוספת של LoginViewModel בחלק אחר של הקוד, יש כפילות בקוד.

  2. צריך להצהיר על יחסי תלות לפי הסדר. צריך ליצור ממנו אובייקט (instantiation) UserRepository לפני LoginViewModel כדי ליצור אותו.

  3. קשה להשתמש שוב באובייקטים. אם ברצונך להשתמש שוב ב-UserRepository בכמה תכונות שונות, צריך לוודא שהוא עומד singleton template. הדפוס 'סינגלטון' מקשה על הבדיקה, מפני שכל הבדיקות שותפות אותו מופע סינגלטון.

ניהול יחסי תלות באמצעות קונטיינר

כדי לפתור את הבעיה של שימוש חוזר באובייקטים, תוכלו ליצור מחלקה של קונטיינרים של תלות שבה משתמשים כדי לקבל יחסי תלות. כל המופעים שסופקו על ידי מאגר התגים הזה יכולים להיות ציבוריים. בדוגמה, מכיוון שצריך רק במופע של UserRepository, אפשר להפוך את יחסי התלות שלו לפרטיים באמצעות להפוך אותם לציבוריים בעתיד אם יהיה צורך לספק אותם:

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

יחסי התלות האלה נמצאים בשימוש בכל האפליקציה, ולכן הם צריכים כדי למקם אותם במקום משותף, כל הפעילויות יכולות להשתמש בהן: Application. יצירת התאמה אישית מחלקה Application שמכילה מופע AppContainer.

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

עכשיו אפשר לקבל את המופע של AppContainer מהאפליקציה מקבלים את החלק המשותף של המכונה UserRepository:

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

כך אין לך UserRepository סינגל. במקום זאת, יש AppContainer משותף בכל הפעילויות שמכילות אובייקטים מהתרשים ויוצרת מופעים של האובייקטים האלה שמחלקות אחרות יכולות לצרוך.

אם יהיה צורך ב-LoginViewModel במקומות נוספים באפליקציה, נדרש מקום מרכזי שבו יוצרים מופעים של LoginViewModel הגיוני. אפשר להעביר את היצירה של LoginViewModel למאגר ולספק כדי ליצור אובייקטים חדשים מהסוג הזה במפעל. קוד של LoginViewModelFactory נראה כך:

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

אפשר לכלול את LoginViewModelFactory בAppContainer ולעשות את LoginActivity צורכים אותו:

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

הגישה הזו טובה יותר מהגישה הקודמת, אבל עדיין יש אתגרים שצריך לשקול:

  1. עליך לנהל את AppContainer בעצמך וליצור מכונות לכל של יחסי התלות באופן ידני.

  2. עדיין יש הרבה קוד סטנדרטי. צריך ליצור מפעלים ידנית לפי הצורך, אם רוצים לעשות שימוש חוזר באובייקט או לא.

ניהול יחסי התלות בתהליכי האפליקציה

AppContainer הוא מורכב יותר כשרוצים לכלול יותר פונקציות את הפרויקט. כשהאפליקציה הופכת לגודל גדול יותר ומתחילים להציג סוגים שונים של זורמים, יש עוד יותר בעיות עולות:

  1. כאשר יש תהליכים שונים, יכול להיות שתרצו שאובייקטים יחיו היקף התהליך הזה. לדוגמה, כשיוצרים את LoginUserData (יכול להיות מורכב משם המשתמש והסיסמה שמשמשים רק בתהליך ההתחברות) שאינך רוצה כדי לשמור נתונים מתהליך התחברות ישן של משתמש אחר. אתם רוצים חשבון חדש למופע של כל תהליך חדש. כדי לעשות את זה, אפשר ליצור FlowContainer של האובייקטים בתוך AppContainer, כמו שמוצג בדוגמת הקוד הבאה.

  2. יכול להיות קשה גם לבצע אופטימיזציה של תרשים האפליקציה ושל מאגרי הזרימה. חשוב לזכור למחוק מכונות שאתם לא צריכים, בהתאם תהליך היצירה.

נניח שיש לך תהליך התחברות שכולל פעילות אחת (LoginActivity) ומספר מקטעים (LoginUsernameFragment ו-LoginPasswordFragment). המטרה של הצפיות האלה:

  1. לגשת לאותו מופע LoginUserData שצריך לשתף עד תהליך ההתחברות

  2. יוצרים מופע חדש של LoginUserData כשהתהליך מתחיל שוב.

אפשר לעשות זאת באמצעות מאגר תגים של תהליך ההתחברות. מאגר התגים הזה צריך להיות שנוצרו כשתהליך ההתחברות מתחיל והוסר מהזיכרון כשהתהליך מסתיים.

נוסיף LoginContainer לקוד לדוגמה. אם רוצים ליצור מספר מופעים של LoginContainer באפליקציה, כך שבמקום להפוך אותו singleton, להפוך אותו למחלקה עם יחסי התלות שנדרשים לתהליך ההתחברות 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;
}

לאחר שיש לכם קונטיינר ספציפי לזרימה, צריך להחליט מתי ליצור ומוחקים את המופע של הקונטיינר. מכיוון שתהליך ההתחברות עצמאי פעילות (LoginActivity), הפעילות היא הפעילות שמנהלת את מחזור החיים של המאגר הזה. LoginActivity יכול ליצור את המכונה ב-onCreate() וגם אפשר למחוק אותה בתיקייה onDestroy().

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

בדומה ל-LoginActivity, מקטעי התחברות יכולים לגשת אל LoginContainer מ- AppContainer ומשתמשים במכונה המשותפת LoginUserData.

כי במקרה הזה מדובר בלוגיקת מחזור חיים של תצוגה, באמצעות תצפיות על מחזור החיים הגיוניות.

סיכום

החדרת תלות היא שיטה טובה ליצירת קשרים ניתנים להרחבה ולבדיקה אפליקציות ל-Android. שימוש בקונטיינרים כדרך לשתף מופעים של כיתות חלקים באפליקציה וכמקום מרוכז ליצירת מופעים של באמצעות מפעלים.

כשהאפליקציה תגדל, תתחילו לראות שאתם כותבים הרבה קוד סטנדרטי (כמו מפעלים), שעלול להוביל לשגיאות. צריך גם לנהל את ההיקף ומחזור החיים של הקונטיינרים בעצמכם, לבצע אופטימיזציה מחיקת מאגרים שכבר לא נחוצים כדי לפנות זיכרון. אם תעשו זאת בצורה שגויה, ייתכן שיהיו באגים קלים ודליפות זיכרון באפליקציה.

בקטע 'פגיעים': לומדים איך להשתמש ב-Dagger כדי להפוך את התהליך לאוטומטי וליצור את אותו קוד אחרת היית כותב בכתב יד.