1
votes

Routage dans MVVM android (meilleure pratique)

J'utilise le modèle MVVM. Je me demandais comment les autres programmeurs effectuaient le routage entre les écrans.

Cela pourrait être fait comme ceci:

class MyViewModel : ViewModel() {
    val routeState = MutableLiveData<String>()

    init {
        //more fun 
        //...
        //...
        routeState.value = "Home"
    }
}

class MyActivity : Activity() {
    private lateinit var viewModel: MyViewModel
    
    onCreate() {
        //viewModel init 
        
        viewModel.routeState.observe(viewLifecycleOwner, Observer {
            when(it) {
                "Home" -> {
                    toHome()
                    //finish()
                }
            }
        })
    }
}

Je comprends que cette approche est mauvaise. Alors j'aimerais vous demander comment vous faites?


1 commentaires

parcourez récursivement ce fil de commentaires, les articles liés, les autres fils de commentaires liés et les articles qui y sont liés old.reddit.com/r/android_devs/comments/hby9ru/... beaucoup de choses à apprendre


5 Réponses :


1
votes

Le routage n'est pas la responsabilité du modèle de vue, il est de la responsabilité de Intent dans Android, et généralement les développeurs créent une classe de routeur qui est le wrapper sur l'intention de faire la navigation entre les écrans,

Vous pouvez stocker une logique dans ViewModel qui décide si l'écran doit naviguer vers différents emplacements ou non.

exemple: splashScreenViewModel peut avoir la logique de l'indicateur isAuthenticated qui, lorsque les vraies routes écran vers la maison, vont à l'écran de connexion.

Donc, selon votre cause, c'est un saut inutile du modèle de vue à l'activité, puis une migration vers un écran différent, plus son sujet aux erreurs parce que chaque fois que routeState.value change, la navigation vers la maison se déclenchera. pas de flux idéal


2 commentaires

Le routage n'est pas la responsabilité du modèle de vue, citation nécessaire. AFAIK le jetpack ViewModel représente la portée qui appartient soit à l'écran, soit à un flux partagé, et s'il appartient à un flux partagé, il peut certainement savoir comment aller vers un autre flux. Il peut donc avoir la responsabilité du routage.


Salut @EpicPandaForce, vous évoquez un bon concept, pour répondre à cela, j'aimerais revenir sur le concept de ViewModel, il y a deux stratégies utilisées lors de la création d'un modèle de vue. La première consiste à avoir VM ayant une logique de présentation de vue à l'intérieur, ce que je considère comme faux comme il rend la VM couplée à la vue, l'autre fait que ViewModel expose des données que n'importe quelle vue peut consommer, ce qui est en fait la bonne façon, même si vous créez un ViewModel partagé qui contient des données qui peuvent être partagées entre les vues mais rien sur le flux ou actuellement visible screen.so view model ne connaît pas l'écran ou la navigation



1
votes

Depuis Jetpack, je préfère vraiment faire la navigation via le composant de navigation:

https : //developer.android.com/guide/navigation/navigation-getting-started

Cependant, si je clique par exemple sur le bouton de connexion, attendez la réponse de connexion et en cas de succès, je veux accéder à écran principal, j'aurais quelque chose comme ceci (ViewModel + Coroutines utilisé dans l'exemple):

class MyActivity : Activity() {
    private lateinit var viewModel: LoginViewModel
    
