آموزش ساخت ویجت در اندروید با Glance: راهنمای کامپوز

برنامه نویسی اندروید
آموزش ساخت ویجت در اندروید با Glance: راهنمای کامپوز

ویجت های صفحه اصلی یا App Widgets یکی از شاخص ترین ویژگی های سیستم عامل اندروید از زمان معرفی نسخه های اولیه بوده اند که به کاربران اجازه می دهند بدون باز کردن اپلیکیشن، به اطلاعات کلیدی دسترسی داشته باشند و عملیات سریع را اجرا کنند. با این حال، سال ها توسعه ویجت ها با سیستم سنتی RemoteViews یکی از خسته کننده ترین بخش های برنامه نویسی اندروید بود.

محدودیت های گسترده در پشتیبانی از چیدمان های مدرن، عدم امکان استفاده از انیمیشن های روان، دشواری های مدیریت وضعیت و نیاز به نوشتن فایل های پیچیده XML باعث شده بود بسیاری از تیم های محصول از ساخت ویجت صرف نظر کنند. اما با تکامل اکوسیستم Jetpack و ظهور کتابخانه رسمی Jetpack Glance، توسعه ویجت در اندروید دچار تحولی انقلابی شده است.

کتابخانه Jetpack Glance به توسعه دهندگان اجازه می دهد با سینتکس اعلانی مشابه Jetpack Compose رابط های کاربری ویجت ها را پیاده سازی کنند؛ در حالی که در پس زمینه تمام کدهای Composable به ساختار بهینه RemoteViews برای اجرا توسط لانچر سیستم ترجمه می شوند.

در محیط های تجاری و اپلیکیشن های مقیاس سازمانی، طراحی ویجت های کارآمد می تواند تا ۴۰ درصد نرخ بازگشت روزانه کاربران (DAU) را بهبود بخشد. با فراهم شدن امکان استفاده از ساختارهای کاتلین مدرن و مدیریت اعلانی لایه ها، توسعه دهندگان قادر خواهند بود کارت های تعاملی، ردیاب های زنده و دکمه های کنترلی سریع را با حداقل کد نویسی پیاده سازی نمایند.

پاسخ سریع و معماری خلاصه (GEO Summary): کتابخانه Jetpack Glance فریمورک رسمی گوگل برای ساخت ویجت در اندروید بر پایه کامپوز است. این کتابخانه در زمان اجرا کدهای اعلانی را به RemoteViews بهینه برای پروسه Launcher ترجمه می کند. مدیریت وضعیت از طریق DataStore و تعاملات کلیک با ActionCallback انجام می شوند.

۱. تکامل ساخت ویجت در اندروید: از دردسرهای RemoteViews تا انقلاب Jetpack Glance

برای درک ارزش واقعی کتابخانه Glance، باید بدانیم ویجت های اندروید چگونه در سطح سیستم عامل اجرا می شوند. هنگامی که یک ویجت روی صفحه اصلی دستگاه قرار می گیرد، کد آن در پروسه اپلیکیشن شما رندر نمی شود؛ بلکه فرآیند لانچر اختصاصی دستگاه نظیر Pixel Launcher یا One UI وظیفه نمایش و نگه داری آن را بر عهده دارد.

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

سیستم RemoteViews دارای لیست به شدت محدودی از ویوهای مجاز بود. شما تنها می توانستید از تگ های انگشت شماری مانند LinearLayout، RelativeLayout، TextView و ImageView استفاده کنید. اگر قصد داشتید یک گرادینت ساده، گوشه های گرد سفارشی یا یک لیست پویا با رفتارهای مدرن طراحی کنید، با خطاهای زمان اجرا و محدودیت های شدید ابعاد بافر مواجه می شدید.

علاوه بر این، در سیستم سنتی هیچ مکانیزم پیش فرضی برای هماهنگی با زبان طراحی مدرن Material You وجود نداشت. توسعه دهندگان ناچار بودند برای هر تم رنگی و هر حالت شب و روز فایل های XML جداگانه و سلکتورهای تودرتو ایجاد کنند که هزینه نگه داری کد را به شدت افزایش می داد. با معرفی Glance، تمام این ساختارها با تم سیستم هماهنگ می شوند.

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

۲. معماری و نحوه عملکرد درونی Jetpack Glance در زیر کاپوت

