Groo

ViewModel & LiveData 라이브러리 본문

Android

ViewModel & LiveData 라이브러리

김주엽 2020. 3. 19. 03:32

안녕하세요, 오늘은 다음 시간에 배울 MVVM 디자인 패턴에 대해서 알아보기 전 꼭 알아야 하는 내용인
ViewModel 뷰 모델과 LiveData 라이브 데이터에 대해서 알아보도록 하겠습니다. 정말 중요한 내용입니다.

🙈 ViewModel 뷰 모델은 무엇이고 왜 사용해야하나요?

ViewModel 클래스는 UI 관련 데이터를 저장하고 관리하도록 설계되었습니다. ViewModel 클래스를 사용해야하는 두 가지 이유를 살펴보겠습니다. 만약 사용자가 앱의 화면을 전환한다면 시스템의 UI 컨트롤러가 종료되어 기존의 데이터를 잃어버리게됩니다.

 

이러한 문제를 해결하기 위해서 간단한 데이터의 경우에는 onSavedInstance 메스드를 활용하여 데이터를 저장하고 복원할 수 있지만 만약 데이터의 값이 복잡하거나 또는 양이 많아질 경우에는 메모리 리크 현상과 많은 리소스 낭비 등 문제가 발생을 합니다.

 

ViewModel 뷰 모델의 수명 주기 구조의 모습입니다.

그러나 ViewModel 뷰 모델을 사용한다면 뷰 모델의 수명 주기 방식에 따라 액티비티 또는 프래그먼트가 onDestroy 되기 전 까지 ViewModel 뷰 모델은 종료가 되지 않기 때문에 UI의 데이터를 계속 끝까지 잃어버리지 않고 저장이 된다는 큰 장점이 존재합니다.

 

ViewModel 뷰 모델을 활용하기 전의 View 뷰 클래스에서의 역할

위의 사진은 ViewModel 뷰 모델을 활용하지 않은 상태에서 뷰 클래스가 해야하는 역할을 나타낸 것입니다. 모든 일 처리를 뷰 클래스에서 모두 진행을 해야하기 때문에 View 뷰 클래스의 코드는 복잡해지고 유지 보수가 힘이 들며 무거운 클래스가 되어버립니다.

 

View 뷰의 역할을 ViewModel 뷰 모델에 분리한 모습입니다.

그러나 위와 같이 View 클래스에서 모두 해야하는 일들을 ViewModel 뷰 모델에 전달을 하면서 View 뷰 클래스의 역할이 훨씬 줄어든 것을 볼 수 있습니다. 즉 ViewModel 뷰 모델을 활용하면서 기존의 UI 컨트롤러 로직에서 뷰 데이터의 소유권을 분립합니다.

🐶 Build.Gradle 설정을 해봅시다!

그럼 먼저 ViewModel 뷰 모델과 추후 LiveData 라이브 데이터를 사용하기 위해서 프로그램 내에 Build.Gradle 파일에 구현 코드를 작성해보겠습니다. 프로그램 내 설정을 하는 코드는 Google Android Developer 공식 사이트에서 더 정확히 알 수 있습니다.

 

Google Android Developer 공식 사이트입니다.

dependencies {
    def lifecycle_version = "2.2.0"

    // ViewModel & LiveData
    implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
}

또한 나중에 View 뷰와 ViewModel 뷰 모델을 서로 연결하여 간단한 예제를 구현해볼 것이므로 저번 시간에 배운 데이터 바인딩 기술을 이번 시간에도 똑같이 사용할 것입니다. 그러므로 Build.Gradle 파일에 추가로 dataBinding 설정 또한 해주도록 하겠습니다.

android {
    ...    
    dataBinding{
        enabled = true
    }
}

🐸 간단한 프로그램을 구현해보자!

이전에는 기술에 관한 설명을 모두 마친 후 프로그램을 구현해보았다면 오늘은 간단한 프로그램을 구현하는 과정을 소개하면서 기술에 대해 자세히 설명해드리도록 하겠습니다. 그럼 먼저 프로그램의 기획을 작성해보도록 하겠습니다. 정말 간단한 프로그램입니다.

 

