본문 바로가기
코딩일기/android studio

안드로이드 독학 22일차 : MVVM 패턴에 대하여(Java)

by 욱파이어니어 2021. 7. 13.
728x90
반응형

MVVM 패턴에 대해서 공부하기 시작한 이유는

안드로이드 프로젝트 대부분이 MVVM 디자인 패턴을 이용해서 만든다는것을 알게되서 였다.

 

그럼 일단 MVVM 디자인 패턴에 대해서 알아보자.

 

 

 

 

MVVM 패턴이란?

 

 

MVVM 패턴은 

Model  - View - ViewModel의 약자인 디자인 패턴이다.

여기서 말하는 디자인 패턴은 소프트웨어를 설계할때 적용하는 아키텍쳐 방식이다.

 

그럼 각각의 약자에 대해 알아보자.

 

 

 

 

Model

데이터를 가져오거나 데이터를 저장하는 등 데이터와 관련된 모든 행위를 하는곳 Model이라고 한다.

 

 

 

View

사용자에게 화면으로 보여지는 모든 곳을 View라고 한다.

 

 

 

ViewModel

ViewModel은 View의 추상화된 형태이다. View에 보여져야하는 데이터와 명령들을 저장하고 있는 부분이다.

따라서 이부분에서는 View로부터 받은 데이터들에 대한 비즈니스 로직들을 처리하는 공간이다.

 

 

 

그렇다면 실제로 MVVM이 어떤 방식으로 동작이 되는지 알아보자.

 

 

 

MVVM 동작 순서

 

1. 사용자의 Action들은 View를 통해서 들어온다.

2. View에서는 사용자로부터 받은 Action을 ViewModel에게 전달한다.

3. ViewModel은 사용자로부터 Action에서 필요한 정보들을 Model에게 요청한다.

4. Model은 ViewModel에게 요청받은 데이터를 가져와서 전달해준다.

5. ViewModel은 Model로부터 전달 받은 데이터를 가공하여서 저장한다.

6. View는 ViewModel과 DataBinding(소스 내용을 xml로 연동하는것)하여 화면에 나타낸다.

 

 

 

 

 

그럼 MVVM이 뭔지 알았으니 실제 예제를 한번 살펴보자.

 

 

 

 


니는 MVVM 패턴을 이전에 내가 만든 RecyclerView 예제를 MVVM 패턴으로 만들어 봤다.

 

그래서 RecyclerView를 잘 모르시는 분들을 아래 링크를 통해서 확인해보고 오시면 이해가 빠를것 같다.

 

https://wpioneer.tistory.com/149?category=1011784 

 

안드로이드 독학 21일차 : Recycler View 사용법

이번에 MVVM 패턴에 대해서 공부하던중 튜토리얼 강의가 Recycler View를 토대로 만들길래 우선적으로 Recycler View에 대해서 공부를 해봤다. Recycle View란? RecyclerView는 이름에 있는 Recycler 라는 단어만..

wpioneer.tistory.com

 

 

 

 

MVVM 디자인 패턴을 사용 하려면 일단 ViewModel과 LiveData를 사용해야하기 때문에 dependencies를

추가해야 한다.

 

 

 

 

1. dependencies 추가하기

 

//    // Lifecycle components 라는데 이건 이제 뷰나 프래그먼트들로부터 받은 데이터들을 어디로 이동하는지 관찰하기위해서 사용하는 라이브러리
//    //mvvm 디자인 패턴을 사용하는데 필수적인 부분
//    def archLifecycleVersion = '1.1.1'
//    implementation "android.arch.lifecycle:extensions:$archLifecycleVersion"
//    annotationProcessor "android.arch.lifecycle:compiler:$archLifecycleVersion"

    //ViewModel 사용에 필요한 변수
    def lifecycle_version = "2.4.0-alpha02"
    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
    // LiveData
    implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"

주석 처리되어 있는 부분들은 androidx로 migrate 안한 사람들은 저 소스로 사용해야 한다. 하지만 나는 최근에 androidx 로 변환 했기 때문에 아래 소스를 통해서 사용했다.

 

