Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 47 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,58 @@
# Clean Architecture Modularization

The "Practical Guide to Building Android Apps with Clean Architecture, Modularization, and Unit Testing" is a comprehensive resource that outlines best practices for developing robust and maintainable Android applications. Detailed in a Medium article [ Practical Guide to Building Powerful and Easy-to-Maintain Android Apps with Clean Architecture, Modularization ](https://murainoyakubu.medium.com/practical-guide-to-building-powerful-and-easy-to-maintain-android-apps-with-clean-architecture-c6c8b592a0f2), this guide emphasizes on the importance of using Clean Architecture, modularization, and unit testing to enhance app quality and scalability.


## Project Modules
* artist-domain
* artist-data
* artist-datasource
* artist-presentation
* artist-ui
* app

### **artist-domain**
Business logic layer - Contains use cases, domain models, and repository interfaces (pure Kotlin, no Android dependencies)

### **artist-data**
Data layer - Implements repositories and handles data mapping between data sources and domain

### **artist-datasource**
Remote data layer - Handles API calls using Retrofit and maps API responses to data models

### **artist-presentation**
Presentation layer - Contains ViewModels, presentation models, and UI state management

### **artist-ui**
UI layer - Contains Fragments, Compose screens, and UI components

### **app**
Application module - App entry point, dependency injection setup (Hilt), and navigation


## Features
- Search for artists (default: Drake) using MusicBrainz API
- View artist albums and release information
- Dark/light mode support with Material Design
- Built with Jetpack Compose and traditional Views


## Tech Stack
- **Architecture**: Clean Architecture, MVVM, Repository Pattern
- **DI**: Hilt
- **UI**: Jetpack Compose + XML Views
- **Networking**: Retrofit, OkHttp, Gson
- **Async**: Kotlin Coroutines
- **Testing**: JUnit, Mockito, MockK, Espresso


## Getting Started

1. Clone the repository
2. Open in Android Studio
3. Build and run

### Features
- search for list of artist.. default is drake as he is my favorite
- dark/light mode supported
```bash
./gradlew build
./gradlew installDebug
```


### Screen
## Screen

<img width="336" alt="Screenshot 2022-10-25 at 22 43 48" src="https://user-images.githubusercontent.com/26343440/197887992-51323194-0dcb-48e6-ae93-e570633aa807.png">

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,25 @@ package com.muryno.data.datasource
import com.muryno.data.model.ArtistAlbumDataModel
import com.muryno.data.model.ArtistDataModel

/**
* Data source interface for retrieving artist and album information.
* This interface defines the contract for fetching data from external sources
* (typically remote APIs). Implementations handle the actual data retrieval logic.
*/
interface ArtistDataSource {
/**
* Retrieves a list of artists matching the search query.
*
* @param artistName The name of the artist to search for
* @return List of [ArtistDataModel] containing matching artists
*/
suspend fun getArtistListFromApi(artistName: String): List<ArtistDataModel>

/**
* Retrieves album releases for a specific artist.
*
* @param artistId The unique identifier of the artist
* @return List of [ArtistAlbumDataModel] containing the artist's albums
*/
suspend fun getArtistAlbumFromApi(artistId: String): List<ArtistAlbumDataModel>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,19 @@ package com.muryno.data.mapper
import com.muryno.data.model.ArtistAlbumDataModel
import com.muryno.domain.artistAlbulm.model.ArtistAlbumDomainModel

/**
* Mapper class for converting album data models to domain models.
* Transforms [ArtistAlbumDataModel] from the data layer into [ArtistAlbumDomainModel]
* used in the domain layer.
*/
class ArtistAlbumDataToDomainMapper {

/**
* Converts an [ArtistAlbumDataModel] to an [ArtistAlbumDomainModel].
*
* @param input The album data model to convert
* @return [ArtistAlbumDomainModel] with mapped album information
*/
fun toDomain(input: ArtistAlbumDataModel): ArtistAlbumDomainModel {
return ArtistAlbumDomainModel(
id = input.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ package com.muryno.data.mapper
import com.muryno.data.model.ArtistDataModel
import com.muryno.domain.artist.model.ArtistDomainModel


/**
* Mapper class for converting artist data models to domain models.
* Transforms [ArtistDataModel] from the data layer into [ArtistDomainModel]
* used in the domain layer.
*/
class ArtistDataToDomainMapper {
/**
* Converts an [ArtistDataModel] to an [ArtistDomainModel].
*
* @param input The artist data model to convert
* @return [ArtistDomainModel] with mapped artist information
*/
fun toDomain(input: ArtistDataModel): ArtistDomainModel {
return ArtistDomainModel(
id = input.id,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
package com.muryno.data.model

/**
* Data layer model representing album/release information for an artist.
* This model is used in the data layer to transfer album data between
* the data source and repository.
*
* @property primaryType Primary type of the release (Album, Single, EP, etc.)
* @property genre Musical genre of the album
* @property label Record label that released the album
* @property shortDescription Brief description of the album
* @property fullDescription Complete description of the album
* @property albumImage URL or path to the album cover image
* @property releaseDate Date when the album was released
* @property title Title of the album
* @property id Unique identifier for the album
* @property disambiguation Additional text to distinguish similar albums
*/
data class ArtistAlbumDataModel(
val primaryType: String,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,20 @@ package com.muryno.data.model

import java.io.Serializable


/**
* Data layer model representing artist information.
* This model is used in the data layer to transfer artist data between
* the data source and repository.
*
* @property id Unique identifier for the artist
* @property name Name of the artist
* @property gender Gender of the artist (if applicable)
* @property type Type/classification of the artist
* @property state State or region information
* @property country Country of origin
* @property disambiguation Additional information to distinguish similar artists
* @property score Relevance score from search results
*/
data class ArtistDataModel(
val id: String,
val name: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,39 @@ import com.muryno.domain.artistAlbulm.model.ArtistAlbumDomainModel
import com.muryno.domain.artist.model.ArtistDomainModel
import com.muryno.domain.artist.repository.ArtistRepository


/**
* Repository implementation for managing artist data operations.
* This class implements the [ArtistRepository] interface from the domain layer,
* coordinating between the data source and mappers to provide domain models.
*
* @property artistDataSource Data source for fetching raw artist data
* @property artistDataToDomainMapper Mapper for converting artist data models to domain models
* @property artistAlbumDataToDomainMapper Mapper for converting album data models to domain models
*/
class ArtistLiveRepository(
private val artistDataSource: ArtistDataSource,
private val artistDataToDomainMapper: ArtistDataToDomainMapper,
private val artistAlbumDataToDomainMapper: ArtistAlbumDataToDomainMapper,
) : ArtistRepository {

/**
* Retrieves a list of artists matching the search query and maps them to domain models.
*
* @param artistName The name of the artist to search for
* @return List of [ArtistDomainModel] containing matching artists
*/
override suspend fun artistList(artistName: String): List<ArtistDomainModel> =
artistDataSource.getArtistListFromApi(artistName).map(
artistDataToDomainMapper::toDomain
)


/**
* Retrieves album releases for a specific artist and maps them to domain models.
*
* @param artistId The unique identifier of the artist
* @return List of [ArtistAlbumDomainModel] containing the artist's albums
*/
override suspend fun artistAlbum(artistId: String): List<ArtistAlbumDomainModel> =
artistDataSource.getArtistAlbumFromApi(
artistId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@ import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Query

/**
* Retrofit service interface for interacting with the MusicBrainz API.
* Provides endpoints for fetching artist information and album data.
*/
interface MusicApiService {
/**
* Fetches a list of artists from the MusicBrainz API based on search query.
*
* @param artistName The name of the artist to search for
* @param fmt Response format (defaults to "json")
* @return [ArtistListApiModel] containing the list of matching artists
*/
@Headers(
"Accept: application/json",
"User-Agent: com.ubn.musicbrainz_place/1.0"
Expand All @@ -19,6 +30,14 @@ interface MusicApiService {
): ArtistListApiModel


/**
* Fetches album releases for a specific artist from the MusicBrainz API.
*
* @param artistId The unique identifier of the artist
* @param type The type of releases to fetch (defaults to "album")
* @param fmt Response format (defaults to "json")
* @return [AristAlbumApiModel] containing the artist's album releases
*/
@Headers(
"Accept: application/json",
"User-Agent: com.ubn.musicbrainz_place/1.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,38 @@ import com.muryno.data.datasource.ArtistDataSource
import com.muryno.data.model.ArtistAlbumDataModel
import com.muryno.data.model.ArtistDataModel


/**
* Remote data source implementation for fetching artist data from the MusicBrainz API.
* This class implements the [ArtistDataSource] interface and handles API calls,
* mapping API models to data layer models.
*
* @property musicApiService The Retrofit service for making API requests
* @property artistAlbumApiToResponseDataMapper Mapper for converting album API models to data models
* @property artistApiToResponseDataMapper Mapper for converting artist API models to data models
*/
class ArtistRemoteDataSource(
private val musicApiService: MusicApiService,
private val artistAlbumApiToResponseDataMapper: ArtistAlbumApiToResponseDataMapper,
private val artistApiToResponseDataMapper: ArtistApiToResponseDataMapper
) : ArtistDataSource {
/**
* Retrieves a list of artists from the API based on the search query.
*
* @param artistName The name of the artist to search for
* @return List of [ArtistDataModel] containing artist information
*/
override suspend fun getArtistListFromApi(artistName: String): List<ArtistDataModel> {
val data = musicApiService.fetchArtistFromServer(artistName = artistName)
return data.artists.map { artistApiToResponseDataMapper.toData(it) }
}


/**
* Retrieves album releases for a specific artist from the API.
*
* @param artistId The unique identifier of the artist
* @return List of [ArtistAlbumDataModel] containing album information
*/
override suspend fun getArtistAlbumFromApi(
artistId: String
): List<ArtistAlbumDataModel> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ package com.muryno.artist_datasource.mapper
import com.muryno.artist_datasource.model.album.AlbumReleaseGroupApiModel
import com.muryno.data.model.ArtistAlbumDataModel


/**
* Mapper class for converting album API models to data layer models.
* Transforms [AlbumReleaseGroupApiModel] from the MusicBrainz API into [ArtistAlbumDataModel]
* used in the data layer.
*/
class ArtistAlbumApiToResponseDataMapper {
/**
* Converts an [AlbumReleaseGroupApiModel] to an [ArtistAlbumDataModel].
*
* @param input The album API model to convert
* @return [ArtistAlbumDataModel] with mapped album information
*/
fun toData(input: AlbumReleaseGroupApiModel): ArtistAlbumDataModel {
return ArtistAlbumDataModel(
id = input.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ package com.muryno.artist_datasource.mapper
import com.muryno.artist_datasource.model.ArtistApiModel
import com.muryno.data.model.ArtistDataModel


/**
* Mapper class for converting artist API models to data layer models.
* Transforms [ArtistApiModel] from the MusicBrainz API into [ArtistDataModel]
* used in the data layer.
*/
class ArtistApiToResponseDataMapper {
/**
* Converts an [ArtistApiModel] to an [ArtistDataModel].
*
* @param input The artist API model to convert
* @return [ArtistDataModel] with mapped artist information
*/
fun toData(input: ArtistApiModel): ArtistDataModel {
return ArtistDataModel(
id = input.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ package com.muryno.artist_datasource.model

import com.google.gson.annotations.SerializedName

/**
* API model representing an alternative name or alias for an artist.
*
* @property beginDate Date when this alias started being used
* @property endDate Date when this alias stopped being used
* @property locale Language/region code for the alias
* @property name The alias name
* @property primary Whether this is the primary name in its locale
* @property sortName Alias name formatted for sorting
* @property type Type of alias (e.g., Legal name, Stage name)
* @property typeId Unique identifier for the alias type
*/
data class Aliase(
@SerializedName("begin-date")
val beginDate: Any,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ package com.muryno.artist_datasource.model

import com.google.gson.annotations.SerializedName

/**
* API model representing a geographic area in MusicBrainz.
*
* @property id Unique identifier for the area
* @property lifeSpan Time period the area existed (for historical areas)
* @property name Name of the area
* @property sortName Name formatted for sorting
* @property type Type of area (e.g., Country, City, State)
* @property typeId Unique identifier for the area type
*/
data class Area(
val id: String,
@SerializedName("life-span")
Expand Down
Loading