• Tworzę aplikację przy użyciu wzorca MVVM Używam Navigation Graph do zarządzania fragmentami w mojej aplikacji i zgodnie z zalecanym podejściem nie musimy umieszczać logiki UI w Activity / Fragments ale w Viewmodel .

  • Moje pytanie brzmi więc, jak przejść z jednego fragmentu do drugiego. Wiem, że można to zrobić bezpośrednio wewnątrz fragmentu za pomocą navController.navigate(R.id.action_here), ale jak poradziłbym sobie z nawigacją z ViewModel po naciśnięciu przycisku ?.

Mój kod:

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>
4
Sumit Shukla 10 marzec 2020, 20:11

2 odpowiedzi

Najlepsza odpowiedź

Nawigacja z wnętrza ViewModel oznaczałaby, że potrzebujesz wystąpienia widoku, który jest sprzeczny z koncepcją MVVM. Zamiast tego użyj LiveData, aby wskazać swojemu fragmentowi, że musi nawigować do następnego miejsca docelowego. możesz użyć następującej klasy Event (z jednej z przykłady-architektury), aby upewnić się, że nawigacja zostanie uruchomiona tylko raz.

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
}

Użyj go z tym 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)
        }
    }
}

To jest Twoje LiveData:

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

I wreszcie możesz to obserwować w następujący sposób:

viewModel.openTaskEvent.observe(this, EventObserver {
    //Do your navigation here
})
3
Mohamed Mohsin 10 marzec 2020, 18:43

Zaktualizowana odpowiedź (podziękowania dla Mohameda Mohsina):

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)
        })
    }
}
1
Sumit Shukla 11 marzec 2020, 15:28