{"id":29895061,"url":"https://github.com/icerockdev/moko-fields","last_synced_at":"2025-08-01T06:19:44.203Z","repository":{"id":36540293,"uuid":"210294803","full_name":"icerockdev/moko-fields","owner":"icerockdev","description":"Input forms for mobile (android \u0026 ios) Kotlin Multiplatform development","archived":false,"fork":false,"pushed_at":"2025-06-24T15:22:56.000Z","size":312,"stargazers_count":21,"open_issues_count":4,"forks_count":9,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-06-24T16:35:36.954Z","etag":null,"topics":["android","forms","ios","kotlin-multiplatform","kotlin-multiplatform-mobile","kotlin-native","livedata","moko"],"latest_commit_sha":null,"homepage":"https://moko.icerock.dev/","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/icerockdev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","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}},"created_at":"2019-09-23T07:46:10.000Z","updated_at":"2024-12-25T09:57:43.000Z","dependencies_parsed_at":"2025-04-11T08:19:30.512Z","dependency_job_id":"28b26a6a-bc39-4959-9563-82b2aeebbc47","html_url":"https://github.com/icerockdev/moko-fields","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/icerockdev/moko-fields","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icerockdev%2Fmoko-fields","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icerockdev%2Fmoko-fields/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icerockdev%2Fmoko-fields/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icerockdev%2Fmoko-fields/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/icerockdev","download_url":"https://codeload.github.com/icerockdev/moko-fields/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icerockdev%2Fmoko-fields/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268178101,"owners_count":24208451,"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-01T02:00:08.611Z","response_time":67,"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":["android","forms","ios","kotlin-multiplatform","kotlin-multiplatform-mobile","kotlin-native","livedata","moko"],"created_at":"2025-08-01T06:19:42.078Z","updated_at":"2025-08-01T06:19:44.194Z","avatar_url":"https://github.com/icerockdev.png","language":"Kotlin","readme":"![moko-fields](img/logo.png)  \n[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) [![Download](https://img.shields.io/maven-central/v/dev.icerock.moko/fields-core) ](https://repo1.maven.org/maven2/dev/icerock/moko/fields-core) ![kotlin-version](https://kotlin-version.aws.icerock.dev/kotlin-version?group=dev.icerock.moko\u0026name=fields-core)\n\n# Mobile Kotlin fields\nThis is a Kotlin MultiPlatform library that add form fields abstraction to implement any input forms\n with validations.\n\n## Table of Contents\n- [Features](#features)\n- [Requirements](#requirements)\n- [Installation](#installation)\n- [Usage](#usage)\n- [Samples](#samples)\n- [Set Up Locally](#set-up-locally)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Features\n- Input field abstraction;\n- Validation based on reactive approach (on `LiveData` from `moko-mvvm` or `Flow` from `kotlinx.coroutines`).\n\n## Requirements\n- Gradle version 6.8+\n- Android API 16+\n- iOS version 11.0+\n\n## Installation\nroot build.gradle  \n```groovy\nallprojects {\n    repositories {\n      mavenCentral()\n    }\n}\n```\n\nproject build.gradle\n```groovy\ndependencies {\n    commonMainApi(\"dev.icerock.moko:fields-core:0.12.0\")\n\n    // integration with reactive flows\n    commonMainApi(\"dev.icerock.moko:fields-livedata:0.12.0\")\n    commonMainApi(\"dev.icerock.moko:fields-flow:0.12.0\")\n \n    androidMainApi(\"dev.icerock.moko:fields-material:0.12.0\")\n}\n```\n\n### Flow additions\nto work correctly on the iOS side, you need to export the \n`mvvm-flow` and `mvvm-core` dependencies to the iOS framework.\n\n## Usage\n\n### Live Data \nCreate `FormField` to text input with empty validation:\n```kotlin\nval textField = FormField\u003cString, StringDesc\u003e(\"\", { inputLiveData -\u003e\n    inputLiveData.map { text -\u003e\n        if (text.isBlank()) \"should be not blank!\".desc()\n        else null\n    }\n})\n```\nUse `liveBlock` to simplify validation create.\n```kotlin\nval textField = FormField\u003cString, StringDesc\u003e(\"\", liveBlock { text -\u003e\n    if (text.isBlank()) \"should be not blank!\".desc()\n    else null\n})\n```\nUse `LiveData` in validation lambda to merge with other fields.\n```kotlin\nval passwordField = FormField\u003cString, StringDesc\u003e(\"\", { inputLiveData -\u003e\n    inputLiveData.map { text -\u003e\n        if (text.isBlank()) \"should be not blank!\".desc()\n        else null\n    }\n})\nval passwordConfirmField = FormField\u003cString, StringDesc\u003e(\"\", { inputLiveData -\u003e\n    passwordField.data.mergeWith(inputLiveData) { password, passwordConfirm -\u003e\n        if (passwordConfirm.isBlank()) \"should be not blank!\".desc()\n        else if(passwordConfirm != password) \"passwords not same\".desc()\n        else null\n    }\n})\n``` \nCall validate to perform validations and show error to user by `field.error` LiveData.\n```kotlin\nclass LoginViewModel(\n    override val eventsDispatcher: EventsDispatcher\u003cEventsListener\u003e\n) : ViewModel(), EventsDispatcherOwner\u003cLoginViewModel.EventsListener\u003e {\n    val emailField = FormField\u003cString, StringDesc\u003e(\"\", liveBlock { email -\u003e\n        if (email.isBlank()) MR.strings.cant_be_blank.desc()\n        else null\n    })\n    val passwordField = FormField\u003cString, StringDesc\u003e(\"\", liveBlock { password -\u003e\n        if (password.isBlank()) MR.strings.cant_be_blank.desc()\n        else null\n    })\n\n    private val fields = listOf(emailField, passwordField)\n\n    fun onLoginPressed() {\n        if (!fields.validate()) return\n\n        val email = emailField.value()\n        val password = passwordField.value()\n        val message = \"$email:$password\"\n\n        eventsDispatcher.dispatchEvent { showMessage(message.desc()) }\n    }\n\n    interface EventsListener {\n        fun showMessage(message: StringDesc)\n    }\n}\n```\nBind `FormField` to UI by `data` and `error` `LiveData`s.\n```xml\n\u003ccom.google.android.material.textfield.TextInputLayout\n    app:error=\"@{viewModel.emailField.error.ld}\"\u003e\n\n    \u003cEditText\n        android:text=\"@={viewModel.emailField.data.ld}\" /\u003e\n\u003c/com.google.android.material.textfield.TextInputLayout\u003e\n```\n```swift\nemailField.bindTextTwoWay(liveData: viewModel.emailField.data)\nemailField.bindError(liveData: viewModel.emailField.error)\n```\n\n### Flow \n\nCreate `FormField` to text input with empty validation:\n```kotlin\nval emailField: FormField\u003cString, StringDesc\u003e = FormField(\n    scope = viewModelScope,\n    initialValue = \"\",\n    validationTransform = { email -\u003e\n        ValidationResult.of(email) {\n            notBlank(MR.strings.cant_be_blank.desc())\n            matchRegex(MR.strings.wrong_format.desc(), EMAIL_REGEX)\n        }\n    }\n)\n```\nFormField to work with `coroutines`, `CoroutineScope` is required.\n\nCall validate to perform validations and show error to user by `field.error` StateFlow.\n```kotlin\nclass LoginViewModel : ViewModel() {\n    private val _actions: Channel\u003cAction\u003e = Channel(Channel.BUFFERED)\n    val actions: CFlow\u003cAction\u003e get() = _actions.receiveAsFlow().cFlow()\n\n    val emailField: FormField\u003cString, StringDesc\u003e = FormField(\n        scope = viewModelScope,\n        initialValue = \"\",\n        validation = flowBlock { email -\u003e\n            ValidationResult.of(email) {\n                notBlank(MR.strings.cant_be_blank.desc())\n                matchRegex(MR.strings.wrong_format.desc(), EMAIL_REGEX)\n            }\n        }\n    )\n\n    @Suppress(\"MagicNumber\")\n    val passwordField: FormField\u003cString, StringDesc\u003e = FormField(\n        scope = viewModelScope,\n        initialValue = \"\",\n        validation = fieldValidation {\n            notBlank(MR.strings.cant_be_blank.desc())\n            minLength(MR.strings.must_contain_more_char.desc(), 4)\n        }\n    )\n\n    private val fields = listOf(emailField, passwordField)\n\n    fun onLoginPressed() {\n        if (!fields.validate()) return\n\n        val email = emailField.value()\n        val password = passwordField.value()\n        val message = \"$email:$password\"\n\n        _actions.trySend(Action.ShowMessage(message.desc()))\n    }\n\n    sealed interface Action {\n        data class ShowMessage(val message: StringDesc) : Action\n    }\n\n    companion object {\n        @Suppress(\"MaxLineLength\")\n        private val EMAIL_REGEX =\n            Regex(\"[a-zA-Z0-9\\\\+\\\\.\\\\_\\\\%\\\\-\\\\+]{1,256}\\\\@[a-zA-Z0-9][a-zA-Z0-9\\\\-]{0,64}(\\\\.[a-zA-Z0-9][a-zA-Z0-9\\\\-]{0,25})+\")\n    }\n}\n```\n\nBind FormField to UI by data and error `StateFlow`s: \n\n```kotlin\nval email: String by viewModel.emailField.data.collectAsState()\n\nTextField(\n    placeholder = @Composable { \n        Text(\"Email\") \n    }, \n    value = email, \n    onValueChange = { viewModel.emailField.data.value = it }\n)\n```\n\nFor ease of use in working with SwiftUI, you can use [this CocoaPods\ndependency](https://github.com/icerockdev/moko-mvvm#swiftui-additions).\n```swift\nstruct LoginScreen: View {\n    @StateObject var viewModel: LoginViewModel = LoginViewModel()\n    \n    var body: some View {\n        LoginScreenBody(\n             email: viewModel.binding(\\.emailField.data),\n             emailError: viewModel.state(\\.emailField.error)\n        )\n    }\n}\n\nstruct LoginScreenBody: View {\n    @Binding var email: String\n    let emailError: StringDesc?\n    \n    var body: some View {\n        VStack {\n            TextField(\"email\", text: viewModel.binding(\\.passwordField.data))\n            if let emailError = emailError {\n                Text(emailError.localized())\n            }\n        }\n    }\n}\n```\n\n#### Validations packet\n\nThere is a useful `ValidationResult` class for building validation monads for a form fields.\nTwo formats for creating validation are implemented:\n\n- Chain/monad validation:\n\n```kotlin\nValidationResult.of(emailFieldValue)\n    .notBlank(blankErrorStringDesc)\n    .matchRegex(wrongEmailErrorStringDesc, EMAIL_REGEX)\n    .validate()\n```\n\n**For this variant, do not forget to call function `validate` at the end!**\n\n- DSL validation:\n```kotlin\nValidationResult.of(emailFieldValue) {\n    notBlank(blankErrorStringDesc)\n    matchRegex(wrongEmailErrorStringDesc, EMAIL_REGEX)\n}\n```\n\nTo create a new function for validation monad, you need to create an extension function of class\n`ValidationResult` using builder `nextValidation`. For example, this is how the ready-made function\nfor checking `String` values for blankness looks like:\n\n```kotlin\nfun ValidationResult\u003cString\u003e.notBlank(errorText: StringDesc) = nextValidation { value -\u003e\n    if (value.isNotBlank()) {\n        ValidationResult.success(value)\n    } else {\n        ValidationResult.failure(errorText)\n    }\n}\n```\n\nAll the ready-made validation functions of the library can be found in the source codes in the files\n`AnyValidations.kt` for `Any` class and `StringValidations.kt` for `String` class.\n\nTo simplify of adding validation to the `FormField` object (without mapping of a `LiveData`\nobjects) you can use the builder-function `fieldValidation`:\n\n```kotlin\nval passwordField = FormField\u003cString, StringDesc\u003e(\n    initialValue = \"\",\n    validation = fieldValidation {\n        notBlank(MR.strings.cant_be_blank.desc())\n        minLength(MR.strings.must_contain_more_char.desc(), 4)\n    }\n)\n```\n\n## Samples\nMore examples can be found in the [sample directory](sample) or [sample-declarative-ui directory](sample-declarative-ui).\n\n## Set Up Locally \n- In [fields-core directory](fields-core) contains the core - validations logic and the interface for interacting with fields;\n- In [fields-flow directory](fields-flow) contains implementation of the FormField interface using `kotlinx.coroutines`;\n- In [fields-livedata directory](fields-livedata) contains implementation of the FormField interface using `moko-mvvm` `LiveData`s\n- In [sample directory](sample) contains samples use `fields-livedata` on Android, iOS \u0026 mpp-library connected to apps\n- In [sample-declarative-ui directory](sample-declarative-ui) contains samples use `fields-flow` on Android with Compose,on iOS with SwiftUI and shared module connected to apps\n\n## Contributing\nAll development (both new features and bug fixes) is performed in `develop` branch. This way `master` sources always contain sources of the most recently released version. Please send PRs with bug fixes to `develop` branch. Fixes to documentation in markdown files are an exception to this rule. They are updated directly in `master`.\n\nThe `develop` branch is pushed to `master` during release.\n\nMore detailed guide for contributers see in [contributing guide](CONTRIBUTING.md).\n\n## License\n        \n    Copyright 2019 IceRock MAG Inc\n    \n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n    \n       http://www.apache.org/licenses/LICENSE-2.0\n    \n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ficerockdev%2Fmoko-fields","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ficerockdev%2Fmoko-fields","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ficerockdev%2Fmoko-fields/lists"}