[Android] MVP 접근 방식으로 상용구 코드를 줄여 봅시다

2023. 3. 28. 19:42모바일프로그래밍/안드로이드

728x90

(BoilerPlate 코드란 모든 코드를 작성하기 위해 항상 필요한 부분을 의미한다)

 

Reduce your boilerplate code using this Android MVP approach

For some time I’ve been reading different ways of implementing a MVP pattern on Android. I’m sure you have too. In this article I’m going…

medium.com

 

 얼마 동안 저는 Android에서 MVP 패턴을 구현하는 다양한 방법들을 읽어 보았습니다.

당신도 그랬을 것이라고 확신 합니다.

 

이 기사에서는 이 패턴을 구현하는 가장 좋은 방법을 보여드릴 예정입니다(적어도 저에게는). 패턴에 대해 더 알고 싶다면 아래 참고 문헌들을 찾아 보시거나, 아니면 구글 검색으로도 충분 할것입니다.

 

 

MVP for Android: how to organize the presentation layer

MVP (Model-View-Presenter) is a software design pattern that works pretty well in Android projects and helps separate presentation layer from domain.

antonioleiva.com

 

 

Cheap and Reliable Web Hosting - fast shared hosting and KVM VPS

Copyright © 2020 Reliable WebHosting All Rights reserved

www.reliable-webhosting.com

내가 생각하는 주요 문제 중 하나는 Presenter, Model 및 View 간의 대화방식을 설정하는 방법을 고민 하는 것이었습니다. 그 중 한 가지 방법은 그들 사이에 여러 인터페이스나 혹은 계약을 만드는 것이 었습니다. 이 방법은 우리가 원하는 모든 메서드들을 구현하고 규챡(계약)을 따르는 것을 강제 하는 것이기에 멋진 방법 중 하나 입니다.

이러한 경우 가장 큰 문제는- 적어도 내 경우에 - 프로젝트가 그다지 크지 않았기 때문에 필요하지 않은 수많은 인터페이스를 만들게 될 것이라는 점입니다.

나는 통신을 위해 버스 이벤트(Otto)를 사용하게 되었습니다. 또 모든 뷰를 쉽게 주입하기 위해 ButterKnife를 사용했습니다.

 

자, 시작해 보겠습니다.

 

Otto로 부터 버스 객체를 얻기 위해 당신만의 싱글톤 클래스를 만듭니다:

import com.squareup.otto.Bus;

public class BusManager {

    private Bus bus;

    private static volatile BusManager instance = null;

    private BusManager() {
        bus = new Bus();
    }

    public static Bus getInstance() {
        if (instance == null) {    // check 1
            synchronized (BusManager.class) {
                if (instance == null) {    // check 2
                    instance = new BusManager();
                }
            }
        }
        return instance.getBus();
    }

    private Bus getBus() {
        return bus;
    }
}
 

이제 우리는 통신을 쉽게 할 수 있는 BusManager 클래스를 가지게 되었습니다.

그럼 BaseModel 추상 클래스를 생성해 봅시다:

import com.squareup.otto.Bus;

public abstract class BaseModel {

    protected Bus bus;
    //this is another singleton class to call any API using Retrofit or Volley 
    protected NetworkService networkService;

    public BaseModel(Bus bus) {
        this.bus = bus;
        networkService = NetworkService.getInstance();
    }
}
 

이제 우리는 추상 클래스 BaseView가 필요합니다:

import android.support.annotation.Nullable;

import com.squareup.otto.Bus;

import java.lang.ref.WeakReference;

import butterknife.ButterKnife;

public abstract class BaseView<T extends AppCompatActivity> {

    protected Bus bus;
    private WeakReference<T> activityRef;

    public BaseView(T activity, Bus bus) {
        ButterKnife.bind(this, activity);

        this.bus = bus;
        this.activityRef = new WeakReference<>(activity);
    }

    @Nullable
    public T getActivity() {
        return activityRef.get();
    }

}
 

우리의 생성자에서 ButterKnife를 사용하여 모든 뷰를 바인딩합니다. 또 사용된 액티비티에 대한 약한 참조를 만듭니다. 때때로 뷰가 파괴된다는 점을 기억 합시다. 그래서 우리는 @Nullable 주석을 사용하는 이유입니다.

즉, getActivity() 메서드를 사용할 때 주어진 액티비티가 null인지 여부를 확인 할 필요가 있습니다.

확인을 수행하는 대신 일종의 NullPointerException을 던질까 생각했지만 그렇게 하면 앱이 충돌하거나 항상 try-catch를 사용해야 하므로 동일한 문제가 발생 할 것입니다.

 

이제 BaseModel 및 BaseView 추상 클래스가 만들었으므로, BasePresenter 추상 클래스를 만들 준비가 되었습니다:

