pinkred's mobile program

pinkred mobile programer

Archive for 7월 2017

Android MVP pattern, Clean Architecture more

leave a comment »

개발을 하다보면 정형화된 틀을 생각하게 된다. 이는 혼자서 개발하든 협업을 하던지 이러한 틀이라는 것을 고민하게 된다. 한동안 안드로이드에서는 MVP 패턴에 대해서 많은 이야기들이 있었고, 구글에서는 기본적인 제안(Android Architecture Components)를 하기 시작했다.  이 글은 아직 구글이 제시하는 안 이전에 구현된 것으로 추후에는 안드로이드가 제공하는 컴포넌트를 연동하게 된다면 다시 한번 그후에 글을 써보도록 하겠다.

현제 추세가 MVP  패턴에 대해서 이야기 하고 있고 MVVM 패턴으로 넘어가기 시작 단계에 있다고 생각된다. 물론 MVP 패턴을 적용하더라도 큰 이슈는 없으며 전체적인 바향은 의존성(Dependence)을 제거하기 위한 방법으로 진행되고 있다고 생각된다.

Clean Architecture를 도입하고 시작한 것은 결국 패턴의 이슈로는 의존성을 완전히 없앨수 없기 때문에 추가적으로 아키텍쳐가 도입되어야 했다.  기존의 패턴은 수평관계의 의존성을 제거 한다면 아키텍처 도입으로 수직 관계의 의존성을 제거하였다. 물론 이러한 내용들을 받아들이는 사람들의 생각에 따라서 다르게 해석하고 다른 소스로 구현되어진다. 결국 큰 방향은 소드 코드의 의존성을 제거하는 쪽으로 흐른다고 볼수 있다.

위의 구조를 만들기 위해서 많은 라이브러리를 도입하게 되면 실제 학습의 시간이 가장 많이 소모 되므로 현 시점에서 가장 많이 쓰이는 라이브러리를 이용하여 구현 하였으며 실제 사용하고 있다.

Retrofit, OkHttp, RxAndroid, Retrolambda의 라이브러리를 이용하고 있다. 여기서 가장 어려운 학습은 역시 RxAndroid이다. 요즘에는 RxJava가 대세이다보니 많은 라이브러리들이 지원을 하고 있어서 적용시 큰 이슈는 없다. Dagger2를 사용하면 좋지만 아직 사용해본적이 없어서 관련 라이브러리는 배제하도록 하겠습니다.

이전글에 MVP 패턴에 대해서 간략적으로 설명하였으나 역시나 사용하다보니 불편한 점들은 업데이트가 되고 아키텍쳐와 합쳐지면서 더욱더 복잡해졌다.

Clean Architecture 적용의 목적은 순수 자바로 만들어서 의존성을 제거한다라고 생각하면 된다.

패키지 구조를 보도록 하겠습니다. 참조 사이트

domain :  모델을 제공 받는 방식에 대한 인터페이스입니다.

entity : 순수 모델입니다.

repository.local : 내부에서 모델을 제공. domain에 해당하는 인터페이스를 구현

repository.local.model : 내부모델

repository.remote : 외부에서 모델을 제공. domain에 해당하는 인터페이스를 구현

repository.remote.model : 외부모델

domain 샘플


public interface CommonInterface
{
Observable<CommonDateTime> getCommonDateTime();
}

repository.remote 샘플


public class CommonRemoteImpl implements CommonInterface
{
private Context mContext;

public CommonRemoteImpl(@NonNull Context context)
{
mContext = context;
}

@Override
Observable<CommonDateTime> getCommonDateTime()
{
return DailyMobileAPI.getInstance(mContext).getCommonDateTime().map((commonDateTimeDataBaseDto) -&gt;
{
CommonDateTime commonDateTime = null;

if (commonDateTimeDataBaseDto != null)
{
if (commonDateTimeDataBaseDto.msgCode == 100 &amp;&amp; commonDateTimeDataBaseDto.data != null)
{
commonDateTime = commonDateTimeDataBaseDto.data.getCommonDateTime();
} else
{
throw new BaseException(commonDateTimeDataBaseDto.msgCode, commonDateTimeDataBaseDto.msg);
}
} else
{
throw new BaseException(-1, null);
}

return commonDateTime;
}).observeOn(AndroidSchedulers.mainThread());
}
}