그리고 floating actionbar를 사용할것이기 떄문에 아래 dependencies도 추가하였다.

    //+ floating action bar 만들기 위해서
    implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"

 

 

 

dependencies를 추가했다면 MVVM 패턴으로 사용할 준비가 완료 되었으니 내가 이번 예제에서 무엇을 할건지

설명을 하겠다.

 

 

 

나는 이번 예제에서 RecyclerView에서 + 버튼을 누르면 리스트가 하나씩 추가되는것을 만들 예정이다.

보이는 화면은 아래와 같다.

 

 

(중간에 +버튼을 2번더 눌러서 그렇지 실제론 하나만 추가됩니다.)

 

따라서 아래 화면을 만들려면 xml을 아래와 같다.

 

 

 

2. xml로 화면 만들기

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/recycler_view">


    </androidx.recyclerview.widget.RecyclerView>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ProgressBar
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/progress_bar"
            android:layout_centerInParent="true"
            android:visibility="gone"/>

    </RelativeLayout>


    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end|bottom"
        android:src="@drawable/ic_add_white_24dp"
        android:layout_margin="16dp" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

 

위소스에서 설명을 하자면 CoordinatorLayout은 FloatingActionBar를 사용하기 위해서 겉에 같쌈 컨테이너고

ProgressBar는 로딩부분을 표현하기 위해서 만든것이다.

 

그럼 이제 RecyclerView의 xml을 보자.

 

layout_listitem.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/parent_layout">

    <de.hdodenhof.circleimageview.CircleImageView
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:id="@+id/image"
        android:src="@mipmap/ic_launcher"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="뭐징"
        android:layout_toRightOf="@id/image"
        android:textColor="#000"
        android:layout_centerVertical="true"
        android:layout_marginLeft="30dp"
        android:textSize="17sp"
        android:id="@+id/image_name"/>

</RelativeLayout>

위 소스 모양은 우리가 list에서 보이는 item의 모양이다.

 

 

그럼 이제 xml을 봤으니 MainActivity를 보자.

 

 

 

3. View 만들기

 

MaintActivity는 약간 View와 같은 역할로 ViewModel을 호출해서 데이터를 받은 후 데이터 바인딩만 해주면 된다.

ViewModel에 대해서 잘 모르시는 분들은 아래 링크를 통해서 확인해보면 될것 같다.

 

https://wpioneer.tistory.com/150?category=1011784 

 

안드로이드 독학 22일차 : ViewModel 설명 및 사용법

MVVM 디자인 패턴에 대해서 공부하던중 ViewModel에 대해서 공부가 필요해 ViewModel에 대해서 공부해보았다. ViewModel이란? ViewModel은 안드로이드 개발자 문서에서는 아래와 같이 적혀져 있다. 이말이

wpioneer.tistory.com

 

