A simple crypto app with Nomics API


This blog post will show you how to create a simple MVVM cryptocurrency app using the Nomics API. Some familiarity with Jetpack Compose is strongly recommended; the purpose of this tutorial is to show developers how to create a fully functional app using Compose as well as dependency injection and REST API calls. The end of this post has links to the reference documentation for the various libraries used in the project.


The complete project can be found here: https://github.com/PopularPenguin/CryptoApp


Setup


First start by going to New Project > Empty Compose Activity. Minimum API level can be set at 21: Android 5.0 Lollipop, the minimum that is required for Jetpack Compose.


Now, we need to start adding our dependencies. We first add kotlin-kapt for annotation processing in the build script block. This is used to convert our Hilt annotations (like @HiltAndroidApp and @AndroidEntryPoint, more on this later) into generated code.


We need Retrofit for HTTP requests, Moshi for JSON parsing, and Hilt for dependency injection. Our Lifecycle ViewModel dependency allows us to call our viewmodel from within our composable functions with viewmodel(). Finally, we add Coil to handle loading SVG images from a network request.


build.gradle (Project)

buildscript {
dependencies {
classpath("com.google.dagger:hilt-android-gradle-plugin:2.40.5")
}
ext {
compose_version = '1.0.5'
}
}

build.gradle (Module)

