مراحل Jetpack Compose

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

مستندات Compose ترکیب را در تفکر در Compose و حالت و Jetpack Compose شرح می‌دهد.

سه مرحله قاب

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

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

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

ازنظر مفهومی، هریک از این مراحل برای هر قاب اتفاق می‌افتد؛ بااین‌حال، برای بهینه‌سازی عملکرد، Compose از تکرار کاری که نتایج یکسانی را از ورودی‌های یکسان در همه این مراحل محاسبه می‌کند، اجتناب می‌کند. ‫Compose اگر بتواند از نتیجه قبلی استفاده مجدد کند، اجرای تابع ترکیب‌شدنی را رد می‌کند و اگر لازم نباشد، چیدمان مجدد یا بازطراحی کل درخت را انجام نمی‌دهد. «ترکیب» فقط حداقل مقدار کار لازم برای به‌روزرسانی واسط کاربر را انجام می‌دهد. این بهینه‌سازی امکان‌پذیر است زیرا وضعیت قطعه‌های «نوشتن» در مراحل مختلف خوانده می‌شود.

فهمیدن مراحل

این بخش نحوه اجرای سه مرحله «ترکیب» برای عناصر ترکیبی را با جزئیات بیشتر توضیح می‌دهد.

قطعه موسیقی

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

معمولاً بااستفاده از mutableStateOf() وضعیت ایجاد می‌کنید و سپس از دو روش به آن دسترسی پیدا می‌کنید: با دسترسی مستقیم به دارایی value، یا بااستفاده از نماینده دارایی Kotlin. در State in 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)
)

در زیر نماینده دارایی، از توابع «دریافت‌کننده» و «تنظیم‌کننده» برای دسترسی و به‌روزرسانی value «وضعیت» استفاده می‌شود. این تابع‌های getter و setter فقط زمانی فراخوانی می‌شوند که به دارایی به‌عنوان مقدار ارجاع دهید، نه زمانی که ایجاد می‌شود، به همین دلیل دو روشی که قبلاً توضیح داده شد معادل هستند.

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

خواندن وضعیت مرحله‌ای

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

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

فاز ۱: ترکیب

وضعیت خواندن در تابع @Composable یا بلوک لامبدا بر ترکیب و احتمالاً مراحل بعدی تأثیر می‌گذارد. وقتی value وضعیت تغییر می‌کند، بازساز اجراهای مجدد همه توابع ترکیبی را که 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 UI مرحله چیدمان را زمان‌بندی می‌کند. اگر اندازه یا موقعیت تغییر کرده باشد، مرحله طراحی را نیز اجرا می‌کند.

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)
    }
)

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

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

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() دارد که از اصلاح‌کننده جابه‌جایی برای جابه‌جایی موقعیت چیدمان نهایی خود استفاده می‌کند و درنتیجه با پیمایش کاربر، جلوه اختلاف منظر ایجاد می‌کند.

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 تغییر خواهد کرد. همان‌طور که یاد گرفتید، «نوشتن» هرگونه خواندن وضعیت را ردیابی می‌کند تا بتواند کد خواندن را بازراه‌اندازی (بازفراخوانی) کند، که در این مثال محتوای Box است.

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

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

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

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

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

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 وضعیت را هنگام خوانده شدن ردیابی می‌کند، این تغییر به این معنی است که اگر firstVisibleItemScrollOffset value تغییر کند، ‫Compose فقط باید مراحل چیدمان و طراحی را بازراه‌اندازی کند.

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

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

این راهنما قبلاً ذکر کرده بود که مراحل «نوشتن» همیشه به یک ترتیب فراخوانی می‌شوند و در یک قاب نمی‌توان به عقب رفت. بااین‌حال، این موضوع مانع از ورود برنامه‌ها به حلقه‌های ترکیب‌بندی در قاب‌های مختلف نمی‌شود. این مثال را درنظر بگیرید:

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 را به ارتفاع واقعی تصویر به‌روز می‌کند. ترکیب می‌کند سپس ترکیب مجددی را برای قاب بعدی زمان‌بندی می‌کند. بااین‌حال، درطول مرحله طراحی کنونی، نوشتار با حاشیه 0 پرداز می‌شود، زیرا مقدار به‌روزشده imageHeightPx هنوز منعکس نشده است.

ترکیب قاب دوم

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

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

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

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

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

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

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