repository.remote.model 샘플


@JsonObject
public class CommonDateTimeData
{
@JsonField(name = "openDateTime")
public String openDateTime;

@JsonField(name = "closeDateTime")
public String closeDateTime;

@JsonField(name = "currentDateTime")
public String currentDateTime;

public CommonDateTimeData()
{
}

public CommonDateTime getCommonDateTime()
{
return new CommonDateTime(openDateTime, closeDateTime, currentDateTime);
}
}

entity 샘플


public class CommonDateTime
{
public String openDateTime;
public String closeDateTime;
public String currentDateTime;

public CommonDateTime()
{
}

public CommonDateTime(String openDateTime, String closeDateTime, String currentDateTime)
{
setDateTime(openDateTime, closeDateTime, currentDateTime);
}

public void setDateTime(String openDateTime, String closeDateTime, String currentDateTime)
{
this.openDateTime = openDateTime;
this.closeDateTime = closeDateTime;
this.currentDateTime = currentDateTime;
}
}

 

사용 방법


mCommonRemoteImpl.getCommonDateTime()//
.subscribe(commonDateTime ->;
{
onCommonDateTime(commonDateTime);
}, throwable -&gt;
{

}));

 

아래와 같은 구조로 되어있다.

스크린샷 2017-07-26 오후 4.09.36

domain에는 interface을 정의하여 데이터를 얻는 방식의 의존성을 버린다.

그리고 따로 로컬/네트워크 모델을 만들어서 저장소의 의존성을 버린다. 여기서 방식이란 예를 들어서 네트워크 / 로컬 데이터를 얻는 라이브러리를 어떠한 것 이용하는 경우이다. 결국 최종으로 얻는 CommonDateTime의 순수 모델은 각각의 단계에서 어떻게 얻어지는 몰라도 상관없다. 관련된 내용은 Clean Architecture의 의존성을 버리는 방식이다.

 

그렇다면 여기서 패턴을 추가 할수가 있다. 기본적을 MVP 패턴을 사용하도록 하겠다.

 

스크린샷 2017-07-26 오후 4.34.19

결국에는 Activity 조차도 의존성을 버리기 위해서 Interface를 두었다. 물론 Presenter, View도 Interface를 두어서 의존성을 없앴다. 계속적인 Interface로 의존성은 떨어지고 있지만 파일의 개수가 늘어나게 된다. 그래서 패키지를 나눌때 같은 속성 별로 패키지를 하지 않고 스크린 단위로 패키지를 하여 같은 스크린안에서 사용하는 파일들을 모아서 쉽게 접근할수 있도록 했다.

그리고 BasePresenter에서 Google Analytics 를 호출 할수 있도록 BaseAnalyticsInterface를 정의한다.

각각의 파일의 구조를 보도록 하겠다. 실제로는 더욱 복잡하지만 소스를 간추렸다.

BaseActivity는 BasePresenter와 연결되어서 Activity에 대한 의존성은 버린다. 대신 Activity는 Intent를 받는 역할로만 처리하였다.

BasePresenter는 BaseActivity, BaseInterface를 받아서 View와 의존성을 버리고, Analytics를 넣으므로써 OnBaseEventListener에서 발생하는 이벤트를 Google Analytics에 반영할수 있도록 했다.

BaseView는 OnBaseEventListener와 ViewDataBind를 받아서 Presenter와 의존성을 버리고 UI를 쉽게 접근할수 있도록 ViewDataBind로 처리하도록 했다. BaseView에서 activity변수를 받아서 약간 의아할수도 있다. 기존에는 Context를 보냈는데 사용하다 보니 View에서 어쩔수 없이 activity를 필요로 할때가 있는데 그래서 일반적으로 사용할때는 getConext()를 사용하여  Context를 넘겨주고 특별한 경우에만 activity를 이용하여 얻어갈수 있도록 따로 구현하였다. 물론 activity값을 View에서 직접 호출할수는 없다.

