【问题标题】:Proper implementation of MVVM in Android在 Android 中正确实现 MVVM
【发布时间】:2023-03-23 16:18:01
【问题描述】:

我一直在努力寻找在 Android 中实现 MVVM 的正确方法。

整个想法对我来说仍然模糊,模式是有一个单独的层来完成逻辑(ViewModel)。

这段代码只为一组片段所​​在的背景的 alpha 设置动画。

public class StartActivity extends AppCompatActivity implements EntryFragment.EntryFragementListener {

    private static final float MINIMUM_ALPHA = 0.4f;
    private static final float MAXIMUM_ALPHA = 0.7f;

    @State
    float mCurrentAlpha = MINIMUM_ALPHA;

    @State
    String mCurrentTag = EntryFragment.TAG;

    private ActivityStartBinding mBinding;

    private StartViewModel mStartViewModel = new StartViewModel();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_start);
        mBinding.setStartViewModel(mStartViewModel);
        mBinding.bgBlackLayer.setAlpha(mCurrentAlpha);

        if (getSupportFragmentManager().findFragmentByTag(mCurrentTag) == null) {
            switch (mCurrentTag) {
                case EntryFragment.TAG:
                    setEntryFragment();
                    break;
                case FreeTrialFragment.TAG:
                    setFreeTrialFragment();
                    break;
            }
        }
    }

    private void setEntryFragment() {
        mCurrentAlpha = MINIMUM_ALPHA;
        mCurrentTag = EntryFragment.TAG;
        FragmentManager fm = getSupportFragmentManager();
        Fragment fragment = new EntryFragment();
        fm.beginTransaction().
                add(R.id.fragment_content, fragment, EntryFragment.TAG).commit();
    }

    private void setFreeTrialFragment() {
        mCurrentTag = FreeTrialFragment.TAG;
        Fragment fragment = new FreeTrialFragment();
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        ft.setCustomAnimations(R.anim.anim_enter_right, R.anim.anim_exit_left, R.anim.anim_enter_left, R.anim.anim_exit_right);
        ft.replace(R.id.fragment_content, fragment, FreeTrialFragment.TAG);
        ft.addToBackStack(FreeTrialFragment.TAG);
        ft.commit();
        StartViewModel.setAnimation(mBinding.bgBlackLayer,true, MAXIMUM_ALPHA);
    }

    private void setForgotPasswordFragmet() {
    }

    private void setLoginFragment() {
    }

    @Override
    public void onBackPressed() {
        super.onBackPressed();
        StartViewModel.setAnimation(mBinding.bgBlackLayer,true, MINIMUM_ALPHA);
        mCurrentAlpha = MINIMUM_ALPHA;
    }

    @Override
    public void onEntryLoginButton() {
        setLoginFragment();
    }

    @Override
    public void onEntryFreeTrialButton() {
        setFreeTrialFragment();
    }
}

-ViewModel 只做动画的逻辑 -Fragments 有一个监听器将事件传递给活动 -绑定有助于定义视图

public class StartViewModel {

    public ObservableBoolean hasToAnimate = new ObservableBoolean(false);
    public float alpha;

    @BindingAdapter(value={"animation", "alpha"}, requireAll=false)
    public static void setAnimation(View view, boolean hasToAnimate, float alpha) {
        if (hasToAnimate) {
            view.animate().alpha(alpha);
        }
    }    
}

问题是,所有的逻辑都应该驻留在视图模型中吗,包括片段事务、方向变化的管理等等?有没有更好的方法来实现 MVVM?

【问题讨论】:

  • 我在 mvvm 上看到一个不错的博客,看看它是否可以帮助你 - labs.ribot.co.uk/…

标签: android mvvm


【解决方案1】:

如果您对“干净”的 Firebase 身份验证感兴趣,可以查看以下文章:

存储库对视图一无所知,而视图对其数据源一无所知。