★ Instagram 인스타그램 팔로잉 관리

1. 인스타그램 현재의 팔로잉 목록에서 친구를 Follow, UnFollow를 할 수 있습니다.     

2. 현재 팔로잉하고 있는 인맥의 수를 저장하는 값은 실시간으로 변경되어야합니다.

지금부터 프로그램을 제작해보겠습니다. 프로그램의 레이아웃은 인스타그램 모바일을 참조하였으며 상단 탭, 하단 탭은 프로그램이 복잡할 수 있기 때문에 이번 예제에서는 제외하였습니다. 상단 탭과 하단 탭에 대해서는 나중에 다시 자세히 설명하도록 하겠습니다.

# XML 레이아웃 구성하기

아래의 코드를 보면 기존의 XML 레이아웃에서 추가된 코드가 존재합니다. 그것은 <layout> 태그와 <data> 태그입니다. <layout> 태그는 XML 레이아웃 코드 전체를 감싸며 저번 시간에 배운 DataBinding 데이터 바인딩 기술을 사용할 수 있게도와주고 있습니다.

<?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="org.techtown.viewmodellivedata.MainViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity" >

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#EEEEEE"
            android:minHeight="?attr/actionBarSize"
            android:theme="?attr/actionBarTheme">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="juyeop03"
                android:textColor="#000000"
                android:textSize="20sp" />

        </androidx.appcompat.widget.Toolbar>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:orientation="horizontal">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="center"
                android:orientation="vertical">

                <TextView
                    android:id="@+id/textView"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="팔로워 118명"
                    android:textSize="16sp" />

                <View
                    android:id="@+id/view"
                    android:layout_width="match_parent"
                    android:layout_height="2dp"
                    android:layout_marginTop="10dp"
                    android:background="#EEEEEE" />
            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="center"
                android:orientation="vertical">

                <TextView
                    android:id="@+id/textView2"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@={viewModel.following}"
                    android:textSize="16sp" />

                <View
                    android:id="@+id/view2"
                    android:layout_width="match_parent"
                    android:layout_height="2dp"
                    android:layout_marginTop="10dp"
                    android:background="#000000" />
            </LinearLayout>
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <androidx.recyclerview.widget.RecyclerView
                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
                android:id="@+id/recyclerView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_margin="10dp"
                tools:listitem="@layout/user_item"/>
        </LinearLayout>
    </LinearLayout>
</layout>

activity_main.xml 레이아웃입니다.

반면에 <data> 태그는 태그 안에 또 한 개의 <variable> 태그가 존재합니다. <variable> 태그 안에 name의 값은 viewModel, type의 값은 이번 시간에 배울 ViewModel 뷰 모델 클래스를 입력하였습니다. 이 코드의 의미는 XML 레이아웃에서 설정한 ViewModel 뷰 모델로 접근할 수 있도록 하기 위해서입니다. 아래의 TextView의 text 값은 ViewModel 뷰 모델의 follwing 변수로 설정합니다.

 

TextView의 text 값을 ViewModel 뷰 모델의 following 변수로 설정한 이유는 텍스트의 값이 실시간으로 바뀌게 하기 위해서입니다. 뷰 모델의 following 변수는 MutableLiveData 라이브 데이터이며 뷰 모델에서 변수의 값이 변경될 시 실시간으로 XML 레이아웃의 텍스트 값도 바뀌게 할 수 있습니다. 서로를 연결시키는 방식은 "@={viewModel.following}" 형식으로 연결을 시켜줍니다.

 

activity_main.xml 레이아웃에 존재하는 리사이클러뷰의 아이템 레이아웃은 위의 사진과 같이 구성을 하였습니다. 사용자의 프로필, 아이디, 이름, 팔로우 버튼 총 4가지가 존재합니다. 위의 레이아웃을 구성하는 것은 크게 어렵지 않아 코드는 생략하겠습니다.

# ViewModel 뷰 모델 구성하기

