فازهای jetpack Compose

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

مستندات Compose، ترکیب‌بندی را در Thinking in Compose and State و Jetpack Compose شرح می‌دهد.

سه مرحله یک قاب

نوشتن سه مرحله اصلی دارد:

  1. ترکیب‌بندی : چه رابط کاربری‌ای نمایش داده شود. Compose توابع قابل ترکیب را اجرا می‌کند و توضیحی از رابط کاربری شما ایجاد می‌کند.
  2. طرح‌بندی : محل قرارگیری رابط کاربری. این مرحله شامل دو مرحله است: اندازه‌گیری و قرارگیری. عناصر طرح‌بندی، خود و هر عنصر فرزند را در مختصات دوبعدی، برای هر گره در درخت طرح‌بندی، اندازه‌گیری و قرار می‌دهند.
  3. طراحی : نحوه رندر شدن. عناصر رابط کاربری در یک بوم نقاشی، معمولاً صفحه نمایش دستگاه، ترسیم می‌شوند.
سه مرحله‌ای که Compose در آن‌ها داده‌ها را به رابط کاربری تبدیل می‌کند (به ترتیب، داده‌ها، ترکیب، طرح‌بندی، ترسیم، رابط کاربری).
شکل ۱. سه مرحله‌ای که Compose در آن‌ها داده‌ها را به رابط کاربری تبدیل می‌کند.

ترتیب این مراحل عموماً یکسان است و به داده‌ها اجازه می‌دهد تا در یک جهت از ترکیب‌بندی به طرح‌بندی و سپس به طراحی جریان یابند تا یک فریم (که به عنوان جریان داده یک‌طرفه نیز شناخته می‌شود) ایجاد شود. BoxWithConstraints ، LazyColumn و LazyRow استثنائات قابل توجهی هستند که در آن‌ها ترکیب‌بندی فرزندانش به مرحله طرح‌بندی والد بستگی دارد.

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

فازها را درک کنید

این بخش نحوه اجرای سه مرحله Compose برای composableها را با جزئیات بیشتری شرح می‌دهد.

ترکیب

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

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

یک زیربخش از کد و درخت رابط کاربری به شکل زیر است:

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

در این مثال‌ها، هر تابع قابل ترکیب در کد به یک گره طرح‌بندی واحد در درخت رابط کاربری نگاشت می‌شود. در مثال‌های پیچیده‌تر، قابل ترکیب‌ها می‌توانند شامل منطق و جریان کنترل باشند و با توجه به حالت‌های مختلف، درخت متفاوتی تولید کنند.

طرح بندی

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

شکل ۴. اندازه‌گیری و قرارگیری هر گره طرح‌بندی در درخت رابط کاربری در طول مرحله طرح‌بندی.

در طول مرحله طرح‌بندی، درخت با استفاده از الگوریتم سه مرحله‌ای زیر پیمایش می‌شود:

  1. اندازه‌گیری فرزندان : یک گره، فرزندان خود را در صورت وجود، اندازه‌گیری می‌کند.
  2. تعیین اندازه : بر اساس این اندازه‌گیری‌ها، یک گره اندازه خود را تعیین می‌کند.
  3. قرار دادن گره‌های فرزند : هر گره فرزند نسبت به موقعیت خود گره قرار می‌گیرد.

در پایان این مرحله، هر گره طرح‌بندی دارای موارد زیر است:

  • عرض و ارتفاع اختصاص داده شده
  • مختصات x و y که باید در آن رسم شود

درخت رابط کاربری را از بخش قبل به یاد بیاورید:

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

