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

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

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

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

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

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

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

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

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

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

الانتقال المتداخل باستخدام "العروض"

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

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

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

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

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

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

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

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