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

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

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

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

یک تکنیک مهم این است که نتایج محاسبات را با 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 فکر می‌کند «مورد 2» قدیمی حذف شده و مورد جدیدی برای مورد 3، مورد 4 و تا آخر ایجاد شده است. نتیجه این است که Compose همه موارد موجود در لیست را دوباره ترکیب می کند، حتی اگر فقط یکی از آنها در واقع تغییر کرده باشد.

راه حل در اینجا ارائه کلیدهای مورد است. ارائه یک کلید پایدار برای هر مورد به Compose اجازه می دهد تا از ترکیب مجدد غیر ضروری جلوگیری کند. در این مورد، Compose می‌تواند تعیین کند که اکنون آیتم در نقطه 3 همان موردی است که قبلاً در نقطه 2 قرار داشت. از آنجایی که هیچ یک از داده‌های آن مورد تغییر نکرده است، 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 برای محدود کردن ترکیب مجدد استفاده کنید

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

val listState = rememberLazyListState()

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

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

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

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

val listState = rememberLazyListState()

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

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

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

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

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

برای دستیابی به این اثر، Title composable به افست اسکرول نیاز دارد تا بتواند خود را با استفاده از یک 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)
    ) {
        // ...
    }
}

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

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

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

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

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

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

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

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

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

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

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

@Composable
fun BadComposable() {
    var count by remember { mutableStateOf(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>
}

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

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

منابع اضافی

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}