الحاجيات على الروبوت. ميزة نادرة لمعرفة

مرحبا يا هابر! اسمي ألكسندر خاكيموف ، وأنا مطور أندرويد في FINCH.

هل حدث أن تصميمك كان لنظام iOS ، وعليك تكييفه لنظام Android؟ إذا كان الأمر كذلك ، فهل غالبًا ما يستخدم المصممون أدوات؟ لسوء الحظ ، تعد القطعة حالة نادرة للعديد من المطورين ، لأنه نادرًا ما يعمل أي شخص معها.

في هذه المقالة سأخبرك بالتفصيل كيفية إنشاء أداة ، والتي تستحق الانتباه وستشارك حالتي.

إنشاء القطعة


لإنشاء أداة تحتاج إلى معرفتها:

  1. ملامح مكونات القطعة.
  2. ميزات عرض القطعة في شبكة الشاشة.
  3. تحديثات القطعة الميزات.

سنقوم بتحليل كل عنصر على حدة.

ملامح مكونات القطعة


أي مطور عمل مع RemoteViews مرة واحدة على الأقل على دراية بهذا العنصر. إذا كنت أحد هؤلاء ، فلا تتردد في الانتقال إلى النقطة التالية.

تم تصميم RemoteViews لوصف وإدارة التدرجات الهرمية لطرق العرض التي تنتمي إلى عملية في تطبيق آخر. باستخدام إدارة التسلسل الهرمي ، يمكنك تغيير الخصائص أو طرق الاتصال التي تنتمي إلى طريقة العرض ، والتي تعد جزءًا من تطبيق آخر. تتضمن RemoteViews مجموعة محدودة من المكونات من مكتبة مكونات android.widget القياسية.

يعمل عرض عناصر واجهة المستخدم الداخلية في عملية منفصلة (عادةً ما تكون هذه هي الشاشة الرئيسية) ، لذلك ، لتغيير واجهة مستخدم الأداة ، استخدم امتداد BroadcastReceiver - AppWidgetProvider ، الذي يعمل في تطبيقنا.

ميزات عرض القطعة في "شبكة" الشاشة


في الواقع ، هذه النقطة ليست معقدة للغاية ، إذا نظرت إلى الإرشادات الرسمية :
يجب على كل أداة تعريف minWidth و minHeight ، مع الإشارة إلى الحد الأدنى من المساحة التي يجب أن تستهلكها بشكل افتراضي. عندما يضيف المستخدمون أداة إلى شاشتهم الرئيسية ، سيشغلون عمومًا أكثر من الحد الأدنى للعرض والارتفاع اللذين تحددهما. توفر الشاشات الرئيسية في Android للمستخدمين شبكة من المساحات المتاحة التي يمكنهم من خلالها وضع الأدوات والأيقونات. يمكن أن تختلف هذه الشبكة حسب الجهاز ؛ على سبيل المثال ، توفر العديد من الهواتف شبكة 4 × 4 ، ويمكن للأجهزة اللوحية تقديم شبكة أكبر 8 × 7.

الترجمة إلى اللغة الروسية: يجب على كل أداة تعيين الحد الأدنى للعرض والارتفاع الخاص بها للإشارة إلى الحد الأدنى للمساحة التي ستشغلها افتراضيًا.

صورة
مثال على إعدادات الأداة عند الإنشاء في Android Studio

ستأخذ الأداة التي تمت إضافتها إلى الشاشة الرئيسية مساحة أكبر من الحد الأدنى للعرض والارتفاع للشاشة التي قمت بتعيينها. توفر الشاشات الرئيسية في Android للمستخدمين شبكة من المساحات المتاحة التي يمكن وضع الأدوات والأيقونات فيها. قد تختلف هذه الشبكة حسب الجهاز ؛ على سبيل المثال ، توفر العديد من الهواتف شبكات 4 × 4 ، ويمكن للأجهزة اللوحية تقديم شبكات كبيرة 8 × 4.

