Skip to content

Conversation

@kikin81
Copy link
Contributor

@kikin81 kikin81 commented Aug 1, 2025

Description

Add comprehensive Activity Result API integration to enable type-safe result navigation
patterns alongside existing Nibel navigation functionality.

Core Runtime Changes

  • Add ResultEntry interface for screens that return results
  • Extend NavigationController with navigateForResult() methods
  • Implement NibelResultContract for Activity Result API integration
  • Add ResultCallback system with registry for handling results
  • Support setResultAndNavigateBack() and cancelResultAndNavigateBack()

Annotation System

  • Add @UiResultEntry annotation for internal result-returning screens
  • Add @UiExternalResultEntry annotation for external result-returning screens
  • Both annotations include resultType parameter for type safety

Compiler Enhancements

  • Create UiResultEntryProcessor for processing result annotations
  • Implement ComposableResultGenerator for dual-interface entry generation
  • Add ResultEntryGeneratingVisitor for result-specific code generation
  • Generate entry classes implementing both ComposableEntry and ResultEntry
  • Add ComposableResultEntryTemplate for result-aware code generation

Sample Implementation

  • Create PhotoPickerScreen demonstrating result navigation
  • Add PhotoPickerResult as sample return type with photo metadata
  • Update FirstScreen to use navigateForResult with callback handling
  • Integrate photo result display in UI with selection timestamp

Developer Experience

  • Type-safe result navigation with compile-time verification
  • Backward-compatible with existing navigation patterns
  • Automatic result callback cleanup and lifecycle management
  • Support for both Fragment and Compose-based implementations

Breaking Changes

None - all additions are backward compatible with existing navigation code.

Testing

  • Full project builds successfully
  • Generated code implements correct interfaces
  • Sample app demonstrates end-to-end result navigation flow

Closes integration of Android Activity Result API patterns into Nibel navigation
library, enabling modern result-based navigation with full type safety.

🤖 Generated with Claude Code

Co-Authored-By: Claude

Changes

  • feat: implement Activity Result API support for result-based navigation
Screen_recording_20250801_124459.mp4

🚀 PR created with fotingo

* with [NavigationController.navigateForResult] and when setting the result
* with [NavigationController.setResultAndNavigateBack].
*/
val resultType: KClass<out Parcelable>
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure if possible, but instead of adding a new UiResultEntry is it possible to introduce the result in the existing UiEntry and default it to NoResult (similar to what NoArgs is doing for args)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated! UiEntry and UiExternalEntry now support optional params. Created a new NoResult.
Currently NoResult is in the same package as UiEntry -> nibel-annotations/src/main/kotlin/nibel/annotations
but i see that NoArgs is defined in the nibel-stub module. Not sure if we should move it there
https://github.com/open-turo/nibel/pull/5/files#diff-846611264d4fb3a7a7b1eed3748b216929be35c386c57522863d6534c6cc48feR10