برای این درخت، الگوریتم به صورت زیر عمل می‌کند:

  1. Row ، فرزندان خود، Image و Column را اندازه‌گیری می‌کند.
  2. Image اندازه‌گیری شده است. هیچ فرزندی ندارد، بنابراین اندازه خود را تعیین می‌کند و اندازه را به Row گزارش می‌دهد.
  3. Column در مرحله بعد اندازه‌گیری می‌شود. ابتدا فرزندان خود (دو ترکیب‌پذیر Text ) را اندازه‌گیری می‌کند.
  4. Text اول اندازه‌گیری شده است. هیچ فرزندی ندارد، بنابراین اندازه خود را تعیین می‌کند و اندازه آن را به Column گزارش می‌دهد.
    1. Text دوم اندازه‌گیری شده است. هیچ فرزندی ندارد، بنابراین اندازه خود را تعیین می‌کند و آن را به Column گزارش می‌دهد.
  5. Column از اندازه‌های فرزند برای تعیین اندازه خود استفاده می‌کند. این ستون از حداکثر عرض فرزند و مجموع ارتفاع فرزندانش استفاده می‌کند.
  6. Column فرزندانش را نسبت به خودش قرار می‌دهد و آنها را به صورت عمودی زیر یکدیگر قرار می‌دهد.
  7. Row از اندازه‌های فرزند برای تعیین اندازه خود استفاده می‌کند. این ردیف از حداکثر ارتفاع فرزند و مجموع عرض فرزندان خود استفاده می‌کند. سپس فرزندان خود را قرار می‌دهد.

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

طراحی

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

شکل ۵. مرحله ترسیم، پیکسل‌ها را روی صفحه نمایش ترسیم می‌کند.

با استفاده از مثال قبلی، محتوای درخت به روش زیر ترسیم می‌شود:

  1. Row هر محتوایی را که ممکن است داشته باشد، مانند رنگ پس‌زمینه، ترسیم می‌کند.
  2. Image خودش را ترسیم می‌کند.
  3. Column خودش را ترسیم می‌کند.
  4. Text اول و دوم به ترتیب خودشان را ترسیم می‌کنند.

شکل ۶. یک درخت رابط کاربری و نمایش ترسیم‌شده‌ی آن.

ایالت می‌خواند

وقتی value یک snapshot state در طول یکی از مراحل ذکر شده قبلی می‌خوانید، Compose به طور خودکار آنچه را که هنگام خواندن value انجام می‌داد، ردیابی می‌کند. این ردیابی به Compose اجازه می‌دهد تا وقتی value وضعیت تغییر می‌کند، خواننده را دوباره اجرا کند و اساس مشاهده‌پذیری وضعیت در Compose است.

شما معمولاً با استفاده از mutableStateOf() وضعیت را ایجاد می‌کنید و سپس از طریق یکی از دو روش زیر به آن دسترسی پیدا می‌کنید: با دسترسی مستقیم به ویژگی value یا با استفاده از یک نماینده ویژگی Kotlin. می‌توانید اطلاعات بیشتر در مورد آنها را در بخش State در composables بخوانید. برای اهداف این راهنما، "خواندن وضعیت" به هر یک از این روش‌های دسترسی معادل اشاره دارد.

// State read without property delegate.
val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.padding(paddingState.value)
)

// State read with property delegate.
var padding: Dp by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.padding(padding)
)

در زیر پوشش نماینده ویژگی ، از توابع "getter" و "setter" برای دسترسی و به‌روزرسانی value State استفاده می‌شود. این توابع getter و setter فقط زمانی فراخوانی می‌شوند که شما به عنوان یک مقدار به ویژگی ارجاع دهید، و نه زمانی که آن ایجاد می‌شود، به همین دلیل است که دو روشی که قبلاً توضیح داده شد معادل هستند.

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

حالت فازی خوانده می‌شود

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

بخش‌های بعدی هر مرحله را شرح می‌دهند و توضیح می‌دهند که هنگام خواندن مقدار حالت در آن، چه اتفاقی می‌افتد.

مرحله ۱: ترکیب‌بندی

