Правильная реализация MVVM в Android

Я пытаюсь найти правильный способ реализовать MVVM в Android.

Вся идея все еще размыта мне, шаблон должен иметь отдельный слой, в котором выполняется логика (ViewModel).

Этот фрагмент кода только оживляет альфа фона, в котором живет куча фрагментов.

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 использует только логику выполнения анимации -Фрагменты имеют прослушиватель для передачи событий в действие -Binding помогает определить представления

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?

Ответ 1

Что касается меня - MVVM, MVP и другие действительно классные модели для действительно классных парней не имеют прямого приема/потока. Конечно, у вас есть много учебников/рекомендаций/шаблонов и подходов к их реализации. Но на самом деле все, о чем идет речь, - просто нужно придумать решение, соответствующее вашим потребностям. В зависимости от видения ваших разработчиков вы можете применить множество принципов к своему решению, чтобы упростить/ускорить разработку/тестирование/поддержку.
В вашем случае я думаю, что лучше перевести эту логику на переходы Фрагмента (как это было сделано в setFreeTrialFragment()), это более настраиваемо и удобно использовать. Но, тем не менее, если ваш подход должен оставаться прежним - существующий нормальный. На самом деле @BindingAdapter больше подходит для атрибутов xml, а затем для прямого использования.
Что касается меня - вся логика пользовательского интерфейса должна находиться в Activity, основная цель - отделить бизнес-логику от пользовательского интерфейса. Из-за этого все анимации, транзакции фрагментов и т.д. Обрабатываются внутри действия - это минный подход. ViewModel - отвечает за уведомление о том, что что-то изменилось в соответствующей модели, и представление должно соответствовать этим изменениям. В идеальном мире вы должны быть в состоянии достичь такого популярного термина, как двусторонняя привязка, но это не всегда необходимо, и не всегда должны быть обработаны пользовательские интерфейсы внутри ViewModel. Как обычно, слишком много MVVM плохо для вашего проекта. Это может вызвать код спагетти," где это? "," Как просмотреть ресайлер? "и другие популярные проблемы. Поэтому его следует использовать только для того, чтобы сделать жизнь лучше, а не делать все идеально, потому что, как и любой другой узор, у него будет большая головная боль, а кто-то, кто будет смотреть ваш код, скажет" OVERENGINEERING!! 11".

По запросу, пример MVP:

Здесь у вас есть полезные статьи:

  • Довольно простой example.
  • Здесь у вас есть хорошее описание с руководством по интеграции.
  • Первый и второй часть этих статей может быть более полезно.
  • Этот один является коротким и действительно описательным.

Короткий пример (обобщенный), вы должны поместить его в свою архитектуру:

Представление пакета:
введите описание изображения здесь

Реализация:

Модель:

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 у вас даже есть много разных подходов. Я просто хочу подчеркнуть, что я предпочитаю инициализировать представления в действии и не передавать их из вида. Вы можете управлять этим через интерфейс, и это действительно удобно не только для разработки, но даже для инструментальных тестов.

Ответ 2

Хороший пример здесь, поэтому проверьте его, стоит прочитать, так как это включает более одного способа включить MVP-архитектуру. Примеры Google MVP

Ответ 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. Потому что я стараюсь держать все андроид отдельно от него. Все для облегчения написания тестов

Затем я просто привязываю viewmodel к виду.

Вид

<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.

ViewModel

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. В противном случае я стараюсь, чтобы логика была максимально чистой. Все сделано, поэтому мне легче создавать и тестировать логику.

Обратите внимание

Я связал username и password с представлением. Поэтому каждое изменение, внесенное в EditText, будет автоматически добавлено в поле. По тому пути. Мне не нужно добавлять каких-либо конкретных слушателей

Надеюсь, что эта небольшая витрина поможет вам немного понять, как реализовать MVVM в своих проектах