بسیاری از برنامه نویسان با این تصور اشتباه روبرو هستند که Jetpack Glance همان فریمورک کامل کامپوز است که درون پروسه لانچر اجرا می شود. اما این تصور از نظر مهندسی نادرست است؛ زیرا اجرای یک Runtime کامل Composable درون لانچرهای مختلف به معنای مصرف شدید رم و ناسازگاری های گسترده خواهد بود.

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

نمودار معماری Jetpack Glance و نحوه ترجمه به RemoteViews در سیستم عامل اندروید
جریان تبدیل کدهای اعلانی Glance به اشیای RemoteViews و مدیریت بین پروسه ای لانچر سیستم

موتور Glance درخت توابع Composable شما را به صورت بلادرنگ ارزیابی کرده و آن را به یک سلسله مراتب بهینه و سازگار از RemoteViews تبدیل می نماید که سیستم عامل در نسخه مقصد قادر به درک آن باشد.

این فرآیند ترجمه هوشمند باعث می شود که حتی قابلیت های پیشرفته بصری نظیر گوشه های گرد و رنگ های داینامیک، بدون شکستن سازگاری با نسخه های قدیمی اندروید (تا API ۲۱) به درستی در لانچرهای گوناگون شبیه سازی و اجرا شوند. در نتیجه توسعه دهنده با یک بار نوشتن کد اعلانی کاتلین، سازگاری فراگیر را تضمین می کند.

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

پیشنهاد مطالعه
آموزش جامع Jetpack Compose برای توسعه رابط کاربری اندروید
آموزش جت پک کامپوز Jetpack Compose در اندروید؛ معماری رابط کاربری اعلانی، مدیریت State و جریان UDF، فازهای Recomposition، ناوبری تایپ سیف و نمونه کده…
مشاهده مقاله ←

۳. جدول مقایسه فنی: Jetpack Glance در برابر RemoteViews سنتی و Compose معمولی

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

شاخص ارزیابی مهندسی سیستم سنتی RemoteViews (XML) کتابخانه Jetpack Glance (AppWidget) فریمورک اصلی Jetpack Compose
پارادایم رابط کاربری دستوری با فایل های XML مجزا اعلانی با کاتلین بدون نیاز به XML اعلانی کامل مبتنی بر توابع کامپوز
محل رندرینگ پروسه لانچر از طریق IPC پروسه لانچر از طریق IPC بهینه شده پروسه خود اپلیکیشن روی RenderNode مستقیم
پشتیبانی از Material ۳ دستی با نوشتن سلکتورها و استایل های متعدد پشتیبانی داخلی از رنگ های پویا و تم Material ۳ پشتیبانی کامل و پیشرفته به صورت پیش فرض
مدیریت وضعیت دستی با SharedPreferences و ارسال Broadcast یکپارچه با PreferencesGlanceStateDefinition یکپارچه با StateFlow، SharedFlow و remember
مدیریت رویدادهای کلیک ایجاد PendingIntent های دستی و پیچیده توابع آماده actionRunCallback و actionStartActivity لامبدا های مستقیم onClick بدون هزینه IPC
پشتیبانی از لیست های اسکرول پذیر نیاز به RemoteViewsService و Factory پیچیده استفاده بسیار ساده از LazyColumn با کاتلین تمیز پشتیبانی از انواع LazyColumn و LazyRow
پایداری و ایمنی نوع بسیار پایین و مستعد خطای زمان اجرای کرش لایه ها بسیار بالا در زمان کامپایل با کامپایلر کاتلین صد در صد Type-Safe

۴. پیش نیازها و افزودن وابستگی های Jetpack Glance به پروژه

برای استفاده از Jetpack Glance، ابتدا باید مطمئن شوید که کتابخانه های سازگار در فایل کانفیگ گریدل ماژول برنامه تعریف شده اند. همچنین پروژه شما باید قابلیت های کامپوز را فعال کرده باشد:

// build.gradle.kts (Module: app)
android {
    buildFeatures { compose = true }
    composeOptions { kotlinCompilerExtensionVersion = "1.5.14" }
}

dependencies {
    implementation("androidx.glance:glance:1.1.0")
    implementation("androidx.glance:glance-appwidget:1.1.0")
    implementation("androidx.glance:glance-material3:1.1.0")
    implementation("androidx.datastore:datastore-preferences:1.1.1")
    implementation("androidx.work:work-runtime-ktx:2.9.1")
}

