فازهای jetpack Compose

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

ترکیب در سراسر اسناد Compose ما، از جمله Thinking in Compose و State و Jetpack Compose توضیح داده شده است.

سه فاز یک قاب

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

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

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

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

مراحل را درک کنید

این بخش نحوه اجرای سه فاز Compose را برای Composable با جزئیات بیشتر توضیح می دهد.

ترکیب

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

شکل 2. درختی که UI شما را نشان می دهد که در مرحله ترکیب ایجاد شده است.

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

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

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

طرح بندی

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

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

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

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

در پایان این مرحله، هر گره چیدمان دارای:

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

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

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

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

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

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

طراحی

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

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

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

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

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

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

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

State معمولاً با استفاده از 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)
)

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

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

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

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

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

فاز 1: ترکیب

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

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

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

فاز 2: چیدمان

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

خواندن حالت در طول هر یک از این مراحل بر روی طرح و به طور بالقوه مرحله ترسیم تأثیر می گذارد. وقتی مقدار حالت تغییر می‌کند، 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)
    }
)

فاز 3: نقاشی

خواندن حالت در حین ترسیم کد بر مرحله ترسیم تأثیر می گذارد. نمونه های رایج عبارتند از Canvas() ، Modifier.drawBehind و Modifier.drawWithContent . وقتی مقدار حالت تغییر می کند، 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) {
        // ...
    }
}

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

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

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

نسخه دیگری از اصلاح کننده افست موجود است: 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 تغییر کند، Compose فقط باید مراحل طرح‌بندی و ترسیم را دوباره راه‌اندازی کند.

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

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

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

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

Box {
    var imageHeightPx by remember { mutableStateOf(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 نشان می‌دهد که کد مشکلی دارد.

مشکل این مثال این است که ما به طرح "نهایی" در یک فریم نمی رسیم. این کد به فریم‌های متعددی متکی است که کارهای غیرضروری انجام می‌دهند و منجر به پرش UI روی صفحه برای کاربر می‌شود.

بیایید در هر فریم قدم بگذاریم تا ببینیم چه اتفاقی می افتد:

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

Compose سپس فریم دوم برنامه ریزی شده با تغییر مقدار imageHeightPx را شروع می کند. حالت در بلوک محتوای جعبه خوانده می شود و در مرحله ترکیب فراخوانی می شود. این بار، متن با یک بالشتک مطابق با ارتفاع تصویر ارائه می شود. در مرحله طرح بندی، کد دوباره مقدار imageHeightPx را تنظیم می کند، اما هیچ ترکیب مجددی برنامه ریزی نمی شود زیرا مقدار ثابت می ماند.

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

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

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

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

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

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