من هذا يصبح من الواضح أن شبكة الجهاز يمكن أن تكون أي شيء ، وقد يختلف حجم الخلية ، اعتمادًا على حجم الشبكة. وفقًا لذلك ، يجب تصميم محتوى الأداة مع مراعاة هذه الميزات.

يمكن حساب الحد الأدنى لعرض وارتفاع عنصر واجهة المستخدم لعدد معين من الأعمدة والصفوف باستخدام الصيغة:

minSideSizeDp = 70 × n - 30 ، حيث n هو عدد الصفوف أو الأعمدة.

في الوقت الحالي ، الحد الأقصى للشبكة التي يمكنك تعيينها هو 4x4. هذا يضمن أن يتم عرض القطعة الخاصة بك على جميع الأجهزة.

تحديثات القطعة الميزات


نظرًا لأن AppWidgetProvider هو في الأساس امتداد لـ BroadcastReceiver ، يمكنك أن تفعل الشيء نفسه كما هو الحال مع BroadcastReceiver العادي. AppWidgetProvider يوزع ببساطة الحقول المقابلة من Intent المتلقاة في onReceive ويستدعي طرق الاعتراض مع الإضافات المستلمة.

نشأت الصعوبة مع تكرار تحديث المحتوى - النقطة الأساسية هي الفرق في التشغيل الداخلي للأدوات على iOS و Android. الحقيقة هي أنه يتم تحديث البيانات الموجودة على عناصر واجهة استخدام iOS عندما تصبح الأداة مرئية للمستخدم. في Android ، لا يوجد مثل هذا الحدث. لا يمكننا معرفة متى يرى المستخدم القطعة.

بالنسبة إلى الأدوات على Android ، فإن طريقة التحديث الموصى بها هي تحديث مؤقت. يتم ضبط إعدادات المؤقت بواسطة معلمة عنصر واجهة المستخدم updatePeriodMillis. للأسف ، لا يسمح هذا الإعداد بتحديث الأداة أكثر من مرة كل 30 دقيقة. أدناه سأتحدث عن هذا بمزيد من التفصيل.

حالة القطعة


علاوة على ذلك ، سنتحدث عن الحالة التي أجريناها في FINCH في تطبيق يانصيب كبير مع تطبيق Stoloto للمشاركة في يانصيب الدولة.

مهمة التطبيق هي تبسيط وجعل شفافة للمستخدم اختيار اليانصيب وشراء تذكرة. لذلك ، فإن الوظيفة المطلوبة للأداة بسيطة للغاية: اعرض الألعاب التي ينصح بها المستخدم للشراء وانقر فوق للانتقال إلى اللعبة المقابلة. يتم تحديد قائمة الألعاب على الخادم ويتم تحديثها بانتظام.

في حالتنا ، تضمن تصميم الأداة حالتين:

  • للمستخدم المصرح به
  • لمستخدم غير مصرح به

يحتاج المستخدم المصرح له إلى إظهار بيانات ملفه الشخصي: حالة المحفظة الداخلية وعدد التذاكر التي تنتظر السحب ومقدار الفوز. لكل عنصر من هذه العناصر ، يتم توفير انتقال إلى الشاشة داخل التطبيق ، يختلف عن العناصر الأخرى.

صورة

صورة

كما لاحظت ، ميزة أخرى للمستخدم المصرح به هي زر "التحديث" ، ولكن المزيد عن ذلك لاحقًا.

لتنفيذ عرض حالتين ، مع مراعاة التصميم ، استخدمت RemoteAdapter كتطبيق لـ RemoteViewsService لإنشاء بطاقات المحتوى.

والآن كود صغير وكيف يعمل كل شيء في الداخل. إذا كانت لديك بالفعل تجربة مع الأداة ، فأنت تعلم أن أي تحديث لبيانات الأداة يبدأ بطريقة onUpdate:

override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        injector.openScope(this, *arrayOf(this))
        // update each of the widgets with the remote adapter
        appWidgetIds
            .forEach {
                updateWidget(context, appWidgetManager, it)
          }
    }

