Quick Start
This guide walks you through building a simple feature using Miru SDK's clean architecture pattern — from API call to screen in 6 steps.
1. Initialize the SDK
// Application.kt or shared entry point
MiruSdkInitializer.initialize(
MiruSdkConfig(
networkConfig = NetworkConfig(
baseUrl = "https://api.yourapp.com/v1/",
enableLogging = BuildConfig.DEBUG
),
enableLogging = true,
tokenProvider = MyTokenProvider(), // optional
additionalModules = listOf(appModule) // your Koin modules
)
)
2. Define Domain Layer
The domain layer has zero external dependencies — only pure Kotlin.
// domain/model/User.kt
data class User(
val id: Int,
val name: String,
val email: String
)
// domain/repository/UserRepository.kt — interface only
interface UserRepository {
suspend fun getUsers(): Resource<List<User>>
suspend fun getUserById(id: Int): Resource<User>
}
// domain/usecase/GetUsersUseCase.kt
class GetUsersUseCase(private val repository: UserRepository) {
suspend operator fun invoke(): Resource<List<User>> = repository.getUsers()
}
3. Implement Data Layer
The data layer implements domain interfaces with concrete data sources.
// data/model/UserDto.kt — API response model
@Serializable
data class UserDto(
val id: Int,
val name: String,
val email: String
)
// data/mapper/UserMapper.kt
class UserMapper : Mapper<UserDto, User> {
override fun map(from: UserDto) = User(
id = from.id,
name = from.name,
email = from.email
)
}
// data/source/UserApi.kt — remote data source
class UserApi(httpClient: HttpClient) : ApiService(httpClient) {
suspend fun getUsers(): Resource<ApiResponse<List<UserDto>>> =
get("users")
suspend fun getUserById(id: Int): Resource<ApiResponse<UserDto>> =
get("users/$id")
}
// data/repository/UserRepositoryImpl.kt
class UserRepositoryImpl(
private val api: UserApi,
private val mapper: UserMapper
) : UserRepository {
override suspend fun getUsers(): Resource<List<User>> =
api.getUsers().map { response ->
response.data?.map { mapper.map(it) } ?: emptyList()
}
override suspend fun getUserById(id: Int): Resource<User> =
api.getUserById(id).map { response ->
mapper.map(response.data!!)
}
}
4. Create ViewModel
Choose the helper that fits your use case. Here's the simplest approach using collectResource():
data class UserListState(
val selectedUser: User? = null
)
sealed interface UserListEvent {
data class ShowError(val message: String) : UserListEvent
}
class UserListViewModel(
private val getUsersUseCase: GetUsersUseCase
) : BaseViewModel<UserListState, UserListEvent>(UserListState()) {
private val _users = MutableStateFlow<Resource<List<User>>>(Resource.Loading())
val users = _users.asStateFlow()
fun loadUsers() = collectResource(_users) { getUsersUseCase() }
}
tip
See ViewModel Patterns for a comparison of all 5 helpers.
5. Build the Screen
Use MiruResourceView to eliminate when (resource) boilerplate:
@Composable
fun UserListScreen(viewModel: UserListViewModel = koinViewModel()) {
val usersResource by viewModel.users.collectAsStateWithLifecycle()
MiruResourceView(
resource = usersResource,
loadingMessage = "Loading users...",
onRetry = { viewModel.loadUsers() }
) { users ->
LazyColumn {
items(users, key = { it.id }) { user ->
MiruCard {
Text(user.name, style = MiruTheme.typography.titleMedium)
Text(user.email, style = MiruTheme.typography.bodySmall)
}
}
}
}
}
6. Wire Dependencies
Bind all layers via Koin:
val userModule = module {
// Data
single { UserMapper() }
single { UserApi(get()) }
single<UserRepository> { UserRepositoryImpl(get(), get()) }
// Domain
factory { GetUsersUseCase(get()) }
// Presentation
viewModel { UserListViewModel(get()) }
}
Pass userModule to MiruSdkInitializer:
MiruSdkInitializer.initialize(
MiruSdkConfig(
// ...
additionalModules = listOf(userModule)
)
)
What's Next?
- Architecture — understand the dependency graph and layer rules
- ViewModel Patterns — learn all 5 async helpers
- Theming — customize colors and typography
- Error Handling — typed exceptions and global error handling