public abstract class BasePresenter<T extends BaseView, U extends BaseModel> {

    private T mView;
    private U mModel;

    public BasePresenter(T mView, U mModel) {
        this.mView = mView;
        this.mModel = mModel;
    }

    public BasePresenter(T mView) {
        this.mView = mView;
    }

    public T getView() {
        return mView;
    }

    public U getModel() {
        return mModel;
    }
}
 

생성자에서 ButterKnife를 사용하여 모든 뷰를 바인딩합니다. 또한 사용된 활동에 대한 약한 참조를 만듭니다. 때때로 뷰가 파괴된다는 점을 기억하세요. 그래서 @Nullable 주석을 사용 해야 하는 이유입니다.

여기서도, getActivity() 메서드를 사용할 때 주어진 활동이 null인지 여부를 확인해야 합니다.

역시나, 확인을 수행하는 대신 일종의 NullPointerException을 던질까 생각했지만 그렇게 하면 앱이 충돌하거나 항상 try-catch를 사용해야 하므로 동일한 문제가 발생합니다.

 

이제 BaseModel 및 BaseView 추상 클래스가 있으므로 BasePresenter 추상 클래스를 만들 준비가 되었습니다:

public abstract class BasePresenter<T extends BaseView, U extends BaseModel> {

    private T mView;
    private U mModel;

    public BasePresenter(T mView, U mModel) {
        this.mView = mView;
        this.mModel = mModel;
    }

    public BasePresenter(T mView) {
        this.mView = mView;
    }

    public T getView() {
        return mView;
    }

    public U getModel() {
        return mModel;
    }
}
 

하나는 Model 클래스를 사용하고 다른 하나는 사용하지 않는 2개의 생성자가 있음을 알 수 있습니다. 때로는 모델 클래스가 필요하지 않았기 때문입니다.

 

또한 우리는 Presenter를 사용할 때 BaseView와 BaseModel 클래스를 모두 구현해야만 합니다.

 

이제 액티비티들에서 이것을 사용하는 경우, 우리는 BasePresenterActivity 추상 클래스를 만들어야 합니다:

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import com.squareup.otto.Bus;

public abstract class BasePresenterActivity<T extends BasePresenter> extends BaseActivity {

    protected Bus bus = BusManager.getInstance();
    protected T mPresenter;

    @NonNull
    protected abstract T getPresenter();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mPresenter = getPresenter();
    }

    @Override
    protected void onResume() {
        super.onResume();
        BusManager.getInstance().register(mPresenter);
    }

    @Override
    protected void onPause() {
        super.onPause();
        BusManager.getInstance().unregister(mPresenter);
    }
}
 

onCreate 메서드에서 presenter를 만든 다음 해당 클래스를 등록하고, onResume 및 onPause 메서드에서 등록을 취소합니다.

 

이제부터 재미있는 부분입니다... 이 멋진 클래스를 어떻게 사용 해야 할까요?!?!?!

간단한 예를 들어 보겠습니다.

간단한 로그인 화면입니다.

public class LoginModel extends BaseModel {

    public LoginModel(Bus bus) {
        super(bus);
    }

    public void login(final String email, final String password) {
		bus.post(new LoginEvents.OnLoginStarted());

		if (FormUtils.isValidEmail(email)) {
			//first we need to call backend for login
			NetworkService networkService = NetworkService.getInstance();

			networkService.login(email, password, new Callback<LoginRequest>{
				@Override
				public void onError(Throwable e) {
					bus.post(new LoginEvents.OnLoginFailed(e.getMessage()));
				}

				@Override
				public void onSuccess(LoginRequest response) {
					bus.post(new LoginEvents.OnLoginSuccess());
				}
				
			});
		} else {
			bus.post(new LoginEvents.OnLoginFailed("INVALID EMAIL ERROR"));
		}
	}
}
 

물론, 이것은 더미 코드일 뿐이며 NetworkService 구현에 따라 다릅니다(예를 들어 나는 프로젝트에서 Rx Observables와 함께 Retrofit 2를 사용했습니다). 요점은 다른 이벤트 객체(예: OnLoginStarted, OnLoginFailed 및 OnLoginSuccess)가 필요하다는 것입니다.

각 클래스 내에서 변수를 보낼 수 있도록 각 이벤트 유형(시작됨, 실패 및 성공)에 대한 내부 클래스와 LoginEvents라는 클래스를 사용했습니다.

 

이제 LoginView 클래스가 필요합니다:

public class LoginView extends BaseView<LoginActivity> {

    @BindView(R.id.editText_email)
    EditText editTextEmail;

    @BindView(R.id.editText_password)
    EditText editTextPassword;