نكتب تحديثًا لكل مثيل من القطعة.

private fun updateWidget(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetId: Int
    ) {
// remoteViews   widgetId
        val remoteViews = RemoteViews(
            context.packageName,
            R.layout.app_widget_layout
...
//        
...
//    remoteViews
updateRemoteAdapter(context, remoteViews, appWidgetId)
 
//   remoteViews 
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
// collection view  
appWidgetManager.notifyAppWidgetViewDataChanged(
            appWidgetId,
            R.id.lvWidgetItems
        )
    }

تحديث المحول.

private fun updateRemoteAdapter(context: Context, remoteViews: RemoteViews, appWidgetId: Int) {
//   RemoteViewsService   RemoteAdapter   
        val adapterIntent = Intent(context, StolotoAppWidgetRemoteViewsService::class.java).apply {
            putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
        }
        remoteViews.setRemoteAdapter(R.id.lvWidgetItems, adapterIntent)
// actionIntent  pendingIntent      
        val actionIntent = Intent(context, StolotoAppWidgetProvider::class.java).apply {
            action = WIDGET_CLICK_ACTION
            putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
        }
        val pendingIntent = PendingIntent.getBroadcast(
            context, 0, actionIntent,
            PendingIntent.FLAG_UPDATE_CURRENT
        )
// pendingIntent      
        remoteViews.setPendingIntentTemplate(R.id.lvWidgetItems, pendingIntent)
    }

نحن نكتب تنفيذ خدمتنا. في ذلك ، من المهم بالنسبة لنا أن نشير إلى أي تطبيق واجهة RemoteViewsService.RemoteViewsFactory لاستخدامه في إنشاء المحتوى.

class StolotoAppWidgetRemoteViewsService : RemoteViewsService() {
    override fun onGetViewFactory(intent: Intent): RemoteViewsFactory =
        StolotoAppWidgetRemoteViewsFactory(
            this.applicationContext,
            intent
        )
}

هذا في الواقع غلاف رقيق فوق المحول. بفضله ، يمكننا ربط بياناتنا بعرض المجموعة عن بعد. يوفر RemoteViewsFactory طرقًا لإنشاء RemoteViews لكل عنصر في مجموعة البيانات. ليس لدى المنشئ متطلبات - كل ما أفعله هو تمرير السياق فيه.

بعد ذلك ، بضع كلمات حول الطرق الرئيسية:

  1. onCreate - إنشاء محول.
  2. getLoadingView - تقترح الطريقة إعادة العرض ، والذي سيعرضه النظام بدلاً من عناصر القائمة أثناء إنشائها. إذا لم تقم بإنشاء أي شيء هنا ، فسيستخدم النظام بعض طريقة العرض الافتراضية.
  3. getViewAt - تقترح الطريقة إنشاء عناصر قائمة. هنا يأتي الاستخدام القياسي لـ RemoteViews.
  4. يتم استدعاء onDataSetChanged عند تلقي طلب لتحديث البيانات في القائمة. أولئك. في هذه الطريقة ، نقوم بإعداد البيانات للقائمة. يتم شحذ هذه الطريقة من خلال تنفيذ رمز ثقيل وطويل.
  5. يتم استدعاء onDestroy عند حذف القائمة الأخيرة التي يستخدمها المحول (يمكن استخدام محول واحد من قبل عدة قوائم).
  6. يعيش RemoteViewsFactory بينما تكون جميع مثيلات القائمة حية ، لذلك يمكننا تخزين البيانات الحالية فيه ، على سبيل المثال ، قائمة العناصر الحالية.

حدد قائمة بالبيانات التي سنعرضها:

private val widgetItems = ArrayList<WidgetItem>()

عند إنشاء المحول ، نبدأ في تحميل البيانات. هنا يمكنك القيام بأية مهام صعبة بأمان ، بما في ذلك المشي بهدوء في الشبكة مما يمنع التدفق.

override fun onCreate() {
        updateDataSync()
}

عند استدعاء الأمر لتحديث البيانات ، فإننا نسمي أيضًا updateDataSync ()

   override fun onDataSetChanged() {
        updateDataSync()
    }

داخل updateDataSync ، كل شيء بسيط أيضًا. نقوم بمسح القائمة الحالية للعناصر. تنزيل ملف التعريف وبيانات اللعبة.

 private fun updateDataSync() {
        widgetItems.clear()
        updateProfileSync()
        updateGamesSync()
    }

إنه أكثر إثارة للاهتمام هنا

private fun updateProfileSync() {

نظرًا لأنه من المهم بالنسبة لنا عرض الملف الشخصي فقط لمستخدم معتمد ، فنحن بحاجة إلى تنزيل معلومات الملف الشخصي فقط في هذه الحالة:

val isUserFullAuth = isUserFullAuthInteractor
            .execute()
            .blockingGet()
        if (isUserFullAuth) {
            val profile = getWidgetProfileInteractor
                .execute()
                .onErrorReturn {
                    WidgetProfile()
//           
                }
                .blockingGet()

يتم تجميع نموذج WidgetProfile من مصادر مختلفة ، لذلك يتم ترتيب منطق استلامها وقيمها الافتراضية بطريقة تشير فيها قيمة المحفظة السلبية إلى بيانات غير صحيحة أو مشاكل في استلامها.

بالنسبة إلى منطق الأعمال ، يعد نقص بيانات المحفظة أمرًا بالغ الأهمية ، لذلك ، في حالة وجود محفظة غير صحيحة ، لن يتم إنشاء نموذج ملف تعريف وإضافته إلى قائمة العناصر.

  if (profile.walletAmount >= 0L) {
                widgetItems.add(
                    WidgetItem.Profile(
                        wallet = profile.walletAmount.toMoneyFormat(),
                        waitingTickets = if (profile.waitingTicketsCount >= 0) profile.waitingTicketsCount.toString() else "",
                        unpaidPrizeAmount = if (profile.unpaidPrizeAmount >= 0) profile.unpaidPrizeAmoount.toMoneyFormat() else ""
                    )
                )
            }
        }
    }

يستخدم أسلوب updateGamesSync () getWidgetGamesInteractor ويضيف مجموعة من الألعاب ذات الصلة بعنصر واجهة المستخدم إلى قائمة عناصر عنصر واجهة المستخدم.

قبل الشروع في إنشاء البطاقات ، فكر في نموذج WidgetItem بمزيد من التفاصيل. يتم تنفيذه من خلال فئة kotlin المختومة ، مما يجعل النموذج أكثر مرونة ، والعمل معه أكثر ملاءمة.

sealed class WidgetItem {
 
    data class Profile(
        val wallet: String,
        val waitingTickets: String,
        val unpaidPrizeAmount: String
    ) : WidgetItem()
 
    data class Game(
        val id: String,
        val iconId: Int,
        val prizeValue: String,
        val date: String
    ) : WidgetItem()
}

إنشاء RemoteViews وتحديد استجابتها من خلال FillInIntent

override fun getViewAt(position: Int): RemoteViews {
        return when (val item = widgetItems[position]) {
            is WidgetItem.Profile -> {
              RemoteViews(
                        context.packageName,
                        R.layout.item_widget_user_profile
                    ).apply {
                        setTextViewText(R.id.tvWidgetWalletMoney, item.wallet)
                        setTextViewText(R.id.tvWidgetUnpaidCount, item.unpaidPrizeAmount)
                        setTextViewText(R.id.tvWidgetWaitingCount, item.waitingTickets)
                        setOnClickFillInIntent(
                            R.id.llWidgetProfileWallet, Intent().putExtra(
                                StolotoAppWidgetProvider.KEY_PROFILE_OPTIONS,
                                StolotoAppWidgetProvider.VALUE_USER_WALLET
                            )
                        )
                        setOnClickFillInIntent(
                            R.id.llWidgetProfileUnpaid, Intent().putExtra(
                                StolotoAppWidgetProvider.KEY_PROFILE_OPTIONS,
                                StolotoAppWidgetProvider.VALUE_UNPAID_PRIZE
                            )
                        )
                        setOnClickFillInIntent(
                            R.id.llWidgetProfileWaiting, Intent().putExtra(
                                StolotoAppWidgetProvider.KEY_PROFILE_OPTIONS,
                                StolotoAppWidgetProvider.VALUE_WAITING_TICKETS
                            )
                        )
                    }
 
            is WidgetItem.Game -> {
                RemoteViews(
                    context.packageName,
                    R.layout.item_widget_game
                ).apply {
                    setImageViewResource(R.id.ivWidgetGame, item.iconId)
                    setTextViewText(R.id.tvWidgetGamePrize, item.prizeValue)
                    setTextViewText(R.id.tvWidgetGameDate, item.date)
                    setOnClickFillInIntent(
                        R.id.llWidgetGame, Intent().putExtra(
                            StolotoAppWidgetProvider.KEY_GAME_CLICK, item.id
                        )
                    )
                }
            }
        }
    }

يعين الأسلوب setOnClickFillInInentent هدف viewId المحدد ، والذي سيتم دمجه مع الأصل PendingIntent لتحديد السلوك عند النقر فوق طريقة العرض باستخدام معرّف viewId هذا. بهذه الطريقة يمكننا الرد على نقرات المستخدم في WidgetProvider.

تحديث القطعة اليدوي


تم تعيين وقت تحديث نصف ساعة لعنصر واجهة المستخدم الخاص بنا. يمكنك تحديثه في كثير من الأحيان ، على سبيل المثال ، من خلال الرقص مع WorkManager ، ولكن لماذا تحميل شبكتك وبطاريتك؟ بدا مثل هذا السلوك في المراحل الأولى من التطور كافيا.

تغير كل شيء عندما لاحظ "العمل" أنه عندما ينظر المستخدم إلى الأداة ، يتم عرض البيانات غير ذات الصلة عليها: "هنا على جهاز iPhone الخاص بي ، أفتح الأداة وهناك أحدث البيانات في ملفي الشخصي."

الوضع شائع: يولد iOS بطاقات جديدة لعرض كل عنصر واجهة مستخدم ، لأنه يحتوي على شاشة خاصة لذلك ، ولا يحتوي Android على مثل هذه الأحداث للأداة من حيث المبدأ. اضطررت إلى مراعاة أن بعض اليانصيب تقام مرة واحدة كل 15 دقيقة ، لذلك يجب أن توفر الأداة معلومات ذات صلة - تريد المشاركة في نوع من السحب ، لكنها مرت بالفعل.

من أجل الخروج من هذا الوضع غير السار وحل المشكلة بطريقة ما بتحديث البيانات ، اقترحت وطبقت حلًا تم اختباره بالوقت - زر "التحديث".

أضف هذا الزر إلى تخطيط التخطيط مع القائمة وقم بتهيئة سلوكه عند استدعاء updateWidget.

...
// Intent   AppWidgetManager.ACTION_APPWIDGET_UPDATE
val intentUpdate = Intent(context, StolotoAppWidgetProvider::class.java)
intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE

//    
val ids = AppWidgetManager.getInstance(context)
   .getAppWidgetIds(ComponentName(context, StolotoAppWidgetProvider::class.java))
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)

//  intent  PendingIntent,  PendingIntent.getBroadcast()
val pendingUpdate = PendingIntent.getBroadcast(
   context,
   appWidgetId,
   intentUpdate,
   PendingIntent.FLAG_UPDATE_CURRENT
)
//  pendingIntent      ‘’
remoteViews.setOnClickPendingIntent(R.id.ivWidgetRefresh, pendingUpdate)

أظهرت التطورات الأولى صورة حزينة: من الضغط على زر "التحديث" إلى التحديث الفعلي ، يمكن أن تمر عدة ثوان. على الرغم من أن الأداة يتم إنشاؤها بواسطة تطبيقنا ، إلا أنها في الواقع تحت سيطرة النظام وتتواصل مع تطبيقنا من خلال عمليات البث.

أولئك. عند النقر فوق الزر "تحديث" في عنصر واجهة المستخدم الخاص بنا ، تبدأ السلسلة:

  1. احصل على النية في "إجراء" مزود onReceive.
  2. AppWidgetManager.ACTION_APPWIDGET_UPDATE.
  3. استدعاء onUpdate لجميع عناصر واجهة المستخدم المحددة في intent-e.
  4. اتصل بالإنترنت للحصول على بيانات جديدة.
  5. قم بتحديث البيانات المحلية وعرض بطاقات القائمة الجديدة.

ونتيجة لذلك ، لم يكن تحديث الأداة لطيفًا جدًا ، لأنه بالنقر على الزر ، نظرنا إلى الأداة المصغرة لبضع ثوانٍ. ولم يتضح ما إذا تم تحديث البيانات. كيف تحل مشكلة الاستجابة البصرية؟

أولاً ، أضفت علامة isWidgetLoading مع وصول عام عبر المتفاعل. دور هذه المعلمة بسيط للغاية - لا تعرض زر التحديث أثناء تحميل بيانات الأداة.

ثانيًا ، قسمت عملية تحميل البيانات في المصنع إلى ثلاث مراحل:

enum class LoadingStep {
   START,
   MIDDLE,
   END
}

ابدأ - بدء التنزيل. في هذه المرحلة ، تتغير حالة جميع طرق عرض المحول وعلامة التنزيل العامة إلى "تحميل".

MIDDLE - مرحلة تحميل البيانات الرئيسية. بعد تنزيلها ، يتم وضع علامة التنزيل العامة في حالة "تحميل" ، ويتم عرض البيانات التي تم تنزيلها في المحول.

النهاية - نهاية التنزيل. لا يحتاج المحول إلى تغيير بيانات المحول في هذه الخطوة. هذه الخطوة مطلوبة لمعالجة مرحلة تحديث طرق العرض في WidgetProvider بشكل صحيح.

دعنا نرى بمزيد من التفاصيل كيف يبدو تحديث الزر في الموفر:

if (isFullAuthorized && !widgetLoadingStateInteractor.isWidgetLoading) {
   remoteViews.setViewVisibility(R.id.ivWidgetRefresh, View.VISIBLE)
...
//     ,    
...   
} else {
   remoteViews.setViewVisibility(
       R.id.ivWidgetRefresh,
       if (isFullAuthorized) View.INVISIBLE else View.GONE //       .
   )
}

الآن دعونا نلقي نظرة على ما يحدث في المحول:

private fun updateDataSync() {
   when (loadingStep) {
       START -> {
           widgetItems.forEach { it.isLoading = true }
           widgetLoadingStateInteractor.isWidgetLoading = true
           loadingStep = MIDDLE
           widgetManager.updateWidgets()
       }
       MIDDLE -> {
           widgetItems.clear()
           updateProfileSync()
           updateGamesSync()
           widgetLoadingStateInteractor.isWidgetLoading = false
           loadingStep = END
           widgetManager.updateWidgets()
       }
       END -> {
           loadingStep = START
       }
   }
}

صورة

منطق العمل:

  1. في نهاية خطوات START و MIDDLE ، أسمي طريقة updateWidgets لتحديث حالة العرض التي يديرها الموفر.
  2. START «» , MIDDLE.
  3. MIDDLE, «».
  4. MIDDLE, END.
  5. , END, «». , END loadingStep START.



بمساعدة مثل هذا التنفيذ ، توصلت إلى حل وسط بين متطلبات "الأعمال" لرؤية البيانات ذات الصلة على القطعة والحاجة إلى "سحب" التحديث كثيرًا.

آمل أن تكون المقالة مفيدة لك. إذا كانت لديك خبرة في إنشاء أدوات لـ Android ، فأخبرنا بذلك في التعليقات.

حظا طيبا وفقك الله

All Articles