ملاحظات دیگر

درحالی‌که انتقال از «نماها» به «ترکیب» صرفاً مربوط به رابط کاربری است، موارد زیادی وجود دارد که باید برای انجام یک انتقال ایمن و افزایشی درنظر گرفته شوند. این صفحه حاوی نکاتی است که باید هنگام انتقال برنامه مبتنی بر «نما» به Compose درنظر بگیرید.

درحال انتقال زمینه برنامه

‫Material Design سیستم طراحی توصیه‌شده برای زمینه‌بندی برنامه‌های Android است.

برای برنامه‌های مبتنی بر «نما»، سه نسخه از «مواد» دردسترس است:

  • «طراحی سه‌بعدی ۱» بااستفاده از کتابخانه AppCompat (یعنی Theme.AppCompat.*)
  • «طراحی سه‌بعدی ۲» بااستفاده از کتابخانه MDC-Android (یعنی Theme.MaterialComponents.*)
  • «طراحی سه‌بعدی ۳» بااستفاده از MDC-Android کتابخانه (یعنی Theme.Material3.*)

برای برنامه‌های Compose، دو نسخه از Material دردسترس است:

  • «طراحی سه‌بعدی» نسخه ۲ بااستفاده از کتابخانه Compose Material (یعنی androidx.compose.material.MaterialTheme)
  • «طراحی سه‌بعدی ۳» بااستفاده از کتابخانه Compose Material 3 (یعنی androidx.compose.material3.MaterialTheme)

توصیه می‌کنیم اگر سیستم طراحی برنامه‌تان در موقعیتی است که می‌تواند از جدیدترین نسخه (Material 3) استفاده کند، این کار را انجام دهید. راهنماهای انتقال برای هر دو «نماها» و «ترکیب» دردسترس است:

هنگام ایجاد صفحه‌های جدید در Compose، صرف‌نظر از اینکه از کدام نسخه Material Design استفاده می‌کنید، مطمئن شوید که قبل‌از هر ترکیب‌شونده‌ای که از کتابخانه‌های Compose Material واسط کاربر منتشر می‌کند، MaterialTheme را اعمال کنید. عناصر Material ‏ (Button،‏ Text، و غیره) به وجود MaterialTheme بستگی دارند و بدون آن، عملکردشان تعریف‌نشده است.

همه نمونه‌های Jetpack Compose از زمینه سفارشی Compose ساخته‌شده روی MaterialTheme استفاده می‌کنند.

برای کسب اطلاعات بیشتر، سیستم‌های طراحی در Compose و انتقال زمینه‌های XML به Compose را ببینید.

اگر در برنامه‌تان از عنصر «پیمایش» استفاده می‌کنید، برای کسب اطلاعات بیشتر، پیمایش از Compose در برنامه مبتنی بر «تکه» و انتقال «پیمایش Jetpack» به «پیمایش Compose» را ببینید.

واسط کاربر ترکیبی «نوشتن/نماها» را آزمایش کنید

پس‌از انتقال بخش‌هایی از برنامه به Compose، آزمایش کردن برای اطمینان از اینکه چیزی را خراب نکرده‌اید بسیار مهم است.

وقتی فعالیت یا تکه‌ای از Compose استفاده می‌کند، باید به‌جای استفاده از ActivityScenarioRule از createAndroidComposeRule استفاده کنید. ‫createAndroidComposeRule با ActivityScenarioRule ادغام می‌شود که 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 کار می‌کنند. اگر برنامه از انواع دیگر الگوهای معماری مثل «ارائه‌دهنده نمای مدل» (MVP) استفاده می‌کند، توصیه می‌کنیم قبل‌از یا درحین استفاده از Compose، آن بخش از واسط کاربر را به UDF انتقال دهید.

استفاده از ViewModel در «نوشتن»

اگر از کتابخانه عناصر معماری ViewModel استفاده می‌کنید، می‌توانید با فراخوانی تابع viewModel() ، همان‌طور که در Compose و کتابخانه‌های دیگر توضیح داده شده است، از هر عنصر ترکیبی به ViewModel دسترسی پیدا کنید.

هنگام استفاده از Compose، مراقب باشید که از نوع ViewModel یکسان در ترکیب‌پذیرهای مختلف استفاده نکنید زیرا عناصر 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") ?: "")
        }
    }
}

منبع حقیقت وضعیت

وقتی «نگارش» را در بخشی از واسط کاربر به‌کار می‌برید، ممکن است «نگارش» و کد سیستم 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
}

برای اطلاعات بیشتر، عوارض جانبی در «نوشتن» را ببینید.

مشاهده سیستم به‌عنوان منبع حقیقت

