A professional Kotlin Multiplatform AR application demonstrating Clean Architecture, DDD principles, and platform-specific AR implementations (ARCore/ARKit)
Features • Architecture • Tech Stack • Getting Started • Documentation
ARSample is a production-ready Augmented Reality application built with Kotlin Multiplatform Mobile (KMM), showcasing enterprise-grade architecture patterns and modern mobile development practices. The app allows users to import, place, and manage 3D objects in AR scenes across both Android and iOS platforms with a fully shared business logic layer.
- 🏗️ Clean Architecture + DDD: Domain-driven design with clear separation of concerns
- 🔄 95%+ Code Sharing: Business logic, UI, and domain layer shared between platforms
- 📐 MVVM Pattern: Reactive state management with Kotlin Flow
- 🎨 Jetpack Compose: Modern declarative UI for both platforms
- 🧪 High Test Coverage: 85-100% test coverage with comprehensive unit tests
- 🔍 Type Safety: Value Objects pattern for domain validation
- 📦 Repository Pattern: Clean data abstraction with DTO/Mapper pattern
- ✅ 3D Model Import: Support for GLB and USDZ formats
- ✅ AR Object Placement: Real-time hit testing and object positioning
- ✅ Scene Persistence: Auto-save/restore AR scenes across app restarts
- ✅ Object Management: Add, remove, and list imported 3D models
- ✅ Cross-Platform UI: Identical user experience on Android and iOS
- 🔐 Domain Validation: Value Objects with sealed classes (ModelUri, ObjectName)
- 🚀 Result Pattern: Type-safe error handling throughout the application
- 🎯 Use Case Pattern: Single-responsibility business logic units
- 🗂️ Local Storage: Platform-specific implementations (DataStore/UserDefaults)
- 🧩 Expect/Actual Pattern: Clean platform-specific abstractions
This project follows Eric Evans' Domain-Driven Design (DDD) + Clean Architecture principles with four distinct layers:
┌─────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ • ViewModels (State Management) │
│ • Compose UI Screens │
│ • Platform-specific AR Views (AndroidView/UIViewWrapper) │
│ • Depends on: Application Layer │
└─────────────────┬───────────────────────────────────────────┘
│
┌─────────────────▼───────────────────────────────────────────┐
│ Application Layer │
│ • Use Cases (ImportObject, PlaceObject, RemoveObject) │
│ • Business Workflows │
│ • Use Case DTOs (Input/Output models) │
│ • Depends on: Domain Layer only │
└─────────────────┬───────────────────────────────────────────┘
│
┌─────────────────▼───────────────────────────────────────────┐
│ Domain Layer │
│ • Entities (ARObject, ARScene, PlacedObject) │
│ • Value Objects (ModelUri, ObjectName) │
│ • Repository Interfaces │
│ • Domain Exceptions │
│ • NO dependencies (innermost layer) │
└────────────────────────────────┬────────────────────────────┘
▲
┌────────────────────────────────┴────────────────────────────┐
│ Infrastructure Layer │
│ • Repository Implementations │
│ • DTOs + Mappers (Persistence) │
│ • Local Data Sources (Platform-specific) │
│ • File Storage (Internal Storage / Documents Directory) │
│ • Depends on: Domain Layer │
└─────────────────────────────────────────────────────────────┘
Presentation → Application → Domain ← Infrastructure
Key Architectural Rules:
- Domain Layer (innermost): Pure business logic, zero dependencies
- Application Layer: Orchestrates domain objects, depends on Domain only
- Infrastructure Layer: Technical implementations, depends on Domain (not Application)
- Presentation Layer: UI layer, depends on Application
sealed class ModelUri private constructor(val value: String) {
companion object {
fun create(uri: String): Result<ModelUri> {
return when {
uri.isBlank() -> Result.failure(ValidationException("URI cannot be blank"))
!uri.matches(Regex(".*\\.(glb|usdz)$")) ->
Result.failure(ValidationException("Invalid model format"))
else -> Result.success(ValidModelUri(uri))
}
}
}
private class ValidModelUri(value: String) : ModelUri(value)
}// Use cases in APPLICATION LAYER (not domain)
// Location: application/usecase/
interface ImportObjectUseCaseInterface : BaseUseCase<ImportObjectInput, ARObject>
class ImportObjectUseCase(
private val repository: ARObjectRepository
) : ImportObjectUseCaseInterface {
override suspend fun invoke(input: ImportObjectInput): Result<ARObject> {
// Validation with Value Objects (from domain)
val nameResult = ObjectName.create(input.name)
if (nameResult.isFailure) return Result.failure(nameResult.exceptionOrNull()!!)
return repository.importObject(input.uri, input.name, input.modelType)
}
}
// Import paths:
import com.trendhive.arsample.application.usecase.ImportObjectUseCase
import com.trendhive.arsample.application.base.BaseUseCase
import com.trendhive.arsample.application.dto.ImportObjectInput// Persistence DTO in INFRASTRUCTURE LAYER
// Location: infrastructure/persistence/dto/
@Serializable
data class ARObjectDTO(
val id: String,
val name: String,
val modelUri: String,
val modelType: String
)
// Mapper in INFRASTRUCTURE LAYER
// Location: infrastructure/persistence/mapper/
class ARObjectMapper : BaseMapper<ARObjectDTO, ARObject> {
override fun toDTO(model: ARObject): ARObjectDTO
override fun toModel(dto: ARObjectDTO): ARObject
}
// Import paths:
import com.trendhive.arsample.infrastructure.persistence.dto.ARObjectDTO
import com.trendhive.arsample.infrastructure.persistence.mapper.ARObjectMapper
import com.trendhive.arsample.infrastructure.persistence.BaseMapper- Kotlin 2.1.0 - Primary programming language
- Compose Multiplatform 1.7.1 - Declarative UI framework
- Kotlin Coroutines - Asynchronous programming
- Kotlin Flow - Reactive state management
- ARCore (Android) - Google's AR platform
- SceneView - ARCore wrapper library
- ARKit (iOS) - Apple's AR platform
- RealityKit - iOS AR rendering
- Kotlin Serialization - JSON serialization
- DataStore (Android) - Preferences storage
- UserDefaults (iOS) - Preferences storage
- Kotlin Test - Testing framework
- MockK - Mocking library
- Turbine - Flow testing utilities
- Gradle Version Catalog - Dependency management
- Android Gradle Plugin 8.7.3 - Android build
- Xcode 15+ - iOS build
Required:
- JDK 17 or higher
- Android Studio Ladybug (2024.2.1) or newer
- Xcode 15+ (for iOS development)
- macOS (for iOS builds)
AR Device Requirements:
- Android: ARCore-supported device (Check compatibility)
- iOS: A12+ chip with ARKit support (iPhone XS and newer)
-
Clone the repository
git clone https://github.com/recepteksi/ARSample.git cd ARSample -
Build Android
./gradlew :composeApp:assembleDebug
-
Build iOS
# Open in Xcode open iosApp/iosApp.xcodeproj # Or use xcodebuild xcodebuild -project iosApp/iosApp.xcodeproj -scheme iosApp -configuration Debug
# Run all tests
./gradlew :composeApp:testDebugUnitTest
# Run specific test class
./gradlew :composeApp:testDebugUnitTest --tests "com.trendhive.arsample.domain.usecase.ImportObjectUseCaseTest"
# Run with coverage
./gradlew :composeApp:testDebugUnitTest --tests "*" --infoARSample/
├── composeApp/src/
│ ├── commonMain/kotlin/com/trendhive/arsample/
│ │ ├── domain/ # Domain Layer (NO dependencies)
│ │ │ ├── base/ # BaseModel, BaseRepository
│ │ │ ├── model/ # Domain entities (ARObject, ARScene, PlacedObject)
│ │ │ │ └── valueobjects/ # Value Objects (ModelUri, ObjectName)
│ │ │ ├── repository/ # Repository interfaces
│ │ │ └── exception/ # Domain exceptions
│ │ │
│ │ ├── application/ # Application Layer (depends on Domain)
│ │ │ ├── base/ # BaseUseCase<Input, Output>
│ │ │ ├── dto/ # Use Case Input/Output DTOs
│ │ │ └── usecase/ # Business workflows (use cases)
│ │ │
│ │ ├── infrastructure/ # Infrastructure Layer (depends on Domain)
│ │ │ └── persistence/
│ │ │ ├── dto/ # Persistence DTOs
│ │ │ ├── mapper/ # DTO ↔ Model mappers
│ │ │ ├── repository/ # Repository implementations
│ │ │ ├── local/ # Data source interfaces
│ │ │ └── BaseMapper.kt # Mapper base class
│ │ │
│ │ └── presentation/ # Presentation Layer (depends on Application)
│ │ ├── viewmodel/ # State management
│ │ └── ui/ # Compose screens and components
│ │
│ ├── androidMain/ # Android-specific (ARCore, DataStore)
│ │ ├── ar/ # ARCore implementation
│ │ └── infrastructure/persistence/local/ # Android data sources
│ │
│ ├── iosMain/ # iOS-specific (ARKit, UserDefaults)
│ │ ├── ar/ # ARKit implementation
│ │ └── infrastructure/persistence/local/ # iOS data sources
│ │
│ └── commonTest/ # Shared unit tests
│
├── iosApp/ # iOS app entry point
├── docs/ # Architecture docs and guides
└── .claude/agents/ # AI agent system documentation
- Domain Layer: 90%+ coverage
- Use Cases: 100% coverage
- ViewModels: 85%+ coverage
- Repositories: 90%+ coverage
class ImportObjectUseCaseTest {
private lateinit var repository: ARObjectRepository
private lateinit var useCase: ImportObjectUseCase
@Test
fun `import valid object succeeds`() = runTest {
// Arrange
val input = ImportObjectInput("file://model.glb", "Chair", ModelType.GLB)
coEvery { repository.importObject(any(), any(), any()) } returns
Result.success(mockARObject)
// Act
val result = useCase(input)
// Assert
assertTrue(result.isSuccess)
coVerify { repository.importObject("file://model.glb", "Chair", ModelType.GLB) }
}
}- Architecture Overview - System design and patterns
- Agent System - Multi-agent development workflow
- Hit Testing Guide - AR interaction implementation
- Android ARCore - Android AR implementation
- iOS ARKit - iOS AR implementation
- Code Review Checklist - Quality standards
interface ARObjectRepository : BaseRepository {
suspend fun importObject(uri: String, name: String, type: ModelType): Result<ARObject>
suspend fun getAllObjects(): Result<List<ARObject>>
suspend fun deleteObject(id: String): Result<Unit>
}// BaseModel and BaseRepository in DOMAIN layer
interface BaseModel
interface BaseRepository
// BaseUseCase in APPLICATION layer
interface BaseUseCase<Input : BaseModel, Output : BaseModel> {
suspend operator fun invoke(input: Input): Result<Output>
}
// BaseMapper in INFRASTRUCTURE layer
interface BaseMapper<DTO, Model> {
fun toDTO(model: Model): DTO
fun toModel(dto: DTO): Model
}
// Import paths:
import com.trendhive.arsample.domain.base.BaseModel
import com.trendhive.arsample.domain.base.BaseRepository
import com.trendhive.arsample.application.base.BaseUseCase
import com.trendhive.arsample.infrastructure.persistence.BaseMappersealed class DomainException(message: String) : Exception(message)
class ValidationException(message: String) : DomainException(message)
class EntityNotFoundException(message: String) : DomainException(message)
class StorageException(message: String) : DomainException(message)Contributions are welcome! This project follows professional development practices:
- Code Standards: Kotlin conventions, Clean Architecture compliance
- Testing: All new features must include unit tests
- Documentation: Update relevant docs with changes
- Review Process: Code review checklist validation
See CONTRIBUTING.md for detailed guidelines.
This project is licensed under the MIT License - see the LICENSE file for details.
Recep Tekşi
- GitHub: @recepteksi
- LinkedIn: Recep Tekşi
This project demonstrates:
✅ Modern Android/iOS Development - KMM, Compose, ARCore/ARKit
✅ Enterprise Architecture - Clean Architecture, DDD, SOLID principles
✅ Professional Practices - High test coverage, type safety, documentation
✅ Platform Expertise - Native AR implementations, platform-specific optimizations
✅ Team Collaboration - Multi-agent system, code review standards
Built with ❤️ using Kotlin Multiplatform
⭐ Star this repo if you find it useful!