is ComposableEntry<*> -> navigateTo(entry, composeSpec)
is FragmentEntry -> navigateTo(entry, fragmentSpec)
is ResultEntry<*> -> {
// ResultEntry can also be a ComposableEntry or FragmentEntry
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if I'm understanding this... ResultEntry cannot be FragmentEntry or ComposableEntry at the same time.

/**
 * [ResultEntry] represents a screen entry that can return a result after navigation.
 * This interface extends [Entry] to support Activity Result API integration.
 *
 * @param R The type of result that this entry returns
 */
interface ResultEntry<R : Any> : Entry {
    /**
     * The result type class for this entry.
     */
    val resultType: Class<R>
}

If I'm missing something and ResultEntry is indeed a FragmentEntry or ComposableEntry then ResultEntry wouldn't be reachable.

Otherwise, I think the nested when will always return error

* Processor for @UiExternalResultEntry and @UiResultEntry annotations.
* Handles both regular entry generation and result-specific metadata.
*/
class UiResultEntryProcessor(
Copy link
Contributor

Choose a reason for hiding this comment

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

some duplication with UIEntryProcessor

@kikin81 kikin81 force-pushed the f/result-feature branch 3 times, most recently from c4df8b3 to 54973bf Compare August 6, 2025 00:15
@kikin81 kikin81 requested a review from a team as a code owner August 6, 2025 00:15
@github-actions
Copy link

github-actions bot commented Aug 6, 2025

Release notes preview

Below is a preview of the release notes if your PR gets merged.


2.0.0 (2025-10-15)

⚠ BREAKING CHANGES

  • deps: kotlin 2.1.20

Miscellaneous

  • add detekt for code quality (d49f5cc)
  • add detekt to .pre-commit-config.yaml (91eb6a4)
  • add sec scan: add sec scan (e7b6079)
  • move detekt to a config/detekt folder (94be28c)
  • remove unused imports (95384a0)
  • rename tests module (2874b81)
  • update AGP to 8.11.1 (bbb1f9b)
  • update CODEOWNERS (e786dc8)
  • update compose-bom 2025.07.00 (4093828)
  • update gradle to 8.5 (9b8c58b)
  • gradle: update Gradle wrapper to 8.13 (8f435ce)
  • update gson 2.13.1 (e4f9da6)
  • update links to materials (fd3e053)
  • Update links to materials (38153eb)
  • update readme (5cd797b)

Continuous Integration

  • add detekt job to ci.yaml (2b97b82)
  • renovate: adds renovate bot configuration for jvm project (7173470), closes #9
  • changes to detekt job (f2fda69)
  • fix ci job failure (46c589f)
  • fix detekt job, updates detekt to latest version (eb44767)
  • remove detekt job - as its part of the lint job (271f5a0)

Documentation

  • add breaking changes documentation for v2 (3617e3e)
  • add detekt.md (f79429b)
  • updates readme with result navigation (15d33e3)

Features

  • implement Activity Result API support for result-based navigation (488d7e1)
  • deps: update androidx libs and target sdk to 35 (ecf9485)
  • deps: update autoService and autoServiceKsp (a27fbab)
  • deps: update kotlin, dagger and ksp (70e3adf)

Bug Fixes

Code Refactoring

  • result-navigation: implements missing external entry result (d02eec8)

Styles

  • add TrailingCommaOnCallSite and TrailingCommaOnDeclarationSite in rules (f86563c)
  • detekt fixes to existing files (4562704)
  • detekt fixes to existing files (11a3101)
  • enable TooManyFunctions and LongMethod rules for detekt (7a1654b)
  • fix detekt issues (ea3d3aa)
  • remove detekt-formatting-1.23.0.jar (b717dda)

@kikin81 kikin81 self-assigned this Aug 6, 2025
@kikin81 kikin81 added the enhancement New feature or request label Aug 6, 2025
@kikin81 kikin81 linked an issue Aug 6, 2025 that may be closed by this pull request
@kikin81 kikin81 requested a review from a team as a code owner August 7, 2025 16:30
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.withType

class NibelAndroidCommonPlugin : NibelConventionPlugin({
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure why this files are duplicated on bin folder. Can we remove them?

navigateForResult(destinationEntry, callback, fragmentSpec, composeSpec)
}

override fun <R : Any> setResultAndNavigateBack(result: R) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make this type save?


@Composable
private fun SideEffectHandler(sideEffects: Flow<FirstSideEffect>) {
private fun SideEffectHandler(sideEffects: Flow<FirstSideEffect>, viewModel: FirstViewModel) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we just pass the viewModel callBack instead of the whole VM?

navigateTo(entry as Entry, fragmentSpec, composeSpec)
}

override fun <R : Any> navigateForResult(
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm trying to update sample app FirstScreen -> SecondScreen -> ThirdScreen and add result to SecondScreen and ThirdScreen with input text of each screen as the result. But I'm getting this exception when using navigateForResult to ThirdScreen(destination)
image

My code, not sure if I'm missing something:

//FeatureBNavigation.kt
@Parcelize
data class ThirdArgs(val inputText: String) : Parcelable

@Parcelize
data class ThirdResult(val inputText: String) : Parcelable

data class ThirdScreenDestination(override val args: ThirdArgs) : DestinationWithArgs<ThirdArgs>

// ThirdScreen.kt
@UiExternalEntry(
    type = ImplementationType.Fragment,
    destination = ThirdScreenDestination::class,
    result = ThirdResult::class,
)
@Composable
fun ThirdScreen(viewModel: ThirdViewModel = hiltViewModel()) {
...

// SecondScreen.kt
is SecondSideEffect.NavigateToThirdScreen -> {
                val args = ThirdArgs(it.inputText)
                navigateForResult(
                    destination = ThirdScreenDestination(args),
                    callback = { result: ThirdResult? ->
                        if (result == null) return@navigateForResult
                        viewModel.onInputTextChanged(result.inputText)
                    },
                )
            }

@kikin81 kikin81 force-pushed the f/result-feature branch 3 times, most recently from 053eedf to 4a864d6 Compare August 12, 2025 16:49
Copilot AI review requested due to automatic review settings August 20, 2025 17:49

This comment was marked as outdated.

@github-actions
Copy link


Breaking changes file docs/breaking-changes/v2.md

Breaking changes in v2

Description of changes

This release includes a major update to Kotlin version 2.1.20, which introduces breaking changes that may affect existing codebases. The update also includes updates to Dagger and KSP dependencies to maintain compatibility with the new Kotlin version.

Key Changes

  • Kotlin version upgrade: Updated from previous version to Kotlin 2.1.20
  • Dagger dependency update: Updated Dagger to maintain compatibility with Kotlin 2.1.20
  • KSP dependency update: Updated KSP to maintain compatibility with Kotlin 2.1.20

Upgrade instructions

Prerequisites

  • Ensure your project is compatible with Kotlin 2.1.20
  • Review any custom KSP processors or annotation processors for compatibility
  • Check Dagger-related code for any deprecated APIs or breaking changes

Required Steps

  1. Update your project's Kotlin version to 2.1.20
  2. Update Dagger dependencies to the latest compatible version
  3. Update KSP dependencies to the latest compatible version
  4. Review and update any custom annotation processors if applicable

Code Examples

If you have custom KSP processors, ensure they implement the latest KSP APIs:

// Update your KSP processor implementations to use the latest APIs
// Check the official KSP documentation for migration guides

Testing

After upgrading:

  1. Run your full test suite to identify any breaking changes
  2. Test annotation processing and code generation
  3. Verify that all Dagger-related functionality works as expected
  4. Check that any custom KSP processors continue to function correctly

Rollback Plan

If you encounter issues, you can temporarily rollback to the previous version while investigating compatibility issues. However, it's recommended to address the breaking changes rather than staying on older versions.


1 similar comment
@github-actions
Copy link


Breaking changes file docs/breaking-changes/v2.md

Breaking changes in v2

Description of changes

This release includes a major update to Kotlin version 2.1.20, which introduces breaking changes that may affect existing codebases. The update also includes updates to Dagger and KSP dependencies to maintain compatibility with the new Kotlin version.

Key Changes

  • Kotlin version upgrade: Updated from previous version to Kotlin 2.1.20
  • Dagger dependency update: Updated Dagger to maintain compatibility with Kotlin 2.1.20
  • KSP dependency update: Updated KSP to maintain compatibility with Kotlin 2.1.20

Upgrade instructions

Prerequisites

  • Ensure your project is compatible with Kotlin 2.1.20
  • Review any custom KSP processors or annotation processors for compatibility
  • Check Dagger-related code for any deprecated APIs or breaking changes

Required Steps

  1. Update your project's Kotlin version to 2.1.20
  2. Update Dagger dependencies to the latest compatible version
  3. Update KSP dependencies to the latest compatible version
  4. Review and update any custom annotation processors if applicable

Code Examples

If you have custom KSP processors, ensure they implement the latest KSP APIs:

// Update your KSP processor implementations to use the latest APIs
// Check the official KSP documentation for migration guides

Testing

After upgrading:

  1. Run your full test suite to identify any breaking changes
  2. Test annotation processing and code generation
  3. Verify that all Dagger-related functionality works as expected
  4. Check that any custom KSP processors continue to function correctly

Rollback Plan

If you encounter issues, you can temporarily rollback to the previous version while investigating compatibility issues. However, it's recommended to address the breaking changes rather than staying on older versions.


@kikin81 kikin81 requested a review from Copilot August 20, 2025 23:16
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements comprehensive Activity Result API support for result-based navigation within the Nibel library, enabling type-safe screens that can return data to calling screens. The changes maintain backward compatibility while adding result navigation capabilities across both Fragment and Compose implementations.

Key changes:

  • Add ResultEntry interface and ResultCallback system for type-safe result handling
  • Extend annotation system with result parameter for @UiEntry and @UiExternalEntry
  • Implement compiler enhancements to generate dual-interface entries for result-based screens
  • Provide sample implementation demonstrating photo picker result navigation workflow

Reviewed Changes

Copilot reviewed 46 out of 47 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/src/test/kotlin/nibel/tests/codegen/TestArgs.kt Add documentation and TestResult data class for compilation tests
tests/src/test/kotlin/nibel/tests/codegen/ResultEntryCompileTest.kt New compilation tests for result-based navigation entries
tests/src/test/kotlin/nibel/tests/codegen/README.md New comprehensive documentation for test structure and purpose
Multiple test files Add detailed documentation for existing compilation test scenarios
tests/build.gradle.kts Update dependencies to support result navigation testing
Sample app files Implement photo picker demo and integrate result navigation into existing screens
Runtime files Add ResultEntry, ResultCallback, NibelResultContract and navigation controller extensions
Compiler files Extend code generation to support result-based entry creation with dual interfaces
Annotation files Add result parameter and NoResult marker class for result type specification
README.md Extensive documentation update with result navigation guide and migration instructions

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

* screen with no result.
*/
@Suppress("ParcelCreator")
object NoResult : Parcelable
Copy link

Copilot AI Aug 20, 2025

Choose a reason for hiding this comment

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

The NoResult object implements Parcelable but lacks the required Parcelable implementation methods. Consider adding proper Parcelable implementation or using a different approach for the marker type.

Suggested change
object NoResult : Parcelable
object NoResult : Parcelable {
override fun writeToParcel(dest: android.os.Parcel, flags: Int) {
// No state to write
}
override fun describeContents(): Int = 0
@JvmField
val CREATOR: Parcelable.Creator<NoResult> = object : Parcelable.Creator<NoResult> {
override fun createFromParcel(source: android.os.Parcel): NoResult = NoResult
override fun newArray(size: Int): Array<NoResult?> = arrayOfNulls(size)
}
}

Copilot uses AI. Check for mistakes.
* This screen simulates a photo picker that allows users to select a photo
* and return it to the calling screen.
*/
@Suppress("LongMethod")
Copy link

Copilot AI Aug 20, 2025

Choose a reason for hiding this comment

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

[nitpick] The PhotoPickerScreen function is marked with @Suppress("LongMethod"), indicating it may be too complex. Consider breaking it down into smaller, more focused composable functions for better maintainability.

Suggested change
@Suppress("LongMethod")

Copilot uses AI. Check for mistakes.
// For Nibel, we don't create separate activities but rather handle navigation
// internally. This method is required by ActivityResultContract but we'll
// handle the actual navigation in the NavigationController.
return Intent()
Copy link

Copilot AI Aug 20, 2025

Choose a reason for hiding this comment

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

The createIntent method returns an empty Intent() and includes a comment about not creating separate activities. This implementation may be confusing - consider documenting why this method is required but unused, or provide a more appropriate implementation.

Suggested change
return Intent()
* This method is required by [ActivityResultContract] but is not used in Nibel,
* as navigation is handled internally through fragments and compose navigation,
* not by launching separate activities.
*
* @throws UnsupportedOperationException Always thrown to indicate this method should not be used.
*/
override fun createIntent(context: Context, input: ResultEntry<R>): Intent {
throw UnsupportedOperationException(
"NibelResultContract does not support createIntent; navigation is handled internally."
)

Copilot uses AI. Check for mistakes.
*/
@Suppress("UNCHECKED_CAST")
fun <R : Any> consume(key: String): ResultCallback<R>? {
return callbacks.remove(key) as? ResultCallback<R>
Copy link

Copilot AI Aug 20, 2025

Choose a reason for hiding this comment

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

The consume method uses an unchecked cast with @Suppress("UNCHECKED_CAST"). Consider adding runtime type checking or using a more type-safe approach to ensure the callback type matches the expected result type.

Suggested change
return callbacks.remove(key) as? ResultCallback<R>
private data class TypedCallback<R : Any>(
val type: KClass<R>,
val callback: ResultCallback<R>
)
private val callbacks = mutableMapOf<String, TypedCallback<*>>()
/**
* Registers a result callback for a specific key.
*/
fun <R : Any> register(key: String, type: KClass<R>, callback: ResultCallback<R>) {
callbacks[key] = TypedCallback(type, callback)
}
/**
* Retrieves and removes a result callback for a specific key.
* Returns the callback only if the type matches.
*/
fun <R : Any> consume(key: String, type: KClass<R>): ResultCallback<R>? {
val typed = callbacks.remove(key) ?: return null
@Suppress("UNCHECKED_CAST")
return if (typed.type == type) {
typed.callback as ResultCallback<R>
} else {
null
}

Copilot uses AI. Check for mistakes.
Add comprehensive Activity Result API integration to enable type-safe result navigation
patterns alongside existing Nibel navigation functionality.

- Add ResultEntry<R> interface for screens that return results
- Extend NavigationController with navigateForResult() methods
- Implement NibelResultContract for Activity Result API integration
- Add ResultCallback system with registry for handling results
- Support setResultAndNavigateBack() and cancelResultAndNavigateBack()

- Add @UiResultEntry annotation for internal result-returning screens
- Add @UiExternalResultEntry annotation for external result-returning screens
- Both annotations include resultType parameter for type safety

- Create UiResultEntryProcessor for processing result annotations
- Implement ComposableResultGenerator for dual-interface entry generation
- Add ResultEntryGeneratingVisitor for result-specific code generation
- Generate entry classes implementing both ComposableEntry and ResultEntry
- Add ComposableResultEntryTemplate for result-aware code generation

- Create PhotoPickerScreen demonstrating result navigation
- Add PhotoPickerResult as sample return type with photo metadata
- Update FirstScreen to use navigateForResult with callback handling
- Integrate photo result display in UI with selection timestamp

- Type-safe result navigation with compile-time verification
- Backward-compatible with existing navigation patterns
- Automatic result callback cleanup and lifecycle management
- Support for both Fragment and Compose-based implementations

None - all additions are backward compatible with existing navigation code.

- Full project builds successfully
- Generated code implements correct interfaces
- Sample app demonstrates end-to-end result navigation flow

Closes integration of Android Activity Result API patterns into Nibel navigation
library, enabling modern result-based navigation with full type safety.

Fixes: [#8](#8)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>

# Conflicts:
#	nibel-annotations/src/main/kotlin/nibel/annotations/UiEntry.kt
#	nibel-annotations/src/main/kotlin/nibel/annotations/UiExternalEntry.kt
#	nibel-compiler/src/main/kotlin/nibel/compiler/generator/AbstractEntryGeneratingVisitor.kt
#	nibel-compiler/src/main/kotlin/nibel/compiler/generator/EntryMetadata.kt
#	sample/feature-A/src/main/kotlin/com/turo/nibel/sample/featureA/firstscreen/FirstState.kt
external entry result was not implemented previously.
@github-actions
Copy link


Breaking changes file docs/breaking-changes/v2.md

Breaking changes in v2

Description of changes

This release includes a major update to Kotlin version 2.1.20, which introduces breaking changes that may affect existing codebases. The update also includes updates to Dagger and KSP dependencies to maintain compatibility with the new Kotlin version.

Key Changes

  • Kotlin version upgrade: Updated from previous version to Kotlin 2.1.20
  • Dagger dependency update: Updated Dagger to maintain compatibility with Kotlin 2.1.20
  • KSP dependency update: Updated KSP to maintain compatibility with Kotlin 2.1.20

Upgrade instructions

Prerequisites

  • Ensure your project is compatible with Kotlin 2.1.20
  • Review any custom KSP processors or annotation processors for compatibility
  • Check Dagger-related code for any deprecated APIs or breaking changes

Required Steps

  1. Update your project's Kotlin version to 2.1.20
  2. Update Dagger dependencies to the latest compatible version
  3. Update KSP dependencies to the latest compatible version
  4. Review and update any custom annotation processors if applicable

Code Examples

If you have custom KSP processors, ensure they implement the latest KSP APIs:

// Update your KSP processor implementations to use the latest APIs
// Check the official KSP documentation for migration guides

Testing

After upgrading:

  1. Run your full test suite to identify any breaking changes
  2. Test annotation processing and code generation
  3. Verify that all Dagger-related functionality works as expected
  4. Check that any custom KSP processors continue to function correctly

Rollback Plan

If you encounter issues, you can temporarily rollback to the previous version while investigating compatibility issues. However, it's recommended to address the breaking changes rather than staying on older versions.


@kikin81 kikin81 added the documentation Improvements or additions to documentation label Oct 15, 2025
@kikin81
Copy link
Contributor Author

kikin81 commented Nov 5, 2025

Closing in favor of #38

@kikin81 kikin81 closed this Nov 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add navigate back with result for UI entries

4 participants