Я изо всех сил пытался найти правильный способ реализовать 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();
    }
}

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

15
george_mx 22 Дек 2016 в 19:20
Я наткнулся на хороший блог на mvvm, проверьте, может ли он вам помочь - labs.ribot.co.uk/…
 – 
Govind
29 Дек 2016 в 11:45

4 ответа

Лучший ответ

Как по мне - MVVM, MVP и другие действительно крутые шаблоны для действительно крутых парней не имеют прямого получения / потока. Конечно, у вас есть много руководств / рекомендаций / шаблонов и подходов к их реализации. Но на самом деле в этом и заключается вся суть программирования - вам просто нужно найти решение, которое соответствует вашим потребностям. В зависимости от видения вашего разработчика вы можете применить к своему решению множество принципов, чтобы упростить / ускорить разработку / тестирование / поддержку.
В вашем случае я думаю, что лучше перенести эту логику на переходы фрагментов (как вы это делали в setFreeTrialFragment()), она более настраиваема и удобна в использовании. Но тем не менее, если ваш подход должен остаться прежним - существующий - нормально. На самом деле @BindingAdapter больше подходит для атрибутов xml, чем для прямого использования.
Как по мне, вся логика UI должна находиться в Activity, основная цель - отделить бизнес-логику от UI. Из-за этого все анимации, транзакции фрагментов и т. Д. Обрабатываются внутри активности - это мой подход. ViewModel - отвечает за уведомление представления о том, что что-то изменилось в соответствующей модели, и представление должно приспособиться к этим изменениям. В идеальном мире вы должны иметь возможность использовать такой популярный термин, как двусторонняя привязка, но это не всегда необходимо, и не всегда изменения пользовательского интерфейса должны обрабатываться внутри ViewModel. Как обычно, слишком много MVVM плохо для вашего проекта. Это может вызвать код спагетти, «откуда это?», «Как переработать представление?» и другие популярные вопросы. Поэтому его следует использовать только для того, чтобы сделать жизнь проще, а не для того, чтобы все было идеально, потому что, как и любой другой шаблон, он вызовет сильную головную боль, и кто-то, кто просмотрит ваш код, скажет: «ПЕРЕРАБОТКА !! 11».

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

Вот несколько полезных статей:

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

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

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

Реализация:

Модель:

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

10
Yurii Tsap 29 Дек 2016 в 13:57
Если вам нужен образец и ссылки на хорошие примеры - дайте мне знать!
 – 
Yurii Tsap
27 Дек 2016 в 15:38
Если вы можете поделиться шаблоном "MVP", это будет хорошо, поскольку он может обрабатывать сложную структуру.
 – 
Reprator
29 Дек 2016 в 05:38
Выполнено.
 – 
Yurii Tsap
29 Дек 2016 в 13:58
Спасибо @YuriiTsap, вы правы. Не существует кулинарной книги для реализации шаблона, но неплохо иметь некоторые ссылки.
 – 
george_mx
2 Янв 2017 в 21:28

Что касается шаблонов проектирования в целом. Вы хотите, чтобы бизнес-логика находилась подальше от действий и фрагментов.

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

Примечания

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

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

2
Jemil Riahi 29 Дек 2016 в 16:43
Спасибо, я действительно хорошо понимаю библиотеку привязки данных, моя проблема в том, насколько далеко представление и модель отделены друг от друга. В моем примере кода проблема связана с транзакциями анимации и фрагментами, поскольку они могут находиться либо в представлении, либо в модели представления. Мне кажется беспорядочным перемещать их в модель просмотра, и я хотел бы кое-что подумать по этому поводу.
 – 
george_mx
2 Янв 2017 в 21:34

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

2
vikas kumar 30 Дек 2016 в 15:01

Если вас интересует "чистая" аутентификация Firebase, вы можете проверить следующую статью:

Репозиторий ничего не знает о представлениях, а представления ничего не знают о своих источниках данных.

0
Alex Mamo 8 Ноя 2019 в 00:03