بهترین شیوه ها را دنبال کنید

ممکن است با مشکلات رایج Compose مواجه شوید. این اشتباهات ممکن است کدی به شما بدهند که به نظر می‌رسد به خوبی اجرا می‌شود، اما می‌تواند به عملکرد رابط کاربری شما آسیب برساند. برای بهینه‌سازی برنامه خود در Compose، بهترین شیوه‌ها را دنبال کنید.

برای به حداقل رساندن محاسبات پرهزینه، remember استفاده کنید

توابع Composable می‌توانند خیلی زیاد اجرا شوند ، به اندازه هر فریم از یک انیمیشن. به همین دلیل، شما باید تا حد امکان محاسبات کمتری در بدنه Composable خود انجام دهید.

یک تکنیک مهم، ذخیره نتایج محاسبات با remember است. به این ترتیب، محاسبه یک بار اجرا می‌شود و می‌توانید هر زمان که به نتایج نیاز داشتید، آنها را بازیابی کنید.

برای مثال، در اینجا کدی وجود دارد که لیستی مرتب شده از نام‌ها را نمایش می‌دهد، اما این مرتب‌سازی را به روشی بسیار پرهزینه انجام می‌دهد:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // DON’T DO THIS
        items(contacts.sortedWith(comparator)) { contact ->
            // ...
        }
    }
}

هر بار که ContactsList دوباره ترکیب می‌شود، کل لیست مخاطبین دوباره مرتب می‌شود، حتی اگر لیست تغییر نکرده باشد. اگر کاربر لیست را اسکرول کند، Composable هر زمان که یک ردیف جدید ظاهر شود، دوباره ترکیب می‌شود.

برای حل این مشکل، لیست را خارج از LazyColumn مرتب کنید و لیست مرتب شده را با remember ذخیره کنید:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    val sortedContacts = remember(contacts, comparator) {
        contacts.sortedWith(comparator)
    }

    LazyColumn(modifier) {
        items(sortedContacts) {
            // ...
        }
    }
}

اکنون، لیست یک بار، زمانی که ContactList برای اولین بار تشکیل می‌شود، مرتب می‌شود. اگر مخاطبین یا مقایسه‌کننده تغییر کنند، لیست مرتب‌شده دوباره تولید می‌شود. در غیر این صورت، composable می‌تواند به استفاده از لیست مرتب‌شده ذخیره‌شده ادامه دهد.

از کلیدهای طرح‌بندی تنبل استفاده کنید

طرح‌بندی‌های تنبل به طور مؤثر از آیتم‌ها دوباره استفاده می‌کنند و فقط در صورت لزوم آنها را بازسازی یا ترکیب مجدد می‌کنند. با این حال، می‌توانید به بهینه‌سازی طرح‌بندی‌های تنبل برای ترکیب مجدد کمک کنید.

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

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes
        ) { note ->
            NoteRow(note)
        }
    }
}

با این حال، مشکلی در این کد وجود دارد. فرض کنید نت پایینی تغییر کرده است. اکنون این نت، جدیدترین نت تغییر یافته است، بنابراین به بالای لیست می‌رود و هر نت دیگر یک رتبه پایین‌تر می‌رود.

بدون کمک شما، Compose متوجه نمی‌شود که موارد بدون تغییر فقط در لیست جابجا می‌شوند. در عوض، Compose فکر می‌کند که "مورد ۲" قدیمی حذف شده و یک مورد جدید برای مورد ۳، مورد ۴ و تا پایین لیست ایجاد شده است. نتیجه این است که Compose تمام موارد موجود در لیست را دوباره ترکیب می‌کند، حتی اگر فقط یکی از آنها در واقع تغییر کرده باشد.

راه حل اینجا ارائه کلیدهای آیتم است. ارائه یک کلید پایدار برای هر آیتم به Compose اجازه می‌دهد از ترکیب‌های مجدد غیرضروری جلوگیری کند. در این حالت، Compose می‌تواند تشخیص دهد که آیتمی که اکنون در نقطه ۳ قرار دارد، همان آیتمی است که قبلاً در نقطه ۲ بود. از آنجایی که هیچ یک از داده‌های آن آیتم تغییر نکرده است، Compose نیازی به ترکیب مجدد آن ندارد.

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes,
            key = { note ->
                // Return a stable, unique key for the note
                note.id
            }
        ) { note ->
            NoteRow(note)
        }
    }
}

استفاده از derivedStateOf برای محدود کردن ترکیب‌های مجدد

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

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

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

راه حل، استفاده از حالت مشتق شده (derive state) است. حالت مشتق شده به شما این امکان را می‌دهد که به Compose بگویید کدام تغییرات حالت باید باعث تغییر ترکیب شوند. در این حالت، مشخص کنید که به زمان تغییر اولین آیتم قابل مشاهده اهمیت می‌دهید. وقتی مقدار آن حالت تغییر می‌کند، رابط کاربری باید دوباره ترکیب کند، اما اگر کاربر هنوز به اندازه کافی اسکرول نکرده باشد تا یک آیتم جدید را به بالا بیاورد، نیازی به ترکیب مجدد نیست.

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

تا حد امکان خواندن را به تعویق بیندازید

