Introduction
Hola everyone!
I am Pranav Konidena (you may know me as pranav on IRC, or pranavkonidena on GitHub.). I am a junior at the Indian Institute of Technology Roorkee (IITR) pursuing a degree in engineering with a major in Electronics and Communication. I learnt about Google Summer of Code from my club seniors and was instantly drawn towards it. While I was going through the potential organizations to which I could contribute, I was instantly drawn towards MetaBrainz as it combined my love for music and programming.
I wanted to explore further the field of Mobile Development, I had sound knowledge of Flutter, but I didn’t know Native Android Development and decided to try and contribute to ListenBrainz Android. With that goal in mind, I started learning about Kotlin and JetPack Compose, as they were mentioned as the tech stack for the LB Android app in its General Overview Doc on its GitHub Repository. My first contribution to LB Android was fixing a minor text overflow bug.
Proposal
Soon, the GSoC contributor application period started. I went through the suggested ideas on MetaBrainz’s Summer of Code 2024 website. I loved the idea of user, artist, and album pages on LB Android, and I started drafting my proposal for the same. I got my proposal reviewed by my mentor Akshat (aka akshaaatt on IRC) and submitted it. I was overjoyed when I found out my proposal was selected by MetaBrainz for GSoC.
Project Overview
The project focused on replicating the User, Artist, and Album Pages from the ListenBrainz website, bringing them to the ListenBrainz Android app with an enhanced UI and Android-specific gestures. The goal was to offer users a more engaging and intuitive experience, allowing them to explore their friends’ profiles, discover similar artists, view likes and dislikes, and access listening stats directly within the app without needing to visit the website. Additionally, I aimed to ensure seamless navigation between user, artist, and album pages, providing a smooth and cohesive user experience aligned with the planned user flow.
Please find the detailed deliverables of my project below.
Deliverables for My Project
As per my Proposal, the deliverables for this project were:

Preliminary Phase and Community Bonding Period
Before the community bonding period, I worked on Year In Music, Brainz Player Revamp. During the community bonding period, I got the design mockups for my project from Simon, the in-house designer of MetaBrainz (who goes by aerozol on IRC) based on the initial wireframes and the design requirement doc which I had made for ease of work. The final mockups provided by Simon looked as follows (He is such a good designer)
User Pages

Artist and Album Pages