    public LoginView(LoginActivity activity, Bus bus) {   
        //remember that this calls ButterKnife.bind internally on BaseView
        super(activity, bus);
    }
    
    @OnClick(R.id.button_login)
    public void onLoginClicked() {
        String email = editTextEmail.getText().toString();
        String password = editTextPassword.getText().toString();
        //we pass the data to the presenter
        bus.post(new LoginEvents.OnLoginButtonClicked(email, password));
    }

    public void onLoginFailed(String message) {
        LoginActivity activity = getActivity();

        if (activity == null) {
            return;
        }

        AlertDialog.Builder builder = new AlertDialog.Builder(activity);
        // Add the buttons
        builder.setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int id) {
                dialog.dismiss();
            }
        });

        builder.setMessage(message);
        builder.setTitle("Error Title");

        builder.create().show();
    }
}
 

 

그리고 이제… LoginPresenter 클래스이구요:

public class LoginPresenter extends BasePresenter<LoginView, LoginModel> {
    
    public LoginPresenter(LoginView loginView, LoginModel loginModel) {
        super(loginView, loginModel);
    }
    //we show a ProgressBar or something on the view
    @Subscribe
    public void onLoginStarted(LoginEvents.OnLoginStarted loginStarted) {
        getView().showLoadingView();
    }
    //We listen to the button being clicked and get our variables
    @Subscribe
    public void onLoginButtonClicked(LoginEvents.OnLoginButtonClicked loginButtonClicked) {
        //we tell our model to start the login using Retrofit
        getModel().login(loginButtonClicked.email, loginButtonClicked.password);
    }
    //we listen to any fail attempt to login
    @Subscribe
    public void onLoginFailed(LoginEvents.OnLoginFailed loginFailed) {
       //we tell our view to display the Alert dialog with the error message
        getView().onLoginFailed(loginFailed.message)
    }

    //we listen when the login was a success
    @Subscribe
    public void onLoginSuccess(LoginEvents.OnLoginSuccess loginSuccess) {
        LoginActivity activity = getView().getActivity();

        if (activity == null) {
            return;
        }
     
        activity.startActivity(new Intent(activity, ANOTHER_ACTIVITY.class));
    }
}
 

마지막으로 LoginActivity가 필요합니다:

public class LoginActivity extends BasePresenterActivity<LoginPresenter> {

    @NonNull
    @Override
    protected LoginPresenter getPresenter() {
        return new LoginPresenter(
                new LoginView(LoginActivity.this, bus),
                new LoginModel(bus)
        );
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //don't forget to call super so we have our presenter
        super.onCreate(savedInstanceState);
    }
}
 

우리는 BasePresenterActivity에서 무료 버스를 가지고 있다는 것을 명심해야 합니다. 이것은 필요에 따라 presenter 클래스를 자동으로 인스턴스화합니다.

이 예에서는 다음과 같이 말할 수 있습니다:

 

“나는 이 액티비티에 하나의 LoginPresenter가 필요합니다. LoginView 및 LoginModel 클래스에서 이 클래스가 제공합니다.”

 

이제 여러 액티비티들을 생성하고 이를 사용하기 위해 getPresenter() 메서드를 구현하기만 하면 되기 때문에 훌륭한 접근 방식이라고 생각 합니다.

만약 우리가 Fragment 또는 다른 클래스에서 사용하려 한다면, BasePresenterActivity 지침을 따르십시오.

예를 들어 프래그먼트에서 다음과 같이 만들 수 있습니다:

public abstract class BasePresenterFragment<T extends BasePresenter> extends Fragment {

    protected Bus bus = BusManager.getInstance();
    protected T mPresenter;

    @NonNull
    protected abstract T getPresenter();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mPresenter = getPresenter();
    }

    @Override
    protected void onStart() {
        super.onStart();       
        BusManager.getInstance().register(mPresenter);       
    }

    @Override
    protected void onStop() {
        super.onStop();
        BusManager.getInstance().unregister(mPresenter);        
    }
}
 

모든 코드는 내가 작업 중인 프로젝트에서 단순화 하였습니다. 이것이 도움이 되기를 진심으로 바라며 이를 구현하는 방법에 대해 의문이 있는 경우 아래에 댓글을 달거나 메시지를 보내주십시오.

 

업데이트: 동일한 클래스를 보여주지만 Pokémon API를 사용하여 임의의 Pokémon을 표시하는 방법을 Github 저장소를 추가했습니다. 여기에서 볼 수 있습니다:

 

 

GitHub - betomaluje/MVP-Example: My way of creating a MVP Android app

My way of creating a MVP Android app. Contribute to betomaluje/MVP-Example development by creating an account on GitHub.

github.com

이상.

 

728x90