توجه داشته باشید که کدهای رابط کاربری Glance در پکیج اختصاصی androidx.glance.* قرار دارند. بسیار مهم است که ایمپورت های کامپوز عادی مانند androidx.compose.material3.Text را با androidx.glance.text.Text اشتباه نگیرید؛ زیرا کامپوننت های عادی درون ویجت پشتیبانی نمی شوند و خطای کامپایل ایجاد می کنند.

همچنین استفاده از ماژول glance-material3 این امکان را فراهم می آورد که پالت رنگ های رسمی سیستم Material You بدون نیاز به کد نویسی اضافی به ویجت اعمال شود. این ویژگی به ویژه در اندروید ۱۲ و بالاتر باعث تطابق بی نظیر ویجت با تم و تصویر زمینه انتخابی کاربر می گردد.

۵. ساخت اولین ویجت با Glance: پیاده سازی GlanceAppWidget و Receiver

معماری پایه یک ویجت در Glance شامل دو بخش اصلی است: یک کلاس مشتق شده از GlanceAppWidget که ظاهر و رفتار ویجت را مشخص می کند، و یک کلاس گیرنده به نام GlanceAppWidgetReceiver که رویدادهای سیستمی چرخه حیات ویجت را گوش می دهد.

گام اول: تعریف کلاس ویجت اختصاصی

import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.glance.*
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.provideContent
import androidx.glance.layout.*
import androidx.glance.text.*

class QuickSummaryWidget : GlanceAppWidget() {
    override suspend fun provideContent(context: Context, id: GlanceId) {
        provideContent {
            GlanceTheme { WidgetMainContent() }
        }
    }

    @Composable
    private fun WidgetMainContent() {
        Column(
            modifier = GlanceModifier.fillMaxSize()
                .background(GlanceTheme.colors.surface).padding(16.dp),
            verticalAlignment = Alignment.Vertical.CenterVertically,
            horizontalAlignment = Alignment.Horizontal.CenterHorizontally
        ) {
            Text(text = "ویجت هوشمند اندروید", style = TextStyle(fontWeight = FontWeight.Bold))
            Spacer(modifier = GlanceModifier.height(8.dp))
            Text(text = "طراحی مدرن بر پایه Jetpack Glance و کاتلین")
        }
    }
}

گام دوم: تعریف کلاس گیرنده برادکست (GlanceAppWidgetReceiver)

کلاس Receiver نقش واسط میان سیستم عامل و شیء GlanceAppWidget شما را بر عهده دارد. پیاده سازی آن فوق العاده خلاصه و تمیز است:

import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver

class QuickSummaryWidgetReceiver : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget = QuickSummaryWidget()
}

گام سوم: ثبت در فایل AndroidManifest.xml و تعریف AppWidgetProviderInfo

همانند تمام کامپوننت های سیستم عامل اندروید، گیرنده ویجت باید درون مانیفست رجیستر شود و فایل متادیتا شامل حداقل ابعاد و فرکانس آپدیت به آن متصل گردد:

<!-- AndroidManifest.xml -->
<receiver
    android:name=".widget.QuickSummaryWidgetReceiver"
    android:exported="true">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/quick_summary_widget_info" />
</receiver>

فایل پیکربندی ویژگی های ویجت در مسیر res/xml/quick_summary_widget_info.xml ذخیره می شود:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/widget_description"
    android:initialLayout="@layout/glance_default_loading_layout"
    android:minWidth="140dp"
    android:minHeight="110dp"
    android:resizeMode="horizontal|vertical"
    android:targetCellWidth="2"
    android:targetCellHeight="2"
    android:updatePeriodMillis="3600000"
    android:widgetCategory="home_screen" />

۶. مدیریت وضعیت و ذخیره سازی پایدار در Glance با DataStore

از آنجا که ویجت ها دارای پروسه دائمی در حال اجرا نیستند، شما نمی توانید از متغیرهای حالت حافظه ای مانند mutableStateOf یا کلاس ViewModel در درون ویجت استفاده کنید. سیستم عامل ممکن است هر زمان پروسه اپلیکیشن شما را متوقف کند؛ در حالی که ویجت همچنان روی صفحه خانه کاربر قابل مشاهده است.

الگوی استاندارد در Glance، ذخیره سازی وضعیت در قالب Preferences DataStore است. کتابخانه Glance یک رابط تخصصی به نام GlanceStateDefinition را مهیا کرده است که با هر بار تغییر در تنظیمات، محتوای ویجت را به طور خودکار به روزرسانی می کند.

