اعتبارات أخرى

مع أنّ عملية نقل البيانات من Views إلى Compose مرتبطة بشكل أساسي بواجهة المستخدم، هناك العديد من الأمور التي يجب أخذها في الاعتبار لإجراء عملية نقل بيانات آمنة وتدريجية. تحتوي هذه الصفحة على بعض الاعتبارات التي يجب مراعاتها أثناء نقل تطبيقك المستند إلى View إلى Compose.

نقل مظهر تطبيقك

‫Material Design هو نظام التصميم الذي يُنصح به لتطبيق السمات على تطبيقات Android.

بالنسبة إلى التطبيقات المستندة إلى العرض، تتوفّر ثلاثة إصدارات من Material:

  • ‫Material Design 1 باستخدام مكتبة AppCompat (أي Theme.AppCompat.*)
  • ‫Material Design 2 باستخدام مكتبة MDC-Android (أي Theme.MaterialComponents.*)
  • ‫Material Design 3 باستخدام مكتبة MDC-Android (أي Theme.Material3.*)

بالنسبة إلى تطبيقات Compose، يتوفّر إصداران من Material:

  • ‫Material Design 2 باستخدام مكتبة Compose Material (أي androidx.compose.material.MaterialTheme)
  • ‫Material Design 3 باستخدام مكتبة Compose Material 3 (أي androidx.compose.material3.MaterialTheme)

ننصحك باستخدام أحدث إصدار (Material 3) إذا كان نظام تصميم تطبيقك يتيح ذلك، وتتوفّر أدلة نقل البيانات لكل من Views وCompose:

عند إنشاء شاشات جديدة في Compose، بغض النظر عن إصدار Material Design الذي تستخدمه، احرص على تطبيق MaterialTheme قبل أي عناصر قابلة للإنشاء تنبعث منها واجهة المستخدم من مكتبات Compose Material. تعتمد مكوّنات Material (Button وText وما إلى ذلك) على توفّر MaterialTheme، ولا يمكن تحديد سلوكها بدونها.

تستخدم جميع أمثلة Jetpack Compose تصميمًا مخصّصًا لـ Compose يستند إلى MaterialTheme.

لمزيد من المعلومات، يمكنك الاطّلاع على أنظمة التصميم في Compose ونقل سمات XML إلى Compose.

إذا كنت تستخدم مكوّن التنقّل في تطبيقك، يمكنك الاطّلاع على التنقّل باستخدام Compose - إمكانية التشغيل التفاعلي ونقل Jetpack Navigation إلى Navigation Compose للحصول على مزيد من المعلومات.

اختبار واجهة المستخدم المختلطة التي تتضمّن Compose وViews

بعد نقل أجزاء من تطبيقك إلى Compose، يصبح الاختبار أمرًا بالغ الأهمية للتأكّد من أنّك لم تُحدث أي مشاكل.

عندما يستخدم نشاط أو جزء Compose، عليك استخدام createAndroidComposeRule بدلاً من ActivityScenarioRule. تتكامل createAndroidComposeRuleActivityScenarioRule مع ComposeTestRule التي تتيح لك اختبار رمز Compose ورمز View في الوقت نفسه.

class MyActivityTest {
    @Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule<MyActivity>()

    @Test
    fun testGreeting() {
        val greeting = InstrumentationRegistry.getInstrumentation()
            .targetContext.resources.getString(R.string.greeting)

        composeTestRule.onNodeWithText(greeting).assertIsDisplayed()
    }
}

يمكنك الاطّلاع على اختبار تخطيط "الكتابة الذكية" لمعرفة المزيد عن الاختبار. للحصول على معلومات حول إمكانية التشغيل التفاعلي مع أُطر اختبار واجهة المستخدم، راجِع إمكانية التشغيل التفاعلي مع Espresso وإمكانية التشغيل التفاعلي مع UiAutomator.

دمج Compose مع بنية تطبيقك الحالية

تعمل أنماط بنية تدفّق البيانات أحادي الاتجاه (UDF) بسلاسة مع Compose. إذا كان التطبيق يستخدم أنواعًا أخرى من أنماط التصميم، مثل Model View Presenter (MVP)، ننصحك بنقل هذا الجزء من واجهة المستخدم إلى UDF قبل أو أثناء استخدام Compose.

