Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serializer for class 'DirectionImpl' is not found in v2 #629

Closed
whitescent opened this issue May 5, 2024 · 10 comments
Closed

Serializer for class 'DirectionImpl' is not found in v2 #629

whitescent opened this issue May 5, 2024 · 10 comments
Labels
feedback needed Extra attention is needed

Comments

@whitescent
Copy link

whitescent commented May 5, 2024

crash log:

kotlinx.serialization.SerializationException: Serializer for class 'DirectionImpl' is not found.
                                                                                                    Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied

when i navigate to some screen, it will crashed

my screen is like:

data class ProfileNavArgs(val account: Account)

@Destination<RootGraph>(navArgs = ProfileNavArgs::class)
@Composable
fun Profile(
  navigator: DestinationsNavigator,
  viewModel: ProfileViewModel = hiltViewModel(),
  resultRecipient: ResultRecipient<StatusDetailDestination, StatusBackResult>
)

and my StatusBackResult is

@Parcelize
data class StatusBackResult(
  val id: String,
  val favorited: Boolean,
  val favouritesCount: Int,
  val reblogged: Boolean,
  val reblogsCount: Int,
  val repliesCount: Int,
  val bookmarked: Boolean,
  val poll: Poll?
) : Parcelable

@Serializable
@Parcelize
data class Poll(
  val id: String,
  @SerialName("expires_at") val expiresAt: String?,
  val expired: Boolean,
  val multiple: Boolean,
  @SerialName("votes_count") val votesCount: Int,
  @SerialName("voters_count") val votersCount: Int?, // nullable for compatibility with other fediverse
  val options: List<PollOption>,
  val emojis: List<Emoji> = emptyList(),
  val voted: Boolean,
  @SerialName("own_votes") val ownVotes: List<Int>?
) : Parcelable {

  @Serializable
  @Parcelize
  data class PollOption(
    val title: String,
    @SerialName("votes_count") val votesCount: Int
  ) : Parcelable
}

btw, Can Compose Destination support Kotlin Serialization's navigation parameters? Because it seems that the latest version of androidx navigation compose just supports 🤔

@raamcosta
Copy link
Owner

raamcosta commented May 5, 2024

btw, Can Compose Destination support Kotlin Serialization's navigation parameters? Because it seems that the latest version of androidx navigation compose just supports 🤔

Yes, it does. Check a list of supported arg types here:
https://composedestinations.rafaelcosta.xyz/v2/arguments/navigation-arguments

But NOT as result types for ResultBackNavigator feature.

Those can be seen here:
https://composedestinations.rafaelcosta.xyz/v2/navigation/backresult

@raamcosta
Copy link
Owner

About the crash, I don't know. I can't help just with the shared code.

If I were to guess, you're trying to do something that is not supported, but it's not in this code.

@raamcosta raamcosta added the feedback needed Extra attention is needed label May 5, 2024
@whitescent
Copy link
Author

whitescent commented May 5, 2024

but it goes well in v1, so I'm not sure what's causing it 👀

@raamcosta
Copy link
Owner

What version are you using? Are you also importing compose navigation dependency or just Compose Destinations?

(for the record, you should ONLY depend on Compose Destinations)

@raamcosta
Copy link
Owner

I know what it is, but I would like to understand how you got to this point.

Check these breaking changes:
https://github.com/raamcosta/compose-destinations/releases/tag/2.1.0-beta02

Because new versions of official jetpack compose navigation, navigate method can receive anything as its parameter, you are calling that function rather than calling Compose Destinations functions.
As explained in the release notes, you need to use a DestinationsNavigator to be able to use Compose Destinations navigate APIs. So make sure to do what it says there.

And do let me know how you ended up calling their functions.. because I specifically removed all previous functions so that you must have gotten some "red imports". The expectation is that when you found those imports, you would try to understand what's happening and figure you needed to do some changes, but I guess if you just delete the import, the app will compile.. but obviously you'd be calling official APIs passing in Direction which doesn't work!

