{"id":27715722,"url":"https://github.com/toptalent-23/compose-swiftui-github-search","last_synced_at":"2025-08-28T08:04:39.159Z","repository":{"id":289373043,"uuid":"971034770","full_name":"TopTalent-23/Compose-SwiftUI-GitHub-Search","owner":"TopTalent-23","description":"Github Repos Search - Android - iOS - Kotlin Multiplatform Mobile using Jetpack Compose, SwiftUI, FlowRedux, Coroutines Flow, Dagger Hilt, Koin Dependency Injection, shared KMP ViewModel, Clean Architecture","archived":false,"fork":false,"pushed_at":"2025-04-22T23:21:15.000Z","size":6696,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-27T01:16:00.389Z","etag":null,"topics":["github","jetpack-compose","kotlin-multiplatform","ktor-client","swiftui"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TopTalent-23.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","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},"funding":{"github":["hoc081098"],"custom":["https://www.buymeacoffee.com/hoc081098"]}},"created_at":"2025-04-22T23:12:05.000Z","updated_at":"2025-04-22T23:21:18.000Z","dependencies_parsed_at":"2025-04-23T00:36:33.942Z","dependency_job_id":null,"html_url":"https://github.com/TopTalent-23/Compose-SwiftUI-GitHub-Search","commit_stats":null,"previous_names":["toptalent-23/compose-swiftui-github-search"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/TopTalent-23/Compose-SwiftUI-GitHub-Search","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TopTalent-23%2FCompose-SwiftUI-GitHub-Search","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TopTalent-23%2FCompose-SwiftUI-GitHub-Search/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TopTalent-23%2FCompose-SwiftUI-GitHub-Search/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TopTalent-23%2FCompose-SwiftUI-GitHub-Search/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TopTalent-23","download_url":"https://codeload.github.com/TopTalent-23/Compose-SwiftUI-GitHub-Search/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TopTalent-23%2FCompose-SwiftUI-GitHub-Search/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272466734,"owners_count":24939458,"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":"2025-08-28T02:00:10.768Z","response_time":74,"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":["github","jetpack-compose","kotlin-multiplatform","ktor-client","swiftui"],"created_at":"2025-04-27T01:15:58.949Z","updated_at":"2025-08-28T08:04:39.124Z","avatar_url":"https://github.com/TopTalent-23.png","language":"Kotlin","funding_links":["https://github.com/sponsors/hoc081098","https://www.buymeacoffee.com/hoc081098"],"categories":[],"sub_categories":[],"readme":"# GithubSearch\n\nGithub Repos Search - Kotlin Multiplatform Mobile using Jetpack Compose, SwiftUI,\n FlowRedux, Coroutines Flow, Dagger Hilt, Koin Dependency Injection, shared KMP ViewModel, Clean Architecture\n\n\nMinimal **Kotlin Multiplatform** project with SwiftUI, Jetpack Compose.\n - Android (Jetpack compose)\n - iOS (SwiftUI)\n\n### Modern Development\n - Kotlin Multiplatform\n - Jetpack Compose\n - Kotlin Coroutines \u0026 Flows\n - Dagger Hilt\n - SwiftUI\n - Koin Dependency Injection\n - FlowRedux State Management\n - Shared KMP ViewModel\n - Clean Architecture\n\n## Tech Stacks\n - Functional \u0026 Reactive programming with **Kotlin Coroutines with Flow**\n - **Clean Architecture** with **MVI** (Uni-directional data flow)\n - [**Multiplatform ViewModel and SavedStateHandle**](https://github.com/hoc081098/kmp-viewmodel) (save and restore states across process death)\n - **Multiplatform FlowRedux** State Management\n - [**Λrrow** - Functional companion to Kotlin's Standard Library](https://arrow-kt.io/)\n   - [Either](https://arrow-kt.io/docs/apidocs/arrow-core/arrow.core/-either/)\n   - [Monad Comprehensions](https://arrow-kt.io/docs/patterns/monad_comprehensions/)\n   - [Option](https://arrow-kt.io/docs/apidocs/arrow-core/arrow.core/-option/)\n   - [parZip](https://arrow-kt.io/docs/apidocs/arrow-fx-coroutines/arrow.fx.coroutines/par-zip.html)\n - Dependency injection\n   - iOS: [**Koin**](https://insert-koin.io/)\n   - Android: [**Dagger Hilt**](https://dagger.dev/hilt/)\n - Declarative UI\n   - iOS: [**SwiftUI**](https://developer.apple.com/xcode/swiftui/)\n   - Android: [**Jetpack Compose**](https://developer.android.com/jetpack/compose)\n - [Ktor client library](https://ktor.io/docs/getting-started-ktor-client-multiplatform-mobile.html) for networking\n - [Kotlinx Serialization](https://github.com/Kotlin/kotlinx.serialization) for JSON serialization/deserialization.\n - [Napier](https://github.com/AAkira/Napier) for Multiplatform Logging.\n - [FlowExt](https://github.com/hoc081098/FlowExt) provides many kotlinx.coroutines.\n - [Touchlab SKIE](https://skie.touchlab.co/) a Swift-friendly API Generator for Kotlin Multiplatform.\n - [kotlinx.collections.immutable](https://github.com/Kotlin/kotlinx.collections.immutable): immutable collection interfaces and implementation prototypes for Kotlin..\n - Testing\n   - [Kotlin Test](https://kotlinlang.org/docs/multiplatform-run-tests.html) for running tests with Kotlin Multiplatform.\n   - [Turbine](https://github.com/cashapp/turbine) for KotlinX Coroutines Flows testing.\n   - [Mockative](https://github.com/mockative/mockative): mocking for Kotlin/Native and Kotlin Multiplatform using the Kotlin Symbol Processing API.\n   - [Kotlinx-Kover](https://github.com/Kotlin/kotlinx-kover) for Kotlin Multiplatform code coverage.\n\n# Screenshots\n\n## Android (Light theme)\n|                                                  |                                                   |                                                  |                                                  |\n|:------------------------------------------------:|:-------------------------------------------------:|:------------------------------------------------:|:------------------------------------------------:|\n| ![](screenshots/Screenshot_Android_Light_01.png) | ![](screenshots/Screenshot_Android_Light_02.png)  | ![](screenshots/Screenshot_Android_Light_03.png) | ![](screenshots/Screenshot_Android_Light_04.png) |\n\n## Android (Dark theme)\n|                                                  |                                                   |                                                  |                                                  |\n|:------------------------------------------------:|:-------------------------------------------------:|:------------------------------------------------:|:------------------------------------------------:|\n| ![](screenshots/Screenshot_Android_Dark_01.png)  |  ![](screenshots/Screenshot_Android_Dark_02.png)  | ![](screenshots/Screenshot_Android_Dark_03.png)  | ![](screenshots/Screenshot_Android_Dark_04.png)  |\n\n## iOS (Light theme)\n|                                              |                                              |                                               |                                              |\n|:--------------------------------------------:|:--------------------------------------------:|:---------------------------------------------:|:--------------------------------------------:|\n| ![](screenshots/Screenshot_iOS_Light_01.png) | ![](screenshots/Screenshot_iOS_Light_02.png) | ![](screenshots/Screenshot_iOS_Light_03.png)  | ![](screenshots/Screenshot_iOS_Light_04.png) |\n\n## iOS (Dark theme)\n|                                             |                                             |                                              |                                             |\n|:-------------------------------------------:|:-------------------------------------------:|:--------------------------------------------:|:-------------------------------------------:|\n| ![](screenshots/Screenshot_iOS_Dark_01.png) | ![](screenshots/Screenshot_iOS_Dark_02.png) | ![](screenshots/Screenshot_iOS_Dark_03.png)  | ![](screenshots/Screenshot_iOS_Dark_04.png) |\n\n## Overall Architecture\n\n### What is shared?\n - **domain**: Domain models, UseCases, Repositories.\n - **presentation**: ViewModels, ViewState, ViewSingleEvent, ViewAction.\n - **data**: Repository Implementations, Remote Data Source, Local Data Source.\n - **utils**: Utilities, Logging Library\n\n### Unidirectional data flow - FlowRedux\n\n - My implementation. **Credits: [freeletics/FlowRedux](https://github.com/freeletics/FlowRedux)**\n - See more docs and concepts at [freeletics/RxRedux](https://github.com/freeletics/RxRedux)\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://raw.githubusercontent.com/freeletics/RxRedux/master/docs/rxredux.png\" width=\"600\" alt=\"RxRedux In a Nutshell\"/\u003e\n\u003c/p\u003e\n\n```kotlin\npublic sealed interface FlowReduxStore\u003cAction, State\u003e {\n  /**\n   * The state of this store.\n   */\n  public val stateFlow: StateFlow\u003cState\u003e\n\n  /**\n   * @return false if cannot dispatch action (this store was closed).\n   */\n  public fun dispatch(action: Action): Boolean\n\n  /**\n   * Call this method to close this store.\n   * A closed store will not accept any action anymore, thus state will not change anymore.\n   * All [SideEffect]s will be cancelled.\n   */\n  public fun close()\n\n  /**\n   * After calling [close] method, this function will return true.\n   *\n   * @return true if this store was closed.\n   */\n  public fun isClosed(): Boolean\n}\n```\n\n### Multiplatform ViewModel\n\n```kotlin\nopen class GithubSearchViewModel(\n  searchRepoItemsUseCase: SearchRepoItemsUseCase,\n  private val savedStateHandle: SavedStateHandle,\n) : ViewModel() {\n  private val effectsContainer = GithubSearchSideEffectsContainer(searchRepoItemsUseCase)\n\n  private val store = viewModelScope.createFlowReduxStore(\n    initialState = GithubSearchState.initial(),\n    sideEffects = effectsContainer.sideEffects,\n    reducer = Reducer(flip(GithubSearchAction::reduce))\n      .withLogger(githubSearchFlowReduxLogger())\n  )\n\n  val termStateFlow: NonNullStateFlowWrapper\u003cString\u003e = savedStateHandle.getStateFlow(TERM_KEY, \"\").wrap()\n  val stateFlow: NonNullStateFlowWrapper\u003cGithubSearchState\u003e = store.stateFlow.wrap()\n  val eventFlow: NonNullFlowWrapper\u003cGithubSearchSingleEvent\u003e = effectsContainer.eventFlow.wrap()\n\n  init {\n    store.dispatch(InitialSearchAction(termStateFlow.value))\n  }\n\n  @MainThread\n  fun dispatch(action: GithubSearchAction): Boolean {\n    if (action is GithubSearchAction.Search) {\n      savedStateHandle[TERM_KEY] = action.term\n    }\n    return store.dispatch(action)\n  }\n\n  companion object {\n    private const val TERM_KEY = \"com.hoc081098.github_search_kmm.presentation.GithubSearchViewModel.term\"\n\n    /**\n     * Used by non-Android platforms.\n     */\n    fun create(searchRepoItemsUseCase: SearchRepoItemsUseCase): GithubSearchViewModel =\n      GithubSearchViewModel(searchRepoItemsUseCase, SavedStateHandle())\n  }\n}\n```\n\n### Platform ViewModel\n\n#### Android\n\nExtends `GithubSearchViewModel` to use `Dagger Constructor Injection`.\n\n```kotlin\n@HiltViewModel\nclass DaggerGithubSearchViewModel @Inject constructor(\n  searchRepoItemsUseCase: SearchRepoItemsUseCase,\n  savedStateHandle: SavedStateHandle,\n) : GithubSearchViewModel(searchRepoItemsUseCase, savedStateHandle)\n```\n\n#### iOS\n\nConform to `ObservableObject` and use `@Published` property wrapper.\n\n```swift\nimport Foundation\nimport Combine\nimport shared\n\n@MainActor\nclass IOSGithubSearchViewModel: ObservableObject {\n  private let vm: GithubSearchViewModel\n\n  @Published private(set) var state: GithubSearchState\n  @Published private(set) var term: String = \"\"\n  let eventPublisher: AnyPublisher\u003cGithubSearchSingleEventKs, Never\u003e\n\n  init(vm: GithubSearchViewModel) {\n    self.vm = vm\n\n    self.eventPublisher = vm.eventFlow.asNonNullPublisher()\n      .assertNoFailure()\n      .map(GithubSearchSingleEventKs.init)\n      .eraseToAnyPublisher()\n\n    self.state = vm.stateFlow.value\n    vm.stateFlow.subscribe(\n      scope: vm.viewModelScope,\n      onValue: { [weak self] in self?.state = $0 }\n    )\n\n    self.vm\n      .termStateFlow\n      .asNonNullPublisher(NSString.self)\n      .assertNoFailure()\n      .map { $0 as String }\n      .assign(to: \u0026$term)\n  }\n\n  @discardableResult\n  func dispatch(action: GithubSearchAction) -\u003e Bool {\n    self.vm.dispatch(action: action)\n  }\n\n  deinit {\n    Napier.d(\"\\(self)::deinit\")\n    vm.clear()\n  }\n}\n```\n\n# Building \u0026 Develop\n\n- `Android Studio Hedgehog | 2023.1.1` (note: **Java 17 is now the minimum version required**).\n- `Xcode 13.2.1` or later (due to use of new Swift 5.5 concurrency APIs).\n- Clone project: `git clone https://github.com/hoc081098/GithubSearchKMM.git`\n- Android: open project by `Android Studio` and run as usual.\n- iOS\n  ```shell\n  # Cd to root project directory\n  cd GithubSearchKMM\n\n  # Setup\n  sh scripts/run_ios.sh\n  ```\n\n  There's a *Build Phase* script that will do the magic. 🧞 \u003cbr\u003e\n  \u003ckbd\u003eCmd\u003c/kbd\u003e + \u003ckbd\u003eB\u003c/kbd\u003e to build\n  \u003cbr\u003e\n  \u003ckbd\u003eCmd\u003c/kbd\u003e + \u003ckbd\u003eR\u003c/kbd\u003e to run.\n\n  You can also build and run iOS app from Xcode as usual.\n\n# LOC\n\n```shell\n--------------------------------------------------------------------------------\n Language             Files        Lines        Blank      Comment         Code\n--------------------------------------------------------------------------------\n Kotlin                 116         7942          996          453         6493\n JSON                     7         3938            0            0         3938\n Swift                   16          960          124          102          734\n Markdown                 1          281           53            0          228\n Bourne Shell             2          249           28          116          105\n Batch                    1           92           21            0           71\n XML                      6           69            6            0           63\n--------------------------------------------------------------------------------\n Total                  149        13531         1228          671        11632\n--------------------------------------------------------------------------------\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftoptalent-23%2Fcompose-swiftui-github-search","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftoptalent-23%2Fcompose-swiftui-github-search","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftoptalent-23%2Fcompose-swiftui-github-search/lists"}