استخدام ViewModel في Compose

إذا كنت تستخدم مكتبة Architecture Components ViewModel، يمكنك الوصول إلى ViewModel من أي عنصر قابل للإنشاء من خلال استدعاء الدالة viewModel()، كما هو موضّح في Compose والمكتبات الأخرى.

عند استخدام Compose، احرص على عدم استخدام النوع ViewModel نفسه في عناصر قابلة للإنشاء مختلفة لأنّ عناصر ViewModel تتّبع نطاقات دورة حياة العرض. سيكون النطاق إما نشاط المضيف أو الجزء أو الرسم البياني للتنقّل في حال استخدام مكتبة Navigation.

على سبيل المثال، إذا كانت العناصر القابلة للإنشاء مستضافة في نشاط، تعرض الدالة viewModel() دائمًا النسخة نفسها التي لا تتم إزالتها إلا عند انتهاء النشاط. في المثال التالي، يتم الترحيب بالمستخدم نفسه ("user1") مرتين لأنّه تتم إعادة استخدام مثيل GreetingViewModel نفسه في جميع العناصر القابلة للإنشاء ضمن النشاط المضيف. تتم إعادة استخدام أول مثيل تم إنشاؤه من ViewModel في عناصر أخرى قابلة للإنشاء.

class GreetingActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MaterialTheme {
                Column {
                    GreetingScreen("user1")
                    GreetingScreen("user2")
                }
            }
        }
    }
}

@Composable
fun GreetingScreen(
    userId: String,
    viewModel: GreetingViewModel = viewModel(  
        factory = GreetingViewModelFactory(userId)  
    )
) {
    val messageUser by viewModel.message.observeAsState("")
    Text(messageUser)
}

class GreetingViewModel(private val userId: String) : ViewModel() {
    private val _message = MutableLiveData("Hi $userId")
    val message: LiveData<String> = _message
}

class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return GreetingViewModel(userId) as T
    }
}

بما أنّ رسومات التنقّل تحدّد أيضًا نطاق عناصر ViewModel، فإنّ الدوال البرمجية القابلة للإنشاء التي تمثّل وجهة في رسم بياني للتنقّل تتضمّن نسخة مختلفة من ViewModel. في هذه الحالة، يكون نطاق ViewModel هو عمر الوجهة، ويتم محوه عند إزالة الوجهة من سجلّ الرجوع. في المثال التالي، عندما ينتقل المستخدم إلى شاشة الملف الشخصي، يتم إنشاء مثيل جديد من GreetingViewModel.

@Composable
fun MyApp() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

مصدر الحقيقة للحالة

عند استخدام Compose في جزء من واجهة المستخدم، من المحتمل أن يحتاج كل من Compose ورمز نظام View إلى مشاركة البيانات. ننصحك، عند الإمكان، بتضمين الحالة المشترَكة في فئة أخرى تتّبع أفضل الممارسات المتعلّقة بالدوال المحدّدة من قِبل المستخدم (UDF) والمستخدَمة في كلتا المنصّتَين، مثلاً في ViewModel تعرض مصدرًا للبيانات المشترَكة لإرسال تعديلات البيانات.

ومع ذلك، لا يكون ذلك ممكنًا دائمًا إذا كانت البيانات التي ستتم مشاركتها قابلة للتغيير أو مرتبطة بشكل وثيق بأحد عناصر واجهة المستخدم. في هذه الحالة، يجب أن يكون أحد النظامَين هو مصدر المعلومات الصحيحة، ويجب أن يشارك هذا النظام أي تعديلات على البيانات مع النظام الآخر. بشكل عام، يجب أن يكون مصدر البيانات الأساسية مملوكًا للعنصر الأقرب إلى جذر التدرّج الهرمي لواجهة المستخدم.

‫Compose هو مصدر المعلومات الموثوق

استخدِم العنصر SideEffect القابل للإنشاء لنشر حالة Compose إلى رمز غير تابع لـ Compose. في هذه الحالة، يتم الاحتفاظ بمصدر البيانات في عنصر قابل للإنشاء، والذي يرسل تحديثات الحالة.

