Skip to content

feat: prepare Ikokuko 0.2.0 release - #2

Merged
eosobande merged 4 commits into
mainfrom
release/0.2.0
Aug 2, 2026
Merged

feat: prepare Ikokuko 0.2.0 release#2
eosobande merged 4 commits into
mainfrom
release/0.2.0

Conversation

@eosobande

@eosobande eosobande commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR prepares Ikokuko 0.2.0.

The release updates the public form API from lessons proved in Yetunde's core:ui:form module. It keeps application UI and product-specific validation outside the library.

The release includes breaking API cleanup because Ikokuko is still below 1.0. It also adds migration guidance, stronger behavior tests, runnable samples, publication metadata, and a release workflow for Android and iOS.

Motivation

The 0.1.0 API had several limitations:

  • Cross-field validators had no standard way to declare and observe field dependencies.
  • Validator lambdas could create unstable equality during recomposition.
  • Form state could not be saved through the Compose saveable-state system.
  • Field error state and UI error visibility were not clearly separated.
  • Initial reactive validation could disable submit UI before the first submit attempt.
  • Numeric validators used generic transforms even though text inputs provide strings.
  • Several validators and field helpers increased the API surface without adding distinct behavior.
  • The release workflow did not provide complete Android, Apple Silicon, and Intel iOS verification.

Form behavior

Field validity

field.isValid is strict. It is false whenever the field has a stored validation or external error.

field.error returns the stored error without applying UI visibility rules.

field.shouldDisplayError is true only when all these conditions are true:

  • The field is dirty.
  • Error reporting is enabled.
  • The field has a stored error.

Form validity and submission

FormState.isValid follows the form's error-reporting state:

!shouldShowErrors || errors.isEmpty()

This behavior prevents initial reactive validation from disabling UI that uses enabled = isValid.

submit() performs these operations in order:

  1. Mark every initialized field as dirty.
  2. Enable error reporting.
  3. Check form validity.
  4. Call onSubmit when valid, or onInvalid when invalid.

After an invalid submit, stored errors disable submit UI. The UI becomes valid again as field changes clear all stored errors.

Applications can pass shouldShowErrors = true when they need stored errors to affect form validity immediately.

Error ownership

Validation and external errors share one active error slot for each field name.

The latest write wins. A later validation result can replace an external error, and a later external write can replace a validation error.

Reset clears values, errors, dirty state, and error visibility. It also restarts validation for values that are equal to their prior initial values.

Cross-field validation

Validators can declare the fields that they read:

interface Validator<in T> {
    val errorMessage: String
    val dependencies: List<Field<*>>
        get() = emptyList()

    fun ValidationScope.validate(value: T): Boolean
}

ValidationScope exposes read-only field values. It does not expose submit, reset, error mutation, dirty-state mutation, or lifecycle operations.

ValidationEffect observes:

  • The validated field value.
  • The validator list.
  • Initialized dependency values.
  • The reset key.

FieldEqualsValidator declares its compared field as a dependency. A change to that field revalidates the target field.

Validator changes

Added

  • FieldEqualsValidator
  • CheckedValidator
  • SelectionRangeValidator

SelectionRangeValidator supports minimum, maximum, exact, and unbounded selection counts. A null maximum means that the upper bound is not limited.

Simplified

  • Built-in validators are data classes with structural equality.
  • Every built-in validator uses errorMessage as its first constructor argument.
  • Numeric validators accept text and parse it with toIntOrNull().
  • Pattern validators accept pattern strings and compile private Regex values.
  • Validator.validate receives a read-only ValidationScope.

Removed

  • EqualsValidator
  • NotEqualsValidator
  • Validator lambda constructors
  • EmailValidator
  • PhoneNumberValidator
  • Specialized minimum, maximum, exact, and nonempty selection validators

Applications can use MatchPatternValidator with application-owned email and phone patterns. Applications can implement custom validators when they need numeric types other than string-backed integers.

Field and state API changes

  • Field<T> now requires T : Any.
  • Field remains keyed by its name.
  • Field.isDirty is writable.
  • markAsDirty() is removed.
  • Field destructuring is removed.
  • Field.Int, Field.Long, and Field.Double are removed.
  • ValidationEffect and FormField use initialValue instead of default.
  • submit(onInvalid) uses a non-null no-op callback by default.
  • rememberSaveableFormState() is added.
  • FormState.saver(valuesSaver) supports custom field-value serialization.

The default saver supports field values accepted by the platform save registry. Applications must provide a custom saver for unsupported value types.

Field identity requirement

Each field name must be unique within one form.

Field equality uses only the field name. Two fields with the same name share one stored value even when their generic types differ. Reading that value through an incompatible field type can fail at runtime.

ValidationEffect does not reject duplicate names because it cannot reliably distinguish a second declaration from normal recomposition.

Validator equality requirement

The validator list remains a ValidationEffect key.

Equivalent built-in validator instances compare structurally because they are data classes. Recreating equivalent built-in validators during recomposition does not restart validation.

Custom validators that store lambdas can compare by lambda identity. Recreating those validators can restart validation without a field value change and can replace an external error. Applications should use structurally stable custom validators or remember lambda-backed instances.

Samples and supported targets

Runnable applications now live under samples/:

  • Gradle sample: :samples:composeApp
  • Xcode host: samples/iosApp