Coding Period (Before Mid Term)
TL;DR
I completed the listens tab and the taste tab in the user pages
The whole project was divided into three parts, namely, User Pages (Phase 1), Artist Pages (Phase 2) and Album Pages (Phase 3).
I will be going over the structural principles I followed for the listens screen as they are consistent across all screens (User, artist, and album).
A brief of the app’s architecture for reference
The app is built using the MVVM architecture, which ensures a clear separation of concerns across different layers. In this architecture:
- Models represent the data classes that define the app’s state.
- Services handle background tasks, such as downloading data or playing music. In this case, services are responsible for fetching data from the server using Retrofit.
- Repositories serve as the data source managers, integrating local, remote, and cache data sources and providing this data to the ViewModels.
- ViewModels contain the business logic and manage the UI state, which includes all the necessary information to be displayed on the screen. This UI state is then used directly in the UI layer for rendering.
Setting Up User Repository, View Model and Service
Next, I set up the User Repository to interact with the service and return a Resource class to the ViewModel. The Resource class contains two key variables: the status of the request (Success/Failure/Loading) and the data retrieved from the request. Since the User Repository depends on the User Service and we used Hilt for dependency injection, I configured the service in the DI (Dependency Injection) directory so that Hilt could inject the User Service dependency as needed.
Following this, I set up the User ViewModel, which extends the BaseViewModel abstract class. For each of the user page tabs, I used a MutableStateFlow, which I combined using Kotlin’s combine operator to form the ProfileUiState class. The ProfileUiState class is structured as follows:
data class ProfileUiState(
// isSelf denotes if the user page is of the one currently logged in
val isSelf: Boolean = false,
val listensTabUiState: ListensTabUiState = ListensTabUiState(),
val statsTabUIState: StatsTabUIState = StatsTabUIState(),
val tasteTabUIState: TasteTabUIState = TasteTabUIState(),
)
To enhance modularity, instead of using a single UI state for the entire user page, I divided the UI state into three separate states—one for each tab. The ProfileUiState is then exposed by the ViewModel to the UI layer, allowing each screen to easily collect the UI state from the ViewModel as needed.
Making the UI using Jetpack Compose
Base Profile Screen
As the sliding bar to switch tabs and the add listens, follow/unfollow button etc were common in all the tabs, I first created a BaseProfileScreen
@Composable
fun BaseProfileScreen(
username: String?,
snackbarState: SnackbarHostState,
viewModel: UserViewModel = hiltViewModel(),
uiState: ProfileUiState,
onFollowClick: (String) -> Unit,
onUnfollowClick: (String) -> Unit,
goToUserProfile: () -> Unit,
feedViewModel: FeedViewModel = hiltViewModel(),
listensViewModel: ListensViewModel = hiltViewModel(),
socialViewModel: SocialViewModel = hiltViewModel(),
goToArtistPage: (String) -> Unit,
goToUserPage: (String?) -> Unit,
)
Navigation to the user pages
As the API Endpoints required the user’s username, I made username a parameter in the screen and used nav arguments to extract it from the route in the app and navigate to the Profile Screen.
composable(route = "${AppNavigationItem.Profile.route}/{username}", arguments = listOf(navArgument("username"){
type = NavType.StringType
}
))
{
val username = it.arguments?.getString("username")
ProfileScreen(
onScrollToTop = onScrollToTop,
scrollRequestState = scrollRequestState,
username = username,
snackbarState = snackbarState,
goToUserProfile = goToUserProfile,
goToArtistPage = goToArtistPage,
goToUserPage = goToUserPage
)
}
In the Profile Screen, after checking if a user is logged in or not, the user is navigated to the Base Profile Screen shown earlier, which then links up with the Listens, Stats and Taste page.
Listens Tab
The Listens Screen doesn’t have any hilt injections. All the injections are done in the base profile screen itself so that instrumentation tests are not affected by hilt. (More on this later)
@Composable
fun ListensScreen(
viewModel: ListensViewModel,
userViewModel: UserViewModel,
socialViewModel: SocialViewModel,
feedViewModel : FeedViewModel,
scrollRequestState: Boolean,
onScrollToTop: (suspend () -> Unit) -> Unit,
snackbarState : SnackbarHostState,
username: String?,
goToArtistPage: (String) -> Unit,
goToUserPage: (String?) -> Unit
) {
val uiState by userViewModel.uiState.collectAsState()
val preferencesUiState by viewModel.preferencesUiState.collectAsState()
val socialUiState by socialViewModel.uiState.collectAsState()
val feedUiState by feedViewModel.uiState.collectAsState()
ListensScreen(
uiState = uiState,
// Many other functions/parameters reqd
)
}
The reason for separating the functions is because Compose Previews don’t work with View Model’s. Hence, the actual function that renders UI to which the UI state is passed is an overloaded instance of the same function as shown. I created components wherever possible so that the code remains modular and easy to read.
The listens tab was implemented in Mobile 203/user pages 1.1
Taste Tab
After implementing the listens tab, I moved on to implementing the taste tab. This was fairly trivial as I had the UI State set up and just had to make the UI. In a similar fashion to the Listens Tab, I overloaded the function and passed it in the UI state so that compose previews are not affected.
The taste tab was implemented in User pages 1.2
This was all I could manage to do before the mid-term evaluation.
Coding Period (After Mid Term)
TL;DR
The stats tab was implemented in
The tests for user pages were implemented in:
The artist pages were implemented in
The album pages were implemented in
Stats Tab in User Pages
After the midterm evaluations, I began working on the Stats page, which was the most challenging part of the project for me. The primary difficulty was dealing with the unconventional data format returned for the charts. After struggling to resolve this independently, I sought guidance from my mentor, Akshat, who pointed me in the right direction, allowing me to address the charting issue successfully.
While working on fetching the top artists, I encountered a problem with rate limiting. Pre-fetching the data led to a “Rate Limit Exceeded” error, as I was making a whooping 42 requests at once. To solve this, I modified the approach so that the data is only fetched when the user clicks on the corresponding top artist, song, or album button. This adjustment required me to call a suspend function from the UI in Kotlin, which I implemented using a LaunchedEffect.
CategoryState.ARTISTS -> {
if(uiState.statsTabUIState.topArtists == null){
LaunchedEffect(Unit) {
fetchTopArtists(username)
}
}
}
In the ViewModel, rather than re-fetching all the data each time the fetchTopArtists function was called, I optimised the process by using the copy operator to update only the relevant parts of the stats flow. Another challenge was providing the user with a visual indicator that the data was being loaded. To address this, I set the isLoading attribute to true when initiating the function and emitted this updated state. This allowed the UI to recognise when the data was still loading. I then utilised AnimatedVisibility to conditionally render the loading animation, ensuring a smooth user experience.
suspend fun getUserTopSongs(inputUsername: String?){
statsStateFlow.value = statsStateFlow.value.copy(isLoading = true)
// Performed fetch here
statsStateFlow.value = statsStateFlow.value.copy(
isLoading = false,
topSongs = topSongs
)
}
The stats tab was implemented in: User Pages 1.3
Unit Testing, Instrumentation testing for User Pages
This was another challenge for me, having never written unit tests or tests of any sort before. I could implement the Unit Tests myself but got a weird Hilt binding error while attempting to call the instrumentation tests. After two days of endless googling and trying various solutions, I finally reached out to Jasjeet (goes by jasje on IRC), who acted as my second mentor since I started contributing to LB Android. We had a meeting, and it turns out I wasn’t passing the View Model, and I wasn’t getting an error because Hilt was injecting it. So, I finally made a few changes to ensure this wouldn’t happen in the future as well and completed the tests for the User Pages.
The tests were implemented in: User Pages 1.4 (Unit Tests)
Artist Pages
The main challenge here was to render the artist’s cover art. I was getting an SVG string with images as href tags embedded in them. I tried a lot of traditional ways (Coil, Glide), etc, but wasn’t able to render them. I asked for Jasjeet’s opinion, who suggested I use web views. I liked the idea and it was a very innovative solution on the part of him. I had to write a bit of custom CSS to ensure the size of the grid art matched the design (I am not going into that, feel free to see the file in the PR linked below).
The artist pages were implemented in Artist Pages 2.1
Album Pages
As is evident from the design, most of the components used in album pages were almost the same as those used in artist pages, so I just re-used the components of artist pages, and with some small tweaks, I could re-use the components I wrote in artist pages.
The album pages were implemented in Album Pages 3.1
Current State
The app now includes fully functional User, Album, and Artist pages, each providing a seamless and intuitive user experience. The current version is being rolled out to Beta, where we’re gathering user feedback to refine and polish the app further.
We’re considering adding Deep Linking to the app. This feature will allow users to navigate directly to specific content within the app from external links, enhancing UX and integrating with other platforms.
What’s Left?
Regarding the final deliverables, I couldn’t implement the Playlists tab on the user pages or the unit and instrumentation tests for the artist and album pages. However, my mentor has kindly agreed that I can address these tasks after GSoC concludes, and I’ll be taking these up after GSoC.
Overall Experience
This summer has been an incredible experience, and I couldn’t have asked for a better opportunity than being part of GSoC with an outstanding organisation like MetaBrainz. Working on this project has been a fantastic experience, allowing me to dive deep into native Android development and gain valuable insights into how large organisations operate. I’ve learned a great deal about effective coding practices and project management.
I am immensely grateful to Akshat, Jasjeet, and Simon. Without their guidance, support, and expertise, completing this project would not have been possible. They are not only exceptional in their fields but also outstanding mentors who have imparted invaluable knowledge and skills.
This project has pushed me to extend my boundaries and reinforced my belief that with dedication and effort, nothing is impossible. I now feel more confident in my open-source capabilities and am deeply thankful to the entire MetaBrainz team for their mentorship and support throughout the summer.