على سبيل المثال، قد تتيح لك مكتبة الإحصاءات تقسيم قاعدة المستخدمين إلى شرائح من خلال إرفاق بيانات وصفية مخصّصة (سمات المستخدم في هذا المثال) بجميع أحداث الإحصاءات اللاحقة. لتحديد نوع المستخدم الحالي في مكتبة الإحصاءات، استخدِم SideEffect لتعديل قيمته.

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        FirebaseAnalytics()
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

لمزيد من المعلومات، يمكنك الاطّلاع على الآثار الجانبية في Compose.

النظر إلى النظام باعتباره مصدر المعلومات الموثوق

إذا كان نظام View يملك الحالة ويشاركها مع Compose، ننصحك بتضمين الحالة في عناصر mutableStateOf لجعلها آمنة للاستخدام في سلاسل محادثات Compose. في حال استخدام هذا الأسلوب، يتم تبسيط الدوال البرمجية القابلة للإنشاء لأنّها لم تعُد تتضمّن مصدر الحقيقة، ولكن يجب أن يحدّث نظام View الحالة القابلة للتغيير والعناصر التي تستخدم هذه الحالة.

في المثال التالي، يحتوي CustomViewGroup على TextView وComposeView مع عنصر TextField قابل للإنشاء داخلها. يجب أن تعرض TextView محتوى ما يكتبه المستخدم في TextField.

class CustomViewGroup @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {

    // Source of truth in the View system as mutableStateOf
    // to make it thread-safe for Compose
    private var text by mutableStateOf("")

    private val textView: TextView

    init {
        orientation = VERTICAL

        textView = TextView(context)
        val composeView = ComposeView(context).apply {
            setContent {
                MaterialTheme {
                    TextField(value = text, onValueChange = { updateState(it) })
                }
            }
        }

        addView(textView)
        addView(composeView)
    }

    // Update both the source of truth and the TextView
    private fun updateState(newValue: String) {
        text = newValue
        textView.text = newValue
    }
}

نقل واجهة المستخدم المشتركة

إذا كنت تنقل البيانات تدريجيًا إلى Compose، قد تحتاج إلى استخدام عناصر واجهة مستخدم مشتركة في كل من Compose ونظام View. على سبيل المثال، إذا كان تطبيقك يتضمّن مكوّن CallToActionButton مخصّصًا، قد تحتاج إلى استخدامه في كل من شاشات Compose والشاشات المستندة إلى View.

في Compose، تصبح عناصر واجهة المستخدم المشترَكة دوال برمجية قابلة للإنشاء ويمكن إعادة استخدامها في جميع أنحاء التطبيق، بغض النظر عمّا إذا كان العنصر مصمّمًا باستخدام XML أو كان طريقة عرض مخصّصة. على سبيل المثال، يمكنك إنشاء دالة CallToActionButton قابلة للإنشاء من أجل مكوّن Button مخصّص لعبارة الحث على اتّخاذ إجراء.

لاستخدام العنصر القابل للإنشاء في الشاشات المستندة إلى طرق العرض، أنشئ برنامج تضمين مخصّصًا لطريقة العرض يمتد من AbstractComposeView. في الدالة Content القابلة للإنشاء التي تم إلغاء تعريفها، ضَع الدالة القابلة للإنشاء التي أنشأتها والمغلَّفة بنسق Compose، كما هو موضّح في المثال أدناه:

@Composable
fun CallToActionButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = MaterialTheme.colorScheme.secondary
        ),
        onClick = onClick,
        modifier = modifier,
    ) {
        Text(text)
    }
}

class CallToActionViewButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {

    var text by mutableStateOf("")
    var onClick by mutableStateOf({})

    @Composable
    override fun Content() {
        YourAppTheme {
            CallToActionButton(text, onClick)
        }
    }
}

لاحظ أنّ المَعلمات القابلة للإنشاء تصبح متغيّرات قابلة للتعديل داخل العرض المخصّص. يؤدي ذلك إلى جعل طريقة العرض CallToActionViewButton المخصّصة قابلة للنفخ والاستخدام، مثل طريقة العرض التقليدية. يمكنك الاطّلاع على مثال على ذلك باستخدام View Binding في ما يلي:

