Android Stock App 2: Dagger-Hilt with Multiple Retrofits

Part ONE < ===

The price of the Stock Exchange is depending on News. On the front page, you shall display the news for the app user. At this time, I choose RapidAPI, which has some free stock APIs. You can choose one as your additional stock news activity. I will divide one activity into three activities:

  • MainActivity => navigator
  • TiingoActivity => Tiingo server
  • BloombergActivity => Bloomberg server

💲1. Register A Free Account

=> Menu

I choose Bloomberg as my stock news API. It’s free if the requests are less than 500/month or 16/day. That is a great tool for the developer who wants to develop tools on the Stock Exchange.

RapidAPI also offers us some sample programming code. I am working on Android. So Java is the best fit for me.

After registration, we can start to work on the App.

💣2. Curl Test

=> Menu

Open a terminal, such as CMD; let’s use the code from Java.

curl -v "https://bloomberg-market-and-financial-news.p.rapidapi.com/stories/list?template=CURRENCY&id=usdjpy" ^
-H "x-rapidapi-host: bloomberg-market-and-financial-news.p.rapidapi.com" ^
-H "x-rapidapi-key:YOUR API KEY"

It returns a JSON response.

>curl -v ...
*...
<
{...JSON CONTENT...}* Connection #0 to host bloomberg-market-and-financial-news.p.rapidapi.com left intact
>

This is a good one.

🍨3. RestConfig and Story Data Class

=> Menu

Add Key and Server

The “secret” will move into the “network” package.

RestConfig.kt: Two sets of API server data: Tiingo and Bloomberg.

class RestConfig {
companion object {
const val DEBUG = true

// Tiingo
const val Tiingo_TEST_END = "/api/test"
const val Tiingo_TOKEN = "Your Token"
const val Tiingo_SERVER = "https://api.tiingo.com"
const val Tiingo_Header1 = "Content-Type: application/json"
const val Tiingo_Header2 = "Authorization: Token $Tiingo_TOKEN"

// Bloomberg
const val Bloomberg_STORY_CURRENCY = "/stories/list?template=CURRENCY&id=usdjpy"
const val Bloomberg_SERVER = "https://bloomberg-market-and-financial-news.p.rapidapi.com"
const val Bloomberg_KEY = "Your Key"
const val Bloomberg_Header1 = "x-rapidapi-host: bloomberg-market-and-financial-news.p.rapidapi.com"
const val Bloomberg_Header2 = "x-rapidapi-key: $Bloomberg_KEY"

}
}

Split Data Class

At the “data” package, I create a “tiingo” package to store Message class, and I create a “bloomberg” package to store the new data.

JSON:

