ملاحظات دیگر

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

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

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

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

  • «طراحی مواد ۱» بااستفاده از کتابخانه 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 استفاده می‌کنید، مطمئن شوید که قبل‌از هر composable که واسط کاربر را از کتابخانه‌های Compose Material منتشر می‌کند، یک MaterialTheme اعمال کنید. مؤلفه‌های Material (Button،‏ Text، و غیره) به وجود MaterialTheme بستگی دارند و رفتار آن‌ها بدون آن تعریف‌نشده است.

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

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

اگر از مؤلفه پیمایش در برنامه‌تان استفاده می‌کنید، برای اطلاعات بیشتر به پیمایش از Compose در برنامه مبتنی بر Fragment و انتقال Jetpack Navigation به Navigation Compose مراجعه کنید.

آزمایش کردن واسط کاربر ترکیبی Compose/Views

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

وقتی فعالیت یا قطعه‌ای از Compose استفاده می‌کند، باید از createAndroidComposeRule به‌جای ActivityScenarioRule استفاده کنید. createAndroidComposeRule با ComposeTestRule ادغام می‌شود ActivityScenarioRule که به شما امکان می‌دهد 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()
    }
}

برای کسب اطلاعات بیشتر درباره آزمایش، به آزمایش کردن چیدمان Compose مراجعه کنید. برای تعامل‌پذیری با چارچوب‌های آزمایش میانای کاربر، تعامل‌پذیری با Espresso و تعامل‌پذیری با UiAutomator را ببینید.

ادغام Compose با معماری برنامه موجود شما

الگوهای معماری جریان داده یک‌سویه (UDF) به‌طور یکپارچه با Compose کار می‌کنند. اگر برنامه به‌جای آن از انواع دیگر الگوهای معماری مانند Model View Presenter (MVP) استفاده می‌کند، توصیه می‌کنیم آن بخش از واسط کاربر را قبل‌از یا درحین استفاده از Compose به UDF منتقل کنید.

Using a ViewModel in Compose

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

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

برای مثال، اگر ترکیب‌پذیرها در یک فعالیت میزبانی شوند، viewModel() همیشه همان نمونه‌ای را برمی‌گرداند که فقط وقتی فعالیت تمام می‌شود پاک می‌شود. در مثال زیر، به همان کاربر («user1») دو بار خوش‌آمد گفته می‌شود زیرا همان نمونه GreetingViewModel در همه composableهای تحت فعالیت میزبان مجدداً استفاده می‌شود. اولین نمونه 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
    }
}

As navigation graphs also scope ViewModel elements, composables that are a destination in a navigation graph have a different instance of the 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 استفاده کنید. در این مورد، منبع حقیقت در یک composable نگهداری می‌شود که به‌روزرسانی‌های وضعیت را ارسال می‌کند.

به‌عنوان مثال، کتابخانه تحلیل داده‌های شما ممکن است به شما اجازه دهد جمعیت کاربرانتان را با پیوست کردن فراداده سفارشی (ویژگی‌های کاربر در این مثال) به همه رویدادهای تحلیل داده‌های بعدی تقسیم‌بندی کنید. برای انتقال نوع کاربر کاربر فعلی به کتابخانه تجزیه‌وتحلیل خود، از 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 باید وضعیت تغییرپذیر و 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 سبک‌بندی شده باشد یا یک نمای سفارشی باشد. برای مثال، برای مؤلفه سفارشی دعوت به اقدام Button، یک CallToActionButton composable ایجاد می‌کنید.

برای استفاده از composable در صفحه‌های مبتنی بر 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 است. While other pieces of code may change the visibility of a View, only the View itself really knows what its current visibility is. The logic for ensuring that a View is visible can be error prone, and is often tied to the View itself.

درمقابل، Compose نمایش ترکیب‌شدنی‌های کاملاً متفاوت را بااستفاده از منطق شرطی در Kotlin آسان می‌کند:

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

طبق طراحی، CautionIcon نیازی ندارد بداند یا اهمیت دهد که چرا نمایش داده می‌شود، و هیچ مفهومی از visibility وجود ندارد: یا در «ترکیب‌بندی» است، یا نیست.

با جدا کردن تمیز منطق مدیریت حالت و ارائه، می‌توانید آزادانه‌تر نحوه نمایش محتوا را به‌عنوان تبدیل حالت به میانای کاربر تغییر دهید. Being able to hoist state when needed also makes composables more reusable, since state ownership is more flexible.

تبلیغ مؤلفه‌های کپسوله‌شده و قابل‌استفاده مجدد

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

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

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

@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

Composables در RecyclerView از نسخه 1.3.0-alpha02 RecyclerView عملکرد خوبی دارد. مطمئن شوید که حداقل از نسخه 1.3.0-alpha02 RecyclerView استفاده می‌کنید تا بتوانید این مزایا را ببینید.

‫WindowInsets تعامل‌پذیری با Views

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

برای مثال، اگر بیرونی‌ترین چیدمان شما چیدمان Android View است، باید از تورفتگی‌ها در سیستم View استفاده کنید و آن‌ها را برای Compose نادیده بگیرید. یا اگر بیرونی‌ترین چیدمان شما یک composable است، باید از درج‌ها در Compose استفاده کنید و AndroidView composables را به‌طور مناسب پد کنید.

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

برای اطلاعات بیشتر، سند WindowInsets در Compose را بخوانید.