    onCreate() {
        //viewModel init 
        
        viewModel.liveData.observe(viewLifecycleOwner, Observer {
            when(it) {
                LoginPayload.StartLoginAction -> //show progress bar, hide login button
                LoginPayload.LoginError -> //hide progress bar, show error dialog, show login button
                LoginPayload.LoginSuccess -> {
                    hideProgressBar()
                    findNavController.navigate(R.id.action_login_to_home)
                }
            }
        })
    }
}
sealed class LoginPayload {
    object StartLoginAction: LoginPayload()
    object LoginSuccess: LoginPayload()
    object LoginError: LoginPayload()
}
class LoginViewModel(
    private val repository: Repository
): ViewModel() {

    val liveData = MutableLiveData<LoginPayload>()

    fun login(username: String, password: String) = viewModelScope.launch {
        liveData.postValue(LoginPayload.StartLoginAction)
        try {
            val response = repository.login(username, password)
            if(response is Success) {
                liveData.postValue(LoginPayload.LoginSuccess)
            } else {
                liveData.postValue(LoginPayload.LoginError)
            }
        } catch(e: Exception) {
            liveData.postValue(LoginPayload.LoginError)
        }
    }
}


1 commentaires

Cela ne fonctionne correctement que si l ' action de login à home est popToInclusive = "true" popUpTo = "@ id / login" < / code>, sinon la navigation en arrière déclenchera immédiatement la navigation en avant



2
votes

Il y a plusieurs façons de faire cela.

Une façon est d'utiliser le même Observer ou MutableLiveData (que vous faites cela)

l'autre façon est d'utiliser l'interface:

BaseViewModel:

class MyActivity : Activity(),MyInterFace {
   private lateinit var viewModel: MyViewModel

  override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)

     //  init viewModel
     //    .
     //    .
     //    .
     //  then set navigator 
     viewmodel.setNavigator(this)
    
  }

override fun test(){
   // do somthing....
}

viewModel

interface MyInterFace{
   fun test()
  
}

MyInterFace:

class MyViewModel : BaseViewModel<MyInterFace>() {
 val routeState = MutableLiveData<String>()

 init {
    //  Wherever you need, you can call your functions  :
     getNavigator().test()

 }
}

Activité:

abstract class BaseViewModel<N> : ViewModel() {

private lateinit var mNavigator: WeakReference<N>

fun getNavigator(): N {
    return mNavigator.get()!!
}

fun setNavigator(navigator: N) {
    this.mNavigator = WeakReference(navigator)
}
} 


0 commentaires

2
votes

Si vous souhaitez que votre routage soit explicite, vous devez suivre quelques étapes:

1.) ne jamais avoir 2 activités sur la pile de tâches en même temps, préférez avoir 1 activité pour l'application, et gérer le routage en interne au sein de cette activité.

2.) vous devez tenir compte du moment où l'application passe en arrière-plan, est supprimée par Android et restaurée. Les fragments prêts à l'emploi sont recréés en fonction de leur état "ajouté" actuel, mais votre private val currentRoute: MutableLiveData serait perdu lors de la mort du processus à moins qu'il ne soit initialisé à partir de savedStateHandle.getLiveData ("route") .

3.) vous devez considérer que les écrans ont des arguments, qui peuvent parfois être plus complexes qu'une simple chaîne, à moins que vous ne commenciez à sérialiser ces "routes" vers Objets JSON par exemple, ou au lieu de String, vous utilisez une classe Parcelable.

4.) vous devez considérer qu'il est généralement invalide pour commencer à naviguer après onStop , donc vous voulez soit ignorer les commandes (comme le fait Jetpack Navigation), soit les mettre en file d'attente après onResume.

5.) vous devez considérer que la navigation peut être asynchrone (pas immédiate), bien que lorsque vous utilisez Fragments, vous ne le faites généralement pas t besoin de s'inquiéter à ce sujet. Jetpack Navigation, par exemple, ne se soucie pas vraiment de cela (sauf dans DynamicNavHostFragment).

6.) vous devez considérer que si la navigation est asynchrone, vous pouvez obtenir des actions de navigation pendant qu'une navigation est en cours. Vous voudrez peut-être vous protéger contre cela ou les mettre en file d'attente. Ou peut-être les mettre en file d'attente lors de la progression, mais les ignorer après le retour (pour éliminer certains états non valides).