MainActivity에 대한 자세한 내용은 내가 소스안에 주석 처리 해놨으니 확인해보면 될것 같다.

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    //vars
    private FloatingActionButton mFab; //+ FloatingButton 담기위한 변수
    private RecyclerView mRecyclerView; // RecyclerView 담기위한 변수
    private RecyclerViewAdapter mAdapter; //RecyclerAdapter 담기위한 변수
    private ProgressBar mProgressBar; // ProgressBar 담기위한 변수
    private MainActivityViewModel mMainActivityViewModel; //MainActivityViewModel 담기위한 변수

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "onCreate: started.");

        //각각의 버튼의 뷰들을 연결 시킴
        mFab = findViewById(R.id.fab);
        mRecyclerView = findViewById(R.id.recycler_view);
        mProgressBar = findViewById(R.id.progress_bar);

        //ViewModel을 생성하는 부분
        mMainActivityViewModel = new ViewModelProvider(this).get(MainActivityViewModel.class);

        //ViewModel의 init() 호출
        mMainActivityViewModel.init();

        //getNicePlaces()에서 변화가 생기면 진입하는데 해당 액티비티의 상태가 Actice가 아니면 바뀐내용을 받아오지 않고
        //Active가 되면 가장최신의 업데이트 내용을 받아온다.
        mMainActivityViewModel.getNicePlaces().observe(this, new Observer<List<NicePlace>>() {
            @Override
            public void onChanged(List<NicePlace> nicePlaces) {
                //해당 메소드 호출
                mAdapter.notifyDataSetChanged(); //해당 메소드르 호출해서 Adapter를 update
            }
        });

        mMainActivityViewModel.getIsUpdating().observe(this, new Observer<Boolean>() {
            //데이터가 바뀌었다면 호출
            @Override
            public void onChanged(Boolean aBoolean) {
                if(aBoolean){ //값이 바뀌었다면 진입
                    showProgressBar(); //ProgressBar 보여줌
                }else{ //값이 바뀌지 않았다
                    hideProgressBar();
                    mRecyclerView.smoothScrollToPosition(mMainActivityViewModel.getNicePlaces().getValue().size()-1);
                }
            }
        });

        mFab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mMainActivityViewModel.addNewValue(new NicePlace( // 새로 값을 추가하는 버튼을 눌렀을때
                        "https://i.imgur.com/ZcLLrkY.jpg",
                        "Washington"
                ));
            }
        });

        initRecyclerView(); //위에서 가져온 ImageList를 리사이클 뷰에 집어 넣는다.
    }

    private void hideProgressBar() {
        mProgressBar.setVisibility(View.GONE);
    }

    private void showProgressBar() {
        mProgressBar.setVisibility(View.VISIBLE);
    }

    private void initRecyclerView(){
        mAdapter = new RecyclerViewAdapter(mMainActivityViewModel.getNicePlaces().getValue(),this);
        RecyclerView.LayoutManager linearLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(linearLayoutManager);
        mRecyclerView.setAdapter(mAdapter);
    }
}

자그럼 이제 View(MainActivity)를 만들었으니 이제 ViewModel을 살펴보자.

 

 

4. ViewModel 만들기

 

ViewModel에서 하는 일은 Model을 호출하여서 데이터를 불러와 LiveData에 저장하는것이다.

(LiveData에 대해서 잘 모르시는 분들은 아래 링크를 통해서 보고오시면 된다.)

 

https://wpioneer.tistory.com/151?category=1011784 

 

안드로이드 독학 22일차 : LiveData와 MutuableLiveData 설명 및 사용법

LiveData는 ViewModel을 배우면서 배워야할거같아서 따로 알아보게 되었다. LiveData란? LiveData는  식별 가능한 데이터 홀더 클래스이다. 이게 무슨 말이냐면 LiveData는 무언가를 관찰하며 바뀌는게 있는

wpioneer.tistory.com

 

소스에 대한 자세한 설명은 주석으로 해놨으니 확인해보면 될것 같다.

public class MainActivityViewModel extends ViewModel {

    //live date 클래스의 서브 클래스이다. 즉 이건 livedata 클래스를 상속하는 클래스이다.
    //mutable 이건 바뀔수있다는 뜻이다. 반면에 livedata는 immutable이다 즉 바뀔수 없다는 뜻이다.
    // 따라서 MutableLiveData는 mNicePlaces.setValue(),  mNicePlaces.postValue()로 값을 바꿀수 있지만
    // LiveData는 바꿀수 없다. 따라서 LiveData는 오직 관찰만가능하다.
    private MutableLiveData<List<NicePlace>> mNicePlaces; //MutuableLiveData 만들었음
    private NicePlaceRepository mRepo;
    private MutableLiveData<Boolean> mIsUpdating = new MutableLiveData<>(); //데이터를 받는게 완료되면 progress bar 숨기고 아니면 보여주려고 만든 변수


    public void init(){
        if(mNicePlaces!= null){ //null이 아니라면 함수 종료
            return;
        }
        //mNicePlate가 Null이 아닐떄
        mRepo = NicePlaceRepository.getInstance(); //NicePlaceRepository 객체 전달 받는다.
        mNicePlaces = mRepo.getNicePlaces(); //전달 받은 NicePlaceRepository로  NicePlace 데이터 전달 받음
    }

