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

Hilt and ViewModel initialization with Navigation Component arguments #692

Open
langsmith opened this issue Sep 5, 2023 · 6 comments
Open

Comments

@langsmith
Copy link
Contributor

langsmith commented Sep 5, 2023

tldr; How do I initialize a Mavericks State with Navigation Component arguments when the fragment's ViewModel is using hiltMavericksViewModelFactory ?

I'm trying to figure out how to blend the use of this Mavericks library (version 3.0.6), Hilt, and Navigation Component arguments. I'm not using Compose. I'm using Views. My app is single activity, multiple fragments.

I've also seen the README and various example files within https://github.com/airbnb/mavericks/tree/main/sample-hilt.


My Gradle setup:

val navigationVersion = "2.7.2"
implementation("androidx.navigation:navigation-fragment-ktx:$navigationVersion")
implementation("androidx.navigation:navigation-ui-ktx:$navigationVersion")

val mavericksVersion = "3.0.6"
implementation ("com.airbnb.android:mavericks:$mavericksVersion")
implementation ("com.airbnb.android:mavericks-testing:$mavericksVersion")
implementation ("com.airbnb.android:mavericks-navigation:$mavericksVersion")
    
val hiltVersion = "2.48"
implementation ("com.google.dagger:hilt-android:$hiltVersion")
kapt ("com.google.dagger:hilt-compiler:$hiltVersion")
implementation("androidx.hilt:hilt-navigation-fragment:1.0.0")    

HomeState in the HomeViewModel is:

data class HomeState(
    val userFullName: String = "",
    val paymentList: Async<List<Payment>> = Uninitialized,
    val coarseLocationPermissionGranted: Boolean = false,
    val deviceLastLocationCoordinates: Pair<Double, Double> = Pair(0.0, 0.0),
    val count: Int = 0
) : MavericksState
Screenshot 2023-09-05 at 10 02 38 AM

I had Mavericks working just fine before I decided to try to install Hilt.

companion object : MavericksViewModelFactory<HomeFragmentViewModel, HomeState> {

        override fun initialState(viewModelContext: ViewModelContext): HomeState {
            val homeFragment: HomeFragment = (viewModelContext as FragmentViewModelContext).fragment()
            val userFullName = homeFragment.arguments?.getString(LoginFragment.SESSION_FULL_NAME_KEY).orEmpty()
            return HomeState(userFullName = userFullName)
        }

        override fun create(viewModelContext: ViewModelContext, state: HomeState): HomeFragmentViewModel {
            return HomeFragmentViewModel(state)
        }
    }
Screenshot 2023-09-04 at 10 36 16 AM

My setup is the following after I dove into setting up Hilt. It works but I'm not initializing the ViewModel with any Navigation Component arguments:

@AssistedFactory interface Factory : AssistedViewModelFactory<HomeFragmentViewModel, HomeState> {
        override fun create(state: HomeState): HomeFragmentViewModel
    }

    /* 
    companion object : MavericksViewModelFactory<HomeFragmentViewModel, HomeState> {
        override fun initialState(viewModelContext: ViewModelContext): HomeState {
            val homeFragment: HomeFragment = (viewModelContext as FragmentViewModelContext).fragment()
            val userFullName = homeFragment.arguments?.getString(LoginFragment.SESSION_FULL_NAME_KEY).orEmpty()
            return HomeState(userFullName = userFullName)
        }
    }
    */

    companion object : MavericksViewModelFactory<HomeFragmentViewModel, HomeState> by hiltMavericksViewModelFactory()
Screenshot 2023-09-04 at 10 35 37 AM

As expected, I get the crash below if I experiment by commenting out the hiltMavericksViewModelFactory() companion object setup

Screenshot 2023-09-04 at 10 48 57 AM Screenshot 2023-09-05 at 10 08 26 AM

Ignoring the crash for a moment, my overall question again is how I would initialize the HomeState with that initial userFullName value from the Navigation Component argument bundle?

I see mention of viewModelContext in the HiltMavericksViewModelFactory class, so do I need to somehow adjust that HiltMavericksViewModelFactory class?

I haven't tried it, but would a hack be to pass the arguments (bundle) from the fragment to the ViewModel in the fragment's onCreate() or onCreateView()?

