ViewModel이란 액티비티와 프래그먼트에서 사용되는 UI관련 데이터를 보관하고, 관리하기 위해 디자인 되었습니다.
왜 UI 데이터를 보관하고 관리하기 위해 디자인 되었냐면,
ViewModel이 있기전에는 UI컨트롤러의 라이플 사이클에 따라 그 안에 저장해 두었던 UI 관련 임시 데이터들이 모두 사라집니다.
그리고 액티비티가 종료되기 직전에 호출되는 onSaveInstanceState() 콜백에서 액티비티의 상태 또는 데이터를 저장할 수 있지만 직렬화할 수 없는 객체는 저장할 수 없습니다. 거기다 많은 양의 데이터를 저장할 수도 없었습니다.
그래서 나온 디자인이 ViewModel 디자인입니다.
그럼 사라지는 가장 대표적인 경우는 가로/세로(PORTRAIT->LANDSCAPE, LANDSCAPE->PORTRAIT)로 변경할 경우를 확인해 보겠습니다. (아래 code에서 MainFragment Code만 수정될 예정입니다.)
우선 File->Project -> NewProject를 선택 후
Fragment+ViewModel을 선택하여 Project를 만들면 MainActivity, MainFragment, MainModelMainViewModel이 생성됩니다. 이렇게 생선된 Project에 ViewModel의 사용안할 때와 할 때를 만들어 보겠습니다. (MainActivity는 따로 수정할 필요가 없습니다.)
Fragment에 layout을 정해보겠습니다.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<LinearLayout
android:layout_width="200dp"
android:layout_height="200dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Team A" />
<TextView
android:id="@+id/team_a_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="50sp"/>
<Button
android:id="@+id/team_a_point"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+1 point"/>
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="150dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:background="#000000"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Team A" />
<TextView
android:id="@+id/team_b_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="50sp"/>
<Button
android:id="@+id/team_b_point"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+1 point"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
그리고 Fragment에서 오른쪽 왼쪽의 버튼 누를 경우 각각의 숫자가 1씩 올라가고, Team B의 점수는 1초마다 1씩 값이 상승하고, Team A는 Team B의 점수가 10일 경우 1씩 추가가 됩니다.
class MainFragment : Fragment(),View.OnClickListener {
companion object {
fun newInstance() = MainFragment()
}
private lateinit var mTeamAScore : TextView
private lateinit var mTeamBScore : TextView
private var mScoreA = 0
private var mScoreB = 0
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.main_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mTeamAScore = view!!.team_a_score
mTeamBScore = view!!.team_b_score
view!!.team_a_point.setOnClickListener(this)
view!!.team_b_point.setOnClickListener(this)
mTeamAScore.text = mScoreA.toString()
mTeamBScore.text = mScoreB.toString()
Thread(Runnable {
while (true){
Thread.sleep(1000)
activity?.runOnUiThread {
mScoreB++
if(mScoreB%10 == 0){
mScoreA++
mTeamAScore.text = mScoreA.toString()
}
mTeamBScore.text =mScoreB.toString()
}
}
}).start()
}
override fun onClick(v: View?) {
v?.let{
when(it.id){
R.id.team_a_point->{
mScoreA++
mTeamAScore.text = mScoreA.toString()
}
R.id.team_b_point->{
mScoreB++
mTeamBScore.text = mScoreB.toString()
}
}
}
}
}
결과를 보면
위의 결과를 볼 수 있습니다. 하지만 위의 결과는 화면을 세로에서 가로로 할 경우 Team A와 Team B의 값이 0 0 으로 초기화 되어 버립니다. 그러면 ViewModel을 사용하여 동작을 시켜보겠습니다.
MainviewModel에 Fragment에 있던 score를 추가합니다.
class MainViewModel : ViewModel() {
var scoreTeamA: Int = 0
var scoreTeamB: Int = 0
fun increascountTeamA(){
scoreTeamA++
}
fun increascountTeamB(){
scoreTeamB++
}
}
MaingFragment에는 ViewModel인 MainViewModel을 추가하고 score 변수들을 제거합니다.
class MainFragment : Fragment(),View.OnClickListener {
companion object {
fun newInstance() = MainFragment()
}
private lateinit var mTeamAScore : TextView
private lateinit var mTeamBScore : TextView
private lateinit var viewModel: MainViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.main_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
mTeamAScore = view!!.team_a_score
mTeamBScore = view!!.team_b_score
view!!.team_a_point.setOnClickListener(this)
view!!.team_b_point.setOnClickListener(this)
mTeamAScore.text = viewModel.scoreTeamA.toString()
mTeamBScore.text = viewModel.scoreTeamB.toString()
Thread(Runnable {
while (true){
Thread.sleep(1000)
activity?.runOnUiThread {
viewModel.increascountTeamB()
if(viewModel.scoreTeamB%10 == 0){
viewModel.increascountTeamA()
mTeamAScore.text = viewModel.scoreTeamA.toString()
}
mTeamBScore.text = viewModel.scoreTeamB.toString()
}
}
}).start()
}
override fun onClick(v: View?) {
v?.let{
when(it.id){
R.id.team_a_point->{
viewModel.increascountTeamA()
mTeamAScore.text = viewModel.scoreTeamA.toString()
}
R.id.team_b_point->{
viewModel.increascountTeamB()
mTeamBScore.text = viewModel.scoreTeamB.toString()
}
}
}
}
}
이렇게 작업 후 실행하면 동작성을 같지만 가로/세로 변경시 데이터 초기화는 사라집니다.
ViewModel에서 이것이 가능한 이유는 우선 ViewModel의 생명주기로 알아 보겠습니다.
위의 그림을 보면 ViewModel을 Activity가 creatae될 때 생성하면 onDestroy이가 될 때 사라집니다. 그렇기 때문에 data를 유지 할 수 있습니다. (Fragment의 경우에 비슷합니다.)
그럼 위의 ViewModel 생성 code를 한단 계식 따라가 보겠습니다.
ViewModelProviders.of(this).get(MainViewModel::class.java)
ViewModel은 위의 code로 생성이 됩니다. 즉
ViewModelProviders.of() 메서드를 통해 ViewModelProvider 객체를 얻고
ViewModelProvider.get() 메서드를 통해 ViewModel 인스턴스를 얻는 것입니다.
그럼 ViewModelProviders.of() 를 확인해 보겠습니다.
public static ViewModelProvider of(@NonNull Fragment fragment) {
return of(fragment, null);
}
위의 code에서 of(@NonNull Fragment fragment)는 제가 Fragment에서 ViewModel을 생성했기 때문입니다. Activity에서도
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
return of(activity, null);
}
이런 형태도 있습니다. 아무튼 다음 of의 code를 확인해보면
public static ViewModelProvider of(@NonNull Fragment fragment, @Nullable Factory factory) {
Application application = checkApplication(checkActivity(fragment));
if (factory == null) {
factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
}
return new ViewModelProvider(fragment.getViewModelStore(), factory);
}
Fragment를 통하여 activity를 획득하고, 그 획득한 activity를 통하여 Application을 획득합니다.
private static Activity checkActivity(Fragment fragment) {
Activity activity = fragment.getActivity();
if (activity == null) {
throw new IllegalStateException("Can't create ViewModelProvider for detached fragment");
}
return activity;
}
private static Application checkApplication(Activity activity) {
Application application = activity.getApplication();
if (application == null) {
throw new IllegalStateException("Your activity/fragment is not yet attached to "
+ "Application. You can't request ViewModel before onCreate call.");
}
return application;
}
그 다음 ViewModelProvider.AndroidViewModelFactory.getInstance(application) 을 통하여 factory를 만드는데 이때 return하는 sInstance는 sigletone 방식입니다.
private static AndroidViewModelFactory sInstance;
//.....
public static AndroidViewModelFactory getInstance(@NonNull Application application) {
if (sInstance == null) {
sInstance = new AndroidViewModelFactory(application);
}
return sInstance;
}
그리고 나서 new ViewModelProvider(fragment.getViewModelStore(), factory) 를 통하여 ViewModelProvider 객체를 생성합니다. 여기서 getViewModelStore()를 타고 들어가보면
@NonNull
ViewModelStore getViewModelStore(@NonNull Fragment f) {
ViewModelStore viewModelStore = mViewModelStores.get(f.mWho);
if (viewModelStore == null) {
viewModelStore = new ViewModelStore();
mViewModelStores.put(f.mWho, viewModelStore);
}
return viewModelStore;
}
이런 형태를 볼 수 있는데, mViewModelStores는 HasMap으로 관리하여 프레그 먼트별로 데이터를 관리하는 것을 볼 수 있습니다. Activity의 경우에는 다음과 같습니다.
public ViewModelStore getViewModelStore() {
if (getApplication() == null) {
throw new IllegalStateException("Your activity is not yet attached to the "
+ "Application instance. You can't request ViewModel before onCreate call.");
}
if (mViewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
// Restore the ViewModelStore from NonConfigurationInstances
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}
이렇게 ViewModelProvider객체를 생성 후에 ViewModelProvider.get()를 통하여 ViewModel 인스턴스를 획득합니다. 그 code를 보면
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
String canonicalName = modelClass.getCanonicalName();
if (canonicalName == null) {
throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
}
return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}
getCanonicalName를 통하여 전체경로의 클래스 명을 획득하고 (ex) com.jupark.viewmodeltest.ui.main.MianViewMoel) 이를 통하여 ViewModel을 획득합니다.
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
//noinspection unchecked
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
} else {
viewModel = (mFactory).create(modelClass);
}
mViewModelStore.put(key, viewModel);
//noinspection unchecked
return (T) viewModel;
}
ViewModel을 획득하는 code를 보면 아까 ViewModelProvider 객체를 생성할 때 만든 ViewModelStore에서 아까 만든 key 값으로 Viewmodel을 찾아서 인스턴스를 생성합니다. 그리고 만일 MainFragment에서 ViewModel 생성 시 ViewProvider.of에 Fragment 대신 Activity를 넣는다면 Activity에 따른 ViewModel을 얻을 수 있습니다.
이렇게 ViewModel은 생성이 되고 이 ViewModel은 fragment나 activity에서 생성시 생성한 곳이 destroy되지 않는 이상 데이터가 유지 되는 것을 확인 할 수 있습니다.
'2023년 이전 > Android' 카테고리의 다른 글
Android - WorkManager(1) (1) | 2019.10.16 |
---|---|
Android -LiveData (0) | 2019.10.15 |
custom 숫자 키 입력 - 보안 숫자 keyboard (0) | 2019.09.06 |
Android - Kotlin을 사용하여 Listener 등록 (0) | 2019.08.29 |
Android 잠금화면 위에 Activity 보여주기 (0) | 2019.08.28 |