{"id":35710768,"url":"https://github.com/felix-leyva/gradingscale2","last_synced_at":"2026-01-06T04:08:04.539Z","repository":{"id":264316371,"uuid":"893022142","full_name":"felix-leyva/gradingscale2","owner":"felix-leyva","description":"Grading Scale2 - a Kotlin Multi Platform App supporting Android, iOS, WasmJs, and JVM - with networking, Firebase Auth and persistance","archived":false,"fork":false,"pushed_at":"2025-12-21T10:46:32.000Z","size":471989,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-12-22T20:47:59.396Z","etag":null,"topics":["arrow","compose-ios","compose-multiplatform","compose-wasm","firebase-auth","kmp-sample","koin","kotlin","kotlin-multiplatform","kotlin-multiplatform-sample","ktor-client","material3","molecule","sqldelight"],"latest_commit_sha":null,"homepage":"https://gradingscale.felixlf.de","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/felix-leyva.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-11-23T10:28:13.000Z","updated_at":"2025-12-21T10:46:36.000Z","dependencies_parsed_at":null,"dependency_job_id":"e7504c0b-5711-4aa6-ba55-176a291ed1ac","html_url":"https://github.com/felix-leyva/gradingscale2","commit_stats":null,"previous_names":["felix-leyva/gradingscale2"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/felix-leyva/gradingscale2","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felix-leyva%2Fgradingscale2","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felix-leyva%2Fgradingscale2/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felix-leyva%2Fgradingscale2/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felix-leyva%2Fgradingscale2/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/felix-leyva","download_url":"https://codeload.github.com/felix-leyva/gradingscale2/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felix-leyva%2Fgradingscale2/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28221561,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2026-01-06T02:00:07.049Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["arrow","compose-ios","compose-multiplatform","compose-wasm","firebase-auth","kmp-sample","koin","kotlin","kotlin-multiplatform","kotlin-multiplatform-sample","ktor-client","material3","molecule","sqldelight"],"created_at":"2026-01-06T04:08:03.801Z","updated_at":"2026-01-06T04:08:04.532Z","avatar_url":"https://github.com/felix-leyva.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# GradingScale2\n\nA multi-platform grading scale calculator built with Kotlin Multiplatform and Compose Multiplatform. GradingScale2 handles non-linear grading systems with precision, allowing educators and students to define custom grading scales and perform exact calculations across all major platforms.\n\n## Features\n\n### Core Functionality\n- **Non-Linear Grade Calculations**: Support for complex, non-linear grading scales that accurately reflect various educational systems\n- **Custom Grade Scale Management**: Create, edit, and manage multiple grading scales with different point systems and grade boundaries\n- **Weighted Grade Calculator**: Calculate weighted averages for courses with different component weights (assignments, exams, projects)\n- **Import Grade Scales**: Import pre-defined grading scales from various educational systems\n- **Real-time Calculations**: Instant grade calculations as you input scores\n- **Multi-Scale Support**: Switch between different grading scales seamlessly\n\n### Platform-Specific Features\n- **Adaptive UI**: Responsive design that adapts to phones, tablets, and desktop screens\n- **Offline-First**: All data stored locally for fast, reliable access\n- **Material 3 Design**: Modern UI following the latest Material Design guidelines\n\n## Architecture\n\n### Layered Architecture Implementation\n\nThe project follows Layered Architecture principles with clear separation of concerns across three main layers such that the Domain Layer\nkeeps the business logic and interfaces, which are then implemented by the data layer whenever `data framework` logic is needed (dependency\ninversion principle). The Presentation layer could also be considered a `presentation framework` layer which however focuses only on the UI\nand navigation.\n\nDue the extension of the app, only `technological` dimension is being used, but no `domain feature` dimension.\n\n```\n\n┌────────────────────────-────────────────────────────────┐\n│                     Domain Layer                        │\n│                  (entities module)                      │\n│    • Use Cases (Business Logic)                         │\n│    • Repository Interfaces                              │\n│    • Domain Models                                      │\n└────────────────────────┬────────────────────────────────┘\n                         |---------------------------------------------------------------┐\n                         |                                                               |\n                         |                                                               |\n                         |                                                               |\n┌─────────────────────────────────────────────────────────┐     ┌────────────────────────┴────────────────────────────────┐\n│                      Data Layer                         │     │                    Presentation Layer                   │\n│               (data/* submodules)                       │     │               (composeApp module)                       │\n│    • Repository Implementations                         │     │    • Compose UI Screens                                 │\n│    • Local Database (SQLDelight)                        │     │    • ViewModels with Molecule State Management          │           \n│    • Remote API (Ktor)                                  │     │    • Platform-specific UI implementations               │           \n│    • Preferences (Multiplatform Settings)               │     |                                                         |\n└─────────────────────────────────────────────────────────┘     └────────────────────────-────────────────────────────────┘\n\n\n```\n\n### Key Architectural Decisions\n\n#### Molecule for Reactive State Management\nThe project uses [CashApp's Molecule](https://github.com/cashapp/molecule) for state management, revolutionizing how UI state is handled by treating it as a Composable function:\n\n```kotlin\ninterface UIModel\u003cUIState, UICommand\u003e {\n    val scope: UIModelScope\n\n    val uiState: StateFlow\u003cUIState\u003e\n\n    @Composable\n    fun produceUI(): UIState\n\n    fun sendCommand(command: UICommand)\n}\n```\n**Benefits of Molecule:**\n- **Reactive by Design**: UI state recomposes automatically when dependencies change\n- **Testable**: State logic can be tested without Android framework dependencies\n- **Composable Logic**: Leverage Compose's powerful state management primitives\n- **Clear Data Flow**: Unidirectional data flow with events and state\n\n\nIt also separates the UIModel, from the Android Framework Specific UIModel, allowing easier testing and use in different platforms even without the Navigation/Jetpack ComposeUI. \n\nIn case you require to link your UIModels to the Android or Jetpack ComposeUI Navigation/Lifecycle, you can then use a ViewModel and tie its scope into the ViewModel. The functionallity can be easily added via interface implementation with the `by` class delegation.\n\n```kotlin\nclass CalculatorViewModel(\n    calculatorUIModel: CalculatorUIModel,\n) : ViewModel(calculatorUIModel.scope),\nUIModel\u003cGradeScaleCalculatorUIState, CalculatorUIEvent\u003e by calculatorUIModel\n\n```\nThis reduces boilerplate and allows to keep this logic out of the platform `:composeApp` module.\n\n\n#### Adaptive Layout with Persistent Scaffolds\n\nThe adaptive layout system uses a unique approach with single per-destination Scaffolds that maintain navigation state across all screens:\n\n```kotlin\n@Composable\nfun AnimatedContentScope.PersistentScaffold(\n    navigationRail: @Composable ScaffoldState.() -\u003e Unit = { DefaultNavigationRail() },\n    bottomBar: @Composable ScaffoldState.() -\u003e Unit = { DefaultNavigationBar() },\n    content: @Composable ScaffoldState.(PaddingValues) -\u003e Unit,\n) {\n    val windowSizeClass = calculateWindowSizeClass()\n    \n    // Automatically adapts between NavigationRail (tablet/desktop) \n    // and BottomNavigation (mobile)\n    when (windowSizeClass.widthSizeClass) {\n        WindowWidthSizeClass.Compact -\u003e {\n            // Mobile: Bottom navigation\n            BottomNavigationScaffold(bottomBar, content)\n        }\n        else -\u003e {\n            // Tablet/Desktop: Navigation rail\n            NavigationRailScaffold(navigationRail, content)\n        }\n    }\n}\n```\n\n**Key Features:**\n- **Shared Element Transitions**: Smooth animations between screens using `SharedTransitionScope`\n- **State Persistence**: Navigation components maintain their state across screen changes\n- **Adaptive Components**: Automatically switches UI components based on screen size\n- **Centralized State**: `ScaffoldState` acts as a container for both `AnimatedVisibilityScope` and `SharedTransitionScope`\n\n#### Immutability with PersistentList\n\nThe project embraces functional programming principles by using **immutable data structures** throughout:\n\n```kotlin\n@Serializable\ndata class GradeScale(\n    val id: String,\n    val gradeScaleName: String,\n    val totalPoints: Double,\n    @Serializable(with = PersistentListSerializer::class)\n    val grades: PersistentList\u003cGrade\u003e,\n) {\n    @Transient\n    val sortedGrades = grades.sortedByDescending { it.percentage }.toImmutableList()\n}\n```\n\n**Why PersistentList?**\n- **Thread Safety**: Immutable collections are inherently thread-safe\n- **Performance**: Structural sharing reduces memory overhead when creating modified copies\n- **Predictability**: Data cannot be accidentally mutated, reducing bugs\n- **Compose Integration**: Works seamlessly with Compose's recomposition system\n- **Custom Serialization**: Handles WasmJS compatibility issues with a custom serializer\n\n#### Reactive Everything with Kotlin Flows\n\nThe architecture is fully reactive using Kotlin Coroutines Flow:\n\n```kotlin\ninterface GradeScaleRepository {\n    fun getGradeScaleById(id: String): SharedFlow\u003cGradeScale?\u003e\n    fun getGradeScales(): SharedFlow\u003cImmutableList\u003cGradeScale\u003e\u003e\n}\n\nclass GradeScaleRepositoryImpl : GradeScaleRepository {\n    override fun getGradeScales(): SharedFlow\u003cImmutableList\u003cGradeScale\u003e\u003e =\n        gradeScaleDao.getGradeScales()\n            .map { list -\u003e list.toImmutableList() }\n            .shareIn(\n                scope = scope,\n                started = SharingStarted.Lazily,\n                replay = 1,\n            )\n}\n```\n\n**Reactive Benefits:**\n- **Real-time Updates**: UI automatically updates when data changes\n- **Efficient Resource Usage**: `SharingStarted.Lazily` only activates flows when collected\n- **Backpressure Handling**: Flow operators handle data stream pressure automatically\n- **Cancellation Support**: Proper lifecycle management with structured concurrency\n\n#### ⚡ Functional Error Handling with Arrow\n\nThe project uses [Arrow](https://arrow-kt.io/) for functional error handling, avoiding exceptions in favor of explicit error types:\n\n```kotlin\n// Using Option for nullable results\nclass InsertGradeScaleUseCaseImpl : InsertGradeScaleUseCase {\n    override suspend operator fun invoke(...): Option\u003cString\u003e = option {\n        val currentScales = gradeScaleRepository.getGradeScales().firstOrNull()\n        // Arrow's bind() for monadic composition\n        gradeScaleRepository.upsertGradeScale(initialGradeScale).bind()\n    }\n}\n\n// Using Either for operations that can fail\ninterface RemoteSyncRepository {\n    suspend fun countriesAndGrades(): Either\u003cRemoteError, List\u003cCountryGradingScales\u003e\u003e\n}\n\n// Clean error handling in UI\nwhen (val result = getRemoteGradeScales()) {\n    is Either.Right -\u003e updateUI(result.value)\n    is Either.Left -\u003e showError(result.value)\n}\n```\n\n**Why Arrow?**\n- **Type-Safe Errors**: Compile-time guarantee of error handling\n- **Composable**: Chain operations without nested try-catch blocks\n- **Explicit**: Makes error cases visible in function signatures\n- **Functional**: Leverages monadic composition for clean code\n\n#### Gradle Convention Plugins\n\nThe build system uses a convention plugin approach:\n\n```kotlin\n// buildSrc/src/main/kotlin/gs-android-app.gradle.kts\nplugins {\n    id(\"com.android.application\")\n    id(\"kotlin-android\")\n    // Common configurations\n}\n\nandroid {\n    // Standardized Android app configuration\n}\n\n// Applied to apps as:\nplugins {\n    id(\"gs-android-app\")\n}\n```\n\nThis approach provides:\n- **Consistency**: All modules follow the same configuration patterns\n- **Maintainability**: Update configurations in one place\n- **Type Safety**: Kotlin DSL provides IDE support and compile-time checking\n- **Modularity**: Different plugins for different module types\n\n#### Dependency Injection with Koin\n\nThe project uses Koin with a modular, platform-aware structure:\n\n```kotlin\n// Feature module\nval calculatorModule = module {\n    factory { CalculatorViewModel(get(), get()) }\n    factory { CalculatorUIModel(get(), get()) }\n}\n\n// Platform-specific module\nexpect val platformModule: Module\n\n// Initialization\nfun initKoin() = startKoin {\n    modules(\n        calculatorModule,\n        gradeScaleModule,\n        platformModule, // Platform-specific implementations\n        dataModule,\n    )\n}\n```\n\n### Module Structure\n\n```\nGradingScale2/\n├── composeApp/          # UI Layer - Compose Multiplatform app\n│   ├── commonMain/      # Shared UI code\n│   ├── androidMain/     # Android-specific UI\n│   ├── iosMain/         # iOS-specific UI\n│   ├── jsMain/          # Web-specific UI\n│   └── jvmMain/         # Desktop-specific UI\n│\n├── entities/            # Domain Layer\n│   ├── models/          # Domain models\n│   ├── repositories/    # Repository interfaces\n│   ├── usecases/        # Business logic\n│   └── uimodel/         # UI state models\n│\n└── data/                # Data Layer\n    ├── network/         # Ktor HTTP client\n    ├── authFirebase/    # Firebase authentication\n    └── persistance/\n        ├── db/          # SQLDelight database\n        └── sharedprefs/ # Multiplatform Settings\n```\n\n## Tech Stack\n\n### Core Technologies\n- **[Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html)** (2.1.21) - Share code across platforms\n- **[Compose Multiplatform](https://www.jetbrains.com/compose-multiplatform/)** (1.8.1) - Modern declarative UI framework\n- **[Material 3 Adaptive](https://m3.material.io/)** - Adaptive design system\n\n### State Management \u0026 UI\n- **[CashApp Molecule](https://github.com/cashapp/molecule)** - Compose-based state management\n- **[Navigation Compose](https://developer.android.com/jetpack/compose/navigation)** - Type-safe navigation\n\n### Networking \u0026 Data\n- **[Ktor](https://ktor.io/)** - Multiplatform HTTP client\n- **[SQLDelight](https://github.com/cashapp/sqldelight)** - Type-safe SQL database\n- **[Kotlinx Serialization](https://github.com/Kotlin/kotlinx.serialization)** - JSON parsing\n- **[Multiplatform Settings](https://github.com/russhwolf/multiplatform-settings)** - Key-value storage\n\n### Dependency Injection\n- **[Koin](https://insert-koin.io/)** - Pragmatic lightweight DI framework\n\n### Platform Integration\n- **[Firebase](https://firebase.google.com/)** - Authentication \u0026 Analytics\n  - Android/iOS: GitLive Firebase SDK\n  - Web: Firebase JS SDK via CDN\n  - Desktop: GitLive Firebase SDK\n- **[Conveyor](https://conveyor.hydraulic.dev/)** - Desktop app distribution\n\n### Code Quality\n- **[Ktlint](https://pinterest.github.io/ktlint/)** - Kotlin code style enforcement\n- **[Arrow](https://arrow-kt.io/)** - Functional programming and error handling\n- **Kotlin Coroutines** - Asynchronous programming\n- **PersistentList** - Immutable collections from Kotlinx Collections\n\n## 📱 Platform Support\n\n| Platform | Status | Min Version | Notes |\n|----------|--------|-------------|-------|\n| Android  | ✅ | API 24 (7.0) | Full feature support |\n| iOS      | ✅ | iOS 14.0 | Native SwiftUI integration |\n| Desktop  | ✅ | JVM 17 | Windows, macOS, Linux |\n| Web      | ✅ | Modern browsers | JS/WASM targets |\n\n## 🚀 Getting Started\n\n### Prerequisites\n- JDK 17 or higher\n- Android Studio (for Android development)\n- Xcode 15+ (for iOS development)\n- Node.js (for web development)\n\n### Clone the Repository\n```bash\ngit clone https://github.com/yourusername/GradingScale2.git\ncd GradingScale2\n```\n\n### Build \u0026 Run\n\n#### 🤖 Android\n```bash\n# Build debug APK\n./gradlew :composeApp:assembleDebug\n\n# Install on connected device\n./gradlew :composeApp:installDebug\n```\n\n#### iOS\n```bash\n# Build iOS framework\n./build-ios.sh\n\n# Open in Xcode\nopen iosApp/iosApp.xcodeproj\n\n# Run from Xcode or use:\n./gradlew :composeApp:iosSimulatorArm64Test\n```\n\n#### Desktop\n```bash\n# Run desktop application\n./gradlew :composeApp:run\n\n# Create distribution\n./gradlew :composeApp:packageDistributionForCurrentOS\n```\n\n#### Web\n```bash\n# Run development server\n./gradlew :composeApp:wasmJsBrowserRun\n\n# Build production bundle\n./gradlew :composeApp:wasmJsBrowserProductionWebpack̨̨̨̨\n```\n\nThe WebWasm version can be deployed using a docker container. Inside `docker` are included the templates to build an nginx docker and configure it to run the deployed wasmjs app.\nTo ease the deployment, the `build-wasmjs.main.kts` script automates the whole process to build the app, copy the content into the `docker` directory and then via ssh transfer the docker to a remote server. Once in the server, it restart the docker with the newly deployed app.̨̨̨̨̨ \n\n### Testing\n```bash\n# Run all tests\n./gradlew test\n\n# Run specific module tests\n./gradlew :entities:test\n./gradlew :data:network:test\n\n# Check code style\n./gradlew ktlintCheck\n\n# Auto-format code\n./gradlew ktlintFormat\n```\n\n### Clean \u0026 Rebuild\n```bash\n# Use the helper script\n./clean-and-rebuild.sh\n\n# Or manually\n./gradlew clean\n./gradlew build\n```\n\n## Testing Strategy\n\nThe project includes comprehensive unit tests focusing on:\n- **Use Cases**: Business logic validation\n- **ViewModels**: UI state management with Molecule\n- **Repositories**: Data layer operations\n- **Platform-specific**: Platform-specific functionality\n\nExample test structure:\n```kotlin\nclass GradeScaleListViewModelTest {\n    @Test\n    fun `should update UI state when grade scale is selected`() = runTest {\n        // Given\n        val viewModel = GradeScaleListViewModel(...)\n        \n        // When\n        viewModel.sendEvent(SelectGradeScale(gradeScaleId))\n        \n        // Then\n        assertEquals(gradeScaleId, viewModel.uiState.value.selectedId)\n    }\n}\n```\n\n## Download\n\nThe desktop application is available for download on the [Download Section](https://felix-leyva.github.io/gradingscale2/download)\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffelix-leyva%2Fgradingscale2","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffelix-leyva%2Fgradingscale2","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffelix-leyva%2Fgradingscale2/lists"}