ملاحظات دیگر

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

انتقال قالب برنامه شما

طراحی متریال، سیستم طراحی پیشنهادی برای تم‌بندی برنامه‌های اندروید است.

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

  • طراحی متریال ۱ با استفاده از کتابخانه 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) استفاده کنید. راهنماهای مهاجرت برای Views و Compose در دسترس هستند:

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

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

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

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

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

پس از انتقال بخش‌هایی از برنامه به 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()
    }
}

برای کسب اطلاعات بیشتر در مورد آزمایش، به بخش «آزمایش طرح‌بندی Compose» مراجعه کنید. برای قابلیت همکاری با چارچوب‌های آزمایش رابط کاربری، به بخش «قابلیت همکاری با Espresso» و «قابلیت همکاری با UiAutomator» مراجعه کنید.

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

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

استفاده از ViewModel در Compose

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

هنگام استفاده از Compose، مراقب باشید که از نوع ViewModel یکسانی در composable های مختلف استفاده نکنید، زیرا عناصر ViewModel از محدوده‌های چرخه حیات View پیروی می‌کنند. محدوده می‌تواند activity میزبان، fragment یا گراف navigation باشد، در صورتی که از کتابخانه Navigation استفاده شود.

برای مثال، اگر composableها در یک activity میزبانی شوند، viewModel() همیشه همان نمونه‌ای را برمی‌گرداند که فقط با پایان activity پاک می‌شود. در مثال زیر، به همان کاربر ("user1") دو بار خوشامدگویی می‌شود زیرا همان نمونه GreetingViewModel در تمام composableهای تحت activity میزبان دوباره استفاده می‌شود. اولین نمونه ViewModel ایجاد شده در composableهای دیگر دوباره استفاده می‌شود.

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 دسترسی دارند، composableهایی که در گراف ناوبری مقصد هستند، نمونه‌ی متفاوتی از ViewModel دارند. در این حالت، ViewModel به چرخه‌ی حیات مقصد دسترسی دارد و هنگامی که مقصد از backstack حذف می‌شود، پاک می‌شود. در مثال زیر، هنگامی که کاربر به صفحه‌ی Profile می‌رود، یک نمونه‌ی جدید از GreetingViewModel ایجاد می‌شود.

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

منبع حقیقت دولتی

وقتی در بخشی از رابط کاربری از Compose استفاده می‌کنید، ممکن است Compose و کد سیستم View نیاز به اشتراک‌گذاری داده‌ها داشته باشند. در صورت امکان، توصیه می‌کنیم آن حالت مشترک را در کلاس دیگری که از بهترین شیوه‌های UDF مورد استفاده هر دو پلتفرم پیروی می‌کند، کپسوله‌سازی کنید؛ به عنوان مثال، در یک ViewModel که جریانی از داده‌های مشترک را برای انتشار به‌روزرسانی‌های داده‌ها در معرض نمایش قرار می‌دهد.

با این حال، اگر داده‌هایی که قرار است به اشتراک گذاشته شوند، قابل تغییر باشند یا به شدت به یک عنصر رابط کاربری وابسته باشند، این امر همیشه ممکن نیست. در این صورت، یک سیستم باید منبع حقیقت باشد و آن سیستم باید هرگونه به‌روزرسانی داده را با سیستم دیگر به اشتراک بگذارد. به عنوان یک قاعده کلی، منبع حقیقت باید متعلق به عنصری باشد که به ریشه سلسله مراتب رابط کاربری نزدیک‌تر است.

آهنگسازی به عنوان منبع حقیقت

از SideEffect composable برای انتشار حالت 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 مالک state است و آن را با Compose به اشتراک می‌گذارد، توصیه می‌کنیم state را در اشیاء mutableStateOf قرار دهید تا برای Compose از نظر thread-safe باشد. اگر از این رویکرد استفاده کنید، توابع composable ساده می‌شوند زیرا دیگر منبع حقیقت را ندارند، اما سیستم View باید state قابل تغییر و Viewهایی که از آن state استفاده می‌کنند را به‌روزرسانی کند.

در مثال زیر، یک 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 فراخوان سفارشی خود ایجاد می‌کنید.

برای استفاده از composable در صفحات مبتنی بر View، یک view wrapper سفارشی ایجاد کنید که از AbstractComposeView امتداد یابد. در Composable Content لغو شده آن، composable ایجاد شده را در قالب 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 دارای وضعیت (stateful) است. یک View فیلدهایی را مدیریت می‌کند که علاوه بر نحوه نمایش ، آنچه را که باید نمایش داده شود، توصیف می‌کنند. هنگامی که یک View به Compose تبدیل می‌کنید، به جداسازی داده‌های رندر شده برای دستیابی به یک جریان داده یک طرفه توجه کنید، همانطور که در بخش state hoisting بیشتر توضیح داده شده است.

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

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

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

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

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

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

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

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

این مشکل در Compose با Composableهای قابل استفاده مجدد کمتر است. والدها می‌توانند به راحتی state و callbackها را مشخص کنند، بنابراین می‌توانید 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 برای ساخت رابط‌های کاربری تطبیقی ​​ارائه می‌دهد، به پشتیبانی از اندازه‌های مختلف نمایشگر مراجعه کنید.

پیمایش تو در تو با Views

برای اطلاعات بیشتر در مورد نحوه فعال کردن تعامل پیمایش تو در تو بین عناصر نمای پیمایش‌پذیر و ترکیبات پیمایش‌پذیر، که در هر دو جهت تو در تو هستند، بخش تعامل پیمایش تو در تو را مطالعه کنید.

نوشتن در RecyclerView

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

تعامل WindowInsets با Views

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

برای مثال، اگر بیرونی‌ترین طرح‌بندی شما یک طرح‌بندی Android View است، باید insetها را در سیستم View مصرف کنید و برای Compose آنها را نادیده بگیرید. از طرف دیگر، اگر بیرونی‌ترین طرح‌بندی شما یک composable است، باید insetها را در Compose مصرف کنید و composableهای AndroidView را بر اساس آن padd کنید.

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

برای اطلاعات بیشتر، مستندات WindowInsets در Compose را مطالعه کنید.

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}