@langsmith
Copy link
Contributor Author

langsmith commented Sep 5, 2023

I'm looking into https://airbnb.io/mavericks/#/fragment-arguments?id=using-fragment-args-in-the-initial-value-for-mavericksstate

The following might work:

LoginFragmentDirections.actionLoginFragmentToHome(userFullName = it.userFullName)
data class HomeState(
    val userFullName: String,
    val paymentList: Async<List<Payment>> = Uninitialized,
    val coarseLocationPermissionGranted: Boolean = false,
    val deviceLastLocationCoordinates: Pair<Double, Double> = Pair(0.0, 0.0),
    val count: Int = 0
) : MavericksState {

constructor(homeFragmentArgs: HomeFragmentArgs) : this(userFullName = homeFragmentArgs.userFullName.orEmpty())

@langsmith
Copy link
Contributor Author

I'm looking into https://airbnb.io/mavericks/#/fragment-arguments?id=using-fragment-args-in-the-initial-value-for-mavericksstate

The following might work:

LoginFragmentDirections.actionLoginFragmentToHome(userFullName = it.userFullName)
data class HomeState(
    val userFullName: String,
    val paymentList: Async<List<Payment>> = Uninitialized,
    val coarseLocationPermissionGranted: Boolean = false,
    val deviceLastLocationCoordinates: Pair<Double, Double> = Pair(0.0, 0.0),
    val count: Int = 0
) : MavericksState {

constructor(homeFragmentArgs: HomeFragmentArgs) : this(userFullName = homeFragmentArgs.userFullName.orEmpty())

Nah, that alternate constructor setup didn't work. Not sure whether or not it's because I'm not doing the steps 👇🏽

Screenshot 2023-09-05 at 2 03 42 PM

@langsmith
Copy link
Contributor Author

Well things ARE working when I follow the steps above

  1. Screenshot 2023-09-05 at 2 16 40 PM
  2. Screenshot 2023-09-05 at 2 16 43 PM
  3. Screenshot 2023-09-05 at 2 16 52 PM

However, this process seems opposite to using the arguments declared in the Navigation Component nav graph XML file. I still have to create custom Parcelizable argument data classes 😕 I'm unable to use generated _____Directions and do something like

findNavController().navigate(LoginFragmentDirections.actionLoginFragmentToHome(userFullName = it.userFullName))

I know that custom Parcelable objects can be passed through as arguments

Screenshot 2023-09-05 at 2 58 31 PM

However, this doesn't really help me from avoiding having to create these custom args classes in the first place 😕

@langsmith
Copy link
Contributor Author

Any thoughts on this @gpeal @elihart ?

@langsmith
Copy link
Contributor Author

Doesn't really address this ticket's topic/question, but another option I just thought of was to save the full name to repo-level persistence in LoginViewModel and just fetch it in HomeViewModel instead of passing it at Navigation Component arguments from one fragment/VM to another.

@langsmith
Copy link
Contributor Author

Closing the loop here. This is what I've ultimately ended up with and I'm posting it here mainly so that others might benefit from it.

I created a setAsMavericksArgs extension function, which has made things pretty easy and minimal when going from one fragment/VM to another.

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
class ParcelableClassWithArgs(
    var stringValue: String? = "",
    var booleanValue: Boolean? = false,
): Parcelable

Parcelable extension function 👇🏽

fun Parcelable.setAsMavericksArgs(fragment: Fragment): Bundle {
    this.asMavericksArgs().let {
        fragment.arguments = it
        return it
    }
}

Fragment extension function 👇🏽

fun Fragment.navigate(actionInt: Int, optionalArgs: Bundle? = null) =
    findNavController().navigate(actionInt, optionalArgs)

Navigating in the Fragment 👇🏽

navigate(
    NavGraphDirections.actionToNextFragment().actionId,
    ParcelableClassWithArgs(
		...various values
	).setAsMavericksArgs(this)
)

Setup in the ViewModel of the Fragment that has been navigated to

data class ReceivingViewModelState(
    val args: ParcelableClassWithArgs? = null,
) : MavericksState {
    constructor(parcelableClassWithArgs: ParcelableClassWithArgs) : this(args = parcelableClassWithArgs)
}

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

No branches or pull requests

1 participant