VxMusic

Back to Log
Development14 min read

Android App Development Journey

May 25, 2026

While web applications offer cross-platform convenience, mobile devices require integration with native APIs to provide a premium background listening experience. Vishesh Gangwar explains the development process of creating the VxMusic Android client.

The Android Challenge: WebViews vs. Native Code

Our initial prototype was built for the web. However, mobile browsers have heavy restrictions when running in the background. If you turn off your screen or switch to another app, Android's process manager immediately throttles JavaScript execution, causing audio to stutter and eventually halt. To save user battery and data, the OS stops active web scripts that consume background resources.

To solve this, we needed to bridge the web-based UI with Android's native audio frameworks. We chose a hybrid architecture:

  • WebView Interface: Runs our React-based front-end with hardware-accelerated CSS and Three.js animations, maintaining our custom design identity across web and mobile.
  • Kotlin Native Bridge: A native Android background service written in Kotlin that handles raw audio playback, caching, and OS hooks.
  • ExoPlayer/Media3 Integration: Google's premium media player library, which handles audio decoding, DASH/HLS segments, and native buffers.

This division of labor gives us the best of both worlds. The frontend remains beautiful, flexible, and unified with our desktop website, while the native background service provides the robust, uninterrupted execution that users expect from a premium music client.

Bridging Web and Kotlin: The JavascriptInterface

To communicate between the WebView UI and the native Kotlin service, we implemented a custom bridge using Android's @JavascriptInterface annotation. When a user clicks "Play" in the React UI, the action triggers a JavaScript function that calls a native Android method:

// React UI Click Handler
const playTrack = (trackId, streamUrl) => {
    if (window.AndroidBridge) {
        window.AndroidBridge.playTrack(trackId, streamUrl);
    } else {
        // Fallback to browser Web Audio API
        audioNode.play(streamUrl);
    }
};

On the native side, our Kotlin code intercepts the stream URL, manages the ExoPlayer instance, and sets up a background service that continues running even if the WebView is closed or suspended. This native wrapper ensures that background listening works flawlessly.

We also pass events in the opposite direction. When ExoPlayer completes a track, it triggers a Kotlin callback. The Kotlin service then calls a JavaScript function inside the WebView to update the active play queue and refresh the UI layout. This bidirectional communication ensures that the player state remains synchronized at all times.

Lock Screen Controls & Media Sessions

A music app feels incomplete if you cannot pause or skip tracks from the lock screen. To implement this, we integrated Android's MediaSessionCompat APIs. The Kotlin service updates the OS with metadata: the track title, artist name, and album artwork bitmap.

We registered receiver callbacks to handle play, pause, next, and previous buttons from bluetooth headphones, lock screens, and quick settings drawers. This level of integration transforms a basic web wrapper into a premium Android application that feels native, responsive, and reliable.

We also handled audio focus changes. If you receive a phone call or open a video app, Android notifies our media service to pause playback. Once the call ends, the music resumes smoothly. Handling these OS integrations makes the difference between an amateur wrapper app and a polished, professional media client.