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
— ===MenU=== —
💲 1. Register A Free Account
💣2. Curl Test
🍨3. RestConfig and Story Data Class
⌘4. Network: Split the API service
🔪5. Split ApiModule: Two Api Services
🐙6. UI: MainActivity, TiingoActivity, and BloombergActivity
📳7. Test Connection and Activities
💲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