وقتی یک مشکل عملکردی شناسایی شد، به تعویق انداختن خواندن وضعیت می‌تواند مفید باشد. به تعویق انداختن خواندن وضعیت تضمین می‌کند که Compose حداقل کد ممکن را در ترکیب مجدد اجرا کند. به عنوان مثال، اگر رابط کاربری شما وضعیتی دارد که در درخت ترکیب‌پذیر در بالا قرار گرفته است و شما وضعیت را در یک ترکیب‌پذیر فرزند می‌خوانید، می‌توانید وضعیت خوانده شده را در یک تابع لامبدا قرار دهید. انجام این کار باعث می‌شود خواندن فقط زمانی که واقعاً مورد نیاز است، انجام شود. برای مرجع، به پیاده‌سازی در برنامه نمونه Jetsnack مراجعه کنید. Jetsnack یک جلوه شبیه به نوار ابزار جمع‌شونده را در صفحه جزئیات خود پیاده‌سازی می‌کند. برای درک اینکه چرا این تکنیک کار می‌کند، به پست وبلاگ Jetpack Compose: اشکال‌زدایی ترکیب مجدد مراجعه کنید.

برای دستیابی به این اثر، ترکیب‌بندی Title به افست اسکرول نیاز دارد تا بتواند با استفاده از یک Modifier ، خود را افست کند. در اینجا یک نسخه ساده‌شده از کد Jetsnack قبل از بهینه‌سازی آمده است:

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack, scroll.value)
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scroll: Int) {
    // ...
    val offset = with(LocalDensity.current) { scroll.toDp() }

    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

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

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack) { scroll.value }
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    val offset = with(LocalDensity.current) { scrollProvider().toDp() }
    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

پارامتر scroll اکنون یک لامبدا است. این بدان معناست که Title هنوز می‌تواند به حالت hoisted اشاره کند، اما مقدار آن فقط درون Title ، جایی که واقعاً مورد نیاز است، خوانده می‌شود. در نتیجه، وقتی مقدار scroll تغییر می‌کند، نزدیکترین محدوده recomposition اکنون Title composable است - Compose دیگر نیازی به recompose کردن کل Box ندارد.

این یک پیشرفت خوب است، اما می‌توانید بهتر عمل کنید! اگر فقط برای طرح‌بندی مجدد یا ترسیم مجدد یک Composable، recomposition ایجاد می‌کنید، باید مشکوک شوید. در این حالت، تنها کاری که انجام می‌دهید تغییر آفست Title composable است که می‌تواند در مرحله طرح‌بندی انجام شود.

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    Column(
        modifier = Modifier
            .offset { IntOffset(x = 0, y = scrollProvider()) }
    ) {
        // ...
    }
}

پیش از این، کد از Modifier.offset(x: Dp, y: Dp) استفاده می‌کرد که offset را به عنوان پارامتر می‌گیرد. با تغییر به نسخه lambda از modifier ، می‌توانید مطمئن شوید که تابع، وضعیت scroll را در مرحله layout می‌خواند. در نتیجه، هنگامی که وضعیت scroll تغییر می‌کند، Compose می‌تواند مرحله composition را به طور کامل نادیده بگیرد و مستقیماً به مرحله layout برود. هنگامی که متغیرهای State را که مرتباً تغییر می‌کنند به modifierها ارسال می‌کنید، باید در صورت امکان از نسخه‌های lambda از modifierها استفاده کنید.

در اینجا مثال دیگری از این رویکرد را مشاهده می‌کنید. این کد هنوز بهینه نشده است:

// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

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

برای بهبود این مورد، از یک اصلاح‌کننده مبتنی بر لامبدا استفاده کنید - در این مورد، drawBehind . این بدان معناست که حالت رنگ فقط در مرحله ترسیم خوانده می‌شود. در نتیجه، Compose می‌تواند مراحل ترکیب و طرح‌بندی را به طور کامل نادیده بگیرد - وقتی رنگ تغییر می‌کند، Compose مستقیماً به مرحله ترسیم می‌رود.

val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(
    Modifier
        .fillMaxSize()
        .drawBehind {
            drawRect(color)
        }
)

از نوشتن معکوس خودداری کنید

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

ترکیب زیر نمونه‌ای از این نوع اشتباه را نشان می‌دهد.

@Composable
fun BadComposable() {
    var count by remember { mutableIntStateOf(0) }

    // Causes recomposition on click
    Button(onClick = { count++ }, Modifier.wrapContentSize()) {
        Text("Recompose")
    }

    Text("$count")
    count++ // Backwards write, writing to state after it has been read</b>
}

این کد، شمارش انتهای composable را پس از خواندن آن در خط قبل، به‌روزرسانی می‌کند. اگر این کد را اجرا کنید، خواهید دید که پس از کلیک روی دکمه، که باعث recomposition می‌شود، شمارنده به سرعت در یک حلقه بی‌نهایت افزایش می‌یابد، زیرا Compose این Composable را recompose می‌کند، وضعیتی را می‌بیند که خوانده شده قدیمی است و بنابراین recomposition دیگری را زمان‌بندی می‌کند.

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

منابع اضافی

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