The video used IEX Cloud, which has shut down. This version uses Finnhub: sign up and copy your free API key. The free plan covers US stocks and 60 requests a minute.
2. Create the project
In Android Studio choose New Project → Empty Views Activity, with Language Java and Minimum SDK API 24.
3. Keep the key out of your code
Add your key to local.properties in the project folder:
properties
FINNHUB_API_KEY=your-key-here
This file is never committed, so your key isn't published with your code.
4. Add Volley and read the key
In app/build.gradle.kts, add the import at the very top:
kotlin
import java.util.Properties
Inside android { defaultConfig { } }:
kotlin
val localProperties =Properties().apply{
rootProject.file("local.properties").takeIf{ it.exists()}?.inputStream()?.use{load(it)}}buildConfigField("String","FINNHUB_API_KEY","\"${localProperties.getProperty("FINNHUB_API_KEY","")}\"")
Inside android { }:
kotlin
buildFeatures {
buildConfig =true}
Inside dependencies { }:
kotlin
implementation("com.android.volley:volley:1.2.1")
Press Sync Now. Also add <uses-permission android:name="android.permission.INTERNET" /> to the manifest.
5. Ask Finnhub
StockClient sends two requests with Volley:
/quote?symbol=AAPL for the price: c current, d change, dp change %, o open, h high, l low, pc previous close.
/stock/profile2?symbol=AAPL once per symbol, for the company name, exchange and currency.
Volley works in the background and calls you back on the main thread, so results go straight onto the screen.
6. Spot unknown symbols
Finnhub doesn't return an error for a symbol it doesn't know; it sends zeros with no time (t). Quote.fromJson returns null then, and the app says "No price found".
7. Show the price
Format prices with NumberFormat.getCurrencyInstance and the stock's currency. Colour the change green with ▲ when it's up and red with ▼ when it's down.
8. Refresh while visible
A Handler reloads the quote every 15 seconds. Start it in onStart and stop it in onStop, and cancel waiting requests, so the app never uses data in the background.
9. Flash on change
When the new price differs from the last one, ObjectAnimator.ofArgb fades the price from green or red back to its normal colour.
10. Run it
Try AAPL, MSFT or TSLA during US market hours to see it move. Try a made-up symbol to see the error.
If something goes wrong
"Finnhub rejected the API key": check local.properties has no spaces or quotes, then Build → Rebuild Project.
"No price found": the free plan covers US symbols; others need a paid plan.
What's improved over the video
A working, free API instead of the closed IEX Cloud, the key kept out of your code, company name and exchange, green and red changes, refreshing only while visible, and clear error messages.
Files affected
Check what to add and what to replace before copying code.
Tracks a symbol, refreshes every 15 seconds while visible, and flashes the price when it moves.
MainActivity.java
packagecom.example.stocktracker;importandroid.animation.ObjectAnimator;importandroid.content.SharedPreferences;importandroid.os.Bundle;importandroid.os.Handler;importandroid.os.Looper;importandroid.text.format.DateFormat;importandroid.view.View;importandroid.view.inputmethod.EditorInfo;importandroid.widget.EditText;importandroid.widget.TextView;importandroidx.appcompat.app.AppCompatActivity;importandroidx.core.content.ContextCompat;importandroidx.core.view.WindowCompat;importandroidx.core.view.WindowInsetsCompat;importorg.json.JSONObject;importjava.text.NumberFormat;importjava.util.Currency;importjava.util.Date;importjava.util.Locale;/**
* Type a stock symbol and watch its price. While the screen is visible, the price refreshes every 15 seconds,
* and flashes green or red when it moves.
*/publicclassMainActivityextendsAppCompatActivity{privatestaticfinallongREFRESH_MS=15_000;privatefinalHandler handler =newHandler(Looper.getMainLooper());privatefinalRunnable refresh =newRunnable(){@Overridepublicvoidrun(){loadQuote();
handler.postDelayed(this,REFRESH_MS);}};privateStockClient client;privateSharedPreferences prefs;privateString symbol;privateNumberFormat money =moneyFormat("USD");privatedouble lastPrice;privateint priceColour;privateEditText symbolInput;privateView content;privateTextView error, company, details, price, change,open, high, low, previousClose, updated;@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);Screens.setUp(this,R.layout.activity_main);
symbolInput =findViewById(R.id.inputSymbol);
content =findViewById(R.id.content);
error =findViewById(R.id.textError);
company =findViewById(R.id.textCompany);
details =findViewById(R.id.textDetails);
price =findViewById(R.id.textPrice);
change =findViewById(R.id.textChange);open=findViewById(R.id.textOpen);
high =findViewById(R.id.textHigh);
low =findViewById(R.id.textLow);
previousClose =findViewById(R.id.textPreviousClose);
updated =findViewById(R.id.textUpdated);
priceColour = price.getCurrentTextColor();
client =newStockClient(this,BuildConfig.FINNHUB_API_KEY);
prefs =getSharedPreferences("stocks",MODE_PRIVATE);
symbol = prefs.getString("symbol","AAPL");
symbolInput.setText(symbol);findViewById(R.id.btnTrack).setOnClickListener(v ->track());
symbolInput.setOnEditorActionListener((v, actionId, event)->{if(actionId !=EditorInfo.IME_ACTION_GO)returnfalse;track();returntrue;});}/** Refresh only while the screen is visible, so the app never uses data in the background. */@OverrideprotectedvoidonStart(){super.onStart();start();}@OverrideprotectedvoidonStop(){super.onStop();
handler.removeCallbacks(refresh);
client.cancelAll();}privatevoidtrack(){String typed = symbolInput.getText().toString().trim().toUpperCase(Locale.ROOT);if(typed.isEmpty()){
symbolInput.setError(getString(R.string.error_empty));return;}WindowCompat.getInsetsController(getWindow(), symbolInput).hide(WindowInsetsCompat.Type.ime());
symbol = typed;
lastPrice =0;
content.setVisibility(View.INVISIBLE);
handler.removeCallbacks(refresh);start();}privatevoidstart(){if(BuildConfig.FINNHUB_API_KEY.isEmpty()){showError(R.string.error_no_key);return;}loadProfile();
handler.post(refresh);}privatevoidloadProfile(){
client.profile(symbol,newStockClient.Listener<JSONObject>(){@OverridepublicvoidonResult(JSONObject profile){String name = profile.optString("name");
company.setText(name.isEmpty()? symbol : name);String exchange = profile.optString("exchange");
details.setText(exchange.isEmpty()? symbol :getString(R.string.details, symbol, exchange));
money =moneyFormat(profile.optString("currency","USD"));}@OverridepublicvoidonError(int message){// The quote request reports problems; the name is a nice extra.
company.setText(symbol);}});}privatevoidloadQuote(){
client.quote(symbol,newStockClient.Listener<Quote>(){@OverridepublicvoidonResult(Quote quote){show(quote);
prefs.edit().putString("symbol", symbol).apply();}@OverridepublicvoidonError(int message){showError(message);}});}privatevoidshow(Quote quote){
error.setVisibility(View.GONE);
content.setVisibility(View.VISIBLE);
price.setText(money.format(quote.price));boolean up = quote.change >=0;int colour =ContextCompat.getColor(this, up ?R.color.up :R.color.down);
change.setText(getString(up ?R.string.change_up :R.string.change_down,
money.format(Math.abs(quote.change)),Math.abs(quote.percent)));
change.setTextColor(colour);open.setText(money.format(quote.open));
high.setText(money.format(quote.high));
low.setText(money.format(quote.low));
previousClose.setText(money.format(quote.previousClose));
updated.setText(getString(R.string.updated,DateFormat.getTimeFormat(this).format(newDate())));// Flash the price when it moves since the last refresh, then fade back.if(lastPrice !=0&& quote.price != lastPrice){int flash =ContextCompat.getColor(this, quote.price > lastPrice ?R.color.up :R.color.down);ObjectAnimator.ofArgb(price,"textColor", flash, priceColour).setDuration(1200).start();}
lastPrice = quote.price;}privatevoidshowError(int message){
error.setText(message);
error.setVisibility(View.VISIBLE);}privatestaticNumberFormatmoneyFormat(String currencyCode){NumberFormat format =NumberFormat.getCurrencyInstance(Locale.getDefault());try{
format.setCurrency(Currency.getInstance(currencyCode));}catch(IllegalArgumentException unknownCurrency){
format.setCurrency(Currency.getInstance("USD"));}return format;}}