Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Navigate from one fragment to another when using MVVM pattern for Android

  • I am creating an app using MVVM pattern.I am using Navigation Graph to manage fragments in my app and as per the recommended approach we don't have to put UI logic inside Activity/Fragments but in Viewmodel .

  • So my question is how to navigate from one fragment to another. I know this can be done directly inside fragment using navController.navigate(R.id.action_here) but how would I handle navigation from the ViewModel on button press?.

My code:

IntroViewModel.kt

class IntroViewModel : ViewModel() {

    fun onBtn1Pressed(view: View) {
        Log.d(IntroViewModel::class.java.simpleName, ": onBtn1Pressed")
    }

    fun onBtn2Pressed(view: View) {
        Log.d(IntroViewModel::class.java.simpleName, ": onBtn2Pressed ")
    }
}

IntroFragment.kt:

class IntroFragment : Fragment() {

    private lateinit var viewModel: IntroViewModel
    private lateinit var navController: NavController
    lateinit var introBinding: IntroFragmentBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        introBinding = DataBindingUtil.inflate(inflater, R.layout.intro_fragment, container, false)
        viewModel = ViewModelProviders.of(this).get(IntroViewModel::class.java)
        introBinding.introModel = viewModel
        return introBinding.root;
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        navController = Navigation.findNavController(view)

    }
}

intro_fragment.xml:

<data>
    <variable
        name="introModel"
        type="example.com.viewmodel.IntroViewModel" />
</data>

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:padding="@dimen/padding_16dp"
    tools:context=".fragments.IntroFragment">

    <TextView
        android:id="@+id/txt_"
        style="@style/TextAppearance.MaterialComponents.Headline5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="Choose one " />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/btn_1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/txt_"
        android:onClick="@{introModel::onBtn1Pressed}"
        android:layout_marginTop="@dimen/margin_8dp"
        android:text="Btn1" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/btn_2"
        style="@style/Widget.MaterialComponents.Button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="@{introModel::onBtn2Pressed}"
        android:layout_below="@id/btn_1"
        android:layout_alignStart="@id/btn_1"
        android:layout_alignEnd="@id/btn_1"
        android:layout_marginTop="@dimen/margin_8dp"
        android:text="Btn2" />

</RelativeLayout>

like image 605
Sumit Shukla Avatar asked Mar 03 '23 17:03

Sumit Shukla


2 Answers

Navigating from inside the ViewModel would mean you need an instance of the view which goes against the concept of MVVM. Instead, use a LiveData to indicate to your fragment that it needs to navigate to the next destination. you can use the following Event class (from one of Google's architecture-samples) to make sure the navigation is only fired once.

open class Event<out T>(private val content: T) {

    @Suppress("MemberVisibilityCanBePrivate")
    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

Use it with this Observer:

/**
 * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has
 * already been handled.
 *
 * [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled.
 */
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
    override fun onChanged(event: Event<T>?) {
        event?.getContentIfNotHandled()?.let {
            onEventUnhandledContent(it)
        }
    }
}

This is your LiveData:

private val _openTaskEvent = MutableLiveData<Event<String>>()
val openTaskEvent: LiveData<Event<String>> = _openTaskEvent

And finally you can observer it as so:

viewModel.openTaskEvent.observe(this, EventObserver {
    //Do your navigation here
})
like image 182
Mohamed Mohsin Avatar answered Apr 07 '23 07:04

Mohamed Mohsin


Updated answer (Thanks to Mohamed Mohsin ):

IntroViewModel.kt:

class IntroViewModel : ViewModel() {

  private val _navigateScreen = MutableLiveData<Event<Any>>()
  val navigateScreen: LiveData<Event<Any>> = _navigateScreen

    fun onBtn1Pressed(view: View) {
       _navigateScreen.value = Event(R.id.action_here)
    }

    fun onBtn2Pressed(view: View) {
        _navigateScreen.value = Event(R.id.action_here)
    }
}

Event.kt:

    open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */

    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
    override fun onChanged(event: Event<T>?) {
        event?.getContentIfNotHandled()?.let {
            onEventUnhandledContent(it)
        }
    }
}

IntroFragment.kt:

class IntroFragment : Fragment() {

    private lateinit var viewModel: IntroViewModel
    private lateinit var navController: NavController
    private lateinit var introBinding: IntroFragmentBinding

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        introBinding = DataBindingUtil.inflate(inflater, R.layout.intro_fragment, container, false)
        viewModel = ViewModelProviders.of(this).get(IntroViewModel::class.java)
        introBinding.introModel = viewModel
        return introBinding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        navController = Navigation.findNavController(view)
        viewModel.navigateScreen.observe(activity!!, EventObserver {
            navController.navigate(it)
        })
    }
}
like image 34
Sumit Shukla Avatar answered Apr 07 '23 07:04

Sumit Shukla