import android.content.Context
import androidx.datastore.preferences.core.*
import androidx.glance.*
import androidx.glance.appwidget.*
import androidx.glance.state.*

class CounterWidget : GlanceAppWidget() {
    override var stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition

    companion object {
        val CLICK_COUNT_KEY = intPreferencesKey("user_click_count")
        val LAST_SYNC_KEY = stringPreferencesKey("last_sync_timestamp")
    }

    override suspend fun provideContent(context: Context, id: GlanceId) {
        provideContent {
            val prefs = currentState<androidx.datastore.preferences.core.Preferences>()
            val count = prefs[CLICK_COUNT_KEY] ?: 0
            val lastSync = prefs[LAST_SYNC_KEY] ?: "همگام سازی نشده"

            androidx.glance.layout.Column(modifier = GlanceModifier.fillMaxSize()) {
                androidx.glance.text.Text(text = "تعداد کلیک های ثبت شده: $count")
                androidx.glance.text.Text(text = "آخرین زمان: $lastSync")
            }
        }
    }
}

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

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

۷. مدیریت تعاملات کاربر و اکشن های کلیک با ActionCallback

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

برای اجرای عملیات پس زمینه پس از کلیک مانند ریفرش داده ها یا افزایش شمارنده، کلاسی پیاده سازی می کنیم که از ActionCallback ارث بری کند:

import android.content.Context
import androidx.glance.GlanceId
import androidx.glance.action.ActionParameters
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.glance.state.PreferencesGlanceStateDefinition
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class IncrementCounterAction : ActionCallback {
    override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
        updateAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId) { prefs ->
            prefs.toMutablePreferences().apply {
                val current = this[CounterWidget.CLICK_COUNT_KEY] ?: 0
                this[CounterWidget.CLICK_COUNT_KEY] = current + 1
                val sdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
                this[CounterWidget.LAST_SYNC_KEY] = sdf.format(Date())
            }
        }
        CounterWidget().update(context, glanceId)
    }
}

حال کافی است در دکمه Composable خود، اکشن را با متد actionRunCallback متصل نماییم:

import androidx.compose.runtime.Composable
import androidx.glance.Button
import androidx.glance.GlanceModifier
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.layout.fillMaxWidth

@Composable
fun WidgetControlButtons() {
    Button(
        text = "افزایش شمارنده",
        onClick = actionRunCallback<IncrementCounterAction>(),
        modifier = GlanceModifier.fillMaxWidth()
    )
}
هشدار کارایی و پیشگیری از ANR: متد onAction درون یک کوروتین اجرا می شود اما دارای مهلت زمانی محدود است. هرگز تماس های طولانی مدت شبکه یا پردازش های سنگین دیتابیس را مستقیماً درون ActionCallback به صورت بلاک کننده قرار ندهید. برای وظایف سنگین از WorkManager بهره بگیرید.

۸. مدیریت به روزرسانی های دوره ای و پس زمینه با WorkManager

پارامتر android:updatePeriodMillis در مانیفست اندروید دارای یک کف فرکانس حداقل ۳۰ دقیقه ای است و در دستگاه های با نسخه اندروید ۸ به بعد، به دلیل مکانیزم های ذخیره انرژی ممکن است تا ساعت ها به تعویق بیفتد. بنابراین اگر ویجت شما نیازمند نمایش اطلاعات با فرکانس بالاتر یا مدیریت بر اساس وضعیت شبکه است، استفاده از WorkManager تنها راهکار استاندارد و تایید شده توسط گوگل می باشد.

import android.content.Context
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.glance.state.PreferencesGlanceStateDefinition
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters

