ארכיטקטורת האפליקציות המומלצת ב-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); } }
יש בעיות בגישה הזו:
יש הרבה קוד סטנדרטי. אם רוצים ליצור מכונה נוספת של
LoginViewModel
בחלק אחר של הקוד, יש כפילות בקוד.צריך להצהיר על יחסי תלות לפי הסדר. צריך ליצור ממנו אובייקט (instantiation)
UserRepository
לפניLoginViewModel
כדי ליצור אותו.קשה להשתמש שוב באובייקטים. אם ברצונך להשתמש שוב ב-
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(); } }
הגישה הזו טובה יותר מהגישה הקודמת, אבל עדיין יש אתגרים שצריך לשקול:
עליך לנהל את
AppContainer
בעצמך וליצור מכונות לכל של יחסי התלות באופן ידני.עדיין יש הרבה קוד סטנדרטי. צריך ליצור מפעלים ידנית לפי הצורך, אם רוצים לעשות שימוש חוזר באובייקט או לא.
ניהול יחסי התלות בתהליכי האפליקציה
AppContainer
הוא מורכב יותר כשרוצים לכלול יותר פונקציות
את הפרויקט. כשהאפליקציה הופכת לגודל גדול יותר ומתחילים להציג סוגים שונים של
זורמים, יש עוד יותר בעיות עולות:
כאשר יש תהליכים שונים, יכול להיות שתרצו שאובייקטים יחיו היקף התהליך הזה. לדוגמה, כשיוצרים את
LoginUserData
(יכול להיות מורכב משם המשתמש והסיסמה שמשמשים רק בתהליך ההתחברות) שאינך רוצה כדי לשמור נתונים מתהליך התחברות ישן של משתמש אחר. אתם רוצים חשבון חדש למופע של כל תהליך חדש. כדי לעשות את זה, אפשר ליצורFlowContainer
של האובייקטים בתוךAppContainer
, כמו שמוצג בדוגמת הקוד הבאה.יכול להיות קשה גם לבצע אופטימיזציה של תרשים האפליקציה ושל מאגרי הזרימה. חשוב לזכור למחוק מכונות שאתם לא צריכים, בהתאם תהליך היצירה.
נניח שיש לך תהליך התחברות שכולל פעילות אחת (LoginActivity
)
ומספר מקטעים (LoginUsernameFragment
ו-LoginPasswordFragment
).
המטרה של הצפיות האלה:
לגשת לאותו מופע
LoginUserData
שצריך לשתף עד תהליך ההתחברותיוצרים מופע חדש של
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 כדי להפוך את התהליך לאוטומטי וליצור את אותו קוד אחרת היית כותב בכתב יד.