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.
Introduction
Previously we looked at the main way that I reduced the amount of effort required to get a simple currency converter app working. However, just because this is a limited user app does not mean that we can cut corners across the board. In this post, we’ll look at some areas where I opted not to cut any corners. We’ll also explore the benefits that I got from that.
Architecture
When writing limited use-case apps, there is a strong temptation to hack things together. However, I opted to keep a well defined, clean architecture within the app. An example of this is that I use a domain data model which contains the core information that the app needs to surface to the user.
In this case I store details of the exchange rate in one structure:
data class ExchangeRate( val from: Currency, val to: Currency, val date: LocalDateTime, val timestamp: Instant, val rate: BigDecimal )The account balance is stored in another:
data class Balance( val currency: Currency, val timestamp: Instant, val balance: BigDecimal )
These are both very different from the API responses the I get from their respective TransferWise API calls. This requires more effort than simply using TransferWise data structures internally. It requires conversions from TransferWise model to my domain model. However, the benefits are worthwhile, in my opinion.
Backend Changes
I haven’t yet mentioned that at first, I used a different currency exchange rates API. While this worked, I started looking at using the technique I had used for driving the LaMetric Time – Google Sheets. While this offered greater flexibility, it came with additional overhead – such as authentication. This would require much more effort.
I then realised that I could bypass much of the complexity by using TransferWise as explained in the previous post. Not only does this simplify the process quite considerably, but it also gives me access to my real account balance.
Letting data structures of the backend provider spread throughout the codebase would have made changing providers much more difficult. However, I was able to switch to TransferWise pretty quickly once I had made the decision to do so.
Repository
I elected to use Room persistence along with a repository to manage data caching. Many third-party APIs have access quotas and will return errors if you exceed those quotas. I was able to limit how often I hit the TransferWise APIs by caching data within the app. Moreover, I was able to adopt different policies for different API calls once I had switched to TransferWise. Exchange Rates are much more fluid than my account balance. I was able to have independent cache durations for those two values. We refreshed exchange rates no more frequently than 15-minute intervals. Whereas my account balance will refresh no more frequently than 60-minute intervals.
This logic gets applied through the repository, but ensures that I will not exceed my API quotas for TransferWise.
Persistence
I designed my domain model with persistence in mind. The data types used are not restricted to the primitives supported by Room & SQLlite. However, I added a few custom converters to marshall between easily consumable types in the domain model and persisted values:
class BigDecimalConverters { @TypeConverter fun fromString(value: String): BigDecimal = BigDecimal(value) @TypeConverter fun toString(bigDecimal: BigDecimal): String = bigDecimal.toString() } class CurrencyConverters { @TypeConverter fun fromString(currencyCode: String): Currency = Currency.getInstance(currencyCode) @TypeConverter fun toString(currency: Currency): String = currency.currencyCode } class DateTimeConverters { @TypeConverter fun fromString(dateTime: String): LocalDateTime = LocalDateTime.parse(dateTime, DateTimeFormatter.ISO_LOCAL_DATE_TIME) @TypeConverter fun toString(dateTime: LocalDateTime): String = dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) } class InstantConverters { @TypeConverter fun fromEpocMillis(epocMillis: Long): Instant = Instant.ofEpochMilli(epocMillis) @TypeConverter fun toEpocMillis(instant: Instant): Long = instant.toEpochMilli() }
It’s worth re-iterating that separating the domain model from the backend data model saved me time. It meant that my persistence layer required no changes when I switched between backend providers.
Background Work
Periodically we need to refresh these values. Restrictions on background activity have become stricter on more recent versions of Android. So trying to cut corners here will not produce the required behaviour. Therefore a WorkManager task handles the periodic refresh.
I schedule a periodic task to run every fifteen minutes whenever there is an active consumer of the data. This task will only run if a data connection is available. The task itself will retrieve both the exchange rate and account balance from the repository. The repository logic will throttle the account balance lookup to once an hour, and return the cached value otherwise.
It may seem like there is no point in caching the exchange rate value as this will expire every 15 minutes. So the periodic task will trigger a backend call whenever it runs. However, caching through the repository prevents race conditions that may occur if multiple distinct consumers are accessing it. If another consumer requests the exchange rate from the repository within the validity period, then a backend call is avoided.
Conclusion
Just because this is a pet project for myself does not mean that I went too hacky. I changed tack a few times with some choices. However, having a clear architectural structure enabled me to do this much easier. Moreover, some things would not have worked well if I had not been mindful of API quotas and background constraints. Using appropriate techniques for those was vital.
In the next article we’ll look at how I opted to surface this information to the user.
Normally I like to include working code to accompany each article in a series of posts. However, in this case, the result will be a fully working app. This will be published along with the next article in the series, I promise!
Many thanks to Sebastiano Poggi for useful discussions around background processing. Seb raised a lot of good points when we were discussing things and the result is a much better implementation. Thanks Seb!
© 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.