class WidgetBackgroundSyncWorker(
    private val appContext: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {

    override suspend fun doWork(): Result {
        val manager = GlanceAppWidgetManager(appContext)
        val glanceIds = manager.getGlanceIds(QuickSummaryWidget::class.java)

        glanceIds.forEach { glanceId ->
            updateAppWidgetState(appContext, PreferencesGlanceStateDefinition, glanceId) { prefs ->
                prefs.toMutablePreferences().apply {
                    this[QuickSummaryWidget.LAST_SYNC_KEY] = "بروزرسانی در " + System.currentTimeMillis()
                }
            }
            QuickSummaryWidget().update(appContext, glanceId)
        }
        return Result.success()
    }
}

با تنظیم قیدهای هوشمند مانند NetworkType.CONNECTED و setRequiresBatteryNotLow(true) در درخواست WorkManager، تضمین می شود که پردازش های همگام سازی ویجت فقط در شرایط مساعد باتری اجرا شده و از جریمه های شاخص های عملکردی Android Vitals جلوگیری به عمل آید.

الگوی ترکیب PeriodicWorkRequest با قیدهای هوشمند باتری به توسعه دهندگان اجازه می دهد داده ها را با کمترین مصرف انرژی تازه سازی کنند. به عنوان مثال، اگر کاربر در طول شب از دستگاه استفاده نکند، سیستم عامل با اتکا به Doze Mode پردازش های پس زمینه را تجمیع می کند. با روشن شدن صفحه نمایش، WorkManager به طور خودکار ویجت را با آخرین اطلاعات همگام می سازد.

همچنین با استفاده از متد enqueueUniquePeriodicWork و سیاست ExistingPeriodicWorkPolicy.KEEP می توان از ایجاد وظایف تکراری و همپوشان جلوگیری نمود تا منابع سخت افزاری دستگاه همواره در بهینه ترین وضعیت مصرف باقی بمانند.

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

پیشنهاد مطالعه
آموزش ورک منیجر WorkManager در اندروید: اجرای تسک پس زمینه
راهنمای جامع آموزش ورک منیجر WorkManager در اندروید؛ مدیریت تسک های پایدار پس زمینه، پیاده سازی CoroutineWorker، محدودیت های هوشمند باتری و شبکه و مق…
مشاهده مقاله ←

۹. چیدمان های واکنش گرا (Responsive) و تغییر ابعاد هوشمند ویجت

یکی از جذاب ترین بخش های ویجت ها برای کاربران، قابلیت تغییر اندازه روی شبکه خانه دستگاه است. ویجت شما ممکن است توسط کاربر در ابعاد کوچک 2x1، متوسط 2x2 یا بزرگ 4x3 کشیده شود. اگر ویجت نتواند خود را متناسب با اندازه جدید تنظیم کند، محتوا بریده شده یا فضای خالی زیادی ایجاد می گردد.

کتابخانه Glance سه حالت ابعادی مختلف (SizeMode) را پشتیبانی می کند:

  • SizeMode.Single: حالت پیش فرض که در آن ویجت تنها دارای یک چیدمان ثابت است و ابعاد تغییر یافته تاثیری در ساختار Composable نمی گذارند.
  • SizeMode.Exact: هر بار که کاربر اندازه ویجت را حتی یک پیکسل تغییر دهد، تابع provideContent مجدداً فراخوانی می شود تا محتوا بازطراحی شود.
  • SizeMode.Responsive: بهینه ترین و پیشنهادی ترین حالت که در آن شما مجموعه ای از نقاط شکست از ابعاد Dp را تعریف می کنید. Glance به صورت خودکار مناسب ترین چیدمان را بدون اجرای مکرر کدهای سنگین انتخاب می کند.
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.glance.appwidget.SizeMode

class AdaptiveNewsWidget : GlanceAppWidget() {
    companion object {
        private val SMALL_BOX = DpSize(120.dp, 80.dp)
        private val MEDIUM_BOX = DpSize(240.dp, 120.dp)
        private val LARGE_BOX = DpSize(240.dp, 240.dp)
    }

    override val sizeMode = SizeMode.Responsive(setOf(SMALL_BOX, MEDIUM_BOX, LARGE_BOX))
}

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

۱۰. پروژه عملی: ساخت ویجت ردیاب ارز دیجیتال (Crypto Tracker) با رفرش و کش آفلاین

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

بخش اول: تعریف کلاس ویجت و مدیریت وضعیت DataStore

import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.glance.*
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.provideContent
import androidx.glance.state.GlanceStateDefinition
import androidx.glance.state.PreferencesGlanceStateDefinition

class CryptoTrackerWidget : GlanceAppWidget() {
    override var stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition

    companion object {
        val BTC_PRICE_KEY = stringPreferencesKey("btc_price")
        val ETH_PRICE_KEY = stringPreferencesKey("eth_price")
        val STATUS_KEY = stringPreferencesKey("update_status")
    }

    override suspend fun provideContent(context: Context, id: GlanceId) {
        provideContent {
            GlanceTheme {
                val prefs = currentState<Preferences>()
                CryptoCardContent(
                    btc = prefs[BTC_PRICE_KEY] ?: "$68,450",
                    eth = prefs[ETH_PRICE_KEY] ?: "$3,520",
                    status = prefs[STATUS_KEY] ?: "آماده به کار"
                )
            }
        }
    }
}

بخش دوم: پیاده سازی چیدمان کارت اعلانی و دکمه رفرش

import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.glance.*
import androidx.glance.layout.*
import androidx.glance.text.*

@Composable
fun CryptoCardContent(btc: String, eth: String, status: String) {
    Column(modifier = GlanceModifier.fillMaxSize().padding(14.dp)) {
        Text(text = "بازار رمزارز", style = TextStyle(fontWeight = FontWeight.Bold))
        Spacer(modifier = GlanceModifier.height(8.dp))
        Text(text = "BTC: $btc | ETH: $eth")
        Text(text = "وضعیت: $status")
    }
}

بخش سوم: پیاده سازی اکشن به روزرسانی قیمت ها با ActionCallback

import android.content.Context
import androidx.glance.GlanceId
import androidx.glance.action.ActionParameters
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.glance.state.PreferencesGlanceStateDefinition

class RefreshPricesCallback : ActionCallback {
    override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
        updateAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId) { prefs ->
            prefs.toMutablePreferences().apply {
                this[CryptoTrackerWidget.BTC_PRICE_KEY] = "$69,200"
                this[CryptoTrackerWidget.ETH_PRICE_KEY] = "$3,590"
                this[CryptoTrackerWidget.STATUS_KEY] = "بروزرسانی موفق"
            }
        }
        CryptoTrackerWidget().update(context, glanceId)
    }
}

