├── .idea
├── .name
├── .gitignore
├── vcs.xml
├── compiler.xml
├── kotlinc.xml
├── migrations.xml
├── misc.xml
├── deploymentTargetDropDown.xml
├── gradle.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── values
│ │ │ │ ├── themes.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── styles.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── values-night
│ │ │ │ └── styles.xml
│ │ │ ├── xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── edorex
│ │ │ │ └── mobile
│ │ │ │ └── composeForm
│ │ │ │ ├── App.kt
│ │ │ │ ├── ui
│ │ │ │ └── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Shape.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ └── Theme.kt
│ │ │ │ ├── models
│ │ │ │ └── Country.kt
│ │ │ │ ├── di
│ │ │ │ └── ResourcesProvider.kt
│ │ │ │ ├── MainViewModel.kt
│ │ │ │ ├── MainForm.kt
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── edorex
│ │ │ └── mobile
│ │ │ └── composeForm
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── edorex
│ │ └── mobile
│ │ └── composeForm
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── composeform
├── .gitignore
├── consumer-rules.pro
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── ch
│ │ │ └── benlu
│ │ │ └── composeform
│ │ │ ├── Validator.kt
│ │ │ ├── FormField.kt
│ │ │ ├── formatters
│ │ │ ├── StringFormatters.kt
│ │ │ └── DateFormatters.kt
│ │ │ ├── validators
│ │ │ ├── NotEmptyValidator.kt
│ │ │ ├── MinLengthValidator.kt
│ │ │ ├── IsEqualValidator.kt
│ │ │ ├── DateValidator.kt
│ │ │ └── EmailValidator.kt
│ │ │ ├── FieldState.kt
│ │ │ ├── fields
│ │ │ ├── CheckboxField.kt
│ │ │ ├── TextField.kt
│ │ │ ├── PasswordField.kt
│ │ │ ├── DateField.kt
│ │ │ └── PickerField.kt
│ │ │ ├── Field.kt
│ │ │ ├── components
│ │ │ ├── RadioButtonComponent.kt
│ │ │ ├── CheckboxComponent.kt
│ │ │ ├── SingleSelectDialogComponent.kt
│ │ │ └── TextFieldComponent.kt
│ │ │ └── Form.kt
│ ├── test
│ │ └── java
│ │ │ └── ch
│ │ │ └── benlu
│ │ │ └── composeform
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── ch
│ │ └── benlu
│ │ └── composeform
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── jitpack.yml
├── images
├── logo.png
└── logo-square.png
├── screenshots
├── png
│ ├── simple-form.png
│ └── extended-form.png
└── gif
│ └── composeform-extended.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle
├── LICENSE
├── gradle.properties
├── gradlew.bat
├── gradlew
└── README.md
/.idea/.name:
--------------------------------------------------------------------------------
1 | Compose Form
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/composeform/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/composeform/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - openjdk11
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/images/logo.png
--------------------------------------------------------------------------------
/images/logo-square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/images/logo-square.png
--------------------------------------------------------------------------------
/screenshots/png/simple-form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/screenshots/png/simple-form.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/screenshots/png/extended-form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/screenshots/png/extended-form.png
--------------------------------------------------------------------------------
/screenshots/gif/composeform-extended.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/screenshots/gif/composeform-extended.gif
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/composeform/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjamin-luescher/compose-form/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/Validator.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform
2 |
3 | abstract class Validator(
4 | val validate: (s: T?) -> Boolean,
5 | val errorText: String
6 | )
7 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/FormField.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform
2 |
3 | @Target(AnnotationTarget.FIELD)
4 | @Retention(AnnotationRetention.RUNTIME)
5 | annotation class FormField() {
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/edorex/mobile/composeForm/App.kt:
--------------------------------------------------------------------------------
1 | package com.edorex.mobile.composeForm
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class App : Application()
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Sep 20 17:59:02 CEST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/formatters/StringFormatters.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.formatters
2 |
3 | fun upperCase(r: String?): String {
4 | return r?.uppercase() ?: ""
5 | }
6 |
7 | fun lowerCase(r: String?): String {
8 | return r?.lowercase() ?: ""
9 | }
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/edorex/mobile/composeForm/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.edorex.mobile.composeForm.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple200 = Color(0xFFBB86FC)
6 | val Purple500 = Color(0xFF6200EE)
7 | val Purple700 = Color(0xFF3700B3)
8 | val Teal200 = Color(0xFF03DAC5)
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/validators/NotEmptyValidator.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.validators
2 |
3 | import ch.benlu.composeform.Validator
4 |
5 | class NotEmptyValidator(errorText: String? = null) : Validator(
6 | validate = {
7 | it != null
8 | },
9 | errorText = errorText ?: "This field should not be empty"
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/edorex/mobile/composeForm/models/Country.kt:
--------------------------------------------------------------------------------
1 | package com.edorex.mobile.composeForm.models
2 |
3 | import ch.benlu.composeform.fields.PickerValue
4 |
5 | data class Country(
6 | val code: String,
7 | val name: String
8 | ): PickerValue() {
9 | override fun searchFilter(query: String): Boolean {
10 | return this.name.startsWith(query)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/validators/MinLengthValidator.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.validators
2 |
3 | import ch.benlu.composeform.Validator
4 |
5 | class MinLengthValidator(minLength: Int, errorText: String? = null) : Validator(
6 | validate = {
7 | (it?.length ?: -1) >= minLength
8 | },
9 | errorText = errorText ?: "This field is too short"
10 | )
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/validators/IsEqualValidator.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.validators
2 |
3 | import ch.benlu.composeform.Validator
4 |
5 | class IsEqualValidator(expectedValue: () -> T, errorText: String? = null) : Validator(
6 | validate = {
7 | it == expectedValue()
8 | },
9 | errorText = errorText ?: "This field's value is not as expected."
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/edorex/mobile/composeForm/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.edorex.mobile.composeForm.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/validators/DateValidator.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.validators
2 |
3 | import ch.benlu.composeform.Validator
4 | import java.util.*
5 |
6 | class DateValidator(minDateTime: () -> Long, errorText: String? = null) : Validator(
7 | validate = {
8 | (it?.time ?: -1) >= minDateTime()
9 | },
10 | errorText = errorText ?: "This field is not valid."
11 | )
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "Compose Form"
16 | include ':app'
17 | include ':composeform'
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/formatters/DateFormatters.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.formatters
2 |
3 | import java.text.DateFormat
4 | import java.util.*
5 |
6 | fun dateShort(r: Date?): String {
7 | return if (r != null) DateFormat.getDateInstance().format(r) else ""
8 | }
9 |
10 | fun dateLong(r: Date?): String {
11 | return if (r != null) DateFormat.getDateInstance(DateFormat.LONG).format(r) else ""
12 | }
13 |
--------------------------------------------------------------------------------
/composeform/src/test/java/ch/benlu/composeform/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Compose Form
3 | Cancel
4 | Search
5 | This field should not be empty
6 | This field doesn\'t match the min-length requirement.
7 | This date should be after start date.
8 |
--------------------------------------------------------------------------------
/app/src/test/java/com/edorex/mobile/composeForm/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.edorex.mobile.composeForm
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/edorex/mobile/composeForm/di/ResourcesProvider.kt:
--------------------------------------------------------------------------------
1 | package com.edorex.mobile.composeForm.di
2 |
3 | import android.content.Context
4 | import androidx.annotation.StringRes
5 | import dagger.hilt.android.qualifiers.ApplicationContext
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | class ResourcesProvider @Inject constructor(
11 | @ApplicationContext private val context: Context
12 | ) {
13 | fun getString(@StringRes stringResId: Int): String {
14 | return context.getString(stringResId)
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/validators/EmailValidator.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.validators
2 |
3 | import ch.benlu.composeform.Validator
4 |
5 | class EmailValidator(errorText: String? = null) : Validator(
6 | validate = {
7 | """(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""".toRegex().matches("$it")
8 | },
9 | errorText = errorText ?: "Please enter a valid e-mail address."
10 | )
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/composeform/src/androidTest/java/ch/benlu/composeform/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("ch.benlu.composeform.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/edorex/mobile/composeForm/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.edorex.mobile.composeForm
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.edorex.mobile.composeForm", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/composeform/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/java/com/edorex/mobile/composeForm/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.edorex.mobile.composeForm
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.ViewModel
5 | import ch.benlu.composeform.validators.NotEmptyValidator
6 | import com.edorex.mobile.composeForm.di.ResourcesProvider
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import javax.inject.Inject
9 |
10 | @HiltViewModel
11 | class MainViewModel @Inject constructor(
12 | resourcesProvider: ResourcesProvider
13 | ): ViewModel() {
14 | var form = MainForm(resourcesProvider)
15 |
16 | fun validate() {
17 | form.validate(true)
18 | form.logRawValue()
19 | Log.d("MainViewModel", "Submit (form is valid: ${form.isValid})")
20 | }
21 |
22 | fun doSomething() {
23 | form.name.validators.removeIf { it::class == NotEmptyValidator::class }
24 | form.name.state.value = "Benji"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/edorex/mobile/composeForm/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.edorex.mobile.composeForm.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | )
16 | /* Other default text styles to override
17 | button = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.W500,
20 | fontSize = 14.sp
21 | ),
22 | caption = TextStyle(
23 | fontFamily = FontFamily.Default,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 12.sp
26 | )
27 | */
28 | )
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Benjamin Lüscher
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/FieldState.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.mutableStateOf
5 |
6 | class FieldState(
7 | val state: MutableState,
8 | val validators: MutableList> = mutableListOf(),
9 | val errorText: MutableList = mutableListOf(),
10 | val isValid: MutableState = mutableStateOf(false),
11 | val isVisible: () -> Boolean = { true },
12 | val hasChanges: MutableState = mutableStateOf(false),
13 | var options: MutableList = mutableListOf(),
14 | val optionItemFormatter: ((T?) -> String)? = null,
15 | ) {
16 | fun hasError(): Boolean {
17 | return isVisible() && isValid.value == false && hasChanges.value == true
18 | }
19 |
20 | fun selectedOption(): T? {
21 | return options.firstOrNull { it == state.value }
22 | }
23 |
24 | fun selectedOptionText(): String? {
25 | val selectedOption = selectedOption() ?: return null
26 | return optionItemFormatter?.invoke(selectedOption) ?: selectedOption.toString()
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/edorex/mobile/composeForm/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.edorex.mobile.composeForm.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 |
9 | private val DarkColorPalette = darkColors(
10 | primary = Purple200,
11 | primaryVariant = Purple700,
12 | secondary = Teal200
13 | )
14 |
15 | private val LightColorPalette = lightColors(
16 | primary = Purple500,
17 | primaryVariant = Purple700,
18 | secondary = Teal200
19 |
20 | /* Other default colors to override
21 | background = Color.White,
22 | surface = Color.White,
23 | onPrimary = Color.White,
24 | onSecondary = Color.Black,
25 | onBackground = Color.Black,
26 | onSurface = Color.Black,
27 | */
28 | )
29 |
30 | @Composable
31 | fun ComposeFormTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
32 | val colors = if (darkTheme) {
33 | DarkColorPalette
34 | } else {
35 | LightColorPalette
36 | }
37 |
38 | MaterialTheme(
39 | colors = colors,
40 | typography = Typography,
41 | shapes = Shapes,
42 | content = content
43 | )
44 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/fields/CheckboxField.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.fields
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.text.input.ImeAction
6 | import ch.benlu.composeform.Field
7 | import ch.benlu.composeform.FieldState
8 | import ch.benlu.composeform.Form
9 | import ch.benlu.composeform.components.CheckboxComponent
10 |
11 | class CheckboxField(
12 | label: String,
13 | form: Form,
14 | modifier: Modifier? = Modifier,
15 | fieldState: FieldState,
16 | isEnabled: Boolean = true,
17 | imeAction: ImeAction = ImeAction.Next,
18 | formatter: ((raw: Boolean?) -> String)? = null,
19 | changed: ((v: Boolean?) -> Unit)? = null
20 | ) : Field(
21 | label = label,
22 | form = form,
23 | fieldState = fieldState,
24 | isEnabled = isEnabled,
25 | modifier = modifier,
26 | imeAction = imeAction,
27 | formatter = formatter,
28 | changed = changed
29 | ) {
30 |
31 | /**
32 | * Returns a composable representing the DateField / Picker for this field
33 | */
34 | @Composable
35 | override fun Field() {
36 | this.updateComposableValue()
37 | if (!fieldState.isVisible()) {
38 | return
39 | }
40 | CheckboxComponent(
41 | modifier = modifier ?: Modifier,
42 | checked = fieldState.state.value == true,
43 | onCheckedChange = {
44 | this.onChange(it, form)
45 | },
46 | label = label,
47 | hasError = fieldState.hasError(),
48 | errorText = fieldState.errorText
49 | )
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/Field.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.runtime.*
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.text.input.ImeAction
7 | import androidx.compose.ui.text.input.KeyboardType
8 | import androidx.compose.ui.text.input.VisualTransformation
9 | import java.util.*
10 |
11 | abstract class Field (
12 | open val fieldState: FieldState,
13 | open val label: String,
14 | open val form: Form,
15 | open val keyboardType: KeyboardType = KeyboardType.Text,
16 | open val visualTransformation: VisualTransformation = VisualTransformation.None,
17 | open val isEnabled: Boolean = true,
18 | open val modifier: Modifier? = Modifier,
19 | open val imeAction: ImeAction? = ImeAction.Next,
20 | open val formatter: ((raw: T?) -> String)? = null,
21 | open var changed: ((v: T?) -> Unit)? = null
22 | ) {
23 | var value: MutableState = mutableStateOf(null)
24 |
25 | /**
26 | * This method is called when the value on the input field is changed
27 | */
28 | fun onChange(v: Any?, form: Form) {
29 | @Suppress("UNCHECKED_CAST")
30 | this.value.value = v as T?
31 | this.updateFormValue()
32 | form.validate()
33 | changed?.invoke(v)
34 | }
35 |
36 | fun updateComposableValue() {
37 | this.value.value = fieldState.state.value
38 | }
39 |
40 | private fun updateFormValue() {
41 | fieldState.state.value = this.value.value
42 | fieldState.hasChanges.value = true
43 | }
44 |
45 | @SuppressLint("NotConstructor")
46 | @Composable
47 | abstract fun Field()
48 | }
49 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/components/RadioButtonComponent.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.components
2 |
3 | import androidx.compose.foundation.layout.Row
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.selection.selectable
7 | import androidx.compose.material.MaterialTheme
8 | import androidx.compose.material.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.tooling.preview.Preview
13 | import androidx.compose.ui.unit.dp
14 |
15 | @Composable
16 | fun RadioButtonComponent(
17 | label: String,
18 | value: T? = null,
19 | selectedValue: T?,
20 | onClickListener: (T?) -> Unit
21 | ) {
22 | Row(
23 | Modifier
24 | .fillMaxWidth()
25 | .selectable(
26 | selected = value == selectedValue,
27 | onClick = {
28 | onClickListener(value)
29 | }
30 | )
31 | ) {
32 | androidx.compose.material.RadioButton(
33 | modifier = Modifier
34 | .padding(start = 16.dp)
35 | .align(alignment = Alignment.CenterVertically),
36 | selected = value == selectedValue,
37 | onClick = {
38 | onClickListener(value)
39 | }
40 | )
41 | Text(
42 | text = label,
43 | style = MaterialTheme.typography.body1.merge(),
44 | modifier = Modifier
45 | .padding(8.dp)
46 | .align(alignment = Alignment.CenterVertically)
47 | )
48 | }
49 | }
50 |
51 | @Composable
52 | @Preview
53 | fun RadioButtonPreview() {
54 | RadioButtonComponent(label = "RadioButton", value = "", onClickListener = {}, selectedValue = "")
55 | }
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/components/CheckboxComponent.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.interaction.MutableInteractionSource
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.material.Checkbox
7 | import androidx.compose.material.MaterialTheme
8 | import androidx.compose.material.Text
9 | import androidx.compose.material.ripple.rememberRipple
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.draw.clip
15 | import androidx.compose.ui.text.TextStyle
16 | import androidx.compose.ui.unit.dp
17 |
18 | @Composable
19 | fun CheckboxComponent(
20 | modifier: Modifier = Modifier,
21 | checked: Boolean,
22 | onCheckedChange: ((Boolean) -> Unit),
23 | label: String,
24 | hasError: Boolean = false,
25 | errorText: MutableList? = null
26 | ) {
27 | Column(modifier = Modifier.padding(top = 8.dp)) {
28 | Column(modifier = modifier
29 | .fillMaxWidth()
30 | .clip(MaterialTheme.shapes.small)
31 | .clickable(
32 | indication = rememberRipple(color = MaterialTheme.colors.primary),
33 | interactionSource = remember { MutableInteractionSource() },
34 | onClick = { onCheckedChange(!checked) }
35 | )
36 | ) {
37 | Row(
38 | verticalAlignment = Alignment.CenterVertically,
39 | modifier = Modifier.padding(vertical = 8.dp)
40 | ) {
41 | Checkbox(
42 | checked = checked,
43 | onCheckedChange = null
44 | )
45 |
46 | Spacer(Modifier.size(6.dp))
47 |
48 | Text(
49 | text = label,
50 | style = MaterialTheme.typography.button
51 | )
52 | }
53 | }
54 |
55 | if (hasError && errorText != null) {
56 | Text(
57 | text = errorText.joinToString(),
58 | modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
59 | style = TextStyle.Default.copy(color = MaterialTheme.colors.error)
60 | )
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/composeform/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'maven-publish'
5 | }
6 |
7 | android {
8 | namespace 'ch.benlu.composeform'
9 | compileSdk 33
10 |
11 | defaultConfig {
12 | minSdk 24
13 | targetSdk 33
14 |
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | consumerProguardFiles "consumer-rules.pro"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_1_8
27 | targetCompatibility JavaVersion.VERSION_1_8
28 | }
29 | kotlinOptions {
30 | jvmTarget = '1.8'
31 | }
32 |
33 | buildFeatures {
34 | compose true
35 | }
36 |
37 | ext {
38 | compose_version = '1.2.0'
39 | kotlin_version = '1.7.0'
40 | }
41 |
42 | publishing {
43 | singleVariant("release") {
44 | withSourcesJar()
45 | withJavadocJar()
46 | }
47 | }
48 | }
49 |
50 | dependencies {
51 | implementation 'androidx.core:core-ktx:1.9.0'
52 | implementation "androidx.compose.ui:ui:$compose_version"
53 | implementation "androidx.compose.material:material:$compose_version"
54 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
55 | implementation "androidx.compose.material:material-icons-extended:$compose_version"
56 | implementation 'androidx.appcompat:appcompat:1.4.1'
57 | testImplementation 'junit:junit:4.13.2'
58 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
59 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
60 |
61 | apply plugin: 'kotlin-kapt'
62 |
63 | // Reflection
64 | implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version")
65 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
66 | }
67 |
68 | publishing {
69 | publications {
70 | release(MavenPublication) {
71 | groupId = 'com.github.benjamin-luescher'
72 | artifactId = 'compose-form'
73 | version = '0.2.8'
74 |
75 | afterEvaluate {
76 | from components.release
77 | }
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/fields/TextField.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.fields
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.foundation.text.KeyboardActions
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.text.input.ImeAction
8 | import androidx.compose.ui.text.input.KeyboardType
9 | import androidx.compose.ui.text.input.VisualTransformation
10 | import ch.benlu.composeform.Field
11 | import ch.benlu.composeform.FieldState
12 | import ch.benlu.composeform.Form
13 | import ch.benlu.composeform.components.TextFieldComponent
14 | import java.util.*
15 |
16 | class TextField(
17 | label: String,
18 | form: Form,
19 | modifier: Modifier? = Modifier,
20 | fieldState: FieldState,
21 | isEnabled: Boolean = true,
22 | imeAction: ImeAction = ImeAction.Next,
23 | formatter: ((raw: String?) -> String)? = null,
24 | keyboardType: KeyboardType = KeyboardType.Text,
25 | visualTransformation: VisualTransformation = VisualTransformation.None,
26 | changed: ((v: String?) -> Unit)? = null
27 | ) : Field(
28 | label = label,
29 | form = form,
30 | fieldState = fieldState,
31 | isEnabled = isEnabled,
32 | modifier = modifier,
33 | imeAction = imeAction,
34 | formatter = formatter,
35 | keyboardType = keyboardType,
36 | visualTransformation = visualTransformation,
37 | changed = changed
38 | ) {
39 |
40 | /**
41 | * Returns a composable representing the DateField / Picker for this field
42 | */
43 | @SuppressLint("NotConstructor")
44 | @Composable
45 | override fun Field() {
46 | this.updateComposableValue()
47 | if (!fieldState.isVisible()) {
48 | return
49 | }
50 |
51 | TextFieldComponent(
52 | modifier = modifier ?: Modifier,
53 | imeAction = imeAction ?: ImeAction.Next,
54 | isEnabled = isEnabled,
55 | keyBoardActions = KeyboardActions.Default,
56 | keyboardType = keyboardType,
57 | onChange = {
58 | this.onChange(it, form)
59 | },
60 | label = label,
61 | text = formatter?.invoke(value.value) ?: (value.value ?: ""),
62 | hasError = fieldState.hasError(),
63 | errorText = fieldState.errorText,
64 | visualTransformation = visualTransformation
65 | )
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | id("dagger.hilt.android.plugin")
5 | }
6 |
7 | android {
8 | compileSdk 33
9 |
10 | defaultConfig {
11 | applicationId "com.edorex.mobile.composeForm"
12 | minSdk 24
13 | targetSdk 33
14 | versionCode 1
15 | versionName "1.0"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | vectorDrawables {
19 | useSupportLibrary true
20 | }
21 | }
22 |
23 | buildTypes {
24 | release {
25 | minifyEnabled false
26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
27 | }
28 | }
29 | compileOptions {
30 | sourceCompatibility JavaVersion.VERSION_1_8
31 | targetCompatibility JavaVersion.VERSION_1_8
32 | }
33 | kotlinOptions {
34 | jvmTarget = '1.8'
35 | }
36 | buildFeatures {
37 | compose true
38 | }
39 | composeOptions {
40 | kotlinCompilerExtensionVersion compose_version
41 | }
42 | packagingOptions {
43 | resources {
44 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
45 | }
46 | }
47 | namespace 'com.edorex.mobile.composeForm'
48 | }
49 |
50 | dependencies {
51 | implementation 'androidx.core:core-ktx:1.9.0'
52 | implementation "androidx.compose.ui:ui:$compose_version"
53 | implementation "androidx.compose.material:material:$compose_version"
54 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
55 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
56 | implementation 'androidx.activity:activity-compose:1.6.1'
57 | implementation project(path: ':composeform')
58 | testImplementation 'junit:junit:4.13.2'
59 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
60 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
61 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
62 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
63 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
64 |
65 | apply plugin: 'kotlin-kapt'
66 |
67 | // Hilt
68 | implementation("com.google.dagger:hilt-android:2.42")
69 | kapt("com.google.dagger:hilt-android-compiler:2.42")
70 | implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
71 |
72 | // RxKotlin
73 | implementation("io.reactivex.rxjava3:rxkotlin:3.0.1")
74 |
75 | // Reflection
76 | implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version")
77 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/fields/PasswordField.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.fields
2 |
3 | import android.annotation.SuppressLint
4 | import android.graphics.drawable.Icon
5 | import androidx.compose.foundation.text.KeyboardActions
6 | import androidx.compose.material.Icon
7 | import androidx.compose.material.IconButton
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.Visibility
10 | import androidx.compose.material.icons.filled.VisibilityOff
11 | import androidx.compose.runtime.*
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.text.input.ImeAction
14 | import androidx.compose.ui.text.input.KeyboardType
15 | import androidx.compose.ui.text.input.PasswordVisualTransformation
16 | import androidx.compose.ui.text.input.VisualTransformation
17 | import ch.benlu.composeform.Field
18 | import ch.benlu.composeform.FieldState
19 | import ch.benlu.composeform.Form
20 | import ch.benlu.composeform.components.TextFieldComponent
21 | import java.util.*
22 |
23 | class PasswordField(
24 | label: String,
25 | form: Form,
26 | modifier: Modifier? = Modifier,
27 | fieldState: FieldState,
28 | isEnabled: Boolean = true,
29 | imeAction: ImeAction = ImeAction.Next,
30 | changed: ((v: String?) -> Unit)? = null
31 | ) : Field(
32 | label = label,
33 | form = form,
34 | fieldState = fieldState,
35 | isEnabled = isEnabled,
36 | modifier = modifier,
37 | imeAction = imeAction,
38 | formatter = null,
39 | changed = changed
40 | ) {
41 |
42 | /**
43 | * Returns a composable representing the DateField / Picker for this field
44 | */
45 | @SuppressLint("NotConstructor")
46 | @Composable
47 | override fun Field() {
48 | this.updateComposableValue()
49 | if (!fieldState.isVisible()) {
50 | return
51 | }
52 |
53 | var passwordVisible by remember { mutableStateOf(false) }
54 | TextFieldComponent(
55 | modifier = modifier ?: Modifier,
56 | imeAction = imeAction ?: ImeAction.Next,
57 | isEnabled = isEnabled,
58 | keyBoardActions = KeyboardActions.Default,
59 | keyboardType = KeyboardType.Password,
60 | onChange = {
61 | this.onChange(it, form)
62 | },
63 | label = label,
64 | text = formatter?.invoke(value.value) ?: (value.value ?: ""),
65 | hasError = fieldState.hasError(),
66 | errorText = fieldState.errorText,
67 | visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
68 | trailingIcon = {
69 | val image = if (passwordVisible)
70 | Icons.Filled.Visibility
71 | else Icons.Filled.VisibilityOff
72 |
73 | // Please provide localized description for accessibility services
74 | val description = if (passwordVisible) "Hide password" else "Show password"
75 |
76 | IconButton(onClick = {passwordVisible = !passwordVisible}){
77 | Icon(imageVector = image, description)
78 | }
79 | }
80 | )
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/Form.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform
2 |
3 | import android.util.Log
4 | import androidx.compose.runtime.*
5 |
6 | abstract class Form {
7 | var isValid by mutableStateOf(true)
8 |
9 | abstract fun self(): Form
10 |
11 | /**
12 | * Returns a list of all fields in the form.
13 | */
14 | private fun getFormFields(): List> {
15 | return this::class.java.declaredFields.map {
16 | Pair(it.name, it.getAnnotation(FormField::class.java))
17 | }.filter { it.second != null }
18 | }
19 |
20 | fun logRawValue() {
21 | getFormFields().forEach { pair ->
22 | val name = pair.first
23 | val f = this::class.java.getDeclaredField(name)
24 | f.isAccessible = true
25 |
26 | try {
27 | val fieldState = (f.get(this) as FieldState<*>)
28 | val value = fieldState.state.value
29 | val isVisible = fieldState.isVisible()
30 | Log.d("Form", "$name:${value} (isVisible: $isVisible)")
31 | } catch (e: Exception) {
32 | Log.e("Form", e.toString())
33 | }
34 | }
35 | }
36 |
37 | /**
38 | * Triggers validation for all fields in the form.
39 | * @param markAsChanged If true, all fields will be marked as changed.
40 | * @param ignoreInvisible If true, invisible fields will be ignored during validation.
41 | */
42 | fun validate(markAsChanged: Boolean = false, ignoreInvisible: Boolean = true) {
43 | var isValid = true
44 | val formFields = getFormFields()
45 |
46 | formFields.forEach { pair ->
47 | val name = pair.first
48 | val f = this::class.java.getDeclaredField(name)
49 | f.isAccessible = true
50 |
51 |
52 | try {
53 | val fieldState = (f.get(this) as FieldState)
54 |
55 | // if we should ignore invisible fields, skip validation
56 | if (ignoreInvisible && !fieldState.isVisible()) {
57 | return@forEach
58 | }
59 |
60 | val value = fieldState.state?.value
61 | val validators = fieldState.validators
62 |
63 | println("$name:${value}")
64 | var isFieldValid = true
65 |
66 | // first clear all error text before validation
67 | fieldState.errorText.clear()
68 |
69 | validators.forEach {
70 | if (!it.validate(value)) {
71 | isValid = false
72 | isFieldValid = false
73 | // add error text to fieldState
74 | fieldState.errorText.add(it.errorText)
75 | }
76 | }
77 | Log.d("Form", "Field Validation ($name): $isFieldValid")
78 | fieldState.isValid.value = isFieldValid
79 |
80 | // if we should ignore untouched fields, every field should be marked as changed
81 | if (markAsChanged) {
82 | fieldState.hasChanges.value = true
83 | }
84 |
85 | } catch (e: Exception) {
86 | Log.e("Form", e.toString())
87 | }
88 | }
89 |
90 | Log.d("Form", "Form Validation: $isValid")
91 |
92 | this.isValid = isValid
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/fields/DateField.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.fields
2 |
3 | import android.app.DatePickerDialog
4 | import android.widget.DatePicker
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.focus.FocusRequester
10 | import androidx.compose.ui.platform.LocalContext
11 | import androidx.compose.ui.platform.LocalFocusManager
12 | import androidx.compose.ui.text.input.ImeAction
13 | import ch.benlu.composeform.Field
14 | import ch.benlu.composeform.FieldState
15 | import ch.benlu.composeform.Form
16 | import ch.benlu.composeform.components.TextFieldComponent
17 | import java.util.*
18 |
19 | class DateField(
20 | label: String,
21 | form: Form,
22 | modifier: Modifier? = Modifier,
23 | fieldState: FieldState,
24 | isEnabled: Boolean = true,
25 | imeAction: ImeAction = ImeAction.Next,
26 | formatter: ((raw: Date?) -> String)? = null,
27 | private val themeResId: Int = 0,
28 | changed: ((v: Date?) -> Unit)? = null
29 | ) : Field(
30 | label = label,
31 | form = form,
32 | fieldState = fieldState,
33 | isEnabled = isEnabled,
34 | modifier = modifier,
35 | imeAction = imeAction,
36 | formatter = formatter,
37 | changed = changed
38 | ) {
39 |
40 | /**
41 | * Returns a composable representing the DateField / Picker for this field
42 | */
43 | @Composable
44 | override fun Field() {
45 | this.updateComposableValue()
46 | if (!fieldState.isVisible()) {
47 | return
48 | }
49 |
50 | val focusRequester = FocusRequester()
51 | val focusManager = LocalFocusManager.current
52 | val context = LocalContext.current
53 | val year: Int
54 | val month: Int
55 | val day: Int
56 |
57 | val calendar = Calendar.getInstance()
58 | calendar.time = value.value ?: Date()
59 | year = calendar.get(Calendar.YEAR)
60 | month = calendar.get(Calendar.MONTH)
61 | day = calendar.get(Calendar.DAY_OF_MONTH)
62 |
63 | val date = remember { mutableStateOf("") }
64 | val datePickerDialog = DatePickerDialog(
65 | context,
66 | themeResId,
67 | { _: DatePicker, yyyy: Int, mm: Int, dd: Int ->
68 | val c = Calendar.getInstance()
69 | c.set(yyyy, mm, dd, 0, 0);
70 | val d = c.time
71 | date.value = d.toString()
72 | value.value = d
73 | this.onChange(d, form)
74 | },
75 | year,
76 | month,
77 | day
78 | )
79 |
80 | datePickerDialog.setOnDismissListener {
81 | focusManager.clearFocus()
82 | }
83 |
84 | TextFieldComponent(
85 | modifier = modifier ?: Modifier,
86 | isEnabled = isEnabled,
87 | label = label,
88 | text = formatter?.invoke(value.value) ?: value.value.toString(),
89 | hasError = fieldState.hasError(),
90 | errorText = fieldState.errorText,
91 | isReadOnly = true,
92 | focusRequester = focusRequester,
93 | focusChanged = {
94 | if (it.isFocused) {
95 | datePickerDialog.show()
96 | }
97 | }
98 | )
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/fields/PickerField.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.fields
2 |
3 | import androidx.compose.material.Icon
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.KeyboardArrowDown
6 | import androidx.compose.material.icons.filled.KeyboardArrowUp
7 | import androidx.compose.runtime.*
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.focus.FocusRequester
10 | import androidx.compose.ui.platform.LocalFocusManager
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.text.input.ImeAction
13 | import ch.benlu.composeform.Field
14 | import ch.benlu.composeform.FieldState
15 | import ch.benlu.composeform.Form
16 | import ch.benlu.composeform.components.SingleSelectDialogComponent
17 | import ch.benlu.composeform.components.TextFieldComponent
18 |
19 | abstract class PickerValue {
20 | abstract fun searchFilter(query: String): Boolean
21 | }
22 |
23 | class PickerField(
24 | label: String,
25 | form: Form,
26 | modifier: Modifier? = Modifier,
27 | fieldState: FieldState,
28 | isEnabled: Boolean = true,
29 | imeAction: ImeAction = ImeAction.Next,
30 | formatter: ((raw: T?) -> String)? = null,
31 | private val isSearchable: Boolean = true,
32 | changed: ((v: T?) -> Unit)? = null
33 | ) : Field(
34 | label = label,
35 | form = form,
36 | fieldState = fieldState,
37 | isEnabled = isEnabled,
38 | modifier = modifier,
39 | imeAction = imeAction,
40 | formatter = formatter,
41 | changed = changed
42 | ) {
43 |
44 | @Composable
45 | override fun Field() {
46 | this.updateComposableValue()
47 | if (!fieldState.isVisible()) {
48 | return
49 | }
50 |
51 | var isDialogVisible by remember { mutableStateOf(false) }
52 | val focusRequester = FocusRequester()
53 | val focusManager = LocalFocusManager.current
54 |
55 | TextFieldComponent(
56 | modifier = modifier ?: Modifier,
57 | isEnabled = isEnabled,
58 | label = label,
59 | text = fieldState.selectedOptionText() ?: "",
60 | hasError = fieldState.hasError(),
61 | errorText = fieldState.errorText,
62 | isReadOnly = true,
63 | trailingIcon = {
64 | Icon(
65 | if (isDialogVisible) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown,
66 | null
67 | )
68 | },
69 | focusRequester = focusRequester,
70 | focusChanged = {
71 | isDialogVisible = it.isFocused
72 | }
73 | )
74 |
75 | if (isDialogVisible) {
76 | SingleSelectDialogComponent(
77 | title = label,
78 | optionsList = fieldState.options!!,
79 | optionItemFormatter = fieldState.optionItemFormatter,
80 | defaultSelected = fieldState.state.value,
81 | submitButtonText = stringResource(id = android.R.string.ok),
82 | onSubmitButtonClick = {
83 | isDialogVisible = false
84 | this.onChange(it, form)
85 | focusManager.clearFocus()
86 | },
87 | onDismissRequest = {
88 | isDialogVisible = false
89 | focusManager.clearFocus()
90 | },
91 | search = if (isSearchable) {
92 | { options, query ->
93 | options.filter { c -> c?.searchFilter(query) == true }
94 | }
95 | } else {
96 | null
97 | }
98 | )
99 | }
100 | }
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/app/src/main/java/com/edorex/mobile/composeForm/MainForm.kt:
--------------------------------------------------------------------------------
1 | package com.edorex.mobile.composeForm
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import ch.benlu.composeform.*
5 | import ch.benlu.composeform.validators.*
6 | import com.edorex.mobile.composeForm.di.ResourcesProvider
7 | import com.edorex.mobile.composeForm.models.Country
8 | import java.util.*
9 |
10 | class MainForm(resourcesProvider: ResourcesProvider): Form() {
11 | override fun self(): Form {
12 | return this
13 | }
14 |
15 | @FormField
16 | val name = FieldState(
17 | state = mutableStateOf(null),
18 | validators = mutableListOf(
19 | NotEmptyValidator(),
20 | MinLengthValidator(
21 | minLength = 3,
22 | errorText = resourcesProvider.getString(R.string.error_min_length)
23 | )
24 | )
25 | )
26 |
27 | @FormField
28 | val lastName = FieldState(
29 | state = mutableStateOf(null)
30 | )
31 |
32 | @FormField
33 | val password = FieldState(
34 | state = mutableStateOf(null),
35 | validators = mutableListOf(
36 | NotEmptyValidator(),
37 | MinLengthValidator(
38 | minLength = 8,
39 | errorText = resourcesProvider.getString(R.string.error_min_length)
40 | )
41 | )
42 | )
43 |
44 | @FormField
45 | val passwordConfirm = FieldState(
46 | state = mutableStateOf(null),
47 | isVisible = { password.state.value != null && password.state.value!!.isNotEmpty() },
48 | validators = mutableListOf(
49 | IsEqualValidator({ password.state.value })
50 | )
51 | )
52 |
53 | @FormField
54 | val email = FieldState(
55 | state = mutableStateOf(null),
56 | validators = mutableListOf(
57 | EmailValidator()
58 | )
59 | )
60 |
61 | @FormField
62 | val country = FieldState(
63 | state = mutableStateOf(null),
64 | options = mutableListOf(
65 | Country(code = "CH", name = "Switzerland"),
66 | Country(code = "DE", name = "Germany"),
67 | Country(code = "FR", name = "France"),
68 | Country(code = "US", name = "United States"),
69 | Country(code = "ES", name = "Spain"),
70 | Country(code = "BR", name = "Brazil"),
71 | Country(code = "CN", name = "China"),
72 | ),
73 | optionItemFormatter = { "${it?.name} (${it?.code})" },
74 | validators = mutableListOf(
75 | NotEmptyValidator()
76 | )
77 | )
78 |
79 | @FormField
80 | val countryNotSearchable = FieldState(
81 | state = mutableStateOf(null),
82 | options = mutableListOf(
83 | null,
84 | Country(code = "CH", name = "Switzerland"),
85 | Country(code = "DE", name = "Germany")
86 | )
87 | ) {
88 | if (it != null) {
89 | "${it.name} (${it.code})"
90 | } else {
91 | "All"
92 | }
93 | }
94 |
95 | @FormField
96 | val startDate = FieldState(
97 | state = mutableStateOf(null),
98 | validators = mutableListOf(
99 | NotEmptyValidator()
100 | )
101 | )
102 |
103 | @FormField
104 | val endDate = FieldState(
105 | state = mutableStateOf(null),
106 | validators = mutableListOf(
107 | NotEmptyValidator(),
108 | DateValidator(
109 | minDateTime = {startDate.state.value?.time ?: 0},
110 | errorText = resourcesProvider.getString(R.string.error_date_after_start_date)
111 | )
112 | )
113 | )
114 |
115 | @FormField
116 | val agreeWithTerms = FieldState(
117 | state = mutableStateOf(null),
118 | validators = mutableListOf(
119 | IsEqualValidator({ true })
120 | )
121 | )
122 | }
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/components/SingleSelectDialogComponent.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.components
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.lazy.LazyColumn
5 | import androidx.compose.foundation.lazy.items
6 | import androidx.compose.foundation.shape.RoundedCornerShape
7 | import androidx.compose.material.*
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.Search
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.unit.dp
16 | import androidx.compose.ui.window.Dialog
17 |
18 | @Composable
19 | fun SingleSelectDialogComponent(
20 | title: String,
21 | text: String? = null,
22 | optionsList: MutableList,
23 | defaultSelected: T?,
24 | submitButtonText: String,
25 | onSubmitButtonClick: (T?) -> Unit,
26 | onDismissRequest: () -> Unit,
27 | optionItemFormatter: ((T?) -> String)? = null,
28 | search: ((options: MutableList, query: String) -> List)? = null
29 | ) {
30 |
31 | val selectedOption =
32 | remember { mutableStateOf(optionsList.indexOfFirst { it == defaultSelected }) }
33 | val query = remember { mutableStateOf("") }
34 |
35 | Dialog(onDismissRequest = { onDismissRequest.invoke() }) {
36 | Surface(
37 | modifier = Modifier.width(300.dp),
38 | shape = RoundedCornerShape(10.dp)
39 | ) {
40 | Column {
41 | Column(modifier = Modifier.padding(16.dp)) {
42 | Text(text = title, style = MaterialTheme.typography.h4)
43 |
44 | Spacer(modifier = Modifier.height(16.dp))
45 |
46 | if (text != null) {
47 | Text(text = text)
48 | Spacer(modifier = Modifier.height(16.dp))
49 | }
50 |
51 | if (search != null) {
52 | TextFieldComponent(
53 | leadingIcon = { Icon(Icons.Filled.Search, null) },
54 | onChange = { query.value = it },
55 | label = stringResource(id = android.R.string.search_go),
56 | isEnabled = true,
57 | text = query.value
58 | )
59 | }
60 | }
61 |
62 | LazyColumn(
63 | modifier = if (search != null) Modifier.height(240.dp) else Modifier.wrapContentHeight()
64 | ) {
65 | item {
66 | }
67 | items(
68 | items = search?.invoke(optionsList, query.value) ?: optionsList,
69 | key = { i ->
70 | i.toString()
71 | }
72 | ) { item ->
73 | RadioButtonComponent(
74 | label = optionItemFormatter?.invoke(item) ?: item.toString(),
75 | value = item,
76 | selectedValue = optionsList.getOrNull(selectedOption.value),
77 | ) { selectedValue ->
78 | selectedOption.value =
79 | optionsList.indexOfFirst { o -> o == selectedValue }
80 | }
81 | }
82 | }
83 |
84 | Column(modifier = Modifier.padding(16.dp)) {
85 | Spacer(modifier = Modifier.height(16.dp))
86 |
87 | Row(
88 | modifier = Modifier.fillMaxWidth(),
89 | horizontalArrangement = Arrangement.End
90 | ) {
91 | TextButton(
92 | onClick = {
93 | onDismissRequest.invoke()
94 | },
95 | shape = MaterialTheme.shapes.medium
96 | ) {
97 | Text(text = stringResource(id = android.R.string.cancel))
98 | }
99 | Spacer(
100 | modifier = Modifier.width(16.dp)
101 | )
102 | Button(
103 | onClick = {
104 | if (selectedOption.value >= 0 && optionsList.size > selectedOption.value) {
105 | onSubmitButtonClick.invoke(optionsList[selectedOption.value])
106 | }
107 | onDismissRequest.invoke()
108 | },
109 | shape = MaterialTheme.shapes.medium
110 | ) {
111 | Text(text = submitButtonText)
112 | }
113 | }
114 | }
115 | }
116 | }
117 | }
118 | }
--------------------------------------------------------------------------------
/composeform/src/main/java/ch/benlu/composeform/components/TextFieldComponent.kt:
--------------------------------------------------------------------------------
1 | package ch.benlu.composeform.components
2 |
3 | import androidx.compose.foundation.interaction.MutableInteractionSource
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.text.KeyboardActions
8 | import androidx.compose.foundation.text.KeyboardOptions
9 | import androidx.compose.material.*
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.KeyboardArrowDown
12 | import androidx.compose.runtime.*
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.focus.FocusRequester
15 | import androidx.compose.ui.focus.FocusState
16 | import androidx.compose.ui.focus.focusRequester
17 | import androidx.compose.ui.focus.onFocusChanged
18 | import androidx.compose.ui.text.TextStyle
19 | import androidx.compose.ui.text.input.ImeAction
20 | import androidx.compose.ui.text.input.KeyboardType
21 | import androidx.compose.ui.text.input.VisualTransformation
22 | import androidx.compose.ui.text.style.TextOverflow
23 | import androidx.compose.ui.tooling.preview.Preview
24 | import androidx.compose.ui.unit.dp
25 |
26 | @Composable
27 | fun TextFieldComponent(
28 | modifier: Modifier = Modifier,
29 | text: String,
30 | label: String,
31 | leadingIcon: @Composable() (() -> Unit)? = null,
32 | trailingIcon: @Composable() (() -> Unit)? = null,
33 | onChange: (String) -> Unit = {},
34 | imeAction: ImeAction = ImeAction.Next,
35 | keyboardType: KeyboardType = KeyboardType.Text,
36 | keyBoardActions: KeyboardActions = KeyboardActions(),
37 | isEnabled: Boolean = true,
38 | hasError: Boolean = false,
39 | errorText: MutableList = mutableListOf(),
40 | interactionSource: MutableInteractionSource? = null,
41 | isReadOnly: Boolean = false,
42 | focusChanged: ((focus: FocusState) -> Unit)? = null,
43 | focusRequester: FocusRequester = FocusRequester(),
44 | visualTransformation: VisualTransformation = VisualTransformation.None
45 | ) {
46 | Column(modifier = modifier) {
47 | OutlinedTextField(
48 | modifier = Modifier
49 | .fillMaxWidth()
50 | .focusRequester(focusRequester)
51 | .onFocusChanged {
52 | focusChanged?.invoke(it)
53 | },
54 | value = text,
55 | onValueChange = {
56 | onChange(it)
57 | },
58 | leadingIcon = leadingIcon,
59 | trailingIcon = trailingIcon,
60 | keyboardOptions = KeyboardOptions(imeAction = imeAction, keyboardType = keyboardType),
61 | keyboardActions = keyBoardActions,
62 | enabled = isEnabled,
63 | colors = TextFieldDefaults.outlinedTextFieldColors(),
64 | isError = hasError,
65 | label = {
66 | Text(
67 | maxLines = 1,
68 | overflow = TextOverflow.Ellipsis,
69 | text = label
70 | )
71 | },
72 | readOnly = isReadOnly,
73 | interactionSource = interactionSource ?: remember { MutableInteractionSource() },
74 | visualTransformation = visualTransformation,
75 | placeholder = null
76 | )
77 | if (hasError) {
78 | Text(
79 | text = errorText.joinToString("\n"),
80 | modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
81 | style = TextStyle.Default.copy(color = MaterialTheme.colors.error)
82 | )
83 | }
84 | }
85 | }
86 |
87 | @Preview
88 | @Composable
89 | fun FormTextFieldPreview() {
90 | Surface {
91 | Column {
92 | TextFieldComponent(
93 | modifier = Modifier.padding(bottom = 8.dp),
94 | text = "My Value",
95 | label = "My Label",
96 | onChange = {},
97 | keyBoardActions = KeyboardActions.Default,
98 | isEnabled = true,
99 | )
100 | TextFieldComponent(
101 | modifier = Modifier.padding(bottom = 8.dp),
102 | text = "",
103 | label = "My Label",
104 | onChange = {},
105 | keyBoardActions = KeyboardActions.Default,
106 | isEnabled = true,
107 | hasError = true,
108 | errorText = mutableListOf("Should not be empty.")
109 | )
110 | TextFieldComponent(
111 | modifier = Modifier.padding(bottom = 8.dp),
112 | text = "",
113 | label = "My Label which is very very very long and should be ellipsized",
114 | onChange = {},
115 | keyBoardActions = KeyboardActions.Default,
116 | isEnabled = true,
117 | hasError = true,
118 | errorText = mutableListOf("Should not be empty.")
119 | )
120 | TextFieldComponent(
121 | modifier = Modifier.padding(bottom = 8.dp),
122 | text = "My Picker",
123 | label = "My Label",
124 | onChange = {},
125 | keyBoardActions = KeyboardActions.Default,
126 | isEnabled = false,
127 | isReadOnly = true,
128 | trailingIcon = { Icon(Icons.Filled.KeyboardArrowDown, null) }
129 | )
130 | TextFieldComponent(
131 | modifier = Modifier.padding(bottom = 8.dp),
132 | text = "My Picker in Confirm",
133 | label = "My Label",
134 | onChange = {},
135 | keyBoardActions = KeyboardActions.Default,
136 | isEnabled = false,
137 | isReadOnly = true,
138 | trailingIcon = { Icon(Icons.Filled.KeyboardArrowDown, null) }
139 | )
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/java/com/edorex/mobile/composeForm/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.edorex.mobile.composeForm
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Bundle
5 | import android.util.Log
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.compose.foundation.layout.*
9 | import androidx.compose.foundation.rememberScrollState
10 | import androidx.compose.foundation.verticalScroll
11 | import androidx.compose.material.*
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.text.input.KeyboardType
15 | import androidx.compose.ui.tooling.preview.Preview
16 | import androidx.compose.ui.unit.dp
17 | import androidx.hilt.navigation.compose.hiltViewModel
18 | import ch.benlu.composeform.fields.*
19 | import ch.benlu.composeform.formatters.dateLong
20 | import ch.benlu.composeform.formatters.dateShort
21 | import com.edorex.mobile.composeForm.models.Country
22 | import com.edorex.mobile.composeForm.ui.theme.ComposeFormTheme
23 | import dagger.hilt.android.AndroidEntryPoint
24 |
25 | @AndroidEntryPoint
26 | class MainActivity : ComponentActivity() {
27 | override fun onCreate(savedInstanceState: Bundle?) {
28 | super.onCreate(savedInstanceState)
29 | setContent {
30 | ComposeFormTheme {
31 | // A surface container using the 'background' color from the theme
32 | Surface(
33 | modifier = Modifier.fillMaxSize(),
34 | color = MaterialTheme.colors.background
35 | ) {
36 | FormPage()
37 | }
38 | }
39 | }
40 | }
41 | }
42 |
43 | @SuppressLint("UnusedMaterialScaffoldPaddingParameter")
44 | @Composable
45 | fun FormPage() {
46 | val viewModel = hiltViewModel()
47 |
48 | Scaffold(
49 | content = {
50 | Column(modifier = Modifier.padding(16.dp)) {
51 | Row(modifier = Modifier
52 | .weight(1f)
53 | .verticalScroll(rememberScrollState())) {
54 |
55 | Column {
56 | TextField(
57 | modifier = Modifier.padding(bottom = 8.dp),
58 | label = "Name",
59 | form = viewModel.form,
60 | fieldState = viewModel.form.name,
61 | changed = {
62 | // log the name to show tat changed is called
63 | Log.d("Form", "Name changed: $it")
64 | // clear countries (for no reason - just to show that options list is now mutable)
65 | viewModel.form.country.options = mutableListOf()
66 | }
67 | ).Field()
68 |
69 | TextField(
70 | modifier = Modifier.padding(bottom = 8.dp),
71 | label = "Last Name",
72 | form = viewModel.form,
73 | fieldState = viewModel.form.lastName,
74 | isEnabled = false,
75 | ).Field()
76 |
77 | TextField(
78 | modifier = Modifier.padding(bottom = 8.dp),
79 | label = "E-Mail",
80 | form = viewModel.form,
81 | fieldState = viewModel.form.email,
82 | keyboardType = KeyboardType.Email
83 | ).Field()
84 |
85 | PasswordField(
86 | modifier = Modifier.padding(bottom = 8.dp),
87 | label = "Password",
88 | form = viewModel.form,
89 | fieldState = viewModel.form.password
90 | ).Field()
91 |
92 | PasswordField(
93 | modifier = Modifier.padding(bottom = 8.dp),
94 | label = "Password Confirm",
95 | form = viewModel.form,
96 | fieldState = viewModel.form.passwordConfirm
97 | ).Field()
98 |
99 | PickerField(
100 | modifier = Modifier.padding(bottom = 8.dp),
101 | label = "Country",
102 | form = viewModel.form,
103 | fieldState = viewModel.form.country
104 | ).Field()
105 |
106 | PickerField(
107 | modifier = Modifier.padding(bottom = 8.dp),
108 | label = "Country Not searchable",
109 | form = viewModel.form,
110 | fieldState = viewModel.form.countryNotSearchable,
111 | isSearchable = false
112 | ).Field()
113 |
114 | DateField(
115 | modifier = Modifier.padding(bottom = 8.dp),
116 | label = "Start Date",
117 | form = viewModel.form,
118 | fieldState = viewModel.form.startDate,
119 | formatter = ::dateShort
120 | ).Field()
121 |
122 | DateField(
123 | modifier = Modifier.padding(bottom = 8.dp),
124 | label = "End Date",
125 | form = viewModel.form,
126 | fieldState = viewModel.form.endDate,
127 | themeResId = R.style.customDatePickerStyle,
128 | formatter = ::dateLong
129 | ).Field()
130 |
131 | CheckboxField(
132 | modifier = Modifier.padding(bottom = 8.dp),
133 | fieldState = viewModel.form.agreeWithTerms,
134 | label = "I agree to Terms & Conditions",
135 | form = viewModel.form
136 | ).Field()
137 | }
138 | }
139 |
140 | ButtonRow(nextClicked = {
141 | viewModel.validate()
142 | })
143 |
144 | }
145 | }
146 | )
147 | }
148 |
149 | @Composable
150 | fun ButtonRow(nextClicked: () -> Unit) {
151 | Row {
152 | Button(
153 | enabled = false,
154 | modifier = Modifier.weight(1f),
155 | onClick = {
156 | // nothing
157 | }
158 | ) {
159 | Text("Back")
160 | }
161 | Spacer(modifier = Modifier.width(16.dp))
162 | Button(
163 | modifier = Modifier.weight(1f),
164 | onClick = {
165 | nextClicked()
166 | }
167 | ) {
168 | Text("Validate")
169 | }
170 | }
171 | }
172 |
173 | @Preview
174 | @Composable
175 | fun FormPagePreview() {
176 | Surface(
177 | modifier = Modifier.fillMaxSize(),
178 | color = MaterialTheme.colors.background
179 | ) {
180 | FormPage()
181 | }
182 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android Compose Form Library
2 | 
3 |
4 | This library provides an easy-to-use and customizable solution for building forms in Android Jetpack Compose. It includes form fields such as text input, pickers, checkbox, and more, with built-in validators to ensure accurate user input. Data binding is also supported, making it easy to work with form data in your code.
5 |
6 | The library uses reflection, to provide more flexibility in your form design. Whether you're building a complex registration form or a simple feedback form, this library has you covered.
7 |
8 | 
9 |
10 | ## Getting Started
11 | To start using the library in your Android Compose project, follow these steps:
12 | 1. Add the JitPack repository to your settings.gradle file
13 | ```kotlin
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | // ...
18 | maven { url "https://jitpack.io" }
19 | }
20 | }
21 | ```
22 | > Note: In older Android projects, the repositories are defined in the root build.gradle file.
23 |
24 | 2. Add the dependency in your build.gradle file.
25 | ```kotlin
26 | implementation 'com.github.benjamin-luescher:compose-form:0.2.8'
27 | ```
28 |
29 | ## Easy example
30 | In a first example we create a simple form with two text fields. The form will look like this:
31 |
32 | 
33 |
34 | 1. Create a your form class with your form field annotations (`@FormField`)
35 | ```kotlin
36 | class MainForm(resourcesProvider: ResourcesProvider): Form() {
37 | override fun self(): Form {
38 | return this
39 | }
40 |
41 | @FormField
42 | val name = FieldState(
43 | state = mutableStateOf(null),
44 | validators = mutableListOf(NotEmptyValidator())
45 | )
46 |
47 | @FormField
48 | val lastName = FieldState(
49 | state = mutableStateOf(null)
50 | )
51 | }
52 | ```
53 | 2. Create a ViewModel for your form.
54 | ```kotlin
55 | @HiltViewModel
56 | class MainViewModel @Inject constructor(
57 | resourcesProvider: ResourcesProvider
58 | ): ViewModel() {
59 | var form = MainForm(resourcesProvider)
60 |
61 | fun validate() {
62 | form.validate(true)
63 | Log.d("MainViewModel", "Validate (form is valid: ${form.isValid})")
64 | }
65 | }
66 | ```
67 | 3. Add the fields in your composable UI.
68 | ```kotlin
69 | Column {
70 | TextField(
71 | label = "Name",
72 | form = viewModel.form,
73 | fieldState = viewModel.form.name,
74 | ).Field()
75 |
76 | TextField(
77 | label = "Last Name",
78 | form = viewModel.form,
79 | fieldState = viewModel.form.lastName
80 | ).Field()
81 | }
82 | ```
83 |
84 | ## Extended Form example
85 | We now try to make a more complex form with different validators, date fields, password fields and
86 | searchable pickers. This is how the form will look like:
87 |
88 | 
89 |
90 | 1. Create a form class with form fields. Define form fields by the `@FormField` annotation.
91 | ```kotlin
92 | // in this example we have a separate data class `Country` for a country picker.
93 | data class Country(
94 | val code: String,
95 | val name: String
96 | ): PickerValue() {
97 | override fun searchFilter(query: String): Boolean {
98 | return this.name.startsWith(query)
99 | }
100 | }
101 |
102 | class MainForm(resourcesProvider: ResourcesProvider): Form() {
103 | override fun self(): Form {
104 | return this
105 | }
106 |
107 | @FormField
108 | val name = FieldState(
109 | state = mutableStateOf(null),
110 | validators = mutableListOf(
111 | NotEmptyValidator(),
112 | MinLengthValidator(
113 | minLength = 3,
114 | errorText = resourcesProvider.getString(R.string.error_min_length)
115 | )
116 | )
117 | )
118 |
119 | @FormField
120 | val lastName = FieldState(
121 | state = mutableStateOf(null)
122 | )
123 |
124 | @FormField
125 | val password = FieldState(
126 | state = mutableStateOf(null),
127 | validators = mutableListOf(
128 | NotEmptyValidator(),
129 | MinLengthValidator(
130 | minLength = 8,
131 | errorText = resourcesProvider.getString(R.string.error_min_length)
132 | )
133 | )
134 | )
135 |
136 | @FormField
137 | val passwordConfirm = FieldState(
138 | state = mutableStateOf(null),
139 | validators = mutableListOf(
140 | IsEqualValidator({ password.state.value })
141 | )
142 | )
143 |
144 | @FormField
145 | val email = FieldState(
146 | state = mutableStateOf(null),
147 | validators = mutableListOf(
148 | EmailValidator()
149 | )
150 | )
151 |
152 | @FormField
153 | val country = FieldState(
154 | state = mutableStateOf(null),
155 | options = mutableListOf(
156 | Country(code = "CH", name = "Switzerland"),
157 | Country(code = "DE", name = "Germany"),
158 | Country(code = "FR", name = "France"),
159 | Country(code = "US", name = "United States"),
160 | Country(code = "ES", name = "Spain"),
161 | Country(code = "BR", name = "Brazil"),
162 | Country(code = "CN", name = "China"),
163 | ),
164 | optionItemFormatter = { "${it?.name}" },
165 | validators = mutableListOf(
166 | NotEmptyValidator()
167 | )
168 | )
169 |
170 | @FormField
171 | val startDate = FieldState(
172 | state = mutableStateOf(null),
173 | validators = mutableListOf(
174 | NotEmptyValidator()
175 | )
176 | )
177 |
178 | @FormField
179 | val endDate = FieldState(
180 | state = mutableStateOf(null),
181 | validators = mutableListOf(
182 | NotEmptyValidator(),
183 | DateValidator(
184 | minDateTime = {startDate.state.value?.time ?: 0},
185 | errorText = resourcesProvider.getString(R.string.error_date_after_start_date)
186 | )
187 | )
188 | )
189 |
190 | @FormField
191 | val agreeWithTerms = FieldState(
192 | state = mutableStateOf(null),
193 | validators = mutableListOf(
194 | IsEqualValidator({ true })
195 | )
196 | )
197 | }
198 | ```
199 | 2. Create a ViewModel for your form.
200 | ```kotlin
201 | @HiltViewModel
202 | class MainViewModel @Inject constructor(
203 | resourcesProvider: ResourcesProvider
204 | ): ViewModel() {
205 | var form = MainForm(resourcesProvider)
206 |
207 | fun validate() {
208 | form.validate(true)
209 | Log.d("MainViewModel", "Validate (form is valid: ${form.isValid})")
210 | }
211 | }
212 | ```
213 | 3. Add the fields in your composable UI.
214 | ```kotlin
215 | Column {
216 | TextField(
217 | modifier = Modifier.padding(bottom = 8.dp),
218 | label = "Name",
219 | form = viewModel.form,
220 | fieldState = viewModel.form.name,
221 | ).Field()
222 |
223 | TextField(
224 | modifier = Modifier.padding(bottom = 8.dp),
225 | label = "E-Mail",
226 | form = viewModel.form,
227 | fieldState = viewModel.form.email,
228 | keyboardType = KeyboardType.Email
229 | ).Field()
230 |
231 | PasswordField(
232 | modifier = Modifier.padding(bottom = 8.dp),
233 | label = "Password",
234 | form = viewModel.form,
235 | fieldState = viewModel.form.password
236 | ).Field()
237 |
238 | PasswordField(
239 | modifier = Modifier.padding(bottom = 8.dp),
240 | label = "Password Confirm",
241 | form = viewModel.form,
242 | fieldState = viewModel.form.passwordConfirm
243 | ).Field()
244 |
245 | TextField(
246 | modifier = Modifier.padding(bottom = 8.dp),
247 | label = "Last Name",
248 | form = viewModel.form,
249 | fieldState = viewModel.form.lastName,
250 | isEnabled = false,
251 | ).Field()
252 |
253 | PickerField(
254 | modifier = Modifier.padding(bottom = 8.dp),
255 | label = "Country",
256 | form = viewModel.form,
257 | fieldState = viewModel.form.country
258 | ).Field()
259 |
260 | DateField(
261 | modifier = Modifier.padding(bottom = 8.dp),
262 | label = "Start Date",
263 | form = viewModel.form,
264 | fieldState = viewModel.form.startDate,
265 | formatter = ::dateShort
266 | ).Field()
267 |
268 | DateField(
269 | modifier = Modifier.padding(bottom = 8.dp),
270 | label = "End Date",
271 | form = viewModel.form,
272 | fieldState = viewModel.form.endDate,
273 | formatter = ::dateLong
274 | ).Field()
275 |
276 | CheckboxField(
277 | modifier = Modifier.padding(bottom = 8.dp),
278 | fieldState = viewModel.form.agreeWithTerms,
279 | label = "I agree to Terms & Conditions",
280 | form = viewModel.form
281 | ).Field()
282 | }
283 | ```
284 |
285 | ## Features
286 | * A variety of form fields to choose from, including text input, pickers, checkbox, and more
287 | * Built-in validators to ensure accurate user input
288 | * Data binding for easy management of form data in your code
289 |
290 | ## License
291 | This library is licensed under the MIT License.
292 |
--------------------------------------------------------------------------------