class ViewBindingActivity : ComponentActivity() {

    private lateinit var binding: ActivityExampleBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityExampleBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.callToAction.apply {
            text = getString(R.string.greeting)
            onClick = { /* Do something */ }
        }
    }
}

إذا كان المكوّن المخصّص يحتوي على حالة قابلة للتغيير، اطّلِع على مصدر الحالة الموثوق.

تحديد أولويات فصل الحالة عن العرض

عادةً، تكون View ذات حالة. يدير View الحقول التي تصف ما سيتم عرضه، بالإضافة إلى كيفية عرضه. عند تحويل View إلى Compose، احرص على فصل البيانات التي يتم عرضها لتحقيق تدفّق بيانات أحادي الاتجاه، كما هو موضّح بالتفصيل في نقل الحالة.

على سبيل المثال، يحتوي View على السمة visibility التي توضّح ما إذا كان العنصر مرئيًا أو غير مرئي أو غير متوفّر. هذه سمة متأصّلة في View. في حين أنّ أجزاء أخرى من الرمز البرمجي قد تغيّر نطاق ظهور View، فإنّ View وحده يعرف نطاق ظهوره الحالي. وقد يكون من السهل حدوث أخطاء في منطق ضمان ظهور View، وغالبًا ما يكون مرتبطًا بـ View نفسه.

في المقابل، تسهّل Compose عرض عناصر قابلة للإنشاء مختلفة تمامًا باستخدام منطق شرطي في Kotlin:

@Composable
fun MyComposable(showCautionIcon: Boolean) {
    if (showCautionIcon) {
        CautionIcon(/* ... */)
    }
}

CautionIcon بطبيعته لا يحتاج إلى معرفة سبب عرضه أو الاهتمام به، ولا يوجد مفهوم visibility: إما أن يكون ضمن &quot;المستند المركّب&quot; أو لا يكون.

من خلال الفصل الواضح بين إدارة الحالة ومنطق العرض، يمكنك تغيير طريقة عرض المحتوى بحرية أكبر كتحويل للحالة إلى واجهة مستخدم. كما أنّ إمكانية نقل الحالة إلى أعلى مستوى عند الحاجة تجعل العناصر القابلة للإنشاء أكثر قابلية لإعادة الاستخدام، لأنّ ملكية الحالة تكون أكثر مرونة.

الترويج للمكوّنات المغلفة والقابلة لإعادة الاستخدام

غالبًا ما تتضمّن عناصر View معلومات عن مكانها، أي داخل Activity أو Dialog أو Fragment أو في مكان ما ضمن تسلسل هرمي آخر من View. ونظرًا لأنّها غالبًا ما يتم تضخيمها من ملفات التنسيق الثابتة، يميل الهيكل العام لـ View إلى أن يكون جامدًا للغاية. ويؤدي ذلك إلى ربط أكثر إحكامًا، ما يصعّب تغيير View أو إعادة استخدامه.

على سبيل المثال، قد يفترض عنصر View مخصّص أنّه يتضمّن عرضًا ثانويًا من نوع معيّن بمعرّف معيّن، ويغيّر خصائصه مباشرةً استجابةً لإجراء معيّن. يؤدي ذلك إلى ربط عناصر View هذه ببعضها بإحكام، إذ قد يتعطّل عنصر View المخصّص أو يتوقف عن العمل إذا لم يتمكّن من العثور على العنصر الفرعي، ومن غير المحتمل أن تتم إعادة استخدام العنصر الفرعي بدون العنصر الرئيسي View المخصّص.

لا تشكّل هذه المشكلة عائقًا كبيرًا في Compose مع العناصر القابلة للإنشاء القابلة لإعادة الاستخدام. يمكن للوالدَين تحديد الحالة وعناصر معالجة البيانات بسهولة، ما يتيح لك كتابة عناصر قابلة لإعادة الاستخدام بدون الحاجة إلى معرفة المكان الدقيق الذي سيتم استخدامها فيه.