이번에 가장 중요한 부분인 ViewModel 뷰 모델을 구성해보겠습니다. 뷰 모델의 이름은 MainViewModel 뷰 모델이라고 지정하였으며 ViewModel 클래스를 상속받고 있습니다. 또한 following, counter 2개의 MutableLiveData 멤버 변수를 가지고 있습니다.

 

following 변수는 XML 레이아웃의 TextView와 연결을 시켜 실시간으로 값을 변경시킵니다. 반면에 counter 변수는 사용자가 팔로우 또는 언팔로우를 눌렀을 때에 따른 값 처리를 실시간으로 진행하며 현재의 값의 데이터를 저장하는 역할을합니다.

public class MainViewModel extends ViewModel {

    public static MutableLiveData<String> following = new MutableLiveData<>();
    public static MutableLiveData<Integer> counter = new MutableLiveData<>();

    public MainViewModel(){
        following.setValue("팔로잉 0명");
        counter.setValue(0);
    }

    public static void increase(){
       counter.setValue(counter.getValue() + 1);
    }

    public static void decrease(){
        counter.setValue(counter.getValue() - 1);
    }
}

MainViewModel 뷰 모델의 생성자에는 following, counter 변수를 각각 초기화 시켜주고있습니다. 아래를 보시면 increase, decrease 메서드가 존재합니다. 사용자가 팔로우, 언팔로우 버튼을 누를 때마다 호출이되며 counter 변수의 값을 1씩 증가 또는 감소시키고 있는 모습을 볼 수 있습니다. 이로서 뷰 모델 클래스에서 라이브 데이터를 사용해보고 그 구조에 대해서 알아보았습니다.

# UserDataList & UserAdapter 구성하기

아까 activity_main.xml 레이아웃에 존재하는 리사이클러뷰에 각 사용자의 정보를 표시하기 위해 아이템의 Data 클래스와 Adapter를 구성해보도록 하겠습니다. 이번 예제에서는 각 사용자의 아이디, 이름만을 아이템의 Data 클래스 변수로 설정합니다.

public class UserDataList {
    String id;
    String name;

    public UserDataList(String id, String name){
        this.id = id;
        this.name = name;
    }
}
public class UserAdapter extends RecyclerView.Adapter<UserAdapter.ViewHolder>{

    Context context;
    ArrayList<UserDataList> userDataList;

    public UserAdapter(Context context, ArrayList<UserDataList> userDataList){
        this.context = context;
        this.userDataList = userDataList;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        View itemView = inflater.inflate(R.layout.user_item, parent, false);

        return new ViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        UserDataList item = userDataList.get(position);

        holder.id.setText(item.id);
        holder.name.setText(item.name);

        holder.button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                if(holder.button.getText() == "팔로잉"){
                    MainViewModel.decrease();
                    holder.button.setBackgroundColor(Color.parseColor("#2196F3"));
                    holder.button.setTextColor(Color.parseColor("#FFFFFF"));
                    holder.button.setText("팔로우");
                    holder.button.isEnabled();
                }else{
                    MainViewModel.increase();
                    holder.button.setBackgroundColor(Color.parseColor("#FFFFFF"));
                    holder.button.setTextColor(Color.parseColor("#000000"));
                    holder.button.setText("팔로잉");
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return userDataList.size();
    }

    class ViewHolder extends RecyclerView.ViewHolder {
        TextView id;
        TextView name;
        Button button;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            id = itemView.findViewById(R.id.id);
            name = itemView.findViewById(R.id.name);
            button = itemView.findViewById(R.id.button);
        }
    }
}

또한 Adapter는 UserDataList 자료형의 ArrayList 변수를 생성자의 매개변수로 전달 받아 각 아이템을 리사이클러뷰에 순차적으로 출력을 해주겠습니다. 리사이클러뷰의 구조에 대해서 만약 이해가 되지 않으시다면 이전에 정리한 자료를 참고해주시기바랍니다.

# MainActivity 구성하기

ViewModel, UserData, Adapter 등 모든 준비를 마쳤습니다. 이제 MainActivity에서는 지금까지 준비한 준비물들을 모두 연결하고 설정해주는 역할만을 하면됩니다. 저번 시간에 배운 데이터 바인딩 기술을 활용하여 XML 레이아웃과 View를 서로 연결해줍니다.

 