【讨论】:

    【解决方案2】:

    这里有一个很好的示例,因此请查看它,值得一读,因为其中包含超过 1 种包含 MVP 架构的方法。 MVP Google Samples

    【讨论】:

      【解决方案3】:

      当涉及到一般的设计模式时。您希望业务逻辑远离活动和片段。

      如果您问我,MVVM 和 MVP 都是非常好的选择。但是既然你想实现 MVVM。然后我将尝试解释一下我是如何实现它的。

      活动

      public class LoginActivity extends BaseActivity {
      
          private LoginActivityViewModel viewModel;
      
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
      
              ActivityLoginBinding binding = DataBindingUtil.setContentView(this,R.layout.activity_login);
              NavigationHelper navigationHelper = new NavigationHelper(this);
              ToastHelper toastHelper = new ToastHelper(this);
              ProgressDialogHelper progressDialogHelper = new ProgressDialogHelper(this);
      
      
              viewModel = new LoginActivityViewModel(navigationHelper,toastHelper,progressDialogHelper);
              binding.setViewModel(viewModel);
          }
      
          @Override
          protected void onPause() {
              if (viewModel != null) {
                  viewModel.onPause();
              }
      
              super.onPause();
          }
      
          @Override
          protected void onDestroy() {
              if (viewModel != null) {
                  viewModel.onDestroy();
              }
      
              super.onDestroy();
          }
      }
      

      这是一个相当简单的活动。没什么特别的。我只是从实例化我的 viewModel 需要的东西开始。因为我试图让所有特定于 android 的东西远离它。一切都可以简化测试的编写

      然后我只是将视图模型绑定到视图。

      观点

      <layout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools">
      
          <data>
              <variable
                  name="viewModel"
                  type="com.community.toucan.authentication.login.LoginActivityViewModel" />
          </data>
      
      
          <RelativeLayout
              android:id="@+id/activity_login_main_frame"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:background="@drawable/background"
              tools:context="com.community.toucan.authentication.login.LoginActivity">
      
              <ImageView
                  android:id="@+id/activity_login_logo"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_centerHorizontal="true"
                  android:layout_marginTop="40dp"
                  android:src="@drawable/logo_small" />
      
              <android.support.v7.widget.AppCompatEditText
                  android:id="@+id/activity_login_email_input"
                  android:layout_width="match_parent"
                  android:layout_height="50dp"
                  android:layout_below="@+id/activity_login_logo"
                  android:layout_marginLeft="20dp"
                  android:layout_marginRight="20dp"
                  android:layout_marginTop="60dp"
                  android:drawableLeft="@drawable/ic_email_white"
                  android:drawablePadding="10dp"
                  android:hint="@string/email_address"
                  android:inputType="textEmailAddress"
                  android:maxLines="1"
                  android:text="@={viewModel.username}" />
      
              <android.support.v7.widget.AppCompatEditText
                  android:id="@+id/activity_login_password_input"
                  android:layout_width="match_parent"
                  android:layout_height="50dp"
                  android:layout_below="@+id/activity_login_email_input"
                  android:layout_marginLeft="20dp"
                  android:layout_marginRight="20dp"
                  android:drawableLeft="@drawable/ic_lock_white"
                  android:drawablePadding="10dp"
                  android:hint="@string/password"
                  android:inputType="textPassword"
                  android:maxLines="1"
                  android:text="@={viewModel.password}" />
      
              <Button
                  android:id="@+id/activity_login_main_button"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_below="@id/activity_login_password_input"
                  android:layout_centerHorizontal="true"
                  android:layout_marginTop="10dp"
                  android:background="@drawable/rounded_button"
                  android:onClick="@{() -> viewModel.tryToLogin()}"
                  android:paddingBottom="10dp"
                  android:paddingLeft="60dp"
                  android:paddingRight="60dp"
                  android:paddingTop="10dp"
                  android:text="@string/login"
                  android:textColor="@color/color_white" />
      
              <TextView
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_below="@+id/activity_login_main_button"
                  android:layout_centerHorizontal="true"
                  android:layout_marginTop="20dp"
                  android:onClick="@{() -> viewModel.navigateToRegister()}"
                  android:text="@string/signup_new_user"
                  android:textSize="16dp" />
      
      
              <LinearLayout
                  android:id="@+id/activity_login_social_buttons"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_alignParentBottom="true"
                  android:layout_centerInParent="true"
                  android:layout_marginBottom="50dp"
                  android:orientation="horizontal">
      
      
                  <ImageView
                      android:layout_width="wrap_content"
                      android:layout_height="wrap_content"
                      android:src="@drawable/facebook" />
      
                  <ImageView
                      android:layout_width="wrap_content"
                      android:layout_height="wrap_content"
                      android:src="@drawable/twitter" />
      
                  <ImageView
                      android:layout_width="wrap_content"
                      android:layout_height="wrap_content"
                      android:src="@drawable/google" />
              </LinearLayout>
      
              <TextView
                  android:id="@+id/activity_login_social_text"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_above="@+id/activity_login_social_buttons"
                  android:layout_centerHorizontal="true"
                  android:layout_marginBottom="20dp"
                  android:text="@string/social_account"
                  android:textSize="16dp" />
          </RelativeLayout>
      </layout>
      

      从视图一侧相当笔直。我绑定了 viewModel 需要对其具有的逻辑进行操作的所有特定值。

      https://developer.android.com/topic/libraries/data-binding/index.html 检查以下链接以获取有关 android 数据绑定库如何工作的更多知识

      视图模型

      public class LoginActivityViewModel extends BaseViewModel implements FirebaseAuth.AuthStateListener {
      
          private final NavigationHelper navigationHelper;
          private final ProgressDialogHelper progressDialogHelper;
          private final ToastHelper toastHelper;
          private final FirebaseAuth firebaseAuth;
      
          private String username;
          private String password;
      
      
          public LoginActivityViewModel(NavigationHelper navigationHelper,
                                        ToastHelper toastHelper,
                                        ProgressDialogHelper progressDialogHelper) {
      
              this.navigationHelper = navigationHelper;
              this.toastHelper = toastHelper;
              this.progressDialogHelper = progressDialogHelper;
      
              firebaseAuth = FirebaseAuth.getInstance();
              firebaseAuth.addAuthStateListener(this);
          }
      
          @Override
          public void onPause() {
              super.onPause();
          }
      
          @Override
          public void onResume() {
              super.onResume();
          }
      
          @Override
          public void onDestroy() {
              firebaseAuth.removeAuthStateListener(this);
              super.onDestroy();
          }
      
          @Override
          public void onStop() {
              progressDialogHelper.onStop();
              super.onStop();
          }
      
          public void navigateToRegister() {
              navigationHelper.goToRegisterPage();
          }
      
          public void tryToLogin() {
              progressDialogHelper.show();
              if (validInput()) {
                  firebaseAuth.signInWithEmailAndPassword(username, password)
                          .addOnCompleteListener(new OnCompleteListener<AuthResult>() {
                              @Override
                              public void onComplete(@NonNull Task<AuthResult> task) {
                                  if (!task.isSuccessful()) {
                                      String message = task.getException().getMessage();
                                      toastHelper.showLongToast(message);
                                  }
                                  progressDialogHelper.hide();
                              }
                          });
              }
          }
      
          private boolean validInput() {
              return true;
          }
      
          @Override
          public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {
              if (firebaseAuth.getCurrentUser() != null) {
                  navigationHelper.goToMainPage();
              }
          }
      
         @Bindable
          public String getUsername() {
              return username;
          }
      
          public void setUsername(String username) {
              this.username = username;
              notifyPropertyChanged(BR.username);
          }
      
          @Bindable
          public String getPassword() {
              return password;
          }
      
          public void setPassword(String password) {
              this.password = password;
              notifyPropertyChanged(BR.password);
          }
      }
      

      所有的乐趣都在这里发生。我使用辅助类来展示和操作 android 系统。否则我尽量保持逻辑尽可能干净。一切都做好了,所以我更容易创建和测试逻辑。

      记下

      我将usernamepassword 与视图绑定。因此,对 EditText 所做的每一次更改都会自动添加到该字段中。以这种方式。我不需要添加任何特定的监听器

      希望这个小展示可以帮助您了解如何将 MVVM 实施到您自己的项目中

      【讨论】:

      • 谢谢,其实我对数据绑定库有很好的了解,我的问题是视图和模型之间的距离有多远。在我的代码示例中,问题在于动画和片段事务,因为它们可能在视图或视图模型中。对我来说,将它们移到 View 模型感觉很混乱,我想对此进行一些思考。
      【解决方案4】:

      至于我 - MVVM、MVP 和其他非常酷的模式并没有直接的收据/流程。当然,您有很多教程/建议/模式以及如何实现它们的方法。但这实际上就是所有编程的意义所在——您只需要提出一个适合您需求的解决方案。根据您的开发人员愿景,您可以将许多原则应用于您的解决方案,以使其更容易/更快地开发/测试/支持。
      在您的情况下,我认为将这种逻辑移动到片段转换中会更好(就像您在setFreeTrialFragment() 中所做的那样),它更可定制且使用起来更舒适。但是,如果您的方法应该保持不变 - 现有的方法是正常的。实际上@BindingAdapter 更适合xml 属性而不是直接使用。
      至于我——所有的 UI 逻辑都应该驻留在 Activity 中,主要目的是将业务逻辑与 UI 分离。因此,所有动画、片段事务等都在活动内部处理——这是我的方法。 ViewModel - 负责通知视图相应模型中的某些内容发生了更改,并且视图应自行安排这些更改。在完美的世界中,您应该能够实现双向绑定这样一个流行的术语,但这并不总是必要的,也不总是应该在 ViewModel 中处理 UI 更改。像往常一样,过多的 MVVM 对您的项目不利。它会导致Spaghetti code,“这是从哪里来的?”,“如何查看回收站?”和其他热门问题。所以它应该只用于让生活更轻松,而不是让一切变得理想,因为就像其他所有模式一样,它会让人很头疼,并且会查看你的代码的人会说“OVERENGINEERING!!11”。

      每个请求,MVP 示例:

      这里有一些有用的文章:

      • 很简单example
      • Here你有一个很好的集成指南描述。
      • Firstsecond这部分文章可能会更多 有帮助。
      • 这个one 很简短,非常具有描述性。

      简短示例(概括),您应该将其适合您的架构:

      包表示:

      实施:

      型号:

      public class GalleryItem {
      
          private String mImagePath;
          //other variables/getters/setters
      }
      

      演讲者:

      //cool presenter with a lot of stuff
      public class GalleryPresenter {
      
          private GalleryView mGalleryView;
      
          public void loadPicturesBySomeCreteria(Criteria criteria){
              //perform loading here
              //notify your activity
              mGalleryView.setGalleryItems(yourGaleryItems);
          }
      
          //you can use any other suitable name
          public void bind(GalleryView galleryView) {
              mGalleryView = galleryView;
          }
      
          public void unbind() {
              mGalleryView = null;
          }
      
          //Abstraction for basic communication with activity.
          //We can say that this is our protocol
          public interface GalleryView {
              void setGalleryItems(List<GalleryItem> items);
      
          }
      }
      

      查看:

      public class NiceGalleryView extends View {
          public NiceGalleryView(Context context) {
              super(context);
          }
      
          public NiceGalleryView(Context context, AttributeSet attrs) {
              super(context, attrs);
          }
      
          // TODO: 29.12.16 do your stuff here
      }
      

      当然还有活动代码:

      public class GalleryActivity extends AppCompatActivity implements GalleryPresenter.GalleryView {
      
          private GalleryPresenter mPresenter;
      
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_gallery);
              //init views and so on
              mPresenter = new GalleryPresenter();
              mPresenter.bind(this);
      
          }
      
          @Override
          public void setGalleryItems(List<GalleryItem> items) {
              //use RecyclerView or any other stuff to fill your UI
          }
      
          @Override
          protected void onDestroy() {
              super.onDestroy();
              mPresenter.unbind();
          }
      }
      

      另外请注意,您在使用 MVP 时甚至有很多不同的方法。我只想强调,我更喜欢在活动中初始化视图,而不是将它们从活动中传递出去。您可以通过界面进行管理,这不仅适用于开发,甚至适用于仪器测试。

      【讨论】:

      • 如果您需要一些示例和优秀示例的链接 - 请告诉我!
      • 如果你能分享“MVP”模式就很好了,它可以处理复杂的结构。
      • @VikramSingh 完成。
      • 谢谢@YuriiTsap,你是对的。没有实现模式的食谱,但有一些参考是很好的。
      猜你喜欢
      • 2020-11-21
      • 2023-03-29
      • 1970-01-01
      • 2019-07-05
      • 1970-01-01
      • 1970-01-01
      • 2017-06-20
      • 1970-01-01
      相关资源
      最近更新 更多