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. Building Beautiful 

    Widgets Experiences

    in Android
    Alexandros Stylianidis

    @alextyl

    View full-size slide

  2. ForecastActivity.kt
    Displays the weekly forecast
    Model-View-Presenter

    View full-size slide

  3. Model-View-Presenter
    data class ForecastModel(
    val date: String,
    val degrees: String,
    @DrawableRes val weatherIcon: Int
    )

    View full-size slide

  4. 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)
    }
    }

    View full-size slide

  5. Model-View-Presenter
    interface {
    }
    WeatherForcastView
    fun displayForecast(viewModels: List)
    fun displayError()

    View full-size slide

  6. 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)
    fun displayError()

    View full-size slide

  7. Building a static Widget

    View full-size slide

  8. App Widget Provider

    View full-size slide

  9. App Widget Provider
    MyWidgetProvider : AppWidgetProvider() widget_configuration.xml
    =
    +

    View full-size slide

  10. onDisabled()
    onUpdate()
    onDeleted()
    MyWidgetProvider : AppWidgetProvider()
    onEnabled()

    View full-size slide

  11. class WeatherForecastWidgetProvider : AppWidgetProvider {
    private lateinit var presenter: WeatherForcastPresenter
    override fun onUpdate(
    context: Context,
    appWidgetManager: AppWidgetManager,
    appWidgetIds: IntArray) {
    val view = ???
    presenter.startPresentingInto(view)
    }

    View full-size slide

  12. What can we can as a View in this case?

    There are no Activities, Fragments or Views

    View full-size slide

  13. AppWidgetManager

    View full-size slide

  14. AppWidgetManager
    appWidgetManager.updateAppWidget(appWidgetId, remoteViews)

    View full-size slide

  15. private fun createRemoteViewsFor(viewModels: List): 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
    }

    View full-size slide

  16. class WeeklyForecastRowView(private val appWidgetManager: AppWidgetManager,
    private val appWidgetIds: List)
    : WeatherForcastView {
    override fun displayForecast(viewModels: List) {
    appWidgetIds.forEach { appWidgetId ->
    val remoteViews = createRemoteViewsFor(viewModels)
    appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
    }
    }

    View full-size slide

  17. Building a resizable Widget

    View full-size slide

  18. App Widget Provider
    onEnabled()
    onDisabled()
    onUpdate()
    onDeleted()

    View full-size slide

  19. App Widget Provider
    onEnabled()
    onDisabled()
    onUpdate()
    onDeleted()
    onAppWidgetOptionsChanged()
    (since Android API 16)

    View full-size slide

  20. The new Bundle will return the new size of the widget
    in dp…

    View full-size slide

  21. The new Bundle will return the new size of the widget
    in dp…

    View full-size slide

  22. See: Standard Widget Anatomy
    App Widget Design Guidelines

    View full-size slide

  23. 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()

    View full-size slide

  24. class WeeklyForecastRowView: WeatherForcastView {
    override fun displayForecast(viewModels: List) {
    appWidgetIds.forEach { appWidgetId ->
    appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
    }
    }
    val remoteViews = createRemoteViewsFor(viewModels)

    View full-size slide

  25. appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
    }
    }
    class WeeklyForecastRowView: WeatherForcastView {
    override fun displayForecast(viewModels: List) {
    appWidgetIds.forEach { appWidgetId ->
    val widgetOptions = appWidgetManager.getAppWidgetOptions(appWidgetId)
    val numberOfColumns = widgetOptions.getMinColumns().capTo(5)
    , numberOfColumns
    val remoteViews = createRemoteViewsFor(viewModels )

    View full-size slide

  26. private fun createRemoteViewsFor(
    viewModels: List): 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
    }

    View full-size slide

  27. private fun createRemoteViewsFor(
    viewModels: List, 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

    View full-size slide

  28. Building a Responsive Design Widget

    View full-size slide

  29. Populating a list of RemoteViews
    using a RemoteFactoryService

    View full-size slide

  30. Populating a list of RemoteViews
    using a RemoteFactoryService

    View full-size slide

  31. RemoteFactoryService
    fun onGetViewFactory(intent: Intent): RemoteViewsFactory
    since Android API 11

    View full-size slide

  32. RemoteViewsFactory
    fun onCreate()
    fun getLoadingView()
    fun onDataSetChanged()
    fun getViewAt(position: Int): RemoteViews
    Initialise the factory

    View full-size slide

  33. RemoteViewsFactory
    fun onCreate()
    Initialise your factory

    View full-size slide

  34. RemoteViewsFactory
    fun onDataSetChanged()
    Widget needs to be update.

    Refresh your data synchronously

    View full-size slide

  35. RemoteViewsFactory
    fun getLoadingView():RemoteViews?
    How does each row look like 

    while loading?

    View full-size slide

  36. RemoteViewsFactory
    fun getViewAt(position: Int): RemoteViews
    RemoteViews of each Row

    View full-size slide

  37. 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
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  40. Building a Customisable Widget

    View full-size slide

  41. Widget Configuration Activity
    using a RemoteFactoryService
    since Android API 1

    View full-size slide

  42. Widget Configuration Activity
    using a RemoteFactoryService
    android:name=".upcomingforcast.appwidget.preview.WidgetPreviewActivity"
    android:noHistory="true">




    View full-size slide

  43. How is the widget linked to the configuration activity?

    View full-size slide

  44. How is the widget linked to the configuration activity?
    through the widget_configuration.xml

    View full-size slide

  45. App Widget Provider
    MyWidgetProvider : AppWidgetProvider() .xml
    =
    + widget_configuration

    View full-size slide

  46. android:configure=".appwidget.preview.WidgetPreviewActivity"
    android:initialLayout="@layout/widget_todays_forecast"
    android:previewImage=“@drawable/preview_widget”
    android:resizeMode="horizontal|vertical"
    android:updatePeriodMillis="3600000"
    android:widgetCategory="home_screen" />
    android:minWidth="48dp"
    android:minHeight="48dp"
    .xml
    widget_configuration

    View full-size slide

  47. android:name=".upcomingforcast.appwidget.WeatherForecastWidgetProvider"
    android:label="@string/widget_name">



    android:name="android.appwidget.provider"
    android:resource=“@xml/ “ />

    widget_configuration
    AndroidManifest.xml

    View full-size slide

  48. 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

    View full-size slide

  49. 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

    View full-size slide

  50. 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);
    }

    View full-size slide

  51. 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

    View full-size slide

  52. Thank you!
    Alexandros Stylianidis

    @alextyl

    View full-size slide