تشجّع بنية التطبيقات المقترَحة في Android على تقسيم الرمز إلى فئات للاستفادة من مبدأ فصل الاهتمامات، حيث تكون لكل فئة من التسلسل الهرمي مسؤولية واحدة محدّدة. ويؤدي ذلك إلى إنشاء فئات أكثر وأصغر حجمًا يجب ربطها ببعضها البعض لتلبية التبعيات.
يمكن تمثيل التبعيات بين الفئات كبيان، حيث يتم ربط كل فئة بالفئات التي تعتمد عليها. ويشكّل تمثيل جميع الفئات والتبعيات الخاصة بها بيان التطبيق. في الشكل 1، يمكنك الاطّلاع على تجريد لبيان التطبيق. عندما تعتمد الفئة A (ViewModel) على الفئة B (Repository)، يكون هناك خط يشير من A إلى B يمثّل هذه التبعية.
تساعد ميزة "إضافة التبعيات" في إنشاء هذه الروابط وتتيح لك تبديل عمليات التنفيذ لأغراض الاختبار. على سبيل المثال، عند اختبار ViewModel يعتمد على مستودع، يمكنك تمرير عمليات تنفيذ مختلفة لـ Repository باستخدام بيانات وهمية أو نماذج أولية لاختبار الحالات المختلفة.
أساسيات إدخال الاعتمادية يدويًا
يتناول هذا القسم كيفية تطبيق ميزة "إدخال التبعيات" يدويًا في سيناريو تطبيق Android حقيقي. ويشرح هذا القسم بالتفصيل نهجًا متكررًا لكيفية بدء استخدام ميزة "إدخال التبعيات" في تطبيقك. ويتم تحسين هذا النهج إلى أن يصل إلى نقطة مشابهة جدًا لما سيُنشئه Dagger تلقائيًا لك. لمزيد من المعلومات عن Dagger، يُرجى قراءة أساسيات Dagger.
يُقصد بالتدفق مجموعة من الشاشات في تطبيقك تتوافق مع ميزة معيّنة. تسجيل الدخول والتسجيل والدفع هي أمثلة على التدفقات.
عند تغطية تدفق تسجيل الدخول لتطبيق Android نموذجي، تعتمد LoginActivity على LoginViewModel، التي تعتمد بدورها على UserRepository. ثم
UserRepository يعتمد على UserLocalDataSource و
UserRemoteDataSource، اللذين يعتمدان بدورهما على خدمة Retrofit.
LoginActivity هي نقطة الدخول إلى تدفق تسجيل الدخول، ويتفاعل المستخدم مع النشاط. وبالتالي، تحتاج LoginActivity إلى إنشاء LoginViewModel مع جميع التبعيات الخاصة بها.
تبدو فئتا Repository وDataSource في التدفق على النحو التالي:
class UserRepository(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) { ... }
class UserLocalDataSource { ... }
class UserRemoteDataSource(
private val loginService: LoginRetrofitService
) { ... }
في Compose، تكون ComponentActivity هي نقطة الدخول، ويتم ربط الاعتماديات مرة واحدة في onCreate، ويتم وصف واجهة المستخدم من خلال الدوال المركّبة التي يتم استدعاؤها من setContent:
class ApiService {
/* Your API implementation here */
}
class UserRepository(private val apiService: ApiService) {
/* Your implementation here */
}
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Satisfy the dependencies of LoginViewModel recursively,
// then pass what the UI needs into setContent.
val apiService = ApiService()
val userRepository = UserRepository(apiService)
setContent {
LoginScreen(userRepository)
}
}
}
@Composable
fun LoginScreen(userRepository: UserRepository) {
val viewModel: LoginViewModel = viewModel(
factory = LoginViewModelFactory(userRepository)
)
// ...
}
تتضمّن هذه الطريقة المشاكل التالية:
- يجب الإعلان عن التبعيات بالترتيب. عليك إنشاء مثيل لـ
UserRepositoryقبلLoginViewModelلإنشائه. - يصعب إعادة استخدام الكائنات. إذا أردت إعادة استخدام
UserRepositoryفي ميزات متعددة، عليك جعله يتبع نمط سينغلتون. يجعل نمط الكائن الوحيد الاختبار أكثر صعوبة لأنّ جميع الاختبارات تشارك مثيل الكائن الوحيد نفسه.
إدارة التبعيات باستخدام حاوية
لحلّ مشكلة إعادة استخدام الكائنات، يمكنك إنشاء فئة حاوية التبعيات الخاصة بك التي تستخدمها للحصول على التبعيات. يمكن أن تكون جميع المثيلات التي توفّرها هذه الحاوية علنية. في المثال، بما أنّك تحتاج فقط إلى مثيل من UserRepository، يمكنك جعل تبعياته خاصة مع إمكانية جعلها علنية في المستقبل إذا كان يجب توفيرها:
// Container of objects shared across the whole app
class AppContainer {
// apiService and userRepository aren't private and will be exposed
val apiService = ApiService()
val userRepository = UserRepository(apiService)
}
بما أنّ هذه التبعيات تُستخدَم في التطبيق بأكمله، يجب أن
توضع في مكان مشترك يمكن لجميع الأنشطة استخدامه: Application
الفئة. أنشِئ فئة Application مخصّصة تحتوي على مثيل AppContainer.
// 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()
}
باستخدام Compose، لا يزال يتم إنشاء AppContainer نفسه في الفئة الفرعية Application. يمكنك الوصول إليه إما في النشاط، قبل استدعاء setContent، أو من داخل دالة مركّبة، باستخدام LocalContext:
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appContainer = (application as MyApplication).appContainer
setContent {
LoginScreen(appContainer.userRepository)
}
}
}
// Alternatively, read AppContainer from inside a composable:
@Composable
fun LoginScreen() {
val context = LocalContext.current
val appContainer = (context.applicationContext as MyApplication).appContainer
val viewModel: LoginViewModel = viewModel(
factory = LoginViewModelFactory(appContainer.userRepository)
)
// ...
}
ننصحك بتمرير التبعيات كمعلّمات مركّبة بدلاً من الوصول إلى LocalContext من مكان بعيد في الشجرة. يحافظ ذلك على إمكانية اختبار العناصر المركّبة ويجعل مدخلاتها واضحة. يمكنك حلّ الحاوية مرة واحدة في جذر الشاشة وتمرير ما هو مطلوب إلى الأسفل.
بهذه الطريقة، لن يكون لديك UserRepository سينغلتون. بدلاً من ذلك، سيكون لديك AppContainer تتم مشاركته بين جميع الأنشطة ويحتوي على كائنات من البيان وينشئ مثيلات من هذه الكائنات التي يمكن للفئات الأخرى استخدامها.
إذا كان LoginViewModel مطلوبًا في أماكن أخرى في التطبيق، من المنطقي أن يكون لديك مكان مركزي تنشئ فيه مثيلات من LoginViewModel.
يمكنك نقل عملية إنشاء LoginViewModel إلى الحاوية وتوفير كائنات جديدة من هذا النوع باستخدام مصنع. يبدو رمز LoginViewModelFactory على النحو التالي:
// 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<LoginViewModel> {
override fun create(): LoginViewModel {
return LoginViewModel(userRepository)
}
}
باستخدام Compose، لا يزال تعديل AppContainer يعرض المصنع. بعد ذلك، يستهلك المصنع
العنصر المركّب viewModel، لذا يتم تحديد نطاق ViewModel لأقرب ViewModelStoreOwner (عادةً ما يكون النشاط المضيف أو، باستخدام
Navigation Compose، إدخال التنقّل):
// AppContainer exposing the factory (unchanged from the snippet above)
class AppContainer {
// ...
val userRepository = UserRepository(localDataSource, remoteDataSource)
val loginViewModelFactory = LoginViewModelFactory(userRepository)
}
// Compose entry point + screen composable
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appContainer = (application as MyApplication).appContainer
setContent {
LoginScreen(appContainer.loginViewModelFactory)
}
}
}
@Composable
fun LoginScreen(factory: LoginViewModelFactory) {
val viewModel: LoginViewModel = viewModel(factory = factory)
// ...
}
هذا النهج أفضل من النهج السابق، ولكن لا تزال هناك بعض التحديات التي يجب أخذها في الاعتبار:
عليك إدارة
AppContainerبنفسك، وإنشاء مثيلات لجميع التبعيات يدويًا.لا يزال هناك الكثير من رموز النصوص النموذجية. عليك إنشاء المصانع أو المعلمات يدويًا حسب ما إذا كنت تريد إعادة استخدام كائن أم لا.
إدارة التبعيات في تدفقات التطبيق
يصبح AppContainer معقدًا عندما تريد تضمين المزيد من الوظائف في المشروع. عندما يصبح تطبيقك أكبر وتبدأ في تقديم تدفقات ميزات مختلفة، تظهر مشاكل أكثر:
عندما يكون لديك تدفقات مختلفة، قد تريد أن تبقى الكائنات ضمن نطاق هذا التدفق فقط. على سبيل المثال، عند إنشاء
LoginUserData(التي قد تتألف من اسم المستخدم وكلمة المرور اللذين يتم استخدامهما فقط في تدفق تسجيل الدخول)، لا تريد الاحتفاظ بالبيانات من تدفق تسجيل دخول قديم من مستخدم مختلف. تريد مثيلاً جديدًا لكل تدفق جديد. يمكنك تحقيق ذلك من خلال إنشاء كائناتFlowContainerداخلAppContainerكما هو موضّح في مثال الرمز التالي.قد يكون تحسين بيان التطبيق وحاويات التدفق صعبًا أيضًا. عليك تذكُّر حذف المثيلات التي لا تحتاج إليها، حسب التدفق الذي تستخدمه.
لنضِف LoginContainer إلى مثال الرمز. نريد أن نتمكّن من إنشاء مثيلات متعددة من LoginContainer في التطبيق، لذا بدلاً من جعله كائنًا وحيدًا، اجعله فئة تحتوي على التبعيات التي يحتاجها تدفق تسجيل الدخول من AppContainer.
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
}
في Compose، يرتبط عمر حاوية التدفق بالتركيب بدلاً من Activity المضيف. لست بحاجة إلى تغيير AppContainer.loginContainer المشترَك، لأنّ العناصر المركّبة تتلقّى تبعياتها كمعلّمات أو تقرأها من ViewModel تم رفعه. أمامك خياران:
- الرسم البياني المتداخل في Navigation Compose (الخيار المفضّل للتدفقات المتعددة الشاشات):
ضَع جميع الشاشات في تدفق تسجيل الدخول ضمن رسم بياني متداخل للتنقّل وحدِّد نطاق الحاوية لـ
NavBackStackEntryفي هذا الرسم البياني. يتم إنشاء الحاوية عندما يدخل المستخدم إلى التدفق ويتم محوها عند إزالة إدخال الأنشطة السابقة، بدون الحاجة إلى استدعاء أي عمليات يدوية لمراحل النشاط. لمزيد من المعلومات، يُرجى الاطّلاع على مقالة تصميم بيان التنقّل. rememberفي جذر الشاشة (للتدفقات ذات الشاشة الواحدة أو عندما لا تستخدم Navigation Compose). أنشِئ الحاوية داخلrememberبحيث يتم إنشاؤها مرة واحدة لكل إدخال في التركيب ويتم جمعها كبيانات غير ضرورية عندما يغادر العنصر المركّب:
@Composable
fun LoginFlow(appContainer: AppContainer) {
val loginContainer = remember(appContainer) {
LoginContainer(appContainer.userRepository)
}
val viewModel: LoginViewModel = viewModel(
factory = loginContainer.loginViewModelFactory
)
// Render the login flow using loginContainer.loginData and viewModel.
}
الخاتمة
تُعدّ ميزة "إضافة التبعيات" تقنية جيدة لإنشاء تطبيقات Android قابلة للتوسّع وقابلة للاختبار. استخدِم الحاويات كطريقة لمشاركة مثيلات الفئات في أجزاء مختلفة من تطبيقك وكمكان مركزي لإنشاء مثيلات الفئات باستخدام المصانع.
عندما يصبح تطبيقك أكبر، ستلاحظ أنّك تكتب الكثير من رموز النصوص النموذجية (مثل المصانع)، ما قد يؤدي إلى حدوث أخطاء. عليك أيضًا إدارة نطاق الحاويات ودورة حياتها بنفسك، وتحسين الحاويات التي لم تعُد هناك حاجة إليها وإزالتها لإخلاء الذاكرة. قد يؤدي إجراء ذلك بشكل غير صحيح إلى حدوث أخطاء دقيقة وتسربات في الذاكرة في تطبيقك.
في قسم Dagger، ستتعرّف على كيفية استخدام Dagger لأتمتة هذه العملية وإنشاء الرمز نفسه الذي كنت ستكتبه يدويًا بخلاف ذلك.