خواندن وضعیت در یک تابع @Composable یا بلوک لامبدا بر ترکیب و به طور بالقوه بر مراحل بعدی تأثیر می‌گذارد. هنگامی که value وضعیت تغییر می‌کند، recomposer اجرای مجدد تمام توابع ترکیب‌پذیری را که value آن وضعیت را می‌خوانند، برنامه‌ریزی می‌کند. توجه داشته باشید که اگر ورودی‌ها تغییر نکرده باشند، ممکن است زمان اجرا تصمیم بگیرد که از برخی یا همه توابع ترکیب‌پذیر صرف نظر کند. برای اطلاعات بیشتر به بخش «اگر ورودی‌ها تغییر نکرده باشند، از قلم انداختن» مراجعه کنید.

بسته به نتیجه ترکیب، رابط کاربری Compose مراحل طرح‌بندی و طراحی را اجرا می‌کند. اگر محتوا ثابت بماند و اندازه و طرح‌بندی تغییر نکند، ممکن است از این مراحل صرف نظر کند.

var padding by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    // The `padding` state is read in the composition phase
    // when the modifier is constructed.
    // Changes in `padding` will invoke recomposition.
    modifier = Modifier.padding(padding)
)

مرحله ۲: طرح‌بندی

مرحله‌ی طرح‌بندی شامل دو مرحله است: اندازه‌گیری و جایگذاری . مرحله‌ی اندازه‌گیری، لامبدا اندازه‌گیری ارسال شده به کامپوننت Layout ، متد MeasureScope.measure از رابط LayoutModifier و موارد دیگر را اجرا می‌کند. مرحله‌ی جایگذاری، بلوک جایگذاری تابع layout ، بلوک لامبدا از Modifier.offset { … } و توابع مشابه را اجرا می‌کند.

خواندن وضعیت در طول هر یک از این مراحل، بر طرح‌بندی و احتمالاً مرحله ترسیم تأثیر می‌گذارد. وقتی value وضعیت تغییر می‌کند، رابط کاربری Compose مرحله طرح‌بندی را زمان‌بندی می‌کند. همچنین اگر اندازه یا موقعیت تغییر کرده باشد، مرحله ترسیم را اجرا می‌کند.

var offsetX by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.offset {
        // The `offsetX` state is read in the placement step
        // of the layout phase when the offset is calculated.
        // Changes in `offsetX` restart the layout.
        IntOffset(offsetX.roundToPx(), 0)
    }
)

مرحله ۳: طراحی

خواندن وضعیت (state) در طول کد ترسیم، بر مرحله ترسیم تأثیر می‌گذارد. نمونه‌های رایج شامل Canvas() ، Modifier.drawBehind و Modifier.drawWithContent است. وقتی value وضعیت تغییر می‌کند، رابط کاربری Compose فقط مرحله ترسیم را اجرا می‌کند.

var color by remember { mutableStateOf(Color.Red) }
Canvas(modifier = modifier) {
    // The `color` state is read in the drawing phase
    // when the canvas is rendered.
    // Changes in `color` restart the drawing.
    drawRect(color)
}

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

بهینه‌سازی خواندن وضعیت

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

به مثال زیر توجه کنید. این مثال یک Image() دارد که از اصلاحگر offset برای جابجایی موقعیت نهایی طرح‌بندی خود استفاده می‌کند و در نتیجه با اسکرول کردن کاربر، یک اثر اختلاف منظر ایجاد می‌کند.

