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

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

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

يُعدّ Material Design نظام التصميم المُقترَح لاستخدامه في تصميم تطبيقات Android.

بالنسبة إلى التطبيقات المستندة إلى View، تتوفّر ثلاثة إصدارات من 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) إذا كان نظام تصميم تطبيقك يسمح بذلك. تتوفّر أدلة نقل البيانات لكلٍّ من "العروض" و"الإنشاء":

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

تستخدِم كل عيّنات Jetpack Compose مظهرًا مخصّصًا لتطبيق Compose تم إنشاؤه استنادًا إلى MaterialTheme.

اطّلِع على أنظمة التصميم في Compose ونقل مظاهر XML إلى Compose لمزيد من المعلومات.

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

اختبار واجهة المستخدم المختلطة/طريقة العرض

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

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

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 في ميزة "الكتابة الذكية"

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

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

على سبيل المثال، إذا كانت العناصر القابلة للتجميع مستضافة في نشاط، 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 على دورة حياة الوجهة، ويتم cleared عند إزالة الوجهة من الحزمة الخلفية. في المثال التالي، عندما ينتقل المستخدم إلى شاشة الملف الشخصي، يتم إنشاء مثيل جديد لعنصر GreetingViewModel.

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

مصدر بيانات الحالة

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

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

إنشاء المحتوى كمصدر للحقائق

استخدِم العنصر SideEffect composable لنشر حالة 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، ننصحك بتغليف الحالة في كائنات mutableStateOf لجعلها آمنة لسلسلة المهام في Compose. في حال استخدام هذا النهج، يتم تبسيط الدوالّ القابلة للتجميع لأنّه لم يعُد لديها مصدر المعلومات، ولكنّ نظام View يحتاج إلى تعديل الحالة المتغيّرة وViews التي تستخدم هذه الحالة.

في المثال التالي، يحتوي العنصر 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
    }
}

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

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

في أداة "الإنشاء"، تصبح عناصر واجهة المستخدم المشترَكة عناصر قابلة للتجميع يمكن إعادة استخدامها في التطبيق، بغض النظر عمّا إذا كان العنصر مصمّمًا باستخدام 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 قابلة للنفخ والاستخدام، مثل طريقة العرض التقليدية. انظر مثالاً على ذلك من خلال ربط طريقة العرض أدناه:

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: إنه موضوع في المقطوعة الموسيقية أو ليس كذلك.

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

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

غالبًا ما يكون لدى عناصر 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 لإنشاء واجهات مستخدم قابلة للتكيّف.

التمرير المدمَج مع طرق العرض

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

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

العناصر القابلة للإنشاء باللغة RecyclerView تحقّق أداءً جيدًا منذ الإصدار 1.3.0-alpha02 من RecyclerView. يُرجى التأكّد من استخدام الإصدار 1.3.0-alpha02 على الأقل من RecyclerView للاطّلاع على هذه المزايا.

WindowInsets إمكانية التشغيل التفاعلي مع "الملف الشخصي على Google"

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

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

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

لمزيد من المعلومات، اطّلِع على مستندات WindowInsets في Compose.