Slide 1

Slide 1 text

Building Beautiful 
 Widgets Experiences
 in Android Alexandros Stylianidis
 @alextyl

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

Weatherino

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Building a static Widget

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

App Widget Provider

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

What can we can as a View in this case?
 There are no Activities, Fragments or Views

Slide 20

Slide 20 text

AppWidgetManager

Slide 21

Slide 21 text

AppWidgetManager appWidgetManager.updateAppWidget(appWidgetId, remoteViews)

Slide 22

Slide 22 text

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 }

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

Building a resizable Widget

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

See: Standard Widget Anatomy App Widget Design Guidelines

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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 )

Slide 35

Slide 35 text

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 }

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Building a Responsive Design Widget

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

Populating a list of RemoteViews using a RemoteFactoryService

Slide 41

Slide 41 text

Populating a list of RemoteViews using a RemoteFactoryService

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

RemoteViewsFactory fun onCreate() Initialise your factory

Slide 45

Slide 45 text

RemoteViewsFactory fun onDataSetChanged() Widget needs to be update.
 Refresh your data synchronously

Slide 46

Slide 46 text

RemoteViewsFactory fun getLoadingView():RemoteViews? How does each row look like 
 while loading?

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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 }

Slide 49

Slide 49 text

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)

Slide 50

Slide 50 text

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)

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

No content

Slide 54

Slide 54 text

Building a Customisable Widget

Slide 55

Slide 55 text

Widget Configuration Activity using a RemoteFactoryService since Android API 1

Slide 56

Slide 56 text

Widget Configuration Activity using a RemoteFactoryService

Slide 57

Slide 57 text

How is the widget linked to the configuration activity?

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

android:minWidth="48dp" android:minHeight="48dp" .xml widget_configuration

Slide 61

Slide 61 text

widget_configuration AndroidManifest.xml

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

No content

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

No content

Slide 67

Slide 67 text

No content

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

Thank you! Alexandros Stylianidis
 @alextyl