如何在 mvvm 模式中登录成功时从 viewmodel 启动 activity

How to start activity from viewmodel on login success in mvvm pattern

嗨,我有一个 activity LoginActivity.ktLoginViewModel。我在LoginViewModellogin方法中调用登录API。成功了,我要开始回家了activity。在 MVVM 方法中正确的做法是什么?

LoginViewModel.kt

class LoginViewModel : BaseViewModel<LoginNavigator>(), AnkoLogger {

    val emailField = ObservableField<String>()

    private val email: String
        get() = emailField.get()

    val passwordField = ObservableField<String>()

    private val password: String
        get() = passwordField.get()

    val progressVisibility: ObservableInt = ObservableInt(View.GONE)

    @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
    fun login(view: View) {
      // here I am calling API and on success
    }

    /**
     * Validate email and password. It checks email and password is empty or not
     * and validate email address is correct or not
     * @param email email address for login
     * @param password password for login
     * @return true if email and password pass all conditions else false
     */
    private fun isEmailAndPasswordValid(email: String, password: String): Boolean {

        if (email.isEmpty()) return false

        if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) return false

        if (password.isEmpty()) return false

        return true
    }

}

LoginActivity.kt

class LoginActivity : BaseActivity(), LoginNavigator {

    @Inject
    lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        performDependencyInjection()
        super.onCreate(savedInstanceState)
        val activityLoginBinding: ActivityLoginBinding = DataBindingUtil.setContentView<ActivityLoginBinding>(this, R.layout.activity_login)
        activityLoginBinding.loginViewModel = loginViewModel
        loginViewModel.mNavigator = this
    }

说一个简单的场景,使用你的登录思路,用户登录失败,应用需要做一个简单的Toast或SnackBar来显示相关信息,例如"Your username and password is incorrect"。您需要的代码是

吐司(要求ui红色Context

Toast.makeText(context, text, duration).show();

小吃店(要求ui红色View

Snackbar.make(findViewById(R.id.myCoordinatorLayout),
                                R.string.email_archived, Snackbar.LENGTH_SHORT);

如果你想在你的 ViewModel 中使用它(我对 Kotlin 不熟悉)

  @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
    void function login(final View view) {
      // here I am calling API and on success
      repo.login(result -> {
           if(result.statusCode == 401)
           Toast.makeText(view.getContext(), "Login failed...", duration).show();
      });
    }

您将以相反的方式找到 activity 的引用,这会使代码更复杂且难以维护,因为每次您都需要获取 的引用 activity 或上下文在视图模型中做与视图或 activity 相关的事情,而不是 Activity

google sample可以看出,doSearch()函数在输入完成后被调用。并且在获取搜索结果之后,绑定会将最新的结果返回给这个观察者,现在是 activity 工作来更新适配器中的结果。

private void initSearchInputListener() {
        binding.get().input.setOnEditorActionListener((v, actionId, event) -> {
            if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                doSearch(v);
                return true;
            }
            return false;
        });
        binding.get().input.setOnKeyListener((v, keyCode, event) -> {
            if ((event.getAction() == KeyEvent.ACTION_DOWN)
                    && (keyCode == KeyEvent.KEYCODE_ENTER)) {
                doSearch(v);
                return true;
            }
            return false;
        });
    }

    private void doSearch(View v) {
        String query = binding.get().input.getText().toString();
        // Dismiss keyboard
        dismissKeyboard(v.getWindowToken());
        binding.get().setQuery(query);
        searchViewModel.setQuery(query);
    }

private void initRecyclerView() {

        binding.get().repoList.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                LinearLayoutManager layoutManager = (LinearLayoutManager)
                        recyclerView.getLayoutManager();
                int lastPosition = layoutManager
                        .findLastVisibleItemPosition();
                if (lastPosition == adapter.get().getItemCount() - 1) {
                    searchViewModel.loadNextPage();
                }
            }
        });
        searchViewModel.getResults().observe(this, result -> {
            binding.get().setSearchResource(result);
            binding.get().setResultCount((result == null || result.data == null)
                    ? 0 : result.data.size());
            adapter.get().replace(result == null ? null : result.data);
            binding.get().executePendingBindings();
        });

        searchViewModel.getLoadMoreStatus().observe(this, loadingMore -> {
            if (loadingMore == null) {
                binding.get().setLoadingMore(false);
            } else {
                binding.get().setLoadingMore(loadingMore.isRunning());
                String error = loadingMore.getErrorMessageIfNotHandled();
                if (error != null) {
                    Snackbar.make(binding.get().loadMoreBar, error, Snackbar.LENGTH_LONG).show();
                }
            }
            binding.get().executePendingBindings();
        });
    }

另外,根据@Emanuel S的回答,你会看到他的说法

WeakReference to a NavigationController which holds the Context of the Activity. This is a common used pattern for handling context-bound stuff inside a ViewModel.

I highly decline this for several reasons. First: that usually means that you have to keep a reference to your NavigationController which fixes the context leak, but doesnt solve the architecture at all.

The best way (in my oppinion) is using LiveData which is lifecycle aware and can do all the wanted stuff.

如果您在 viewmodel 中实现 ui 操作,您可能会考虑另一个问题,如果您在视图或上下文中得到 NullPointerException 或对其进行一些增强,class 您将先找? ViewModel 还是 Activity ?由于第一个您持有 UI 操作,第二个您持有数据绑定。两者都可能在故障排除中出现。