본문 바로가기
2023년 이전/Android

Android - ViewModel

by JeongUPark 2019. 10. 15.
반응형

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되지 않는 이상 데이터가 유지 되는 것을 확인 할 수 있습니다.

 

반응형