0Pricing
Android Academy · Lesson

Feature and Core Modules

Draw module boundaries.

Feature and Core Modules is a free Android Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Android Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Two Kinds of Modules

Most modularized Android apps organize code into two main kinds of modules: feature modules and core modules, plus a thin :app module on top.

  • Feature modules hold a user-facing slice of the app (a screen or flow).
  • Core modules hold shared infrastructure used by many features.

In this lesson you will learn how to draw these boundaries well.

Anatomy of a Feature Module

A feature module like :feature:profile contains everything a single feature needs: its Compose screens, its ViewModel, and its UI state. It is vertical: it owns the full slice from UI down to its view-model.

It depends on core modules for shared pieces, but it should not depend on other feature modules.

// feature/profile/ProfileScreen.kt
@Composable
fun ProfileScreen(viewModel: ProfileViewModel = hiltViewModel()) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    when (state) {
        is ProfileUiState.Loading -> CircularProgressIndicator()
        is ProfileUiState.Success -> ProfileContent((state as ProfileUiState.Success).user)
        is ProfileUiState.Error -> ErrorMessage()
    }
}

The Feature ViewModel

Each feature owns its own ViewModel. It pulls data through a repository from a core module and exposes UI state. The feature module knows nothing about how data is fetched, only the repository contract.

// feature/profile/ProfileViewModel.kt
@HiltViewModel
class ProfileViewModel @Inject constructor(
    private val userRepository: UserRepository // from :core:data
) : ViewModel() {
    val uiState: StateFlow<ProfileUiState> =
        userRepository.observeUser()
            .map { ProfileUiState.Success(it) }
            .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), ProfileUiState.Loading)
}

Anatomy of a Core Module

A core module is horizontal: it provides one capability used across features. Common core modules include:

  • :core:model — plain data classes shared everywhere
  • :core:network — Retrofit/Ktor clients
  • :core:database — Room setup
  • :core:data — repositories combining network + database
  • :core:designsystem — theme and reusable composables
// core/model/User.kt
data class User(
    val id: String,
    val name: String,
    val avatarUrl: String
)

The Design System Module

:core:designsystem is one of the most reused modules. It holds your MaterialTheme, color schemes, typography, and reusable composables like buttons and cards. Every feature applies the same look without copying code.

// core/designsystem/AppTheme.kt
@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) DarkColors else LightColors
    MaterialTheme(
        colorScheme = colors,
        typography = AppTypography,
        content = content
    )
}

The Data Module Owns Repositories

The :core:data module exposes repository interfaces that features depend on, while hiding the implementation. It usually depends on :core:network and :core:database, combining them into a single source of truth.

// core/data/UserRepository.kt
interface UserRepository {
    fun observeUser(): Flow<User>
    suspend fun refresh()
}

// core/data/OfflineFirstUserRepository.kt
internal class OfflineFirstUserRepository @Inject constructor(
    private val api: UserApi,        // :core:network
    private val dao: UserDao         // :core:database
) : UserRepository {
    override fun observeUser(): Flow<User> = dao.observe().map { it.toUser() }
    override suspend fun refresh() { dao.upsert(api.fetch().toEntity()) }
}

Keep model and designsystem Lightweight

The lowest-level core modules should depend on as little as possible. :core:model ideally has no Android dependencies at all — just plain Kotlin data classes. This keeps it usable everywhere and fast to build.

If :core:model started depending on Retrofit or Room, every module that uses your data classes would drag in those heavy libraries.

// core/model/build.gradle.kts
plugins {
    id("myapp.jvm.library") // pure Kotlin, no Android
}
// No Retrofit, no Room, no Compose here — just data classes

The Thin :app Module

The :app module is the assembler. It should contain very little logic: the Application class, the single MainActivity, the top-level NavHost, and dependency-injection wiring. All real screens live in feature modules.

A thin app module means most changes happen in features, so the app module rarely rebuilds.

// app/MainActivity.kt
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AppTheme {            // from :core:designsystem
                AppNavHost()      // routes into :feature:* screens
            }
        }
    }
}

Drawing Good Boundaries

How do you decide what becomes a module? Some practical heuristics:

  • A feature = a screen or flow a user can name (Home, Profile, Checkout).
  • A core module = a capability reused by 2+ features.
  • If two features both need the same code, push it down into a core module.
  • If a module does too many unrelated things, split it.

Optional: api vs impl Split

Large apps sometimes split a feature into a public :feature:profile:api (interfaces, navigation routes) and a private :feature:profile:impl (screens, view-models). Other features depend only on the small api, never the implementation.

This is advanced; for most apps a single module per feature is plenty. Just know the pattern exists for very large codebases.

// Other features see only the contract, not the screens
// feature/home depends on :feature:profile:api
interface ProfileEntry {
    val route: String
    fun NavGraphBuilder.register(navController: NavController)
}

Putting the Graph Together

Here is how the layers wire up for our example. Notice dependencies only ever point downward: app -> feature -> data -> network/database -> model.

// :app           -> :feature:home, :feature:profile
// :feature:home  -> :core:data, :core:designsystem
// :feature:profile -> :core:data, :core:designsystem
// :core:data     -> :core:network, :core:database, :core:model
// :core:network  -> :core:model
// :core:database -> :core:model
// :core:model    -> (nothing)

Quick Check

Your :feature:home and :feature:profile modules both need to fetch and cache user data. Where should that repository code live?

Recap: Feature and Core Modules

You learned to split code into two layers:

  • Feature modules are vertical slices (screen + ViewModel + UI state).
  • Core modules are horizontal capabilities (model, network, database, data, designsystem).
  • The :app module stays thin and just assembles features.
  • Shared code moves down into core; :core:model stays Android-free and light.

Next, you will manage the dependencies between these modules and keep the graph clean.

Frequently asked questions

Is the “Feature and Core Modules” lesson free?

Yes — the full text of “Feature and Core Modules” is free to read here on the web, and the Android Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Android Academy course, upgrade to CoddyKit PRO.

What will I learn in “Feature and Core Modules”?

Draw module boundaries. You practise Android Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Android Academy?

No prior experience is required. Android Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Feature and Core Modules” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Android Academy lesson?

Yes. Every Android Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Why Modularize
  2. Feature and Core Modules
  3. Managing Module Dependencies
  4. Navigation Across Modules
← Back to Android Academy