@raamcosta
Copy link
Owner

Added a big WARNING section on README to make this more visible!

https://github.com/raamcosta/compose-destinations

@whitescent
Copy link
Author

whitescent commented May 5, 2024

@raamcosta After I looked at the warnings on the README and I replaced all navControllers with navigators, this issue was solved! 😄👍
(Sorry, I didn't read the 2.1.0-beta02 description carefully, as I was focused on the lack of NavGraphs being generated 😭)

But the overall code makes me confuse a bit:

val engine = rememberNavHostEngine()
val navController = engine.rememberNavController()
val navigator = navController.rememberDestinationsNavigator()

DestinationNavHost(
  engine = engine,
  navController = navController
)

DestinationNavHost still need a navController, But our navigational behavior doesn't need it anymore 👀💦.

BTW, I have two more questions that are not related to this issue, but I'd love to hear from you, and it would be great if you could answer them or give me some suggestions! 😍

1. Specify animations for specific screens

I have a custom default animations for DestinationNavHost

object DefaultAppTransitions : NavHostAnimatedDestinationStyle() {
  override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
     = { defaultSlideIntoContainer() }
  override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
    = { defaultSlideOutContainer() }
  override val popEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
    = {  defaultSlideIntoContainer(forward = false) }
  override val popExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
    = { defaultSlideOutContainer(forward = false) }
}

fun <T> AnimatedContentTransitionScope <T>.defaultSlideIntoContainer(
  forward: Boolean = true
): EnterTransition {
  return when (forward) {
    true -> slideIntoContainer(Start, tween(slideAnimationTween, easing = FastOutSlowInEasing))
    false -> scaleIn(initialScale = scaleSize, animationSpec = tween(300, easing = EaseOutCubic)) +
      fadeIn(animationSpec = tween(300, delayMillis = 80), initialAlpha = 0.15f)
  }
}

fun <T> AnimatedContentTransitionScope<T>.defaultSlideOutContainer(
  forward: Boolean = true
): ExitTransition = when (forward) {
  true -> scaleOut(targetScale = scaleSize, animationSpec = tween(400, easing = EaseInOutCubic)) +
    fadeOut(targetAlpha = 0.15f)
  false -> slideOutOfContainer(End, tween(slideAnimationTween))
}

DestinationsNavHost(
  engine = engine,
  navController = navController,
  defaultTransitions = DefaultAppTransitions,
  navGraph = NavGraphs.root,
  startRoute = startRoute,
  dependenciesContainerBuilder = {
    navGraph(NavGraphs.app) {
      dependency(drawerState)
      dependency(appState)
    }
    dependency(this@SharedTransitionLayout)
  }
)

The effect of this navigation is overall very similar to Twitter in that it's a slide-in/slide-out animation.

Media.-.whitescent.@whitescent@androiddev.social.mp4

But since the advent of the shared element animation, I need to use the fadeIn/fadeOut animation instead of this animation between the post and image detail screens, what is the best practice in this case? 👀👀

If I understand correctly, it seems like it's possible to write a separate object that inherits DestinationStyle.Animated() and then use it on the specified @Destination

object StatusMediaScreen: DestinationStyle.Animated() {
  ...
}

@Destination<RootGraph>(style = ...)

2. NavigationDrawer display conditions

This question doesn't have much to do with Compose Destination, but I don't have any good ideas.

val destination: DestinationSpec = navController.currentDestinationAsState().value ?:
  startRoute.startDestination

SharedTransitionLayout {
  ModalNavigationDrawer(
    drawerState = drawerState,
    drawerContent = {
      if (destination.shouldShowScaffoldElements() && activeAccount != null) {
       AppDrawer(
          drawerState = drawerState,
          activeAccount = activeAccount!!,
          accounts = accounts.toImmutableList(),
          changeAccount = {
            scope.launch { drawerState.close() }
            viewModel.changeActiveAccount(it)
          },
          navigateToLogin = {...},
          navigateToProfile = {...}
          },
          navigateToSettings = {...}
        )
      }
    },
    gesturesEnabled = destination.shouldShowScaffoldElements()
  ) {
    Scaffold(
      bottomBar = {
        if (destination.shouldShowScaffoldElements() && !appState.hideBottomBar) {
          BottomBar(
            appState = appState,
            navigator = navigator,
            destination = destination,
            scrollToTop = {
              scope.launch { appState.scrollToTop() }
            }
          )
        }
      }
    ) {
      DestinationsNavHost(...) { ... }
  }
}

val DestinationSpec.isBottomBarScreen: Boolean
  get() = this == HomeDestination  this == ExploreDestination  this == NotificationDestination ||
    this == DirectMessageDestination

fun DestinationSpec.shouldShowScaffoldElements(): Boolean {
  if (NavGraphs.login.destinations.contains(this)) return false
  else {
    BottomBarItem.entries.forEach {
      if (this == it.direction) return true
    }
  }
  return false
}
Export-1714916570055.mp4

My current method is to determine if it belongs to a Screen in the bottomBar and then decide if it will show/enable the side swipe expand gesture. This method worked fine before, but after I migrated to v2, every time I start the App or jump back to the homepage from a non-bottomBar screen, it automatically pops up the NavigationDrawer, so I wonder if there is a better solution for this scenario. I don't know if there is a better solution for this scenario 🤔

@whitescent
Copy link
Author

@raamcosta Can you check the second issue I mentioned, in the v2 version, if I add some if conditions to control the show/hide of the Drawer, then going back from some screen popBack will result in not completing the navigation behavior correctly (it'll be very strange, refer to the video like that)

@whitescent
Copy link
Author

This is the log of the normal popBack navigation to Home (navController.currentDestinationAsState().value)

From launching the App -> click on the Status detail screen -> go back to the home page

destination HomeDestination
destination HomeDestination
destination StatusDetailDestination
destination HomeDestination

but, if i add if condition (if(destination.shouldShowScaffoldElements() && activeAccount != null)) in drawer.

it will be:

destination HomeDestination
destination HomeDestination
destination StatusDetailDestination
destination HomeDestination
destination HomeDestination
Export-1714916570055.mp4

@whitescent
Copy link
Author

whitescent commented May 11, 2024

After some research, I found that if log include two "HomeDestination" when i popBack from Status detail screen, it will trigger this incorrect navigation behavitor (perhaps it performed twice navigation?)

But in reality I really didn't change much of the code, I followed the documentation and migrated most of the API names, this is my previous v1 code and it's works very well

https://github.com/whitescent/Mastify/blob/main/app/src/main/java/com/github/whitescent/mastify/ui/component/AppScaffold.kt#L104-L111

My v2 code:

@Composable
fun AppScaffold(
  startRoute: Route,
  viewModel: AppViewModel
) {
  val accounts by viewModel.accountList.collectAsStateWithLifecycle()
  val activeAccount by viewModel.activeAccount.collectAsStateWithLifecycle()

  val engine = rememberNavHostEngine()
  val navController = engine.rememberNavController()
  val navigator = navController.rememberDestinationsNavigator()

  val destination: DestinationSpec = navController.currentDestinationAsState().value ?:
    startRoute.startDestination

  val scope = rememberCoroutineScope()
  val drawerState = rememberDrawerState(DrawerValue.Closed)
  val appState = rememberAppState()

  SharedTransitionLayout {
    ModalNavigationDrawer(
      drawerState = drawerState,
      drawerContent = {
        if (destination.shouldShowScaffoldElements() && activeAccount != null) {
 ......

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feedback needed Extra attention is needed
Projects
None yet
Development

No branches or pull requests

2 participants