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?
5 Réponses :
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
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
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) } } }
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
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) } }
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 >
À 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.
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