۱۱. آزمون های خودکار و تست نویسی ویجت های Glance در خطوط CI/CD

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

import androidx.glance.appwidget.testing.unit.runGlanceAppWidgetUnitTest
import androidx.glance.testing.unit.hasText
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class CryptoTrackerWidgetTest {
    @Test
    fun cryptoWidget_rendersInitialDefaultPriceCorrectly() = runGlanceAppWidgetUnitTest {
        setAppWidget(CryptoTrackerWidget())
        onNode(hasText("بازار رمزارز")).assertExists()
    }
}

با ادغام تست های Glance در سرور هایی یکپارچه سازی مداوم (مانند GitHub Actions یا GitLab CI)، تیم های توسعه اطمینان حاصل می کنند که تغییرات جدید در منطق برنامه منجر به ایجاد کرش در زمان رندر ویجت های کاربران نخواهد شد.

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

۱۲. محدودیت های فنی حافظه (Memory Limits) و تجربیات طلایی مهندسی

هنگام توسعه ویجت با Glance در محیط های تولیدی با ترافیک بالا، در نظر داشتن محدودیت های لایه سیستم عامل اندروید بسیار حیاتی است:

  • سقف حجم بافر تراکنش IPC: تمام درخت المان های ویجت به صورت Parcelable بین پروسه برنامه شما و پروسه لانچر جابجا می شود. حجم این بافر در کل سیستم اندروید حداکثر ۱ مگابایت است. بنابراین هرگز تصاویر بزرگ بیت مپ با کیفیت بالا را مستقیماً درون ویجت بارگذاری نکنید؛ همیشه ابعاد تصویر را دقیقاً به اندازه Dp فیزیکی ویجت ریسایز نمایید.
  • پرهیز از عملیات بلاک کننده در provideContent: تابع provideContent یک تابع تعلیق پذیر است اما تا زمانی که خروجی ندهد، لانچر صفحه خاکستری یا اسکلت بارگذاری را نشان می دهد. داده های خود را ابتدا از کش بخوانید و رندر کنید، سپس در پس زمینه به روزرسانی را کلید بزنید.
  • سازگاری با گوشه های گرد اندروید ۱۲+: از اندروید ۱۲ به بعد، سیستم عامل به طور خودکار گوشه های گرد به ویجت ها اعمال می کند. از دادن حاشیه های گرد نامتعارف و هاردکد شده خودداری کنید تا ظاهر ویجت در دستگاه های مختلف دچار ناهماهنگی نشود.
  • پشتیبانی از تم تیره و روشن: به جای هاردکد کردن کدهای هگز رنگی، از GlanceTheme.colors بهره بگیرید تا ویجت با تغییر تم سیستم عامل بلافاصله به رنگ بندی هماهنگ درآید.

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

پرسش های متداول و جمع بندی ساخت ویجت با Glance

