Android Multi-Module Architecture & Material3 Component Guide
Building scalable Android apps requires more than just stacking screens in a single module. This guide covers why multi-module architecture matters and how to leverage Material3 components effectively.
Why Multi-Module Architecture?
1. Build Time Optimization
Splitting your app into modules allows the build system to compile only what changed, dramatically reducing build times during development. A single monolithic module forces recompilation of the entire project.
// Instead of one massive module:
// app/
// └── src/main/java/com/example/... [1000+ files]
// Use modular structure:
// app/
// core/
// feature-auth/
// feature-dashboard/
// feature-settings/
2. Improved Testability
Isolated modules are easier to test in isolation. You can unit test a feature module without loading the entire app.
// feature-auth/build.gradle.kts
dependencies {
implementation(project(":core"))
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.3.0")
}
3. Team Development Scalability
Multiple teams can work on different feature modules simultaneously without constant merge conflicts. Clear module boundaries enforce separation of concerns.
4. Code Reusability
Modules can be shared across projects. A core-ui module containing design system components becomes a library usable in multiple apps.
Basic Multi-Module Structure
Recommended Architecture
MyApp/
├── app/ # Application layer (launcher, navigation)
├── core/ # Shared utilities, networking, storage
│ ├── core-common/ # Utilities, extensions, constants
│ ├── core-network/ # HTTP client, API definitions
│ └── core-database/ # Room entities, DAOs
├── feature-auth/ # Authentication feature
├── feature-dashboard/ # Dashboard feature
├── feature-profile/ # User profile feature
└── build-logic/ # Convention Plugins
Module Responsibilities
- app/: Entry point, navigation graph, app-level configuration
- core-common/: Strings, constants, utility functions, extension functions
- core-network/: Retrofit, Ktor, or OkHttp configuration
- core-database/: Room database setup, DAOs, entities
- feature-/**: Screen, ViewModel, repository specific to that feature
Dependency Direction (The Golden Rule)
Feature modules can depend on core, but core cannot depend on features.
app ──→ feature-auth ──→ core
feature-dashboard ──→ core
feature-profile ──→ core
Never:
core ──→ feature-auth ❌ (violates layering)
This prevents circular dependencies and keeps core generic.
Convention Plugins for Standardization
Convention Plugins (in build-logic/) define shared configurations, eliminating repetition:
// build-logic/src/main/kotlin/com.example.android.library.gradle.kts
plugins {
id("com.android.library")
kotlin("android")
}
android {
compileSdk = 34
defaultConfig {
minSdk = 24
}
}
dependencies {
implementation("androidx.appcompat:appcompat:1.6.1")
testImplementation("junit:junit:4.13.2")
}
Then in feature modules:
// feature-auth/build.gradle.kts
plugins {
id("com.example.android.library")
}
dependencies {
implementation(project(":core-common"))
}
Benefits:
- Single source of truth for SDK versions
- Consistent dependencies across all modules
- Changes to build config cascade automatically
Material3 Components Overview
Material3 is Google's latest design system. Here's what you need to know:
Buttons
Elevated Button (highest prominence)
ElevatedButton(onClick = { /* action */ }) {
Text("Elevated Button")
}
Filled Button (default, recommended)
Button(onClick = { /* action */ }) {
Text("Filled Button")
}
Outlined Button (secondary action)
OutlinedButton(onClick = { /* action */ }) {
Text("Outlined Button")
}
Text Button (least prominent)
TextButton(onClick = { /* action */ }) {
Text("Text Button")
}
Cards
Elevated Card (surface elevation + shadow)
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Card Title", style = MaterialTheme.typography.headlineSmall)
Text("Card content goes here")
}
}
Outlined Card (border instead of shadow)
OutlinedCard(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text("Outlined Card Content")
}
Chips
Input Chip (user selections)
InputChip(
selected = isSelected,
onClick = { isSelected = !isSelected },
label = { Text("Select Option") }
)
Filter Chip (filtering)
FilterChip(
selected = isActive,
onClick = { isActive = !isActive },
label = { Text("Filter") }
)
Suggestion Chip (recommendations)
SuggestionChip(
onClick = { /* action */ },
label = { Text("Suggestion") }
)
Floating Action Button (FAB)
Extended FAB (icon + text)
ExtendedFloatingActionButton(
text = { Text("Create") },
icon = { Icon(Icons.Default.Add, contentDescription = null) },
onClick = { /* action */ }
)
Standard FAB (icon only)
FloatingActionButton(
onClick = { /* action */ },
) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
Switches & Checkboxes
Switch
var enabled by remember { mutableStateOf(false) }
Switch(
checked = enabled,
onCheckedChange = { enabled = it }
)
Checkbox
var checked by remember { mutableStateOf(false) }
Checkbox(
checked = checked,
onCheckedChange = { checked = it }
)
Progress Indicators
Linear Progress
LinearProgressIndicator(
progress = { 0.75f },
modifier = Modifier.fillMaxWidth()
)
Circular Progress
CircularProgressIndicator(
progress = { 0.75f }
)
Putting It Together: Feature Module Example
// feature-auth/src/main/kotlin/com/example/auth/LoginScreen.kt
@Composable
fun LoginScreen(onLoginSuccess: () -> Unit) {
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.Center
) {
// Email Input
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// Password Input
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
// Login Button
Button(
onClick = { /* validate & login */ },
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = MaterialTheme.colorScheme.onPrimary
)
} else {
Text("Login")
}
}
Spacer(modifier = Modifier.height(16.dp))
// Sign Up Link
TextButton(
onClick = { /* navigate to signup */ },
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Text("Don't have an account? Sign up")
}
}
}
Best Practices
- Keep modules focused: One feature = one module
-
Use provided theme: Don't hardcode colors; use
MaterialTheme.colorScheme - Test modules independently: Each module should have tests
- Document dependencies: Create a dependency graph diagram for team reference
- Use ViewModel for state: Don't manage UI state in composables
Next Steps
- Implement multi-module structure in your next project
- Adopt Convention Plugins for consistency
- Explore Material3 themes for your brand colors
- Set up Compose Preview for each feature module
8 Android App Templates to accelerate your multi-module architecture setup → https://myougatheax.gumroad.com
Top comments (0)