المفاهيم والتنفيذ في Jetpack Compose
أحداث واجهة المستخدم هي الإجراءات التي يجب التعامل معها في طبقة واجهة المستخدم، إما من خلال واجهة المستخدم أو من خلال ViewModel. النوع الأكثر شيوعًا من الأحداث هو أحداث المستخدم. ينشئ المستخدم أحداثًا من خلال التفاعل مع التطبيق، مثل النقر على الشاشة أو إنشاء إيماءات. تستهلك واجهة المستخدم بعد ذلك هذه الأحداث باستخدام عمليات ردّ الاتصال، مثل برامج معالجة onClick().
تكون ViewModel مسؤولة عادةً عن معالجة منطق النشاط التجاري لحدث مستخدم معيّن، مثل نقر المستخدم على زر لإعادة تحميل بعض البيانات. عادةً ما تتعامل ViewModel مع هذا الأمر من خلال توفير دوال يمكن أن تستدعيها واجهة المستخدم. قد تتضمّن أحداث المستخدم أيضًا منطق سلوك واجهة المستخدم الذي يمكن لواجهة المستخدم التعامل معه مباشرةً، مثل الانتقال إلى شاشة مختلفة أو عرض Snackbar.
مع أنّ منطق النشاط التجاري يظل كما هو بالنسبة إلى التطبيق نفسه على منصات الأجهزة الجوّالة أو أشكال الأجهزة المختلفة، فإنّ منطق سلوك واجهة المستخدم هو تفصيل تنفيذي قد يختلف بين هذه الحالات. تحدّد صفحة طبقة واجهة المستخدم أنواع المنطق هذه على النحو التالي:
- تشير منطق النشاط التجاري إلى الإجراءات التي يجب اتّخاذها عند حدوث تغييرات في الحالة، مثل إجراء دفعة أو تخزين الإعدادات المفضّلة للمستخدم. تتعامل طبقتَا النطاق والبيانات عادةً مع هذه المنطق. في هذا الدليل، يتم استخدام فئة ViewModel في "مكوّنات البنية" كحلّ محدّد للأسلوب للفئات التي تتعامل مع منطق النشاط التجاري.
- تشير منطق سلوك واجهة المستخدم أو منطق واجهة المستخدم إلى كيفية عرض تغييرات الحالة، مثل منطق التنقّل أو كيفية عرض الرسائل للمستخدم. تتولّى واجهة المستخدم معالجة هذا المنطق.
معالجة أحداث المستخدم
يمكن لواجهة المستخدم التعامل مع أحداث المستخدم مباشرةً إذا كانت هذه الأحداث مرتبطة بتعديل حالة عنصر في واجهة المستخدم، مثل حالة عنصر قابل للتوسيع. إذا كان الحدث يتطلّب تنفيذ منطق النشاط التجاري، مثل إعادة تحميل البيانات على الشاشة، يجب أن تتم معالجته بواسطة ViewModel.
يوضّح المثال التالي كيفية استخدام أزرار مختلفة لتوسيع عنصر في واجهة المستخدم (منطق واجهة المستخدم) ولإعادة تحميل البيانات على الشاشة (منطق النشاط التجاري):
class LatestNewsActivity : AppCompatActivity() {
private lateinit var binding: ActivityLatestNewsBinding
private val viewModel: LatestNewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
// The expand details event is processed by the UI that
// modifies a View's internal state.
binding.expandButton.setOnClickListener {
binding.expandedSection.visibility = View.VISIBLE
}
// The refresh event is processed by the ViewModel that is in charge
// of the business logic.
binding.refreshButton.setOnClickListener {
viewModel.refreshNews()
}
}
}
أحداث المستخدمين في RecyclerViews
إذا تم إنشاء الإجراء في موضع أبعد في شجرة واجهة المستخدم، مثل RecyclerView
عنصر أو View مخصّص، يجب أن يظل ViewModel هو العنصر الذي يتعامل مع أحداث المستخدم.
على سبيل المثال، لنفترض أنّ جميع عناصر الأخبار من NewsActivity تحتوي على زر إضافة إلى المفضّلة. يجب أن يعرف ViewModel معرّف الخبر الذي تمّت إضافته إلى الإشارات المرجعية. عندما يضيف المستخدم إشارة مرجعية إلى خبر، لا يستدعي محوّل RecyclerView الدالة addBookmark(newsId) المعروضة من ViewModel، الأمر الذي يتطلّب إنشاء تبعية في ViewModel. بدلاً من ذلك، يعرض ViewModel عنصر حالة
يُسمى NewsItemUiState يحتوي على رمز برمجي لمعالجة
الحدث:
data class NewsItemUiState(
val title: String,
val body: String,
val bookmarked: Boolean = false,
val publicationDate: String,
val onBookmark: () -> Unit
)
class LatestNewsViewModel(
private val formatDateUseCase: FormatDateUseCase,
private val repository: NewsRepository
)
val newsListUiItems = repository.latestNews.map { news ->
NewsItemUiState(
title = news.title,
body = news.body,
bookmarked = news.bookmarked,
publicationDate = formatDateUseCase(news.publicationDate),
// Business logic is passed as a lambda function that the
// UI calls on click events.
onBookmark = {
repository.addBookmark(news.id)
}
)
}
}
بهذه الطريقة، لا يعمل محوّل RecyclerView إلا مع البيانات التي يحتاجها، أي قائمة عناصر NewsItemUiState. لا يمكن للمحوّل الوصول إلى ViewModel بالكامل، ما يقلّل من احتمالية إساءة استخدام الوظائف التي يعرضها ViewModel. عندما تسمح لفئة النشاط فقط بالعمل مع ViewModel،
فإنّك تفصل بين المسؤوليات. يضمن ذلك عدم تفاعل الكائنات الخاصة بواجهة المستخدم، مثل طرق العرض أو برامج RecyclerView، مباشرةً مع ViewModel.
اصطلاحات التسمية لوظائف أحداث المستخدم
في هذا الدليل، تم تسمية دوال ViewModel التي تعالج أحداث المستخدمين بأفعال استنادًا إلى الإجراء الذي تعالجه، مثل: addBookmark(id) أو logIn(username, password).
التعامل مع أحداث ViewModel
يجب أن تؤدي إجراءات واجهة المستخدم التي تنشأ من ViewModel، أي أحداث ViewModel، دائمًا إلى تعديل حالة واجهة المستخدم. يتوافق ذلك مع مبادئ تدفق البيانات أحادي الاتجاه. ويتيح إعادة إنتاج الأحداث بعد إجراء تغييرات في الإعدادات، ويضمن عدم فقدان إجراءات واجهة المستخدم. يمكنك أيضًا، بشكل اختياري، جعل الأحداث قابلة لإعادة الإنتاج بعد إيقاف العملية إذا كنت تستخدم وحدة الحالة المحفوظة.
إنّ ربط إجراءات واجهة المستخدم بحالة واجهة المستخدم ليس دائمًا عملية بسيطة، ولكنّه يؤدي إلى منطق أبسط. يجب ألا تتوقف عملية التفكير عند تحديد كيفية انتقال واجهة المستخدم إلى شاشة معيّنة، على سبيل المثال. عليك التفكير بشكل أعمق والنظر في كيفية تمثيل تدفق المستخدم هذا في حالة واجهة المستخدم. بعبارة أخرى، لا تفكّر في الإجراءات التي يجب أن تتّخذها واجهة المستخدم، بل فكِّر في كيفية تأثير هذه الإجراءات في حالة واجهة المستخدم.
على سبيل المثال، لنفترض أنّ المستخدم ينتقل إلى الشاشة الرئيسية عندما يكون مسجّلاً الدخول على شاشة تسجيل الدخول. يمكنك نمذجة ذلك في حالة واجهة المستخدم على النحو التالي:
data class LoginUiState(
val isLoading: Boolean = false,
val errorMessage: String? = null,
val isUserLoggedIn: Boolean = false
)
تتفاعل واجهة المستخدم هذه مع التغييرات التي تطرأ على حالة isUserLoggedIn وتنتقل إلى الوجهة الصحيحة حسب الحاجة:
class LoginViewModel : ViewModel() {
private val _uiState = MutableStateFlow(LoginUiState())
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
/* ... */
}
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
if (uiState.isUserLoggedIn) {
// Navigate to the Home screen.
}
...
}
}
}
}
}
يمكن أن يؤدي استخدام الأحداث إلى تشغيل تعديلات الحالة
قد يؤدي استخدام أحداث معيّنة في ViewModel في واجهة المستخدم إلى تعديلات أخرى في حالة واجهة المستخدم. على سبيل المثال، عند عرض رسائل مؤقتة على الشاشة لإعلام المستخدم بحدوث أمر ما، يجب أن تُعلم واجهة المستخدم ViewModel لتفعيل تحديث آخر للحالة بعد عرض الرسالة على الشاشة. يمكن التعامل مع الحدث الذي يحدث عندما يستهلك المستخدم الرسالة (عن طريق إغلاقها أو بعد انتهاء المهلة) على أنّه "بيانات أدخلها المستخدم"، وبالتالي يجب أن يكون ViewModel على دراية بذلك. في هذه الحالة، يمكن تصميم حالة واجهة المستخدم على النحو التالي:
// Models the UI state for the Latest news screen.
data class LatestNewsUiState(
val news: List<News> = emptyList(),
val isLoading: Boolean = false,
val userMessage: String? = null
)
سيعدّل ViewModel حالة واجهة المستخدم على النحو التالي عندما تتطلّب منطق النشاط التجاري عرض رسالة عابرة جديدة للمستخدم:
class LatestNewsViewModel(/* ... */) : ViewModel() {
private val _uiState = MutableStateFlow(LatestNewsUiState(isLoading = true))
val uiState: StateFlow<LatestNewsUiState> = _uiState
fun refreshNews() {
viewModelScope.launch {
// If there isn't internet connection, show a new message on the screen.
if (!internetConnection()) {
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = "No Internet connection")
}
return@launch
}
// Do something else.
}
}
fun userMessageShown() {
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = null)
}
}
}
لا تحتاج ViewModel إلى معرفة كيفية عرض واجهة المستخدم للرسالة على الشاشة، بل تعرف فقط أنّ هناك رسالة مستخدم يجب عرضها. بعد عرض الرسالة المؤقتة، يجب أن تُعلم واجهة المستخدم ViewModel بذلك، ما يؤدي إلى تعديل آخر لحالة واجهة المستخدم من أجل محو السمة userMessage:
class LatestNewsActivity : AppCompatActivity() {
private val viewModel: LatestNewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
uiState.userMessage?.let {
// TODO: Show Snackbar with userMessage.
// Once the message is displayed and
// dismissed, notify the ViewModel.
viewModel.userMessageShown()
}
...
}
}
}
}
}
على الرغم من أنّ الرسالة مؤقتة، إلا أنّ حالة واجهة المستخدم تمثّل بدقة ما يتم عرضه على الشاشة في كل لحظة. إما أن يتم عرض رسالة المستخدم أو لا يتم عرضها.
أحداث التنقّل
يوضّح قسم يمكن أن تؤدي الأحداث المستهلكة إلى تشغيل عمليات تعديل الحالة كيفية استخدام حالة واجهة المستخدم لعرض رسائل المستخدم على الشاشة. أحداث التنقّل هي أيضًا نوع شائع من الأحداث في تطبيق Android.
إذا تم تشغيل الحدث في واجهة المستخدم لأنّ المستخدم نقر على زر، ستتولّى واجهة المستخدم ذلك من خلال استدعاء أداة التحكّم في التنقّل.
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
binding.helpButton.setOnClickListener {
navController.navigate(...) // Open help screen
}
}
}
إذا كان إدخال البيانات يتطلب بعض عمليات التحقّق من صحة منطق النشاط التجاري قبل الانتقال، يجب أن يعرض ViewModel هذه الحالة لواجهة المستخدم. ستستجيب واجهة المستخدم لتغيير الحالة هذا وتنتقل إلى الصفحة المناسبة. يتناول قسم معالجة أحداث ViewModel حالة الاستخدام هذه. في ما يلي رمز مشابه:
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
if (uiState.isUserLoggedIn) {
// Navigate to the Home screen.
}
...
}
}
}
}
}
في المثال أعلاه، يعمل التطبيق على النحو المتوقّع لأنّه لن يتم الاحتفاظ بالوجهة الحالية، وهي Login، في سجلّ الرجوع. ولا يمكن للمستخدمين الرجوع إلى هذه الشاشة إذا ضغطوا على زر الرجوع. ومع ذلك، في الحالات التي قد يحدث فيها ذلك، سيتطلّب الحل منطقًا إضافيًا.
أحداث التنقّل عندما يتم الاحتفاظ بالوجهة في سجلّ الرجوع
عندما يضبط ViewModel حالة معيّنة تؤدي إلى إنشاء حدث تنقّل من الشاشة (أ) إلى الشاشة (ب) مع إبقاء الشاشة (أ) في سجلّ التصفّح الخلفي، قد تحتاج إلى منطق إضافي لعدم الانتقال تلقائيًا إلى الشاشة (ب). لتنفيذ ذلك، يجب توفُّر حالة إضافية تشير إلى ما إذا كان يجب أن تراعي واجهة المستخدم الانتقال إلى الشاشة الأخرى أم لا. في العادة، يتم الاحتفاظ بهذه الحالة في واجهة المستخدم لأنّ منطق التنقّل يخص واجهة المستخدم، وليس ViewModel. لتوضيح ذلك، لنأخذ حالة الاستخدام التالية كمثال.
لنفترض أنّك في مسار التسجيل في تطبيقك. في شاشة التحقّق من تاريخ الميلاد، عندما يُدخل المستخدم تاريخًا، يتم التحقّق من صحة التاريخ بواسطة ViewModel عندما ينقر المستخدم على الزر "متابعة". تفوّض ViewModel منطق التحقّق من الصحة إلى طبقة البيانات. إذا كان التاريخ صالحًا، ينتقل المستخدم إلى الشاشة التالية. كميزة إضافية، يمكن للمستخدمين الرجوع إلى شاشات التسجيل المختلفة أو الانتقال منها في حال أرادوا تغيير بعض البيانات. لذلك، يتم الاحتفاظ بجميع الوجهات في مسار التسجيل في حزمة الخلفية نفسها. في ضوء هذه المتطلبات، يمكنك تنفيذ هذه الشاشة على النحو التالي:
// Key that identifies the `validationInProgress` state in the Bundle
private const val DOB_VALIDATION_KEY = "dobValidationKey"
class DobValidationFragment : Fragment() {
private var validationInProgress: Boolean = false
private val viewModel: DobValidationViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = // ...
validationInProgress = savedInstanceState?.getBoolean(DOB_VALIDATION_KEY) ?: false
binding.continueButton.setOnClickListener {
viewModel.validateDob()
validationInProgress = true
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.collect { uiState ->
// Update other parts of the UI ...
// If the input is valid and the user wants
// to navigate, navigate to the next screen
// and reset `validationInProgress` flag
if (uiState.isDobValid && validationInProgress) {
validationInProgress = false
navController.navigate(...) // Navigate to next screen
}
}
}
return binding
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(DOB_VALIDATION_KEY, validationInProgress)
}
}
إنّ عملية التحقّق من تاريخ الميلاد هي منطق تجاري يتولّى ViewModel مسؤوليتها. في معظم الأحيان، ستفوّض ViewModel هذه المنطق إلى طبقة البيانات. إنّ منطق توجيه المستخدم إلى الشاشة التالية هو منطق واجهة المستخدم لأنّ هذه المتطلبات قد تتغيّر حسب إعدادات واجهة المستخدم. على سبيل المثال، قد لا تريد الانتقال تلقائيًا إلى شاشة أخرى على جهاز لوحي إذا كنت تعرض عدة خطوات تسجيل في الوقت نفسه. يتم تنفيذ هذه الوظيفة من خلال المتغيّر validationInProgress في الرمز البرمجي أعلاه، كما يتم تحديد ما إذا كان يجب أن تنتقل واجهة المستخدم تلقائيًا كلما كان تاريخ الميلاد صالحًا وأراد المستخدم الانتقال إلى خطوة التسجيل التالية.
اقتراحات مخصصة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون JavaScript غير مفعّلة.
- طبقة واجهة المستخدم
- عناصر الاحتفاظ بالحالة وحالة واجهة المستخدم {:#mad-arch}
- دليل هندسة التطبيق