The published and documented targets for 0.2.0 are:

  • Android
  • iOS device
  • iOS simulator
  • Intel iOS simulator artifacts verified by the release workflow

Desktop, JavaScript, and Wasm are not part of the 0.2.0 support promise.

Publication and workflow

The release workflow follows the Gbeewa release structure.

It:

  • Requires an exact release-MAJOR.MINOR.PATCH tag.
  • Requires the tag commit to be reachable from origin/main.
  • Verifies the matching changelog section.
  • Passes uppercase VERSION_NAME to Gradle.
  • Uses root Gradle publication properties.
  • Uses Node.js 24 GitHub Action majors.
  • Verifies Android and Apple Silicon on macos-15.
  • Verifies Intel iOS on macos-15-intel before publication.
  • Publishes only after all required verification jobs succeed.

The workflow uses:

  • actions/checkout@v5
  • actions/setup-java@v5
  • gradle/actions/setup-gradle@v5

Breaking-change migration

0.1.0 usage 0.2.0 replacement
Nullable Field<T?> Use non-null Field<T> and represent optional input with a non-null application value.
Field.Int, Field.Long, or Field.Double Use Field<T>(name) when a non-text numeric value is required.
Field destructuring Access the field directly.
default = ... Use initialValue = ....
field.markAsDirty() Use field.isDirty = true.
Nullable submit(onInvalid) Omit the argument or pass a non-null callback.
field.error for rendering decisions Use field.shouldDisplayError for styling and message visibility.
Strict form validity before error reporting FormState.isValid remains true until error reporting is enabled.
Validator lambda constructor Implement Validator<T> with ValidationScope.
Numeric transform lambda Use the string-backed integer numeric validators or a custom validator.
Regex constructor argument Pass a pattern string.
EmailValidator or PhoneNumberValidator Use MatchPatternValidator with an application-owned pattern.
Specialized selection-count validators Use SelectionRangeValidator.
General equality or inequality validator Use FieldEqualsValidator for field equality or implement a custom dependency-aware validator.

See README.md and CHANGELOG.md for the complete public migration notes.

Test coverage

The release adds or updates tests for:

  • Initial validation with error reporting disabled.
  • Error-reporting-aware form validity.
  • Strict field validity.
  • Submit and invalid-submit behavior.
  • Dirty-state tracking.
  • Reset and same-value revalidation.
  • External-error replacement.
  • Equivalent inline validator recomposition.
  • Single and multiple cross-field dependencies.
  • Uninitialized dependencies.
  • Conditional validation disposal.
  • Read-only validation scope.
  • Default and custom saver round trips.
  • Built-in validator behavior and equality.

Local verification

The complete clean release gate passed:

./gradlew clean check -PVERSION_NAME=0.2.0 --stacktrace

Result:

  • 184 Gradle tasks completed successfully.
  • Android library tests passed.
  • Android sample tests passed.
  • iOS simulator library tests passed.
  • iOS simulator sample tests passed.
  • Lint passed.
  • The local Apple Silicon host skipped iosX64Test as expected.

Additional verification passed:

  • Android sample assembly.
  • iOS device and simulator framework links.
  • Xcode host build.
  • Release workflow YAML parsing.
  • Complete release-workflow Gradle task-graph resolution.
  • GitHub Action runtime inspection for Node.js 24.
  • Android APK class inspection.
  • Android emulator installation and cold launch.
  • Initial submit button enabled state.
  • Invalid-submit error display and disabled button state.
  • Independent code review with RECOMMEND_ACCEPT.

Actual Intel iosX64Test execution remains owned by the macos-15-intel release job.

Release process

This PR does not tag, push a release tag, or publish an artifact.

After this PR is approved and merged:

  1. Confirm that the release commit is on main.
  2. Create the release-0.2.0 tag on that commit.
  3. Push the tag.
  4. Let the workflow complete Android, Apple Silicon, and Intel verification.
  5. Let the workflow publish only after every required job succeeds.

Checklist

  • Public API changes implemented.
  • Breaking changes documented.
  • Migration guide updated.
  • Changelog updated for 0.2.0.
  • Android tests passed locally.
  • iOS simulator tests passed locally.
  • Android sample launched successfully.
  • Xcode host built successfully.
  • Release workflow task graph resolved.
  • Independent review accepted the candidate.
  • Pull request approved and merged.
  • Intel iOS verification passed in GitHub Actions.
  • release-0.2.0 tag created and pushed.
  • Maven Central publication completed.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@eosobande eosobande self-assigned this Aug 2, 2026

@eosobande eosobande left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the 0.2.0 changes at 4e46dae.

The review found one lifecycle edge case. Replacing FormState at the same composition position could retain the previous ValidationEffect instances when their explicit keys remained equal. This could leave the old DisposableEffect active and skip validation for the new form.

Commit 4e46dae now keys the form content subtree by FormState. Compose therefore disposes the old form effects and creates fresh validation effects for the new form. The added regression test verifies both old-form disposal and new-form validation.

Local verification passed:

  • ./gradlew :ikokuko:allTests
  • ./gradlew :ikokuko:check
  • git diff --check

No unresolved material findings remain from this review.

@eosobande
eosobande merged commit 503b8a3 into main Aug 2, 2026
2 checks passed
@eosobande
eosobande deleted the release/0.2.0 branch August 2, 2026 21:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant