MVVM 패턴에서 ViewModel이 직접적으로 Activity나 Fragment와 같은 View 요소에 접근해서 전환을 제어하는 것은 지양해야 한다.
대신 ViewModel은 View에게 전환 이벤트를 전달하고, View는 이를 수신하여 적절한 액션을 수행하도록 만들어야 한다.
ViewModel에서 Activity 전환을 구현하는 방법으로 Event Wrapper가 있다.
이벤트 래퍼는 한 번 발생한 이벤트를 처리한 후에는 다시 처리하지 않도록 해준다.
주로 ViewModel에서 LiveData<Event<T>> 형태로 사용되며, ViewModel에서 이벤트를 발생시키고, 이를 관찰하는 View에서 처리하는 방식으로 구현된다.
다음은 Event Wrapper를 이용해 ViewModel에서 이벤트를 발생시켜 액티비티를 전환하는 예시이다.
먼저 DataBinding을 사용하기 위해 build.gradle 에 dataBinding을 추가하였다.
android {
...
buildFeatures{
dataBinding = true
}
}
다음으로 Event Wrapper 클래스를 만들었다.
public class Event<T> {
private T content;
private boolean hasBeenHandled = false;
public Event(T content){
this.content = content;
}
public T getContentIfNotHandled() {
if(hasBeenHandled)
return null;
else{
hasBeenHandled = true;
return content;
}
}
public T peekContent() {
return content;
}
}
Event 클래스에는 단일 이벤트가 이미 실행됐는지를 판단하는 hasBeenHandled 변수가 포함되어 있다.
초기값은 false였다가 단일 이벤트가 실행되면 hasBeenHandled 값을 true로 변경해줍니다.
getContentIfNotHandled() 메소드는 이벤트를 한 번만 반환하고, 그 이후에는 null을 반환합니다.
다음은 ViewHolder 클래스를 만들었다.
public class SampleViewModel extends ViewModel {
private final MutableLiveData<Event<String>> nextActivityEvent = new MutableLiveData<>();
public LiveData<Event<String>> getNextActivityEvent() {
return nextActivityEvent;
}
public void onButtonClicked(View view) {
nextActivityEvent.setValue(new Event<String>("event"));
}
}
SampleViewModel에선 MutableLiveData<Event<String>> 타입 객체를 만들고
onButtonClicked 메소드에서 해당 객체에 Event 객체를 만들어 setValue()로 값을 만들어 넣어주었다.
onButtonClicked 메소드는 데이터바인딩을 통해 activity_main.xml의 버튼에 바인딩해주었다.
<?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.example.viewmodel_event.SampleViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:onClick="@{viewModel::onButtonClicked}"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

MainActivity.java에선 데이터바인딩과 뷰모델을 설정하고,
Observer를 통해 화면전환이 이루어지는 코드를 작성하였다.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Data Binding 설정
//setContentView(R.layout.activity_main);
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// viewModel 생성
SampleViewModel viewModel = new ViewModelProvider(this).get(SampleViewModel.class);
// viewModel을 레이아웃에 바인딩
binding.setViewModel(viewModel);
binding.setLifecycleOwner(this);
// Observer 설정
viewModel.getNextActivityEvent().observe(this, new Observer<Event<String>>() {
@Override
public void onChanged(Event<String> voidEvent) {
if(voidEvent != null && voidEvent.getContentIfNotHandled() != null){
Intent intent = new Intent(MainActivity.this, NextActivity.class);
startActivity(intent);
}
}
});
}
}
SampleViewModel의 getNextActivityEvent()를 참조하여 값이 변한다면 이벤트 레퍼의 getContentIfNotHandled()메소드를 실행, 이벤트가 발생되었는지 확인한다.
이벤트가 발생되지 않았다면(voidEvent != null && voidEvent.getContentIfNotHandled() != null)
액티비티를 전환하는 코드가 실행된다.
해당 이벤트가 실행되면 hasBeenHandled 값이 true로 변경되어 이벤트가 더 이상 실행되지 않도록 해준다.