Переход от одного фрагмента к другому при использовании шаблона MVVM для Android

  • Я создаю приложение с использованием шаблона MVVM. Я использую Navigation Graph для управления фрагментами в своем приложении, и в соответствии с рекомендуемым подходом нам не нужно размещать логику пользовательского интерфейса внутри Activity/Fragments< /strong> но в Viewmodel .

  • Итак, мой вопрос заключается в том, как перейти от одного фрагмента к другому. Я знаю, что это можно сделать непосредственно внутри фрагмента, используя navController.navigate(R.id.action_here), но как мне обрабатывать навигацию из ViewModel при нажатии кнопки?

Мой код:

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>


person Sumit Shukla    schedule 10.03.2020    source источник
comment
не нарушит ли правила выбора вашего дизайна размещение экземпляра NavController в вашей ViewModel и инициализация его в onViewCreated(), чтобы ViewModel мог обрабатывать навигацию?   -  person Jacob Collins    schedule 10.03.2020


Ответы (2)


Навигация изнутри ViewModel будет означать, что вам нужен экземпляр представления, который противоречит концепции MVVM. Вместо этого используйте LiveData, чтобы указать вашему фрагменту, что ему нужно перейти к следующему пункту назначения. вы можете использовать следующий класс Event (из одного из Google architecture-samples), чтобы навигация запускалась только один раз.

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
}

Используйте его с этим 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)
        }
    }
}

Это ваш LiveData:

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

И, наконец, вы можете наблюдать это так:

viewModel.openTaskEvent.observe(this, EventObserver {
    //Do your navigation here
})
person Mohamed Mohsin    schedule 10.03.2020

Обновленный ответ (спасибо Мохамеду Мохсину):

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)
    }
}

Событие.кт:

    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)
        })
    }
}
person Sumit Shukla    schedule 11.03.2020
comment
Вам нужно передать идентификатор вашего следующего пункта назначения при использовании navController.navigate(). вы можете найти идентификатор в себе nav_graph.xml. пример: navController.navigate(R.id.detailFragment). - person Mohamed Mohsin; 11.03.2020
comment
Тогда откуда мне знать, какая кнопка была нажата! - person Sumit Shukla; 11.03.2020
comment
Хорошо, в таком случае измените Event<Any> на Event<Int>, так как вы передаете идентификатор ресурса. и убедитесь, что переданный идентификатор (например, R.id.action_here) является фактическим идентификатором пункта назначения в nav_graph - person Mohamed Mohsin; 11.03.2020
comment
изменить navController.navigate(viewModel.navigateScreen.value) на navController.navigate(it). - person Mohamed Mohsin; 11.03.2020