plugins {
id "kotlin-kapt"
id "dagger.hilt.android.plugin"
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
dependencies {
...

// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha02"

// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-moshi:2.9.0"
implementation "com.squareup.okhttp3:logging-interceptor:3.9.0"

// Hilt
implementation "com.google.dagger:hilt-android:2.40.5"
kapt "com.google.dagger:hilt-android-compiler:2.40.5"

// Moshi
implementation "com.squareup.moshi:moshi-kotlin:1.13.0"
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.13.0"

// Coil
implementation "io.coil-kt:coil-compose:1.3.2"
implementation "io.coil-kt:coil-svg:1.3.2"
    ...
}

At the bottom of your build.gradle file add this block required by kapt for allowing references to generated code:


kapt {
correctErrorTypes = true
}

We now have to set up our API key. Get a free one from nomics.com and put it inside your global gradle.properties file (should be under Gradle Scripts in the Android project view).


gradle.properties


NomicsApiKey="INSERT YOUR API KEY STRING HERE"

Adding your API key here will prevent it from being stored in version control. It is important to not make your API key visible if you plan on making your code publicly available!


Now inside your modules’ build.gradle file add under buildTypes:


buildTypes {
debug {
buildConfigField 'String', 'NOMICS_API_KEY', NomicsApiKey
}
release {
buildConfigField 'String', 'NOMICS_API_KEY', NomicsApiKey
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}

Data


Create a new package called util and create a Kotlin object called Constants.kt

Inside, put the Nomics URL, a reference to our API key, and a list of currencies to load from our API.

object Constants {
const val BASE_URL = "https://api.nomics.com/v1/"
const val API_KEY = BuildConfig.NOMICS_API_KEY

val currencies = listOf(
"BTC", "ETH", "LTC", "XRP", "ADA", "USDT", "BNB", "USDC", "SOL", "HEX",
"LUNA", "DOGE"
)
}

Now, let’s set up Hilt. We will need dependency injection for our repository as well as accessing Retrofit and Moshi from anywhere in the app.


Create BaseApplication.kt in your projects root package. The following steps are needed for Hilt to work with your app.


BaseApplication.kt

@HiltAndroidApp
class BaseApplication : Application()

In your android manifest first add the INTERNET permission:


AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET"/>

In the manifest inside the application block assign the base application class to your custom application, this is required for hilt:

<application
android:name=".BaseApplication"
...

In MainActivity.kt annotate the MainActivity class with @AndroidEntryPoint, which will allow you to use your dependencies in this class.

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    ...

Create a new package named di and create a Kotlin class called AppModule.kt, you will provide the Moshi and Retrofit builders here.

@Module
@InstallIn(SingletonComponent::class)
class AppModule {

@Singleton
@Provides
fun provideMoshi(): Moshi {
return Moshi.Builder().build()
}

@Singleton
@Provides
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create())
.build()
}

...

Create a new package called data. Within, create 3 sub packages called api and model. The api package will hold the Retrofit interface for making API calls, while the model package will contain the Currency class which will hold the cryptocurrency data.


Create the Currency class.This class will model the JSON data returned by the Nomics API. Also create the OneDay class, which is needed for modeling one day percent changes.


data/model/Currency.kt

@JsonClass(generateAdapter = true)
data class Currency(
val currency: String?,
val id: String?,
val price: String?,
@Json(name = "price_date") val date: String?,
@Json(name = "price_timestamp") val timestamp: String?,
val symbol: String?,
val name: String?,
@Json(name = "logo_url") val logoUrl: String?,
val rank: String?,
@Json(name = "1d") val oneDay: OneDay
)

@JsonClass(generateAdapter = true)
data class OneDay(
@Json(name = "price_change") val priceChange: String?,
@Json(name = "price_change_pct") val priceChangePct: String?
)

Now create the ApiService interface in the api package. Create the first function, prefaced with @GET(“markets/currencies/ticker”). getTicker() is a suspend function and must be called asynchronously from within a coroutine. It will return a list of crypto currencies and their data from the API’s ticker. Note that the API key is required as a query parameter; if we don't include it, we will get an authorization error response from the server. The second query parameter is a comma separated list of cryptocurrency ids.


data/api/ApiService.kt

interface ApiService {

@GET("currencies/ticker")
suspend fun getTicker(
@Query("key") apiKey: String = Constants.API_KEY,
@Query("ids") idsCsv: String
) : List<Currency>
}

Provide the ApiService within AppModule.kt for Hilt:


di/AppModule.kt

...
@Singleton
@Provides
fun provideApiService(retroFit: Retrofit): ApiService {
return retroFit.create(ApiService::class.java)
}

Before we create our UI, we still need two more classes, the Repository and the ViewModel.

Create the Repository class inside the data/repository package. This class will act as an intermediary between the ViewModel and API. Create a suspend function called getTicker() to access the API.


data/repository/Repository.kt

class Repository(private val apiService: ApiService) {

suspend fun getTicker(ids: List<String>): List<Currency> {
return apiService.getTicker(idsCsv = ids.joinToString(separator = ","))
}
}

Provide the API and Repository in di/AppModule.


di/AppModule.kt

@Singleton
@Provides
fun provideRepository(apiService: ApiService): Repository {
return Repository(apiService)
}

Create the MainViewModel class inside ui/main.


ui/main/MainViewModel.kt

@HiltViewModel
class MainViewModel
@Inject
constructor(
private val repository: Repository
) : ViewModel() { ... }

Hilt will inject the Repository into your ViewModel for you when you use the @HiltViewModel and @Inject annotations.


Create a variable called uiState. This variable is a MutableStateFlow, which is a stateful object that your composable functions will observe and recompose whenever the value held by the state flow is updated.

var uiState = MutableStateFlow<List<Currency>>(listOf())
private set

Create a private suspend function called getTickerData() and fetch the repository’s ticker data from within a coroutine and assign it to our uiState variable. This function will be called asynchronously from our init block’s coroutine.

private suspend fun getTickerData() {
uiState.value = repository.getTicker(Constants.currencies)
}

Finally, we will at an init block that will be run when the ViewModel is instantiated. This will update the cryptocurrency list once on creation then again periodically every 30 seconds.


ui/main/MainViewModel.kt

@HiltViewModel
class MainViewModel
@Inject
constructor(
private val repository: Repository
) : ViewModel() {

var uiState = MutableStateFlow<List<Currency>>(listOf())
private set

init {
viewModelScope.launch {
while (true) {
getTickerData()
delay(30_000L) // update every 30 seconds
}
}
}

private suspend fun getTickerData() {
uiState.value = repository.getTicker(Constants.currencies)
}
}


UI


Time to finally start on our composable UI! The MainScreen composable function gets a reference to our viewmodel and then fetches our currency list. Since this list is a stateful object (a MutableStateFlow), the MainScreen function will be automatically recomposed if the currency list is updated.


Inside the ui/main/ListComponents.kt package, add our first composable function:


ui/main/ListComponents.kt

@Composable
fun MainScreen() {
val viewModel: MainViewModel = viewModel()
val currencyList = viewModel.uiState.collectAsState().value
// ImageLoader required for decoding SVG files
// put here to avoid having to construct a new one for every list item
val imageLoader = ImageLoader.Builder(LocalContext.current)
.componentRegistry {
add(SvgDecoder(LocalContext.current))
}
.build()

LazyColumn {
items(currencyList) { currency ->
CryptoListItem(currency = currency, imageLoader = imageLoader)
}
}
}

Call the MainScreen() function from MainActivity (I moved MainActivity into a new package here).


ui/main/MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
CryptoAppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
MainScreen()
}
}
}
}