아직 부족한 점도 많고 새로이 뜨고 있는 MVVM 패턴과 구글에서 제공하는 AAC 라이브러리도 다시 적용해보아야 하기 때문에 계속적인 업데이트가 필요하지만 현시점에서 협업으로 사용하기에는 적절한 크기일것이라고 생각된다. 물론 접근하시는 분들의 입장에서는 다소 어렵다는 말씀이 있기도 하다. 추가적으로 Dagger2를 적용하는 부분도 고려해볼만하다고 생각됩니다.

 

이에 별도로 View의 xml이 점차 복잡해지기 때문에 해당 부분을 커스텀 뷰로 만들어서 재사용 목적으로 소스가 더 간략해질수 있다고 생각된다.

 

 

 

BaseActivity


public abstract class BaseActivity<T1 extends BasePresenter> extends AppCompatActivity
{
private BasePresenter mPresenter;

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

mPresenter = createInstancePresenter();

if (mPresenter.onIntent(getIntent()) == false)
{
finish();
return;
}

mPresenter.onPostCreate();
}

protected abstract
@NonNull
T1 createInstancePresenter();

public
@NonNull
T1 getPresenter()
{
if (mPresenter == null)
{
mPresenter = createInstancePresenter();
}

return (T1) mPresenter;
}

@Override
protected void onStart()
{
super.onStart();

if (mPresenter != null)
{
mPresenter.onStart();
}
}

@Override
protected void onResume()
{
super.onResume();

if (mPresenter != null)
{
mPresenter.onResume();
}
}

@Override
protected void onSaveInstanceState(Bundle outState)
{
if (mPresenter != null)
{
mPresenter.onSaveInstanceState(outState);
}

super.onSaveInstanceState(outState);
}

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

if (mPresenter != null)
{
mPresenter.onRestoreInstanceState(savedInstanceState);
}
}

@Override
protected void onPause()
{
super.onPause();

if (mPresenter != null)
{
mPresenter.onPause();
}
}

@Override
protected void onDestroy()
{
super.onDestroy();

if (mPresenter != null)
{
mPresenter.onDestroy();
}
}

@Override
public void onBackPressed()
{
if (mPresenter != null)
{
if (mPresenter.onBackPressed() == false)
{
super.onBackPressed();
}
} else
{
super.onBackPressed();
}
}

@Override
public void finish()
{
super.finish();

if (mPresenter != null)
{
mPresenter.onFinish();
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
super.onActivityResult(requestCode, resultCode, data);

if (mPresenter != null)
{
mPresenter.onActivityResult(requestCode, resultCode, data);
}
}
}

BaseActivityInterface


public interface BaseActivityInterface
{
boolean onIntent(Intent intent);

void onStart();

void onResume();

void onPause();

void onDestroy();

void onFinish();

boolean onBackPressed();

void onSaveInstanceState(Bundle outState);

void onRestoreInstanceState(Bundle savedInstanceState);

void onActivityResult(int requestCode, int resultCode, Intent data);
}

BasePresenter


