Simple Currency is an application that handles currency conversion from 168 countries.
Here's a quick demo of how it works:
Here are some screenshots:
- convert currency from one to another
- support decimal point
- long press on
'x'
will clear all input - swap button to switch between the base and target currency quickly
- automatic currency rate updates every 30 minutes
- pick from 168 currencies
- filter currency by currency name or code in currency picker
- comma seperation for big numbers
- landscape mode supported
The API used for obtaining the latest currency data is CurrencyLayer.com. A free account can be made easily and it will provide an API key. You need to fill up the API key in api_keys.properties
file at the root of this project. Instructions:
- make a file named
api_keys.properties
in the root of the project - add this line in the file:
CURRENCY_LAYER_ACCESS_TOKEN=<fill in API key obtained from Currency Layer>
This is how the file looks like:
༼つ◕_◕༽つ RootOfSimpleCurrency (master)$ cat api_keys.properties
CURRENCY_LAYER_ACCESS_TOKEN=d0bbf06c7xxxxxxxxxxxx47d2e56ed6f
The following dependencies are used in this project:
- RxJava
- RxBinding
- Dagger2
- Retrofit
- Moshi
- Room
- Android Arch Lifecycle
- Android Arch ViewModel
- WorkManager
- Material Design Library
- Mockk
- JUnit
- etc..
This project is written in Kotlin.
MVVM is used in this app. The view layer is made reactive and passed into the ViewModel
as the input. External sources that are needed (such as database access, network calls, shared preference access) will be passed into ViewModel
as Repo
. This way ViewModel
doesn't have access to Android-related code so that it can be unit tested.
Room Persistence library is used to access SQLite easily. This is used to store the currency data. USD
is used as the base currency, so all the currency stored in the database is referenced against USD
. Let's say we wanted to find out Japanese Yen (JPY)
vs. Pound Sterling (GBP)
, simple math calculation will be done.
In every periodic interval (currently set at every 30 minutes), the WorkManager
will fire up a Retrofit call to get the latest currency rate. The obtained json will be deserialized by Moshi and write into the database.
The currency layer network library is extracted into a separate module so that it can be decoupled from the main app. It can be launched as a standalone separate network library or being swapped out and replaced by another currency API.
The project follows the unidirectional data flow rule to better structure the code.
Here's a brief diagram of the main activity architecture.
- RxView - The flow begins from the views. Every user view interactions are reactive and fed into the
ViewModel
. - Repo - Any external data access that is not from the user interactions, such as network calls, shared preference, database access, etc... will all go through the
Repo
class that is passed intoViewModel
. - ScreenState - The reactive signals from
Views
will be processed insideViewModel
by some business logic. After that, it will produce the nextScreenState
by using.copy()
from Kotlin to ensure immutability. - View Update - Finally, all the views will listen to the
ScreenState
and update itself accordingly.
This architecture is quite similar to MVI. The difference is that it doesn't use reducer or model the input as 'intent'.
ViewModel
doesn't have access to Android related code. Therefore, it can be tested by JUnit test
, without using androidTest
. Example Unit Test can be found MainViewModelTest.kt.
The general idea of testing an Activity can be described by this diagram:
The RxViews
signals are replaced by fake inputs from the test. Reactive RxBinding signals are be replaced by RxJava's Subjects
, reactive database access Flowable
are replaced by RxJava's Processors
and normal method calls are mocked by mockk
. This way, user interactions can be controlled by us. After that, we can make Assertion
on the ScreenState
to check if it behaves correctly.
Here's an example of testing a simple conversion (MainViewModelTest.kt#L112):
fun testSimpleConversion() {
// 1. arrange
val fakeRate = 2.0
viewModel.onCreate()
val screenStateTestObserver = viewModel.screenState.test()
// 2. act
populateDbIfFirstTime.onNext(true)
getLatestSelectedRateFlowable.offer(fakeRate)
onNumpad1Click.onNext('1')
onNumpad0Click.onNext('0')
onNumpad0Click.onNext('0')
// 3. assert
verify(exactly = 1) { repo.setupPeriodicUpdate() }
screenStateTestObserver.assertNoErrors()
screenStateTestObserver.lastValue.apply {
Assert.assertEquals("200", outputNumberString)
}
}
-
Arrange - we first setup the necessary objects
-
Act - next, we make some actions.
While we run the following:
// 2. act
populateDbIfFirstTime.onNext(true)
getLatestSelectedRateFlowable.offer(fakeRate)
onNumpad1Click.onNext('1')
onNumpad0Click.onNext('0')
onNumpad0Click.onNext('0')
It is actually doing these:
-
populateDbIfFirstTime.onNext(true)
- seeded the db -
getLatestSelectedRateFlowable.offer(fakeRate)
- taken the latest conversion rate from db -
onNumpad1Click.onNext('1')
,onNumpad1Click.onNext('0')
,onNumpad1Click.onNext('0')
- click on1
,0
,0
(100) -
Assert - finally, we check on the output:
screenStateTestObserver.lastValue.apply {
Assert.assertEquals("200", outputNumberString)
}
Since the fake conversaion rate is set to 2.0
, the output should be 200
when input is 100
.
Stetho library is used for debugging purpose. Network calls through OkHttp3 and SQLite Database can be viewed by accessing chrome://inspect/#devices
on a chrome browser. This is an example of how the database look like in DevTools: devtool db debug screenshot.
An adaptive icon is created using Sketch. The sketch file can be found in logo.sketch
file at the root of this project.
.editorconfig
is used in this project to make sure that the spacing and indentations are standardized, the editorconfig
is obtained from ktlint project.