Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building Beautiful Widget experiences on Android

Alex Styl
February 18, 2022

Building Beautiful Widget experiences on Android

A talk I did as part of my Android Dev Rel job interview at Google. The talk was on topic of building beautiful widgets on Android. It covers the cases of static, responsive and customisable widgets.

The talk was given on November 2018 and the material is still relevant today.

Alex Styl

February 18, 2022
Tweet

More Decks by Alex Styl

Other Decks in Technology

Transcript

  1. Model-View-Presenter fun startPresentingInto(view: WeatherForcastView) { subscription = forecastRepository .getWeeklyForecast() .map

    { weeklyForecast -> weeklyForecast.map { forecast -> viewModelFactory.viewModelFor(forecast) } } .subscribeOn(workScheduler) .observeOn(resultScheduler) .subscribe { view.displayForecast(it) } }
  2. Model-View-Presenter class ForecastActivity : AppCompatActivity(), { private lateinit var forecastAdapter:

    ForecastAdapter override { forecastAdapter.displayForecast(viewModels) } override { Toast.makeText(this, R.string.error_message, Toast.LENGTH_SHORT).show() } // rest of implementation } WeatherForcastView fun displayForecast(viewModels: List<ForecastModel>) fun displayError()
  3. class WeatherForecastWidgetProvider : AppWidgetProvider { private lateinit var presenter: WeatherForcastPresenter

    override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { val view = ??? presenter.startPresentingInto(view) }
  4. What can we can as a View in this case?


    There are no Activities, Fragments or Views
  5. private fun createRemoteViewsFor(viewModels: List<ForecastModel>): RemoteViews { val remoteViews = RemoteViews(context.packageName,

    R.layout.widget_weekly_forecast) viewModels .take(5) // display up to 5 days .forEachIndexed { index, forecastModel -> val widgetIds = indexToWidgetIds[index]!! remoteViews.setTextViewText(widgetIds.dateId, forecastModel.date) remoteViews.setTextViewText(widgetIds.degreesId, forecastModel.degrees) remoteViews.setImageViewResource(widgetIds.iconId, forecastModel.weatherIcon) } return remoteViews }
  6. class WeeklyForecastRowView(private val appWidgetManager: AppWidgetManager, private val appWidgetIds: List<Int>) :

    WeatherForcastView { override fun displayForecast(viewModels: List<ForecastModel>) { appWidgetIds.forEach { appWidgetId -> val remoteViews = createRemoteViewsFor(viewModels) appWidgetManager.updateAppWidget(appWidgetId, remoteViews) } }
  7. private fun Int.dpToCells() = (this + 30) / 70 fun

    Bundle.getMinColumns(): Int = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH).dpToCells() fun Bundle.getMaxColumns(): Int = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH).dpToCells() fun Bundle.getMinRows(): Int = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT).dpToCells() fun Bundle.getMaxRows(): Int = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT).dpToCells()
  8. class WeeklyForecastRowView: WeatherForcastView { override fun displayForecast(viewModels: List<ForecastModel>) { appWidgetIds.forEach

    { appWidgetId -> appWidgetManager.updateAppWidget(appWidgetId, remoteViews) } } val remoteViews = createRemoteViewsFor(viewModels)
  9. appWidgetManager.updateAppWidget(appWidgetId, remoteViews) } } class WeeklyForecastRowView: WeatherForcastView { override fun

    displayForecast(viewModels: List<ForecastModel>) { appWidgetIds.forEach { appWidgetId -> val widgetOptions = appWidgetManager.getAppWidgetOptions(appWidgetId) val numberOfColumns = widgetOptions.getMinColumns().capTo(5) , numberOfColumns val remoteViews = createRemoteViewsFor(viewModels )
  10. private fun createRemoteViewsFor( viewModels: List<ForecastModel>): RemoteViews { val remoteViews =

    RemoteViews(context.packageName, R.layout.widget_weekly_forecast) viewModels .take(5) .forEachIndexed { index, forecastModel -> val widgetIds = indexToWidgetIds[index]!! remoteViews.setTextViewText(widgetIds.dateId, forecastModel.date) remoteViews.setTextViewText(widgetIds.degreesId, forecastModel.degrees) remoteViews.setImageViewResource(widgetIds.iconId, forecastModel.weatherIcon) } return remoteViews }
  11. private fun createRemoteViewsFor( viewModels: List<ForecastModel>, numberOfColumns: Int): RemoteViews { val

    remoteViews = RemoteViews(context.packageName, R.layout.widget_weekly_forecast) viewModels .take(numberOfColumns) .forEachIndexed { index, forecastModel -> val widgetIds = indexToWidgetIds[index]!! remoteViews.setViewVisibility(widgetIds.groupID, View.VISIBLE) remoteViews.setTextViewText(widgetIds.dateId, forecastModel.date) remoteViews.setTextViewText(widgetIds.degreesId, forecastModel.degrees) remoteViews.setImageViewResource(widgetIds.iconId, forecastModel.weatherIcon) } return remoteViews } remoteViews.setViewVisibility(widgetIds.groupID, View.VISIBLE) .take(numberOfColumns) numberOfColumns: Int
  12. private fun createListRemoteViews(context: Context): RemoteViews { val remoteViews = RemoteViews(context.packageName,

    R.layout.widget_weekly_forecast_list) val intent = Intent(context, ForecastRemoteViewService::class.java) remoteViews.setRemoteAdapter(R.id.widget_forecast_list, intent) return remoteViews }
  13. presenter.startPrentingInto(WeeklyForecastRowView(appWidgetManager, context, rowWidgets)) } class WeatherForecastWidgetProvider : AppWidgetProvider() { private

    lateinit var presenter: WeatherForcastPresenter override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { super.onUpdate(context, appWidgetManager, appWidgetIds)
  14. val listWidgets = appWidgetIds.filter { appWidgetManager.getAppWidgetOptions(it).getMaxRows() > 1 } listWidgets.forEach

    { appWidgetId -> val remoteViews = createListRemoteViews(context) appWidgetManager.updateAppWidget(appWidgetId, remoteViews) } val rowWidgets = appWidgetIds.toList() - listWidgets if (rowWidgets.isNotEmpty()) { presenter.startPrentingInto(WeeklyForecastRowView(appWidgetManager, context, rowWidgets)) } class WeatherForecastWidgetProvider : AppWidgetProvider() { private lateinit var presenter: WeatherForcastPresenter override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { super.onUpdate(context, appWidgetManager, appWidgetIds)
  15. Widget Configuration Activity Provide options to customise the widget, utilities

    and/or styling Keep user’s journey in mind; don’t go deeper in navigation See Material Design’s Platform Guidance > Android widget
  16. Widget Configuration Activity val intent = intent val extras =

    intent.extras if (extras != null) { val mAppWidgetId = extras.getInt( AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) // TODO save user preferences updateWidgetWithId(mAppWidgetId) } setResult(Activity.RESULT_OK) finish() Saving changes and placing the widget
  17. Pinning Widgets on Homescreen since Android API 26 val mAppWidgetManager

    = context.getSystemService(AppWidgetManager::class); val myProvider = ComponentName(context, MyAppWidgetProvider:class); if (mAppWidgetManager.isRequestPinAppWidgetSupported()) { Intent pinnedWidgetCallbackIntent = new Intent( ... ); val successCallback = PendingIntent.createBroadcast(context, 0, pinnedWidgetCallbackIntent); mAppWidgetManager.requestPinAppWidget(myProvider, null, successCallback); }
  18. To sum up A widgets let users access your content

    faster Building responsive Widgets in steps: Create a Static Widget Allow configuration via the Configuration Activity Prompt user to Pin your Widget if they keep coming back Create resizable widgets with onAppWidgetOptionsChanged() Scrollable widgets are populated via the RemoteViewsService