public abstract class BasePresenter<T1 extends BaseActivity, T2 extends BaseViewInterface> implements BaseActivityInterface
{
private T1 mActivity;

private T2 mOnViewInterface;

public BasePresenter(@NonNull T1 activity)
{
mActivity = activity;

mLock = new DailyLock(activity);

mOnViewInterface = createInstanceViewInterface();

constructorInitialize(activity);
}

protected abstract
@NonNull
T2 createInstanceViewInterface();

public abstract void constructorInitialize(T1 activity);

public abstract void setAnalytics(BaseAnalyticsInterface analytics);

public abstract void onPostCreate();

public T1 getActivity()
{
return mActivity;
}

public void setContentView(@LayoutRes int layoutResID)public interface BaseActivityInterface
{
boolean onIntent(Intent intent);

void onStart();

void onResume();

void onPause();

void onDestroy();

void onFinish();

boolean onBackPressed();

void onSaveInstanceState(Bundle outState);

void onRestoreInstanceState(Bundle savedInstanceState);

void onActivityResult(int requestCode, int resultCode, Intent data);
}
{
if (mOnViewInterface == null)
{
throw new NullPointerException("mOnViewInterface is null");
} else
{
mOnViewInterface.setContentView(layoutResID);
}
}

public
@NonNull
T2 getViewInterface()
{
if (mOnViewInterface == null)
{
mOnViewInterface = createInstanceViewInterface();
}

return mOnViewInterface;
}

@Override
public void onStart()
{

}

@Override
public void onResume()
{

}

@Override
public void onPause()
{
}

@Override
public void onDestroy()
{
}

@Override
public void onFinish()
{

}

@Override
public boolean onBackPressed()
{
return false;
}

@Override
public void onSaveInstanceState(Bundle outState)
{

}

@Override
public void onRestoreInstanceState(Bundle savedInstanceState)
{

}

protected void finish()
{
mActivity.finish();
}
}

BaseViewInterface


public interface BaseViewInterface
{
void setContentView(int layoutResID);

void setContentView(int layoutResID, ViewGroup viewGroup);
}

BaseView


public abstract class BaseView<T1 extends OnBaseEventListener, T2 extends ViewDataBinding> implements BaseViewInterface
{
private BaseActivity mActivity;
private T2 mViewDataBinding;
private T1 mOnEventListener;

protected abstract void setContentView(T2 viewDataBinding);

public BaseView(BaseActivity activity, T1 listener)
{
if (activity == null || listener == null)
{
throw new NullPointerException();
}

mActivity = activity;
mOnEventListener = listener;
}

@Override
public final void setContentView(int layoutResID)
{
if (layoutResID != 0)
{
mViewDataBinding = DataBindingUtil.setContentView(mActivity, layoutResID);
}

setContentView(mViewDataBinding);
}

@Override
public final void setContentView(int layoutResID, ViewGroup viewGroup)
{
mViewDataBinding = DataBindingUtil.inflate(LayoutInflater.from(mActivity), layoutResID, viewGroup, false);

setContentView(mViewDataBinding);
}

protected void setVisibility(int visibility)
{
if (mViewDataBinding == null)
{
return;
}

mViewDataBinding.getRoot().setVisibility(visibility);
}

protected
@NonNull
Context getContext()
{
return mActivity;
}

protected T2 getViewDataBinding()
{
return mViewDataBinding;
}

protected
@NonNull
T1 getEventListener()
{
return mOnEventListener;
}

protected int getColor(int resId)
{
return mActivity.getResources().getColor(resId);
}

protected String getString(int resId)
{
return mActivity.getString(resId);
}
}

OnBaseEventListener


public interface OnBaseEventListener
{
void onBackClick();
}

BaseAnalyticsListener

public interface BaseAnalyticsInterface
{

}

 

 

Written by pinkredmobile

2017/07/26 at 4:59 pm

Android Custom View 만들때 merge태그

leave a comment »

Custom View를 만들때 일반적으로 ViewGroup을 상속 받아서 layer를 inflate시켜서 구현하는데 layer의 깊이를 줄이기 위해서 merge태그를 사용하다보니 merge 태그안의 UI가 제대로 정렬되지 않아서 수정시에 다시 merge태그를 없애고 다시 원래 ViewGroup 태그를 넣어서 GUI  수정후에 주석 처리하는 방식으로 진행했는데 매번 보는 사람이나 수정하는 사람이나 매우 귀찮았다고 할수 있다. 해결 방안이다.

Written by pinkredmobile

2017/07/26 at 10:47 am

프로그래밍(programming)에 게시됨

Tagged with ,