Skip to main content
The Space Flight News app implements Clean Architecture with three distinct layers, each with specific responsibilities and clear boundaries.

Architecture Layers

Clean Architecture Diagram The architecture follows the Dependency Rule: dependencies only point inward. Outer layers depend on inner layers, never the reverse.

Layer Breakdown

1. Domain Layer (Core Business Logic)

The domain layer is the innermost layer and contains:
  • Business logic
  • Domain models (entities)
  • Repository interfaces
  • Use cases
The domain layer has zero dependencies on Android framework or external libraries. It’s pure Kotlin/Java code.

Domain Models

Domain models represent business entities:
com/bsvillarraga/spaceflightnews/domain/model/Article.kt
data class Article(
    val id: Long,
    val title: String,
    val imageUrl: String,
    val newsSite: String,
    val publishedAt: String
)

Repository Interfaces

The domain layer defines what data operations are needed, not how they’re implemented:
com/bsvillarraga/spaceflightnews/domain/repository/ArticleRepository.kt
interface ArticleRepository {
    suspend fun getArticles(
        query: String? = null
    ): Resource<List<Article>>

    suspend fun getArticleById(articleId: Long): Resource<ArticleDetail>
}
Repository interfaces belong in the domain layer, while implementations belong in the data layer. This ensures the domain layer doesn’t depend on data sources.

Use Cases

Use cases encapsulate single business operations:
com/bsvillarraga/spaceflightnews/domain/usecase/GetArticlesUseCase.kt
class GetArticlesUseCase @Inject constructor(
    private val repository: ArticleRepository
) {
    suspend fun getArticles(query: String? = null): Resource<List<Article>> {
        return repository.getArticles(query)
    }
}
While this use case seems like a simple pass-through, it provides:
  • A single entry point for this business operation
  • A place to add validation or business rules
  • Easy mocking for testing the presentation layer
  • Future extensibility without changing the repository interface
For example, you could add caching logic, combine multiple repository calls, or add analytics tracking without modifying the ViewModel or Repository.

2. Data Layer (Data Management)

The data layer implements repository interfaces and manages data sources:
  • Repository implementations
  • API clients (Retrofit)
  • Local database (Room)
  • DTOs (Data Transfer Objects)
  • Data mappers

Data Transfer Objects (DTOs)

DTOs represent the API response structure:
com/bsvillarraga/spaceflightnews/data/model/ArticleDto.kt
data class ArticleDto(
    val id: Long,
    val title: String,
    val authors: List<AuthorDto>,
    val url: String,
    @SerializedName("image_url")
    val imageUrl: String,
    @SerializedName("news_site")
    val newsSite: String,
    val summary: String,
    @SerializedName("published_at")
    val publishedAt: String,
    @SerializedName("updated_at")
    val updatedAt: String,
    val featured: Boolean,
    val launches: List<LaunchDto>,
    val events: List<EventDto>,
) {
    fun toArticleDomain(): Article = Article(
        id = id,
        title = title,
        imageUrl = imageUrl,
        newsSite = newsSite,
        publishedAt = publishedAt
    )

    fun toArticleDetailDomain(): ArticleDetail = ArticleDetail(
        id = id,
        title = title,
        authors = authors.map { it.toDomain() },
        url = url,
        newsSite = newsSite,
        imageUrl = imageUrl,
        summary = summary,
        publishedAt = publishedAt,
        updatedAt = updatedAt
    )
}
DTOs include mapping functions to convert API responses into domain models, keeping the transformation logic close to the data.

Repository Implementation

The repository implementation handles data fetching and transformation:
com/bsvillarraga/spaceflightnews/data/repository/ArticleRepositoryImpl.kt:29-58
class ArticleRepositoryImpl @Inject constructor(
    private val api: ArticlesApiClient,
    private val apiHelper: ApiHelper,
    private val paginationManager: PaginationManager
) : ArticleRepository {

    override suspend fun getArticles(
        query: String?
    ): Resource<List<Article>> {
        return withContext(Dispatchers.IO) {
            val response: ApiResponse<PaginationDto> =
                apiHelper.safeApiCall {
                    api.getArticles(query, paginationManager.getCurrentOffset())
                }

            when (response) {
                is ApiSuccessResponse -> {
                    val pagination = response.body
                    paginationManager.updatePagination(pagination)
                    Resource.Success(pagination.articles.map { it.toArticleDomain() })
                }

                is ApiErrorResponse -> Resource.Error(response.code, response.msg, response.error)
            }
        }
    }
    
    // ... getArticleById implementation
}
Benefits of separation:
  • API changes don’t affect business logic
  • Domain models contain only what the app needs
  • DTOs can include serialization annotations
  • Clear boundary between external and internal data representations
Example: The API returns published_at with snake_case, but the domain model uses publishedAt with camelCase.
The apiHelper.safeApiCall wrapper handles:
  • Network connectivity checks
  • Exception catching and transformation
  • Consistent error response format
  • Timeout handling
