안드로이드 개발 정보
(글 수 1,069)
이 글은 "Android를 통해 이해하는 GUI 구현 패턴" 의 속편입니다.
먼저 전편에서 다루고자 했던 내용을 요약 정리를 해보겠습니다.
1. GUI 구성에 중요한 것은 View와 다른 객체간의 직교성이다.
-> 이것은 OOP의 중요한 개념인 재사용성을 확보할 수 있게 해줍니다.
-> 이를 위해 오랜 시간 GUI 구현 패턴은 MVC, MVC#2 로 발전되어 왔으며 점차 연결성이 느슨해져 왔습니다. ( decoupling ) = 직교성 확보.
-> OS의 발전으로 인해 MVC 패턴에서 Controller의 역할부가 필요없어 졌으며, 동시에 View의 역할이 강화됐습니다.
-> 최근 패턴은 MVP, MVVM 등이 있다.
2. Android에서 adapter계열을 사용하는 View는 MVC #2로 구성되어 있다.
-> 부분 model 갱신에 대한 전체 view 갱신 등의 단점이 잔존합니다.
-> ViewHolder 패턴으로 부분 개선이 가능.
저번 글에서는 전체적으로 GUI 구현 패턴이 목적하는 바를 설명하고, 보편적인 예로 ListView를 가지고 예를 보였습니다. 하지만 ListView와 같이 컴포넌트 자체가 MVC#2로 구현되어 있으면 이를 개선하는 것이 부분적으로만 가능함을 알 수 있었습니다. 그래서, 이번에는 실제로 MVP 이후 패턴들이 말하고자 하는 바를 새로운 View를 제작하며 살펴볼까 합니다.
OOP에서 한 객체는 다른 객체와 연결성 없이 독자적으로 존재할 수 있어야 합니다.
이것은 View도 마찬가지입니다.
VIew는 View 자체로 존재할 수 있어야 하죠. 너무 당연한 말입니다.
그렇기에 View는 아무곳에나 마음대로 붙혔다 뗐다 할 수 있어야 합니다... :)
이를 주지하시고 진도를 나가봅시다.
![]() |
| 우리의 완성품 |
우리가 만들고자 하는 UI는 이렇습니다.
버튼을 누르면 현아가 뭔가 해줄지도 모르는 기대감이 드는 UI 입니다.
일단 잘 만든 것 같은데, 어떻게 만들었는지를 한번 검토해보겠습니다.
위의 UI 구현에 가장 쉬운 방법은, FrameLayout을 사용하는 것입니다.
FrameLayout으로 설정하고, Activity에서 setContentView로 xml을 불러온 뒤에 findViewById() 메소드를 사용하여 각 컴퍼넌트에 적합한 작업을 해주면 간단히 끝납니다.
어때요? 참 쉽죠?
일을 가볍게 처리하고 다음 건으로 넘어가려고 보고, 쾌재를 불렀습니다.
왜냐면, 중복되는 사항이 많았기 때문이죠.
![]() |
| 캐서린 UI |
왜냐면, 지나치게 많은 내용을 copy&paste해야 했기 때문입니다.
FrameLayout으로 변경후 컴포넌트들을 xml에 붙혀넣는 작업 뿐 아니라, Activity에서 각종 리스너들을 똑같은 방식으로 또 붙혀넣어야 했습니까요. 해당 작업을 하면서 김아무개는 위의 작업을 보다 간단하게 할 수 있는 방법은 없을까 고민이 됐습니다.
![]() | |
| 현아 UI |
![]() | |
| Overlay UI |
앞서 작업한 내용으로 돌아가서, 만약 김아무개가 현아UI를 작업할 때 요소들을 둘로 나눠서 작업했다면 어떨까? 하고 의문을 던져봅니다.
현아UI 와는 별도로 OverlayUI를 만들었다면?
즉, [ 현아UI+OverlayUI = 완성품 ] 이 되도록 만들었다면 어땠을까요?
![]() |
| 캐서린 UI |
![]() | |
| Overlay UI |
그렇다면, 우리는 반복 작업을 좀 덜 할 수 있지 않았을까요?
[ 캐서린UI + OverlayUI = 완성품 ]이 나온다면, 소스를 카피해넣는 작업이
다소 수월해질 수 있지 않을까? 하는 생각이 듭니다.
먼저, 이를 구현하는 가장 간단한 방법을 먼저 알아보도록 하겠습니다.
Android에서는 Ui를 Xml로 구성할 수 있고 이를 inflater를 통해서 쉽게 이를 View로 구현할 수 있습니다. 그러므로 Xml 을 별도로 구성한 뒤에 캐서린Ui 에 addView 시켜주는 방법이 가장 간단합니다.
캐서린UI 와 OverlayUI 를 별도의 XML로 구성했습니다.
<캐서린XML>
<오버레이XML>
그리고 MainActivity에서는 이를 캐서린UI의 Framelayout에 add시켜줍니다. :D
<MainActivity 소스>
public class MainActivity extends Activity {PTOverlayUi mPTOUi = null;
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FrameLayout catherinLayout = (FrameLayout) this.findViewById( R.id.catherin_ui );
LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
RelativeLayout overlayUI = (RelativeLayout)inflater.inflate(R.layout.overlay_ui, null);
catherinLayout.addView( overlayUI );
}
UI를 오버레이 하는 작업은 이렇게 간단하게 해낼 수 있습니다~~
(물론 이와 같은 방식의 OverlayUI 는 FrameLayout 혹은 RelativeLayout 에만 가능합니다)
하지만 UI 에 각종 listener를 추가하고 View단에서 버튼 관련 작업 등을 하고자 한다면 아직 부족합니다. Listener등을 추가하는 작업량 증가 문제를 해결하고자 한다면 OverlayUI XML 에서 ImageButton들에 onClick을 추가하는 방법이 있습니다만, 아쉬운 점은 현재 Android에서는 onClick 이외의 이벤트를 Xml에서 추가할 수 없습니다.
<onClick 이벤트 xml >
----------------------
때문에, findViewById()의 반복 호출 혹은 Listener의 반복 setting 작업을 줄이고자 하나의 View Xml과 하나의 View Presenter( Presentation 해주는 녀석-표현계층) 을 함께 사용하기로 마음을 먹습니다.
그 결과물은 아래와 같습니다.
<< 추상 클래스 Presenter>>
public abstract class Presenter {
protected Context mContext;
protected LayoutInflater mInflater = null;
protected int mUIdescriptorId = 0;
protected View mUI = null;
public Presenter( Context context, int id ) {
mContext = context;
mUIdescriptorId = id;
parsingUi();
}
public View getRootView(){
if ( mInflater == null ) {
mInflater = (LayoutInflater) mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
}
if ( mUI == null ) {
mUI = mInflater.inflate( mUIdescriptorId, null );
}
return mUI;
}
public void addViewto(View root){
((ViewGroup)root).addView( mUI );
}
abstract public void parsingUi();
}<< Presenter 구현체 PTOverlayUi >>
public class PTOverlayUi extends Presenter
implements View.OnClickListener,
OnSeekBarChangeListener
{
private static int overlayUi_id = R.layout.overlay_ui;
private Activity mAct;
private OverlayUIEventListener mListener;
private ImageButton imageButton1;
private ImageButton imageButton2;
private ImageButton imageButton3;
private ImageButton imageButton4;
private ImageButton imageButton5;
private ImageButton imageButton6;
private ImageButton imageButton7;
private SeekBar pageseeker;
private ImageView next;
private ImageView prev;
public interface OverlayUIEventListener {
public void OnButton1Clicked();
public void OnButton2Clicked();
public void OnButton3Clicked();
public void OnButton4Clicked();
public void OnButton5Clicked();
public void OnButton6Clicked();
public void OnButton7Clicked();
public void OnNextClicked();
public void OnPrevClicked();
public void OnSeekBarEvent();
}
public PTOverlayUi(Activity act) {
super(act.getApplicationContext(), overlayUi_id);
mAct = act;
try {
mListener = (OverlayUIEventListener)act;
} catch (ClassCastException e) {
throw new ClassCastException( act.toString()
+ " must implement OverlayUIEventListener");
}
}
@Override
public void parsingUi() {
getRootView();
imageButton1 = (ImageButton) setListener(mUI.findViewById( R.id.imageButton1 ));
imageButton2 = (ImageButton) setListener(mUI.findViewById( R.id.imageButton2 ));
imageButton3 = (ImageButton) setListener(mUI.findViewById( R.id.imageButton3 ));
imageButton4 = (ImageButton) setListener(mUI.findViewById( R.id.imageButton4 ));
imageButton5 = (ImageButton) setListener(mUI.findViewById( R.id.imageButton5 ));
imageButton6 = (ImageButton) setListener(mUI.findViewById( R.id.imageButton6 ));
imageButton7 = (ImageButton) setListener(mUI.findViewById( R.id.imageButton7 ));
pageseeker = (SeekBar) setListener(mUI.findViewById( R.id.pageseeker ));
next = (ImageView) setListener(mUI.findViewById( R.id.next ));
prev = (ImageView) setListener(mUI.findViewById( R.id.prev ));
}
public View setListener(View v) {
if ( v instanceof ImageButton ) {
v.setOnClickListener(this);
return v;
} else if ( v instanceof SeekBar ) {
((SeekBar)v).setOnSeekBarChangeListener(this);
return v;
} else if ( v instanceof ImageView ) {
v.setOnClickListener(this);
return v;
}
return v;
}
@Override
public void onClick(View v) {
switch( v.getId() ){
case R.id.imageButton1:
Log.v("test", "onbutton1clicked");
Toast.makeText(mAct, "Button1 clicked", Toast.LENGTH_LONG).show();
mListener.OnButton1Clicked();
break;
case R.id.imageButton2:
break;
case R.id.imageButton3:
break;
case R.id.imageButton4:
break;
case R.id.imageButton5:
break;
case R.id.imageButton6:
break;
case R.id.imageButton7:
break;
case R.id.next:
break;
case R.id.prev:
break;
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
}
<< 바뀐 MainActivity >>
public class MainActivity extends Activity
implements PTOverlayUi.OverlayUIEventListener {
PTOverlayUi mPTOUi = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FrameLayout catherinLayout = (FrameLayout) this.findViewById( R.id.catherin_ui );
PTOverlayUi overlayUi = new PTOverlayUi(this);
overlayUi.addViewto( catherinLayout );
}
이렇게 바뀐 코드는 PTOverlayUi 에 있는 interface를 통해서 MainActivity와 View가 Presenter라는 중간 계층을 통해 통신할 수 있도록 설계되었습니다. Presenter가 View에 대한 명세와 이에 대한 Listener들을 가지고 등록된 callback 인 mListener를 호출하는 방식입니다.
이런 구조 상에서, Android의 Activity는 하나 이상의 View를 소유하고 통신 매개체 개념을 갖게 됩니다.
예를 들어,
캐서린UI --(page변경)--> MainActivity --> OverlayUI의 seekbar 갱신
OverlayUI의 seekbar Event --> MainActivity --> 캐서린UI
같은 경우가 있을 수 있겠습니다. ~
자, 그럼 이번에는 Fragment를 살펴볼텐데요.
Fragment는 우리가 앞서 살펴본 Presenter 계통의 구현 개념들을 보다 고등화 시킨 개념이라고 생각하시면 됩니다. Honeycomb 이후 Android는 화면에 하나 이상의 View를 보여줄 수 있을 정도로 큰 해상도의 기기도 대상으로 하게 되었고,
이런 환경 하에서 개발자들이 OverlayUI의 경우처럼 두개의 UI를 나누거나 붙히기 위해서 xml을 편집하고 소스코드를 수정하는 번거로움을 줄이고자 나온 것입니다.
허니컴 이전 Android OS 의 UI 지향점
하나의 VIew에 하나의 Activity.
고로 Activity의 생명주기를 View가 따라가게 됩니다.
허니컴 이후 Android OS 의 UI 지향점
하나의 Activity에 다수의 View.
View는 독립적인 생명주기를 갖습니다.
이를 위해서 Fragment는 Activity에 존재하는 생명주기와 같은 개념도 함께 탑재하고 있습니다~ 제가 생각하기에는 웬만하면 Fragment를 사용해 View를 구성하는 것이 좋습니다만, 위의 그림처럼 Fragment를 사용하여 한 화면에 2개 이상의 View를 구성할 생각이 없다면 꼭 사용할 필요는 없습니다.
그럼 앞서 만들었던 Presenter를 Fragment로 한번 구현해볼까요?
<< MainActivity XML >>
<<MainActivity 를 FragmentActivity로 변경>>
public class MainActivity extends FragmentActivity
implements OverlayUIFragment.OverlayUIEventListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (findViewById(R.id.fragment_container) != null) {
CatherinUIFragment firstFragment = new CatherinUIFragment();
firstFragment.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, firstFragment).commit();
}
OverlayUIFragment secondFragment = new OverlayUIFragment();
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, secondFragment).commit();
}
@Override
public void OnButton1Clicked() {
}
... 중략 ...
<< 캐서린 UI XML >>
<< 캐서린 Fragment>>
public class CatherinUIFragment extends Fragment
{
@Override
public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.catherin_ui, container, false);
}
}
<< Overlay UI XML >>
--------------변동 없음---------------
<< Overlay UI Fragment >>
public class OverlayUIFragment extends Fragment
implements View.OnClickListener,
OnSeekBarChangeListener
{
private static int overlayUi_id = R.layout.overlay_ui;
private Activity mAct;
private OverlayUIEventListener mListener;
private ImageButton imageButton1;
private ImageButton imageButton2;
private ImageButton imageButton3;
private ImageButton imageButton4;
private ImageButton imageButton5;
private ImageButton imageButton6;
private ImageButton imageButton7;
private SeekBar pageseeker;
private ImageView next;
private ImageView prev;
View mOverlayUI;
public interface OverlayUIEventListener {
public void OnButton1Clicked();
public void OnButton2Clicked();
public void OnButton3Clicked();
public void OnButton4Clicked();
public void OnButton5Clicked();
public void OnButton6Clicked();
public void OnButton7Clicked();
public void OnNextClicked();
public void OnPrevClicked();
public void OnSeekBarEvent();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void onStart() {
super.onStart();
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mAct = activity;
try {
mListener = (OverlayUIEventListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OverlayUIEventListener");
}
}
@Override
public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mOverlayUI = inflater.inflate(R.layout.overlay_ui, container, false);
imageButton1 = (ImageButton) setListener(mOverlayUI.findViewById( R.id.imageButton1 ));
imageButton2 = (ImageButton) setListener(mOverlayUI.findViewById( R.id.imageButton2 ));
imageButton3 = (ImageButton) setListener(mOverlayUI.findViewById( R.id.imageButton3 ));
imageButton4 = (ImageButton) setListener(mOverlayUI.findViewById( R.id.imageButton4 ));
imageButton5 = (ImageButton) setListener(mOverlayUI.findViewById( R.id.imageButton5 ));
imageButton6 = (ImageButton) setListener(mOverlayUI.findViewById( R.id.imageButton6 ));
imageButton7 = (ImageButton) setListener(mOverlayUI.findViewById( R.id.imageButton7 ));
pageseeker = (SeekBar) setListener(mOverlayUI.findViewById( R.id.pageseeker ));
next = (ImageView) setListener(mOverlayUI.findViewById( R.id.next ));
prev = (ImageView) setListener(mOverlayUI.findViewById( R.id.prev ));
return mOverlayUI;
}
public View setListener(View v) {
if ( v instanceof ImageButton ) {
v.setOnClickListener(this);
return v;
} else if ( v instanceof SeekBar ) {
((SeekBar)v).setOnSeekBarChangeListener(this);
return v;
} else if ( v instanceof ImageView ) {
v.setOnClickListener(this);
return v;
}
return v;
}
@Override
public void onClick(View v) {
switch( v.getId() ){
case R.id.imageButton1:
Log.v("test", "onbutton1clicked");
mListener.OnButton1Clicked();
break;
case R.id.imageButton2:
break;
case R.id.imageButton3:
break;
case R.id.imageButton4:
break;
case R.id.imageButton5:
break;
case R.id.imageButton6:
break;
case R.id.imageButton7:
break;
case R.id.next:
break;
case R.id.prev:
break;
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
이번 글을 통해서 View를 표현 계층으로 다루는 방법을 알아봤습니다.
앞서간 유능한 개발자들이 만든 패턴들을 학습하고 제대로 다루기도 만만한 일은 아니네요.









디자인 패턴 강좌 꾸준히 올려주셔서 감사합니다.
그런데 강좌내 코드는 code highlighter를 사용하시면 더 좋을 것 같네요.
xml 레이아웃 코드도 안보이는 것 같습니다.