اگر «سیستم View» مالک وضعیت است و آن را با Compose هم‌رسانی می‌کند، توصیه می‌کنیم وضعیت را در mutableStateOf شیء بپیچید تا برای Compose نخ‌امن شود. اگر از این رویکرد استفاده کنید، توابع ترکیب‌پذیر ساده‌تر می‌شوند زیرا دیگر منبع حقیقت ندارند، اما سیستم «نما» باید وضعیت تغییرپذیر و «نماهایی» را که از آن وضعیت استفاده می‌کنند به‌روز کند.

در مثال زیر، 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 سفارشی دارد، ممکن است لازم باشد از آن در هر دو صفحه «نوشتن» و «نمای مبتنی بر View» استفاده کنید.

در Compose، عناصر رابط کاربری هم‌رسانی‌شده به عناصر ترکیبی تبدیل می‌شوند که می‌توانند در سراسر برنامه مورد استفاده مجدد قرار گیرند، صرف‌نظر از اینکه عنصر بااستفاده از XML سبک‌بندی شده باشد یا نمای سفارشی باشد. برای مثال، برای تماس سفارشی‌تان با عنصر کنش Button، یک CallToActionButton ترکیب‌شدنی ایجاد می‌کنید.

برای استفاده از عنصر ترکیبی در صفحه‌های مبتنی بر View، یک پوشش نمای سفارشی ایجاد کنید که از 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 وجود ندارد: یا در «ترکیب» است یا نیست.

با جدا کردن مدیریت حالت و منطق ارائه، می‌توانید نحوه نمایش محتوا را به‌عنوان تبدیل حالت به میانای کاربر آزادانه‌تر تغییر دهید. امکان بالابردن وضعیت درصورت نیاز باعث می‌شود عناصر ترکیبی بازکاربردپذیرتر شوند، زیرا مالکیت وضعیت انعطاف‌پذیرتر است.

ترویج کردن عناصر کپسولی و قابل‌استفاده مجدد

عناصر View اغلب ایده‌ای از محل زندگی خود دارند: داخل Activity، Dialog، Fragment یا جایی در سلسله مراتب View دیگر. زیرا آن‌ها اغلب از فایل‌های چیدمان ایستا باد می‌شوند، ساختار کلی View معمولاً بسیار سخت است. این امر منجر به جفت‌سازی محکم‌تر می‌شود و تغییر یا استفاده مجدد از View را دشوارتر می‌کند.

برای مثال، View سفارشی ممکن است فرض کند نمای فرزندی از نوع خاصی با شناسه خاصی دارد و ویژگی‌های آن را مستقیماً در پاسخ به کنش خاصی تغییر دهد. این کار آن View عنصر را به‌شدت با هم جفت می‌کند: اگر View سفارشی نتواند فرزند را پیدا کند، ممکن است ازکار بیفتد یا خراب شود، و فرزند احتمالاً نمی‌تواند بدون View سفارشی والد دوباره استفاده شود.

این مشکل در «نوشتن» با عناصر ترکیبی قابل‌استفاده مجدد کمتر است. والدین می‌توانند به‌راحتی وضعیت و برگشتی‌ها را مشخص کنند، بنابراین می‌توانید بدون نیاز به دانستن مکان دقیق استفاده از عناصر ترکیبی، عناصر ترکیبی قابل‌استفاده مجدد بنویسید.

@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 قابلیت تعامل با «نماها»

وقتی صفحه‌نمایش شما هم «نماها» و هم کد «ترکیب» را در سلسله‌مراتب یکسانی دارد، ممکن است لازم باشد پیش‌فرض‌های حاشیه را ملغی کنید. در این مورد، باید به‌طور واضح مشخص کنید کدام‌یک باید حاشیه‌ها را مصرف کند و کدام‌یک باید آن‌ها را نادیده بگیرد.

برای مثال، اگر چیدمان بیرونی‌ترین شما چیدمان Android View باشد، باید حاشیه‌های داخلی را در سیستم View مصرف کنید و آن‌ها را برای Compose نادیده بگیرید. یا اگر چیدمان بیرونی‌ترین شما قابل ترکیب است، باید حاشیه‌های داخلی را در Compose مصرف کنید و عناصر قابل ترکیب AndroidView را براساس آن حاشیه اضافه کنید.

به‌طور پیش‌فرض، هر ComposeView همه درونه‌ها را در سطح مصرف WindowInsetsCompat مصرف می‌کند. برای تغییر دادن این رفتار پیش‌فرض، ComposeView.consumeWindowInsets را روی false تنظیم کنید.

برای اطلاعات بیشتر، WindowInsets در مستندات «نگارش» را بخوانید.