This centralizes error handling logic instead of repeating it in every repository method.

API Client

Retrofit interface defines API endpoints:
com/bsvillarraga/spaceflightnews/data/remote/articles/ArticlesApiClient.kt
interface ArticlesApiClient {
    @GET("v4/articles/")
    suspend fun getArticles(
        @Query("search") query: String?,
        @Query("offset") offset: Int
    ): PaginationDto

    @GET("v4/articles/{id}")
    suspend fun getArticleById(
        @Path("id") articleId: Long
    ): ArticleDto
}

3. Presentation Layer (UI)

The presentation layer handles UI logic and user interactions:
  • Fragments and Activities
  • ViewModels
  • Adapters
  • UI state management

ViewModel

ViewModels coordinate between UI and use cases:
com/bsvillarraga/spaceflightnews/presentation/ui/articles/viewmodel/ArticlesViewModel.kt:20-46
@HiltViewModel
class ArticlesViewModel @Inject constructor(
    private val articleUseCase: GetArticlesUseCase
) : ViewModel() {

    private val _articles = MutableLiveData<Resource<List<Article>>>(Resource.Loading())
    val articles: LiveData<Resource<List<Article>>> = _articles

    private val _searchQuery = MutableStateFlow("")
    private val _currentList = mutableListOf<Article>()

    init {
        observeSearchQuery()
    }

    private fun observeSearchQuery() {
        viewModelScope.launch {
            _searchQuery.debounce(300).distinctUntilChanged().collectLatest { query ->
                _currentList.clear()
                fetchArticles(query.ifEmpty { null }, reload = true)
            }
        }
    }
    
    // ... other methods
}
The ViewModel uses debounce and distinctUntilChanged on the search query to prevent excessive API calls while typing.

Fragment (UI)

Fragments observe ViewModel state and render UI:
com/bsvillarraga/spaceflightnews/presentation/ui/articles/ArticlesFragment.kt:41-49
@AndroidEntryPoint
class ArticlesFragment : Fragment(), MenuProvider {
    private lateinit var binding: FragmentArticlesBinding
    private lateinit var adapter: ArticleAdapter

    private var searchView: SearchView? = null
    private var searchMenuItem: MenuItem? = null

    private val viewModel: ArticlesViewModel by viewModels()
Observing data:
com/bsvillarraga/spaceflightnews/presentation/ui/articles/ArticlesFragment.kt:103-116
private fun observeArticle() {
    viewModel.articles.observe(viewLifecycleOwner) { resource ->
        handleResource(resource)
    }
}

private fun handleResource(resource: Resource<List<Article>>) {
    when (resource) {
        is Resource.Error -> showError()
        is Resource.Loading -> showLoading()
        is Resource.Success -> loadData(resource.data)
    }
}
Fragments use the @AndroidEntryPoint annotation to enable Hilt dependency injection.

Dependency Flow

The dependency graph flows inward:
Fragment/Activity
    ↓ (depends on)
ViewModel
    ↓ (depends on)
Use Case
    ↓ (depends on)
Repository Interface (Domain)
    ↑ (implemented by)
Repository Implementation (Data)
    ↓ (depends on)
API Client / Database
1

Fragment depends on ViewModel

UI obtains ViewModel through Hilt injection
2

ViewModel depends on Use Case

ViewModel receives Use Case via constructor injection
3

Use Case depends on Repository Interface

Use Case works with the interface, not implementation
4

Repository Implementation injected

Hilt provides the implementation at runtime
5

Repository depends on Data Sources

Repository uses API clients and databases

Benefits of This Architecture

Testability

Each layer can be tested independently with mocked dependencies

Maintainability

Changes in one layer don’t affect others

Scalability

New features follow the same pattern

Separation of Concerns

Each class has a single, well-defined responsibility

Real-World Example: Fetching Articles

Let’s trace a complete flow from user action to UI update:
1

User opens the app

ArticlesFragment is created and calls viewModel.fetchArticles()
2

ViewModel executes Use Case

viewModelScope.launch {
    handleResult(articleUseCase.getArticles(query), loadMore)
}
3

Use Case calls Repository

suspend fun getArticles(query: String? = null): Resource<List<Article>> {
    return repository.getArticles(query)
}
4

Repository fetches from API

val response = apiHelper.safeApiCall {
    api.getArticles(query, paginationManager.getCurrentOffset())
}
5

Repository transforms data

Resource.Success(pagination.articles.map { it.toArticleDomain() })
6

ViewModel updates LiveData

_articles.value = Resource.Success(_currentList.toList())
7

Fragment observes and updates UI

viewModel.articles.observe(viewLifecycleOwner) { resource ->
    when (resource) {
        is Resource.Success -> loadData(resource.data)
        // ...
    }
}

Next Steps

Dependency Injection

Learn how Dagger Hilt wires all these components together

Build docs developers (and LLMs) love