فازهای jetpack Compose

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

مستندات Compose ترکیب را در Thinking in Compose and 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. درخت رابط کاربری و نمایش ترسیم شده آن.

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

هنگامی که value یک snapshot state در یکی از مراحل فهرست شده قبلی می‌خوانید، Compose به‌طور خودکار کارهایی را که هنگام خواندن 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)
)

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

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

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

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

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

فاز 1: ترکیب

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

بسته به نتیجه ترکیب، 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 { … } و توابع مشابه را اجرا می کند.

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

فاز 3: نقاشی

خواندن حالت در حین ترسیم کد بر مرحله ترسیم تأثیر می گذارد. نمونه های رایج عبارتند از 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 تغییر خواهد کرد. همانطور که آموخته اید، Compose هر حالت خوانده شده را دنبال می کند تا بتواند کد خواندن را که در این مثال محتوای 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 به این معنی است که اگر value firstVisibleItemScrollOffset تغییر کند، 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 به ارتفاع واقعی تصویر به روز می کند. Compose سپس یک ترکیب مجدد را برای فریم بعدی برنامه ریزی می کند. با این حال، در طول مرحله ترسیم فعلی، متن با یک بالشتک 0 ارائه می شود، زیرا مقدار imageHeightPx به روز شده هنوز منعکس نشده است.

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

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

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

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

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

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

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

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