آیا Jetpack Glance مستقیماً از تمام Composable های Jetpack Compose پشتیبانی می کند؟

خیر، کتابخانه Glance از کدهای کامپوز صرفاً به عنوان یک زبان اعلانی برای تولید RemoteViews استفاده می کند. بنابراین تنها می توانید از کامپوننت های اختصاصی پکیج androidx.glance نظیر Column، Row، Box، Text، Button، Image و LazyColumn استفاده نمایید. کامپوننت های عادی مانند Card یا Canvas در Glance پشتیبانی نمی شوند.

حداقل نسخه اندروید مورد نیاز برای استفاده از Jetpack Glance چیست؟

کتابخانه Jetpack Glance از API Level ۲۱ (اندروید ۵.۰ لالی پاپ) به بالا به طور کامل پشتیبانی می کند. این بدان معناست که بیش از ۹۹ درصد دستگاه های فعال در بازار بدون هیچ مشکلی قادر به اجرای ویجت های ساخته شده با Glance هستند.

چرا نباید از SharedPreferences سنتی برای مدیریت وضعیت Glance استفاده کرد؟

کتابخانه Glance به صورت پیش فرض با DataStore Preferences یکپارچه شده است. این ابزار عملیات خواندن و نوشتن را به صورت کاملاً غیرهمگام، ایمن در برابر کرش و سازگار با Coroutines مدیریت می کند؛ در حالی که SharedPreferences سنتی روی Thread اصلی اجرا شده و مستعد بروز خطای ANR است.

چگونه می توان یک لیست اسکرول پذیر از اقلام را در ویجت Glance ساخت؟

بر خلاف روش سنتی که نیازمند پیاده سازی RemoteViewsService و Factory پیچیده بود، در Glance کافی است از کامپوننت androidx.glance.appwidget.lazy.LazyColumn استفاده کرده و اقلام لیست را با متد items همانند کدهای عادی کامپوز تعریف نمایید.

چگونه می توان با لمس ویجت، یک اکتیویتی مشخص از برنامه را باز کرد؟

با استفاده از تابع کمکی actionStartActivity می توانید رویداد کلیک روی هر کامپوننت یا کل ویجت را مستقیماً به باز شدن اکتیویتی مقصد متصل کنید بدون اینکه نیازی به ساخت دستی PendingIntent باشد.

چرا تصاویر با کیفیت بالا در ویجت نمایش داده نمی شوند یا کرش می کنند؟

سیستم انتقال داده بین پروسه ای در اندروید دارای سقف حجم ۱ مگابایت برای بافر Transaction IPC است. اگر تصویری بیش از حد بزرگ باشد، سیستم عامل استثنای TransactionTooLargeException صادر می کند. باید تصاویر را پیش از ارسال به ویجت به ابعاد دقیق کادر مورد نظر مقیاس دهی کنید.

بهترین راهکار برای به روزرسانی دوره ای ویجت هر چند دقیقه یک بار چیست؟

پارامتر updatePeriodMillis سیستم عامل دارای کف ۳۰ دقیقه ای است. برای فواصل دقیق تر یا زمان بندی های واکنشی متصل به شبکه، باید از PeriodicWorkRequestBuilder در کتابخانه WorkManager همراه با قیدهای هوشمند مصرف انرژی استفاده کنید.

آیا Glance مصرف باتری دستگاه را نسبت به روش قدیمی افزایش می دهد؟

خیر، از آنجا که خروجی نهایی Glance دقیقاً همان اشیای بهینه RemoteViews است، مصرف باتری در سمت پروسه لانچر هیچ تفاوتی با روش سنتی ندارد و به دلیل مدیریت وضعیت کارآمدتر DataStore، در بسیاری از سناریوها حتی بهینه تر عمل می کند.

چک لیست نهایی و نکات کلیدی انتشار ویجت اندروید

چک لیست بازبینی کیفیت پیش از انتشار (Production Checklist): ایمپورت کامپوننت ها از پکیج glance، ثبت اکشن در مانیفست، تنظیم دقیق ابعاد سلولی در provider، اتصال وضعیت به DataStore، هندل کردن کلیک ها با ActionCallback و تنظیم Responsive SizeMode.

امتیاز و نظرات

۰.۰ (۰ نظر)
برای ثبت دیدگاه باید وارد حساب کاربری شوید. ورود

هنوز نظری ثبت نشده است. اولین نفر باشید.