Box {
    val listState = rememberLazyListState()

    Image(
        // ...
        // Non-optimal implementation!
        Modifier.offset(
            with(LocalDensity.current) {
                // State read of firstVisibleItemScrollOffset in composition
                (listState.firstVisibleItemScrollOffset / 2).toDp()
            }
        )
    )

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

این کد کار می‌کند، اما منجر به عملکرد نامطلوب می‌شود. همانطور که نوشته شده است، کد value حالت firstVisibleItemScrollOffset را می‌خواند و آن را به تابع Modifier.offset(offset: Dp) منتقل می‌کند. با اسکرول کاربر، value firstVisibleItemScrollOffset تغییر خواهد کرد. همانطور که آموخته‌اید، Compose هر حالت خوانده شده را ردیابی می‌کند تا بتواند کد خواندن را مجدداً راه‌اندازی (دوباره فراخوانی) کند، که در این مثال محتوای Box است.

این مثالی از خواندن یک وضعیت در فاز ترکیب است. این لزوماً چیز بدی نیست و در واقع اساس ترکیب مجدد است که به تغییرات داده‌ها اجازه می‌دهد تا رابط کاربری جدیدی را منتشر کنند.

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

جبران با لامبدا

نسخه دیگری از اصلاحگر offset نیز موجود است: Modifier.offset(offset: Density.() -> IntOffset) .

این نسخه یک پارامتر لامبدا می‌گیرد که در آن مقدار offset حاصل توسط بلوک لامبدا برگردانده می‌شود. کد را برای استفاده از آن به‌روزرسانی کنید:

Box {
    val listState = rememberLazyListState()

    Image(
        // ...
        Modifier.offset {
            // State read of firstVisibleItemScrollOffset in Layout
            IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2)
        }
    )

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

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

البته، اغلب خواندن حالت‌ها در مرحله ترکیب کاملاً ضروری است. با این حال، مواردی وجود دارد که می‌توانید با فیلتر کردن تغییرات حالت، تعداد ترکیب‌های مجدد را به حداقل برسانید. برای اطلاعات بیشتر در این مورد، به derivedStateOf مراجعه کنید: تبدیل یک یا چند شیء حالت به حالت دیگر .

حلقه بازترکیب (وابستگی فاز چرخه‌ای)

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

Box {
    var imageHeightPx by remember { mutableIntStateOf(0) }

    Image(
        painter = painterResource(R.drawable.rectangle),
        contentDescription = "I'm above the text",
        modifier = Modifier
            .fillMaxWidth()
            .onSizeChanged { size ->
                // Don't do this
                imageHeightPx = size.height
            }
    )

    Text(
        text = "I'm below the image",
        modifier = Modifier.padding(
            top = with(LocalDensity.current) { imageHeightPx.toDp() }
        )
    )
}

این مثال یک ستون عمودی را پیاده‌سازی می‌کند که تصویر در بالا و سپس متن در زیر آن قرار می‌گیرد. از Modifier.onSizeChanged() برای دریافت اندازه نهایی تصویر استفاده می‌کند و سپس Modifier.padding() روی متن برای تغییر آن به پایین استفاده می‌کند. تبدیل غیرطبیعی از Px به Dp نشان می‌دهد که کد دارای مشکل است.

مشکل این مثال این است که کد در یک فریم به طرح‌بندی «نهایی» نمی‌رسد. کد به چندین فریم نیاز دارد که کار غیرضروری انجام می‌دهد و منجر به پرش رابط کاربری روی صفحه برای کاربر می‌شود.

ترکیب‌بندی فریم اول

در طول مرحله ترکیب فریم اول، imageHeightPx در ابتدا 0 است. در نتیجه، کد متن را با Modifier.padding(top = 0) ارائه می‌دهد. مرحله بعدی طرح‌بندی، فراخوانی اصلاح‌کننده onSizeChanged را فراخوانی می‌کند که imageHeightPx به ارتفاع واقعی تصویر به‌روزرسانی می‌کند. سپس Compose یک ترکیب‌بندی مجدد را برای فریم بعدی زمان‌بندی می‌کند. با این حال، در طول مرحله طراحی فعلی، متن با padding برابر با 0 رندر می‌شود، زیرا مقدار به‌روزرسانی‌شده imageHeightPx هنوز منعکس نشده است.

ترکیب‌بندی فریم دوم

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

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

این مثال ممکن است ساختگی به نظر برسد، اما مراقب این الگوی کلی باشید:

  • Modifier.onSizeChanged() ، onGloballyPositioned() یا برخی عملیات‌های دیگر در طرح‌بندی
  • به‌روزرسانی برخی از حالت‌ها
  • از آن حالت به عنوان ورودی برای یک اصلاح‌کننده‌ی طرح‌بندی ( padding() ، height() یا موارد مشابه) استفاده کنید.
  • به طور بالقوه تکرار شود

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

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

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