@Composable
fun AScreen() {
    var isEnabled by rememberSaveable { mutableStateOf(false) }

    Column {
        ImageWithEnabledOverlay(isEnabled)
        ControlPanelWithToggle(
            isEnabled = isEnabled,
            onEnabledChanged = { isEnabled = it }
        )
    }
}

في المثال أعلاه، تكون الأجزاء الثلاثة أكثر تغليفًا وأقل ارتباطًا:

  • لا يحتاج ImageWithEnabledOverlay إلا إلى معرفة حالة isEnabled الحالية، ولا يحتاج إلى معرفة أنّ ControlPanelWithToggle متوفّر أو حتى كيفية التحكّم فيه.

  • لا يعرف ControlPanelWithToggle أنّ ImageWithEnabledOverlay موجود. يمكن أن تكون هناك طريقة واحدة أو أكثر لعرض isEnabled، ولا يلزم تغيير ControlPanelWithToggle.

  • بالنسبة إلى العنصر الأصل، لا يهم مدى تداخل ImageWithEnabledOverlay أو ControlPanelWithToggle. ويمكن أن تتضمّن هذه التغييرات تحريك العناصر أو استبدال المحتوى أو تمريره إلى أطفال آخرين.

يُعرف هذا النمط باسم انعكاس التحكّم، ويمكنك الاطّلاع على مزيد من المعلومات عنه في مستندات CompositionLocal.

التعامل مع تغييرات حجم الشاشة

يُعدّ توفير موارد مختلفة لأحجام النوافذ المختلفة إحدى الطرق الرئيسية لإنشاء تخطيطات View سريعة الاستجابة. على الرغم من أنّ الموارد المؤهَّلة لا تزال خيارًا لاتخاذ قرارات بشأن التنسيق على مستوى الشاشة، يسهّل Compose كثيرًا تغيير التنسيقات بالكامل في الرمز باستخدام منطق شرطي عادي. اطّلِع على استخدام فئات حجم النافذة لمعرفة المزيد.

بالإضافة إلى ذلك، يمكنك الرجوع إلى إتاحة أحجام عرض مختلفة للتعرّف على التقنيات التي يوفّرها Compose لإنشاء واجهات مستخدم قابلة للتكيّف.

التمرير المتداخل باستخدام طرق العرض

لمزيد من المعلومات حول كيفية تفعيل إمكانية التشغيل التفاعلي للتمرير المتداخل بين عناصر View قابلة للتمرير وعناصر قابلة للإنشاء قابلة للتمرير، مع إمكانية التداخل في كلا الاتجاهين، يمكنك الاطّلاع على إمكانية التشغيل التفاعلي للتمرير المتداخل.

إنشاء رسالة في RecyclerView

تتسم العناصر القابلة للإنشاء في RecyclerView بالأداء العالي منذ الإصدار RecyclerView‏1.3.0-alpha02. تأكَّد من استخدام الإصدار 1.3.0-alpha02 على الأقل من RecyclerView للاستفادة من هذه المزايا.

WindowInsets إمكانية التشغيل التفاعلي مع "طرق العرض"

قد تحتاج إلى تجاهل الإعدادات التلقائية للحواف الداخلية عندما تحتوي شاشتك على كلٍّ من طرق العرض ورمز Compose في التسلسل الهرمي نفسه. في هذه الحالة، عليك تحديد أيّهما يجب أن يستخدم الحواف الداخلية وأيّهما يجب أن يتجاهلها.

على سبيل المثال، إذا كان التصميم الخارجي هو تصميم Android View، عليك استخدام الحواف في نظام View وتجاهلها في Compose. بدلاً من ذلك، إذا كان التنسيق الخارجي عبارة عن عنصر قابل للإنشاء، عليك استخدام الحواف الداخلية في Compose، وتعبئة العناصر القابلة للإنشاء AndroidView وفقًا لذلك.

بشكل تلقائي، يستهلك كل ComposeView جميع الحواف الداخلية عند WindowInsetsCompat مستوى الاستهلاك. لتغيير هذا السلوك التلقائي، اضبط قيمة ComposeView.consumeWindowInsets على false.

لمزيد من المعلومات، اطّلِع على مستندات WindowInsets في "الكتابة الذكية".