{"stories":[...}

At data/bloomberg, the response message is “stories”. So I copy the JSON code into JK plugin to generate the Story class.

Story.kt,

data class Story(
val stories: List<StoryX>,
val title: String
)

StoryX.kt,

data class StoryX(
val card: String,
val internalID: String,
val longURL: String,
val primarySite: String,
val published: Int,
val resourceType: String,
val shortURL: String,
val thumbnailImage: String,
val title: String
)

We will insert StoryX data on a RecyclerView.

⌘4. Network: Split the API service

=> Menu

I have two API services in my app. So I split them into two packages.

At network/tiingo/:

TiingoApiService.kt,

interface TiingoApiService {
// Tiingo Test Message
@Headers(
RestConfig.Tiingo_Header1,
RestConfig.Tiingo_Header2
)
@GET(RestConfig.Tiingo_TEST_END)
suspend fun getTestMessage(): Message
}

TiingoApiServiceHelper.kt,

interface TiingoApiServiceHelper {
suspend fun getTestMsg(): Message
}

TiingoApiServiceHelperImpl.kt

@Singleton
class TiingoApiServiceHelperImpl @Inject constructor(
private val tiingoApiService: TiingoApiService
) : TiingoApiServiceHelper {
override suspend fun getTestMsg():
Message = tiingoApiService.getTestMessage()
}

At network/bloomberg:

BloombergApiService.kt,

interface BloombergApiService {
// Bloomberg Stories: Currency
@Headers(
RestConfig.Bloomberg_Header1,
RestConfig.Bloomberg_Header2
)
@GET(RestConfig.Bloomberg_STORY_CURRENCY)
suspend fun getStory(): Story
}

BloombergApiServiceHelper.kt,

interface BloombergApiServiceHelper {
// Bloomberg Stories: Currency
suspend fun getBloombergStory(): Story
}

BloombergApiServiceHelperImpl.kt,

@Singleton
class BloombergApiServiceHelperImpl @Inject constructor(
private val bloombergApiService: BloombergApiService
) : BloombergApiServiceHelper {
// Bloomberg Stories: Currency
override suspend fun getBloombergStory():
Story = bloombergApiService.getStory()
}

🔪5. Split ApiModule: Two Api Services

=> Menu

I split AppModule into two modules: BloombergNetworkModule and TiingoNetworkModule.

Qualifier: Tag Your Provides

Also, I create two qualifiers to separate the API services. At dagger/qualifier:

TypeEnum.kt, it contains all of the types of API service.

enum class TypeEnum {
URL, SERVER, GSON, OKHTTP, RETROFIT, APISERVICE, APIHELPER
}

BloombergNetwork.kt,

@Qualifier
@Documented
@Retention(AnnotationRetention.RUNTIME)
annotation class BloombergNetwork(val value: TypeEnum)

TiingoNetwork.kt,

@Qualifier
@Documented
@Retention(AnnotationRetention.RUNTIME)
annotation class TiingoNetwork(val value: TypeEnum)

👩🏻ApiModule: 👶🏻BloombergNetworkModule 👶🏻TiingoNetworkModule

BloombergNetworkModule.kt,

@Module
@InstallIn(ApplicationComponent::class)
class BloombergNetworkModule {
@Provides @BloombergNetwork(URL)
fun provideBloombergUrl() = RestConfig.Bloomberg_SERVER
@Provides @BloombergNetwork(GSON)
fun provideGson(): Gson = GsonBuilder().setLenient().create()
@Provides @BloombergNetwork(OKHTTP)
fun provideOkHttpClient() =
if (RestConfig.DEBUG) { // debug ON
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BODY
OkHttpClient.Builder()
.addInterceptor(logger)
.readTimeout(100, TimeUnit.SECONDS)
.connectTimeout(100, TimeUnit.SECONDS)
.build()
} else // debug OFF
OkHttpClient.Builder()
.readTimeout(100, TimeUnit.SECONDS)
.connectTimeout(100, TimeUnit.SECONDS)
.build()
@Provides @BloombergNetwork(RETROFIT)
fun provideRetrofit(
@BloombergNetwork(OKHTTP) okHttpClient: OkHttpClient,
@BloombergNetwork(URL) BaseURL: String):
Retrofit = Retrofit.Builder()
.baseUrl(BaseURL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides @BloombergNetwork(APISERVICE)
fun provideBloombergApiService(
@BloombergNetwork(RETROFIT) retrofit: Retrofit):
BloombergApiService = retrofit.create(BloombergApiService::class.java)
@Provides @BloombergNetwork(APIHELPER)
fun provideBloomberApiServiceHelper(apiHelper: BloombergApiServiceHelperImpl):
BloombergApiServiceHelper = apiHelper
}

Please check the Bold words, it’s easy to understand qualifier. Let’s tag BloombergApiServiceHelperImpl.kt, at network/bloombBloomberg, to create the binding.

class BloombergApiServiceHelperImpl @Inject constructor(
@BloombergNetwork(APISERVICE)
private val bloombergApiService: BloombergApiService
) : BloombergApiServiceHelper {...

Use Hilt anchors to check the binding until you hit the URL tag. Next,

TiingoNetworkModule.kt,

@Module
@InstallIn(ApplicationComponent::class)
class TiingoNetworkModule {
@Provides @TiingoNetwork(URL)
fun provideBaseUrl() = RestConfig.Tiingo_SERVER
@Provides @TiingoNetwork(GSON)
fun provideGson(): Gson = GsonBuilder().setLenient().create()
@Provides @TiingoNetwork(OKHTTP)
fun provideOkHttpClient() =
if (RestConfig.DEBUG) { // debug ON
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BODY
OkHttpClient.Builder()
.addInterceptor(logger)
.readTimeout(100, TimeUnit.SECONDS)
.connectTimeout(100, TimeUnit.SECONDS)
.build()
} else // debug OFF
OkHttpClient.Builder()
.readTimeout(100, TimeUnit.SECONDS)
.connectTimeout(100, TimeUnit.SECONDS)
.build()
@Provides @TiingoNetwork(RETROFIT)
fun provideRetrofit(
@TiingoNetwork(OKHTTP) okHttpClient: OkHttpClient,
@TiingoNetwork(URL) BaseURL: String
): Retrofit = Retrofit.Builder()
.baseUrl(BaseURL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides @TiingoNetwork(APISERVICE)
fun provideApiService(
@TiingoNetwork(RETROFIT) retrofit: Retrofit
): TiingoApiService = retrofit.create(TiingoApiService::class.java)
@Provides @TiingoNetwork(APIHELPER)
fun provideApiServiceHelper(apiHelper: TiingoApiServiceHelperImpl):
TiingoApiServiceHelper = apiHelper
}

Let’s fix the TiingoApiServiceHelperImpl.kt, at network/tiingo.

class TiingoApiServiceHelperImpl @Inject constructor(
@TiingoNetwork(APISERVICE) private val tiingoApiService...

🐙6. UI: MainActivity, TiingoActivity, and BloombergActivity

=> Menu

The MainActivity has no longer to display the Tiingo’s content. Now, it acts as a navigator to show sites that users can visit.

AndroidManifest.xml,

<application
...>
<activity android:name=".view.bloomberg.BloombergNewsActivity"></activity>
<activity android:name=".view.tiingo.TiingoActivity"></activity>
<activity android:name=".view.main.MainActivity">

It’s MVVM, so I place all the UI into the “view” package.

MainActivity

The activity_main.xml,

That’s it: two buttons.

MainActivity.kt,

private const val REQUEST_CODE_PERMISSIONS = 1111
private val REQUIRED_PERMISSIONS = arrayOf(...)

@AndroidEntryPoint // Hilt
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

setupButton()
}

fun setupButton() {
lgd("Setup buttons")
tiingo_bt.setOnClickListener {
val mIntent = Intent(this,
TiingoActivity::class.java)
startActivity(mIntent)
}

bloomberg_bt.setOnClickListener {
val mIntent = Intent(this,
BloombergNewsActivity::class.java)
startActivity(mIntent)
}
}


override fun onRequestPermissionsResult(...) {...}

private fun allPermissionsGranted()...

companion object {
private val TAG = "MYLOG MainActivity"
fun lgd(s: String) = Log.d(TAG, s)
// len: 0=>short; 1=>long
fun msg(context: Context, s: String, len: Int) =
if (len == 0)
Toast.makeText(context, s,
Toast.LENGTH_SHORT).show()
else
Toast.makeText(context, s,
Toast.LENGTH_LONG).show()

const val INTERNET = "INTERNET"
}
}

TiingoActivity

The activity_tiingo.xml,

TiingoActivity.kt, it is as same as old MainActivity. Please rename the ViewModel to TiingoViewModel.

@AndroidEntryPoint  // Hilt
class TiingoActivity : AppCompatActivity() {

private val tiingoViewModel : TiingoViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tiingo)

setupObserver()
}

fun setupObserver() {
lgd("setup observer")
tiingoViewModel.message.observe(this, Observer {...})
}

companion object {
private val TAG = "MYLOG TiingoAct"
fun lgd(s: String) = Log.d(TAG, s)
// len: 0=>short; 1=>long
fun msg(context: Context, s: String, len: Int) =
if (len == 0)
Toast.makeText(context, s,
Toast.LENGTH_SHORT).show()
else
Toast.makeText(context, s,
Toast.LENGTH_LONG).show()

const val INTERNET = "INTERNET"
}
}

TiingoViewModel.kt, it is as same as old MainViewModel. Please rename the Repository to TiingoRepository.

class TiingoViewModel @ViewModelInject constructor(
private val tiingoRepository: TiingoRepository,
networkHelper: NetworkHelper
) : ViewModel() {...

TiingoRepository.kt, at the “room” package.

@Singleton
class TiingoRepository @Inject constructor(
@TiingoNetwork(APIHELPER)
private val tiingoTiingoApiHelper: TiingoApiServiceHelper
) {
suspend fun getTestMsg():
Message = tiingoTiingoApiHelper.getTestMsg()
}

Please check the Hilt link. You must be able to jump back to TiingoNetworkModule by clicking 🔨,

BloombergActivity

The activiy_bloomberg_news.html,

BloombergNewsActivity.kt,

@AndroidEntryPoint  // Hilt
class BloombergNewsActivity : AppCompatActivity() {

private val newsViewModel : BloombergNewsViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_bloomberg_news)

setupObserver()
}

fun setupObserver() {
newsViewModel.story.observe(this, Observer {
when (it.status) {
Status.SUCCESS -> {
news_tv.text = it.data!!.stories.toString()
news_pb.visibility = View.GONE
}
Status.LOADING -> {
news_pb.visibility = View.VISIBLE
}
Status.ERROR -> {
news_pb.visibility = View.GONE

val tempStr = it.message

// internet error
val foundIdx = tempStr?.indexOf(
INTERNET, 0)!!
if (foundIdx > 0) {
val spStr = SpannableString(tempStr)
val color = Color.RED
spStr.setSpan(
ForegroundColorSpan(color),
foundIdx,
foundIdx+8,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
news_tv.text = spStr
} else { // other error
news_tv.text = tempStr
}
msg(this, tempStr, 1)
}
}
})
}
companion object {
private val TAG = "MYLOG BloombergAct"
fun lgd(s: String) = Log.d(TAG, s)
fun lge(s: String) = Log.e(TAG, s)
fun lgi(s: String) = Log.i(TAG, s)
// len: 0=>short; 1=>long
fun msg(context: Context, s: String, len: Int) =
if (len == 0)
Toast.makeText(context, s,
Toast.LENGTH_SHORT).show()
else
Toast.makeText(context, s,
Toast.LENGTH_LONG).show()

const val INTERNET = "INTERNET"
}
}

BloombergNewsViewModel.kt,

class BloombergNewsViewModel @ViewModelInject constructor(
private val bloombergRepository: BloombergRepository,
networkHelper: NetworkHelper
) : ViewModel() {

// cached
private val _story = MutableLiveData<Resource<Story>>()
// public
val story: LiveData<Resource<Story>> = _story

init {
_story.postValue(Resource.loading(null))

if (networkHelper.isNetworkConnected()) {
fetchStory()
} else {
_story.postValue(
Resource.error(
data = null,
message = internetErr))
}
}

fun fetchStory() {
viewModelScope.launch {
lgd("fetching story...")
try {
_story.value = Resource.success(
data = bloombergRepository.getCurrencyStory())
} catch (exception: Exception) {
_story.value = Resource.error(
data = null,
message = exception.message ?: otherErr
)
}
}
}

companion object {
private val TAG = "MYLOG BB-NewsVM"
const val otherErr = "Error Occurred!"
const val internetErr = "Internet Connection Error"
fun lgd(s: String) = Log.d(TAG, s)
}
}

BloombergRepository.kt, at the “room” package.

@Singleton
class BloombergRepository @Inject constructor(
@BloombergNetwork(APIHELPER)
private val bloombergApiHelper: BloombergApiServiceHelper
) {
suspend fun getCurrencyStory():
Story = bloombergApiHelper.getBloombergStory()
}

📳7. Test Connection and Activities

=> Menu

Run,

Tiingo Activity:

Bloomberg Activity:

Both activities have downloaded JSON data. That proves both network connections are working properly.

😏Enjoy!

===> Part THREE

Computer Science BS from SFSU. I studied and worked on Android system since 2017. If you are interesting in my past works, please go to my LinkedIn.