We add Coil for image loading, Coil uses Kotlin Coroutines and is lightweight and very fast. We also need an additional dependency for loading SVG images (which we already set up). We will set up an imageLoader and pass it into a CompositionLocalProvider, then call the URL from within an Image composable with rememberImagePainter().


We will add this later

CompositionLocalProvider(LocalImageLoader provides imageLoader) {
Image(
modifier = Modifier.size(64.dp),
painter = rememberImagePainter( // Coil loading the SVG from the network
data = currency.logoUrl,
builder = {
transformations(CircleCropTransformation())
}
),
contentDescription = currency.name,
contentScale = ContentScale.FillBounds
)
}

The ImageLoader object should be created outside of our list and passed as an argument in order to needlessly avoid recreating this object for every recomposition. It should only be reconstructed if the entire app is being recreated. Remember this for performance purposes; you shouldn’t needlessly create objects to be recomposed, especially if it is to be used inside a list with many elements.


Create a composable function called CoinImage, which will hold the cryptocurrency logo as well as its name and symbol.


ui/main/CoinImage.kt

@OptIn(ExperimentalCoilApi::class)
@Composable
fun CoinImage(currency: Currency, imageLoader: ImageLoader) {
Row {
// required to wrap Image in a CompositionLocalProvider to use the ImageLoader
CompositionLocalProvider(LocalImageLoader provides imageLoader) {
Image(
modifier = Modifier.size(64.dp),
painter = rememberImagePainter( // Coil loading the SVG from network
data = currency.logoUrl,
builder = {
transformations(CircleCropTransformation())
}
),
contentDescription = currency.name,
contentScale = ContentScale.FillBounds
)
}

Column(modifier = Modifier.padding(all = 8.dp)) {
// currency name
Text(
text = currency.name ?: "",
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
// currency symbol (abbreviation)
Text(
text = currency.symbol ?: "",
fontSize = 12.sp,
fontWeight = FontWeight.Light
)
}
}
}


Create Format.kt. This object will have two functions. The first, formatUS will convert the price returned from our API into a 2 digit US dollar formatted string. The second, formatPercent, will format a string from the API, adding a positive or negative sign prefix and a percent symbol postfix. This will be needed for our CoinPrice composable function.


util/Format.kt

object Format {
fun formatUS(price: String?): String {
price?.toDoubleOrNull()?.let { value ->
val format = NumberFormat.getCurrencyInstance().apply {
maximumFractionDigits = 2
minimumFractionDigits = 2
currency = Currency.getInstance("USD")
}

return format.format(value)
}

return "$0.00"
}

fun formatPercent(percent: String?): String {
percent?.toDoubleOrNull()?.let { value ->
val format = NumberFormat.getPercentInstance().apply {
maximumFractionDigits = 2
minimumFractionDigits = 2
}
val sign = if (value < 0) "-" else "+"

return "$sign${format.format(value)}"
}

return "+0.00%"
}
}

We will now create a CoinPrice composable, which is a column with 2 values, the current price and the daily percentage change below it.


Create the arrow graphics in File > New > Vector Asset. Select Clip Art and search for the trending arrows. Pick a green color for trending up and save it. Do this 2 more times for the down arrow and the flat arrow. Color them green and black respectively.

Now write the code for the composable. We need to format the change percent from a string into type double so we can check which direction the cryptocurrency is trending. Once we do that, we can assign an image for the arrow and a color for the percentage text. Alternatively, you could just copy this xml into 3 files and put them in res/drawable.


res/drawable/up.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#129C4B"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16,6l2.29,2.29 -4.88,4.88 -4,-4L2,16.59 3.41
,18l6,-6 4,4 6.3,-6.29L22,12V6z" />
</vector>

res/drawable/down.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FF2B2B"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16,18l2.29,-2.29 -4.88,-4.88 -4,4L2,7.41 3.41,6l6,6 4
,-4 6.3,6.29L22,12v6z" />
</vector>

res/drawable/flat.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#000000">
<path
android:fillColor="@android:color/black"
android:pathData="M22,12l-4,-4v3H3v2h15v3z"/>
</vector>

Add to our compose theme colors (I moved the theme package into my ui package):


ui/theme/Color.kt

val Gray300 = Color(0xFFCCD1D1)
val Green300 = Color(0xFF129C4B)
val Red300 = Color(0xFFFF2B2B)

Create the CoinPrice composable function.


ui/main/ListComponents.kt

@Composable
fun CoinPrice(currency: Currency) {
val percent = currency.oneDay.priceChangePct
// formatted percent change
val changePercent = Format.formatPercent(currency.oneDay.priceChangePct)
// set red or green arrow depending on which direction the price is headed
val trendingImageResId = percent?.toDoubleOrNull()?.let { value ->
if (value < 0) R.drawable.down else R.drawable.up
} ?: R.drawable.flat
// set the color of the percentage
val textColor = percent?.toDoubleOrNull()?.let { value ->
if (value < 0) Red300 else Green300
} ?: Color.Black

Row(modifier = Modifier.padding(all = 8.dp)) {
// trend direction image
Image(
modifier = Modifier
.size(24.dp)
.align(Alignment.CenterVertically),
painter = painterResource(id = trendingImageResId),
contentDescription = null
)
Spacer(modifier = Modifier.width(24.dp))
Column(modifier = Modifier.width(100.dp)) {
// current price
Text(
modifier = Modifier.align(Alignment.End),
text = Format.formatUS(currency.price),
fontSize = 18.sp
)
Spacer(modifier = Modifier.height(4.dp))
// daily change percentage
Text(
modifier = Modifier.align(Alignment.End),
text = changePercent,
color = textColor,
fontSize = 12.sp
)
}
}
}


Finally, we create a composable for each individual list item, which will connect to the previous two composables that we created.


ui/main/ListComponents.kt

@Composable
fun CryptoListItem(currency: Currency, imageLoader: ImageLoader) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(brush = Brush.horizontalGradient(colors = listOf(Color.White, Gray300)))
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
CoinImage(currency = currency, imageLoader = imageLoader)
CoinPrice(currency = currency)
}
}

That’s all for now. Currently we have a functional cryptocurrency list but there is more that needs to be done for a complete app. We could add a detail screen that displays trend data for each item in the list... stay tuned for part 2!


Documentation

Project Github Page

https://github.com/PopularPenguin/CryptoApp

Compose


https://developer.android.com/jetpack/compose


Nomics API



Hilt


https://developer.android.com/training/dependency-injection/hilt-android


Moshi


https://github.com/square/moshi


Coil


Comments

Popular posts from this blog

Jetpack Compose Login UI

Gradients