App Design / App Widget

Currency Converter: UI

At the end of 2020, I published a post on displaying currency conversion rates on a LaMetric Time smart clock. That solution only worked when I was working in my home office because that’s where the LaMetric Time lives. So I decided to build something similar on Android for other times. The app I produced is not production quality, because of scalability. But I have no intention of releasing it through any app stores. However, it suites my needs perfectly. In this series, we’ll look at various aspects of the app.

Previously in this series, we have looked at some of the app design decisions that I made with this app. But now we’ll consider how I want to surface the information to the user.

There is quite a clear, single use-case that I want to address: Surfacing the information with minimal user action required. The obvious candidate for this is to use an AppWidget to allow the user to add a widget to their home screen and see the currency rates by waking the device. This requires far less user action than launching an app in order to view this information.

AppWidgets

Everyone knows that Apple invented home screen widgets when it released iOS 14 in 2020. Although they have been part of Android since version 1.5 (API 3 – Cupcake), many app designers have neglected them because they were an Android-only feature for so long. But, of course, now that they are available on iOS there is more demand for them.

Unfortunately for us, as Android app developers, the APIs for App Widgets have remained largely the same since API 3. Many newer Android may not have even had cause to use them. So, it makes sense to give a refresher for those that haven’t used them for a long time, or those who have never used them.

On the surface things appear easy enough, but the complexity for App Widgets is that they are not running within the context of you application. They actually run within the context of whatever home screen app the user is using. The UI surface that we have to work with is therefore much more limited than for UI which runs within our app context.

RemoteViews

The mechanism for updating a UI that is running within another application is achieved by using RemoteViews. We define an XML layout for our widget, but we must update the widgets within that layout using a RemoteViews instance. A quick look at the API reference docs for RemoveViews shows that a limited set of layout types and widgets are actually supported. There’s no support for ConstraintLayout, or any MDC components. Also, there is (at the time of writing – January 2021) no way of driving the UI from Compose UI.

All in all, the App Widget APIs are in need of some love, and bringing in to line with modern Android tools and development.

AppWidgetProvider

The workhorse for driving an App Widget is the AppWidgetProvider. This is a subclass of BroadcastReceiver which will receive a broadcast whenever the data in the App Widget needs updating:

@AndroidEntryPoint
class CurrencyAppWidgetProvider : AppWidgetProvider() {

    @Inject
    lateinit var updateScheduler: UpdateScheduler

    private var balance: String? = null
    private var converted: String? = null

    override fun onEnabled(context: Context?) {
        super.onEnabled(context)
        Timber.d("OnEnabled")
        updateScheduler.startUpdates()
    }

    override fun onReceive(context: Context, intent: Intent) {
        balance = intent.getStringExtra("BALANCE")
        converted = intent.getStringExtra("CONVERTED")
        super.onReceive(context, intent)
    }

    override fun onUpdate(context: Context, widgetManager: AppWidgetManager, widgetIds: IntArray) {
        widgetIds.forEach { id ->
            Timber.d("onUpdate")
            val views = RemoteViews(context.packageName, R.layout.currency_widget).apply {
                balance?.also {
                    setTextViewText(R.id.balance, it)
                }
                converted?.also {
                    setTextViewText(R.id.converted, it)
                }
            }
            widgetManager.updateAppWidget(id, views)
        }
    }

    override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
        super.onDeleted(context, appWidgetIds)
        Timber.d("OnDisabled")
        updateScheduler.stopUpdates()
    }
}

The onEnabled() method is called whenever a new widget instance is created. This makes a request to updateScheduler to request regular updates. requestScheduler will create a WorkManager task to run periodically to retrieve updated data. When this task completes successfully, it will send a Broadcast containing the balance and converted balance values.

The onReceive() method is invoked whenever a Broadcast is received. It extracts the balance and converted balance values from the Intent. Internally the onReceive() method of the base class will extract other information from the Broadcast Intent, and call the onUpdate() method.

The onUpdate() method is where the app widget instances are updated. It receives an array of widgetIds – each of these represents a widget instance on the user’s home screen. For each of these we create a RemoteViews instance based upon the XML layout for the widget. Then we update the two TextView instance values within the RemoteViews. Finally we apply the RemoteViews to the widgetId. This will cause the values of the TextViews on the widget to be updated.

The final method is onDisabled() which will be called when the last app widget has been removed from the home screen. In this case, we call stopUpdates() on updateScheduler. This will cancel the periodic WorkManager task.

UpdateScheduler

I won’t give a detailed explanation of UpdateScheduler – it is starting and stopping WorkManager tasks. However, it is worth looking at one aspect of it.

I opted to use a simple DataStore to persist a boolean value which indicates whether there is a periodic worker active. I tried querying WorkManager directly to determine this, but with limited success. So decided to track this manually.

We must access DataStore asynchronously, so a lifecycleOwner is used to provide a CoroutineContext. Hilt injects this lifecycleOwner and it is bound to the running process. This is a little broader in scope than an Activity, or Fragment. So works when called from a BroadcastReceiver (the AppWidgetProvider) which may run when our main app is not active.

AppWidgetProviderInfo

Next we need to create an XML file which defines our App Widget:


This defines various aspects of the app widget instance. In this case I have created a 2 x 1 cell wide widget that is not resizeable. I have also specified the default layout which matches the layout ID used for creating a RemoteViews instance earlier on. Also, I have provided a preview image. When the user adds a new home screen widget, this will be displayed in the system widget selector.

For a fuller description of these attributes, please see the earlier article I wrote on this.

AndroidManifest

The final thing we need to do is add an entry to our Manifest:



    

    
        
            
                

                
            
        

        

        
            
                
            
            
        

    


This links the CurrencyAppWidgetProvider class that we looked at earlier to the app widget provider info XML that we just looked at.

Conclusion

We now have a working widget. I have skipped over much of the internals, although we explored the overall architecture in the previous article.

Initially, my plan was to leave things there. However, I wasn’t happy with an edge-case which I discovered. Occasionally the CurencyAppWidgetProvider would be invoked without the balance and converted balance values set. As a result, the widget would appear blank. This would resolve in time because a periodic update would happen at some point and things would work well, again. If I was just using this myself and hadn’t published the code, then I probably wouldn’t bother fixing it. However, as it has been published, I feel I should fix it. In the next, and concluding article in this series, we’ll look at how to resolve that,

The source code for this article is available here.

© 2021, Mark Allison. All rights reserved.

Copyright © 2021 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.