    public void addNewValue(final NicePlace nicePlace){
        mIsUpdating.setValue(true); //true로 해준다.

        new AsyncTask<Void, Void, Void>(){
            @Override
            protected void onPostExecute(Void aVoid) {
                super.onPostExecute(aVoid);
                List<NicePlace> currentPlace = mNicePlaces.getValue(); //가장 최신의 NicePlace가 담긴 List를 받음
                currentPlace.add(nicePlace); //List에 매개변수로 받은 값 추가
                mNicePlaces.postValue(currentPlace); //mNicePlace 수정
                mIsUpdating.postValue(false); //true로 했음
            }

            @Override
            protected Void doInBackground(Void... voids) { //2초 뒤에 실행시킴
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return null;
            }
        }.execute();
    }

    public LiveData<List<NicePlace>> getNicePlaces(){
        return mNicePlaces;
    } //mNicePlace return

    public LiveData<Boolean> getIsUpdating(){
        return mIsUpdating;
    } //misUpdating return
}

 

 

그럼 이제 해야할것은 ViewModel에서 호출한 Model을 만드는것이다.

 

 

5. Model 만들기

 

Model에서는 서버나 DB에서 데이터를 불러오고 저장하고 수정하는곳이다.

나는 예제를 통해서 간단하게 할거라 실제로 서버와 연동은 하진않고 대충 Model에서 값을 설정해서

값을 전달하는것으로 했다.

 

public class NicePlaceRepository {

    private static NicePlaceRepository instance;
    private ArrayList<NicePlace> dataSet = new ArrayList<>();

    public static NicePlaceRepository getInstance(){
        if(instance == null){ //instance가 비어 있다면 진입
            instance = new NicePlaceRepository();
        }
        return instance;
    }

    public MutableLiveData<List<NicePlace>> getNicePlaces(){
        setNicePlaces(); //NicePlace 데이터 받음

        MutableLiveData<List<NicePlace>> data = new MutableLiveData<>(); //MutableLiveData 객체 생성
        data.setValue(dataSet); //MutableLiveData 객체에 위에서 setNicePlaces()로 저장한 데이터로 초기화한다.
        return data; //MutableLiveData return
    }

    private void setNicePlaces(){
        dataSet.add(
                new NicePlace("https://i.redd.it/tpsnoz5bzo501.jpg",
                        "Trondheim")
        );
        dataSet.add(
                new NicePlace("https://i.redd.it/qn7f9oqu7o501.jpg",
                        "Portugal")
        );
        dataSet.add(
                new NicePlace("https://i.redd.it/j6myfqglup501.jpg",
                        "Rocky Mountain National Park")
        );
        dataSet.add(
                new NicePlace("https://i.redd.it/0h2gm1ix6p501.jpg",
                        "Havasu Falls")
        );
        dataSet.add(
                new NicePlace("https://i.redd.it/k98uzl68eh501.jpg",
                        "Mahahual")
        );
        dataSet.add(
                new NicePlace("https://i.redd.it/k98uzl68eh501.jpg",
                        "Frozen Lake")
        );
        dataSet.add(
                new NicePlace("https://i.redd.it/obx4zydshg601.jpg",
                        "Austrailia")
        );
    }
}

 

그리고 데이터를 저장할때 중요한 Class 정보는 아래와 같다.

 

NicePlace Class

public class NicePlace {

    private String title; //사진 제목을 담을 변수
    private String imageUrl; // imageurl을 담을 변수

    public NicePlace() {}

    public NicePlace(String imageUrl,String title) {
        this.title = title;
        this.imageUrl = imageUrl;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getImageUrl() {
        return imageUrl;
    }

    public void setImageUrl(String imageUrl) {
        this.imageUrl = imageUrl;
    }

    @Override
    public String toString() {
        return "NicePlace{" + "title='" + title + '\'' + ", imageUrl='" + imageUrl + '\'' + '}';
    }
}

 

자세한 내용은 소스에 주석으로 달아놨으니 확인해보면 된다.

 

 

 

 

 

이렇게 위 소스들을 보면 View에서는 ViewModel을 호출하기만 하고 ViewModel은 Model로 부터 데이터를 받아오고

받아온 데이터를 View로 전달하는것을 볼수가 있다.

 

 

 

이렇게 각각의 역할을 나눠서 모듈화 하면 나중에 유지보수가 편하고 성능도 좋아진다.

 

 

 

참고 사이트

https://youtu.be/ijXjCtCXcN4

 

반응형