1. 개요
Android 개발을 하면서 단순 기능 구현 하드코딩 하기 보다는 점차 리팩토링 과정을 거치고, 새로운 라이브러리 또는 디자인 패턴등을 적용시켜 가면서 프로그래밍의 실력을 키워나가보자는 의미에서 이 글을 작성해 봅니다.
Android 개발에 적합한 디자인 패턴은 무엇이 있을까요? 대표적으로는 MVP / MVVM 패턴이 있을것 같습니다. 무엇이 더 적합하다고 말하긴 그렇지만 자신의 프로젝트 성격과 규모, 개인의 취향에 따라 선택하면 될 것 같습니다. 본 글에서는 MVVM 패턴을 이용하여 일반적인 Android Project 코드를 좀 더 효율적이고 편리하게 바꿔보려고 합니다.
Google 에서 Android MVVM 키워드만 쳐도 수많은 글들이 나오고 다들 자신만의 패턴으로 구현한 예제들이 많이 있습니다. 저는 이 중에서 https://github.com/MindorksOpenSource/android-mvvm-architecture 해당 예제를 참고하여 새로 구현하였습니다.
2. 예제
https://github.com/chosw1029/Android_MVVM_StarterKit
기본적인 뼈대만 구성한 프로젝트 입니다. 해당 소스를 다운 받은 다음 아래처럼 패키지명을 변경하고 개발을 진행하시면 될 것 같습니다.
* 해당 프로젝트는 Android Databinding, Dagger 2, RxJava, FastAndroidNetworking, Glide를 기반으로 구성되어있습니다.
1) 패키지명을 변경
위 그림에서 com.nextus.baseapp 부분을 선택한 후 화살표 부분의 설정 아이콘을 누르면 붉은색으로 밑줄 친 부분처럼 Hide Empty Middle Packages가 체크된 상태로 되있습니다. 이 부분을 눌러서 체크를 해제합니다.
해제하면 위 그림처럼 분리가 되는데 여기서 우측클릭을 한후 Refactor -> Rename 버튼을 누릅니다.
이러한 경고 팝업이 뜨는데 Rename package 버튼을 눌러 패키지 변경을 수행합니다.
2) Application ID 변경
붉은색으로 밑줄 친 부분의 applicationId 부분을 변경한 package 명과 일치하게 수정합니다.
3. 구성
4. 구현 방법
@Module
public abstract class ActivityBuilder {
@ContributesAndroidInjector(modules = MainModule.class)
abstract MainActivity bindMainActivity();
}
각각의 Activity에서 Module을 만들고 해당 Module을 ActivityBuilder 에서 이들을 모아서 합쳐주는 방식으로 구현합니다.
이렇게 MainActivity에 대한 모듈을 추가해주고 MainActivity에서도 모듈을 작성해줍니다.
우선 큰 화면 하나당 ui package 안에 해당 화면의 패키지를 만들어 줍니다. 여기서는 main 이라는 이름으로 만들었습니다.
해당 화면안에는 하나의 Activity가 존재하고, Activity와 연결된 ViewModel ( VM ) , Activity와 ViewModel을 연결해주는 Navigator (Nav), Activity 에 Inject를 할 수 있도록 해주는 Module 이렇게 4개의 파일로 구성되어있습니다.
MainModule 에서 MainVM을 Provides 했기 때문에 MainActivity에서@Inject를 통해 주입할 수 있습니다.
5. 기본 코드 구성
public class MainActivity extends BaseActivity<ActivityMainBinding, MainVM> {
// MainModule에서 Provides로 선언한 MainVM을 주입한다.
@Inject MainVM viewModel;
public static Intent newIntent(Context context) {
return new Intent(context, MainActivity.class);
}
@Override
public int getBindingVariable() {
return BR.viewModel;
}
@Override
public int getLayoutId() {
return R.layout.activity_main;
}
@Override
public MainVM getViewModel() {
viewModel.setNavigator(this::showToastMsg);
return viewModel;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
public void showToastMsg(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
}
기본 구성은 위 코드처럼 이루어져 있습니다. BaseActivity에는 <T extends ViewDataBinding, V extends BaseViewModel> 형태의 Generic Class 요소를 받고 있습니다.
ActivityMainBinding 은 android databinding 을 이용하면 자동 생성되는 클래스로 아래처럼 레이아웃을 <layout></layout> 으로 감싸면 생성됩니다.
자세한 android databinding 사용 방법은 https://developer.android.com/topic/libraries/data-binding/start 여기를 참고하면 됩니다.
아무튼 해당 파일은 activity_main.xml 이라는 이름으로 작성되어 있으며, 별다른 설정을 하지 않는 한 해당 파일의 이름을 기반으로 Binding Class가 생성됩니다.
( activity_main.xml -> ActivityMainBinding / activity_user.xml -> ActivityUserBinding )
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.nextus.baseapp.ui.main.MainVM"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Toast"
android:onClick="@{() -> viewModel.onShowToastMsg()}"
app:layout_constraintEnd_toEndOf="@+id/textView"
app:layout_constraintStart_toStartOf="@+id/textView"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
< MainActivity.class >
@Override
public MainVM getViewModel() {
viewModel.setNavigator(this::showToastMsg);
return viewModel;
}
해당 부분에서 setNavigator부분은 Navigator를 ViewModel에 등록했을 경우 Activity에서 해당 Nav 함수를 구현하기 위해 작성한 부분입니다. Nav가 필요 없으면 setNavigator부분은 지워도 됩니다. 지금은 MainVM과 MainActivity를 MainNav로 연결하였고 MainNav에게 MainActivity의 showToastMsg 함수를 넘겨주었습니다.
< MainVM.class >
public class MainVM extends BaseViewModel<MainNav> {
public MainVM(DataManager dataManager, SchedulerProvider schedulerProvider) {
super(dataManager, schedulerProvider);
}
public void onShowToastMsg() {
getNavigator().showToastMsg("Test Toast!!");
}
}
MainVM는 BaseViewModel을 상속하고 있으며, BaseViewModel은 Navigator Interface 를 Generic 인자로 받고 있습니다. Nav를 사용하는 경우 인자로 전달해주면 됩니다.
onShowToastMsg() 함수는 activity_main.xml 화면에 있는 버튼과 binding 되어있는 함수입니다. 버튼을 클릭시 해당 이벤트가 여기로 넘어오고, getNavigator를 통해 Message를 MainActivity에 전달합니다.
< activity_main.xml >
<data>
<variable
name="viewModel"
type="com.nextus.baseapp.ui.main.MainVM"/>
</data>
<-- 중략 -->
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Toast"
android:onClick="@{() -> viewModel.onShowToastMsg()}" << Databinding
app:layout_constraintEnd_toEndOf="@+id/textView"
app:layout_constraintStart_toStartOf="@+id/textView"
app:layout_constraintTop_toBottomOf="@+id/textView" />
해당 xml의 <data>부분에서 viewModel 인자로 MainVM 을 선언하여 연결하였습니다.
android:onClick="{@() -> viewModel.onShowToastMsg()} 이부분을 통해 onClick 이벤트 발생 시 viewModel에 선언되어 있는 onShowToastMsg()함수를 호출 하게 됩니다.