Si vous prenez en compte ces 6 éléments, vous vous retrouvez avec la bibliothèque que j'ai écrite: href = "https://github.com/Zhuinden/simple-stack" rel = "nofollow noreferrer"> https://github.com/Zhuinden/simple-stack

Maintenant votre la navigation est aussi simple que

class MyViewModel(private val navigationDispatcher: NavigationDispatcher) : ViewModel() {
    fun toOtherScreen() {
        navigationDispatcher.emit { navController ->
            navController.navigate(HomeDirections.toOtherScreen())
        }
    }
}

class MyActivity : Activity() {
    private val navigationDispatcher by viewModels<NavigationDispatcher>()

    private val viewModel by viewModels {
        object: ViewModelProvider.Factory {
            override fun <T: ViewModel?> create(clazz: Class<T>): T = MyViewModel(navigationDispatcher)
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.my_activity)            

        navigationDispatcher.navigationCommands.observeEvent(this) { command ->
            command.invoke(Navigation.findNavController(this, R.id.nav_host))
        }
    }
}

typealias NavigationCommand = (NavController) -> Unit

class NavigationDispatcher: ViewModel() {
    private val navigationEmitter: MutableLiveData<Event<NavigationCommand>> = MutableLiveData()
    val navigationCommands: LiveData<Event<NavigationCommand>> = navigationEmitter

    fun emit(navigationCommand: NavigationCommand) {
        navigationEmitter.value = Event(navigationCommand)
    }
}

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

    var hasBeenHandled = false
        private set

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

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

inline fun <T> LiveData<Event<T>>.observeEvent(
    lifecycleOwner: LifecycleOwner,
    crossinline observer: (T) -> Unit
): Observer<Event<T>> = EventObserver<T> { t -> observer(t) }.also {
    this.observe(lifecycleOwner, it)
}

Et c'est à peu près tout, sauf qu'il est possible que vous souhaitiez utiliser Fragment, consultez le readme pour savoir comment utiliser le DefaultFragmentStateChanger au lieu des éléments intégrés.


D'accord, supposons donc que vous n'avez pas acheté dans ma bibliothèque pour une raison quelconque. De nos jours, les gens utilisent généralement Jetpack Navigation.

Dans ce cas, vous voudriez un ViewModel à portée d'activité qui est passé dans votre ViewModel où le ViewModel à portée d'activité contient un LiveData > (en supposant que vous n'ayez pas acheté mon autre lib EventEmitter et utilisez le wrapper d'événement à la place), qui est observé par l'activité afin de gérer la navigation déclenchée à partir d'un ViewModel, mais l'état de navigation est toujours géré par le NavController de Jetpack Navigation.

class MyViewModel(private val backstack: Backstack) : CustomViewModel() {
    fun toOtherScreen() {
        backstack.goTo(OtherScreen())
    }
}

class MyActivity: AppCompatActivity(), SimpleStateChanger.NavigationHandler {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.my_activity)

        // ...

        Navigator.configure()
                 .setStateChanger(SimpleStateChanger(this))
                 .install(this, container, History.of(HomeScreen())
    }

     override fun onNavigationEvent(stateChange: StateChange) {
         val screen = stateChange.topNewKey<Any>()
         when {
             screen is HomeScreen -> {
                ...
             }
             screen is OtherScreen -> {
                ...
             }
         }
     }
}

@Parcelize object HomeScreen: Parcelable // i prefer data classes for a stable `toString()`

@Parcelize object OtherScreen: Parcelable // i prefer data classes for a stable `toString()`

p >


0 commentaires

0
votes

À juste titre. Le routage n'est pas la responsabilité du modèle de vue. C'est le rôle du cadre. En fait, ma façon de faire est d'avoir la navigation en tant que service qui est implémenté par le framework (android). C'est le ViewModel qui peut déléguer cela au service de navigateur et c'est ainsi que la navigation est effectuée. Toute autre approche aimerait entendre.


0 commentaires