ViewModel 또한 ViewModelProviders 매서드를 통해 뷰와 뷰 모델을 서로 연결 시켜주는 작업을 진행합니다. 그 후 ArrayList 배열에 UserDataList 클래스의 정보에 맞게 유저 정보를 담았다면 XML 레이아웃의 리사이클러뷰와 어뎁터를 서로 연결시켜줍니다.

public class MainActivity extends AppCompatActivity {

    ActivityMainBinding binding;
    MainViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        viewModel = ViewModelProviders.of(this).get(MainViewModel.class);

        binding.setViewModel(viewModel);
        binding.setLifecycleOwner(this);
        toolbarInit();
        settingAdapter();
        observerViewModel();
    }

    public void toolbarInit(){
        setSupportActionBar(binding.toolbar);
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_keyboard_backspace_black_24dp);
        getSupportActionBar().setDisplayShowTitleEnabled(false);
    }

    public void settingAdapter(){
        ArrayList<UserDataList> userDataList = new ArrayList<>();
        userDataList.add(new UserDataList("e0eun_qt", "이영은"));
        userDataList.add(new UserDataList("iiveryi", "정성훈"));
        userDataList.add(new UserDataList("deep_shining_", "최석준"));
        userDataList.add(new UserDataList("g_yyuu_", "임규민"));
        userDataList.add(new UserDataList("seo._.6", "김서준"));
        userDataList.add(new UserDataList("ttingho812", "추명호"));
        userDataList.add(new UserDataList("sun_nowplaying", "김혜선"));
        userDataList.add(new UserDataList("0_haribro", "오하형"));
        userDataList.add(new UserDataList("wonbin622", "김원빈"));
        userDataList.add(new UserDataList("yang.0626", "양동욱"));
        userDataList.add(new UserDataList("galpos3_", "이수민"));


        UserAdapter adapter = new UserAdapter(getApplicationContext(), userDataList);
        binding.recyclerView.setAdapter(adapter);
    }

    public void observerViewModel(){
        viewModel.counter.observe(this, new Observer<Integer>() {
            @Override
            public void onChanged(Integer integer) {
                viewModel.following.setValue("팔로잉 " + integer + "명");
            }
        });
    }
}

또한 obsererViewModel 이라는 새로운 매서드가 존재합니다. 이 매서드에서는 뷰 모델의 맴버 변수인 counter 변수를 observe 하고 있습니다. 이 과정을 통해 counter 변수의 값이 만약 변경된다면 그 순간을 감지하여 viewModel의 following 변수의 값을 변경시켜 XML 레이아웃에서의 텍스트 값 또한 실시간으로 변경해 사용자가 팔로우, 언팔로우를 자유자재로 사용할 수 있도록합니다.

🙉 시연 영상을 함께 보자!

그럼 지금까지 구현 한 프로그램의 시연 영상을 함께 보겠습니다. 사용자는 리사이클러뷰 목록에 표시된 사람들을 팔로우 또는 언팔로우를 자유롭게 할 수 있습니다. 이 과정 속에서는 특정 값을 실시간으로 관찰하며 결과에 따라 데이터를 변경 하고 있습니다.

 

열심히 구현한 프로그램의 시연 영상입니다.

👍 글을 마치며

오늘은 ViewModel 뷰 모델과 LiveData 라이브 데이터의 의미, 사용 이유 그리고 이를 활용하여 프로그램을 제작해보았습니다. 이 두가지의 기능을 사용한다면 효율적으로 코드를 작성할 수 있고 그 만큼 더 재미있을 것입니다. 다음 시간에는 MVVM 디자인 패턴에 대해서 알아볼 것입니다. 이 내용에 대해서 정확히 알기 위해서는 데이터 바인딩, 뷰 모델, 라이브 데이터에 대한 개념을 이해해야만 합니다. 그래서 여러분은 이미 이 내용에 대해서 먼저 간략하게 알아보았기 때문에 다음 시간에 배우는 내용은 어렵지 않을 것입니다.

Comments