본문 바로가기
Android/Sample Source

[안드로이드] ViewModel 샘플 소스

by Jay Son 아기 냥이 해린 짱💖 2020. 6. 28.

원본 소스 경로

https://github.com/JsonCorp/ViewModel

 

JsonCorp/ViewModel

Contribute to JsonCorp/ViewModel development by creating an account on GitHub.

github.com

1. 개요

ViewModel 을 사용 할 경우 UI 관련 데이터를 저장하고 관리하고 화면 회전과 같이 구성을 변경할 때도 데이터를 유지 할 수 있음.

 

2. ViewModel 사용하지 않을 경우 문제점.

- UI Controller를 제거 하거나 다시 만들 경우 UI 관련 데이터 손실

- 전달 데이터가 단순할 경우 문제가 없지만 대용량일 경우 UI 딜레이 발생

- 비동기 호출로 인한 데이터, 메모리 관리 필요로 인한 유지 보수 비용가 필요하여 리소스 낭비

- UI Controller에서 데이터를 관리 할 경우 UI Controller 코드 복잡도 증가.

 

3. ViewModel 생명 주기

- Activity 실행 후 화면 회전 및 Fragment 화면 변경 할 경우 데이터 유지됨.

4. ViewModel 구조

- 기존 UI Controller

> UI 화면 별로 데이터를 가져와야함.

- ViewModel

> UI에서는 Activity 실행 후 각 화면별로 ViewModel에 데이터를 저장하여 공유

5. Fragment간 데이터 공유

- Activity 실행 후 ViewModel 생성

- 각 Fragment에서 데이터를 ViewModel 에 저장

- Fragment에서는 ViewModelProvider를 통해 변경된 데이터를 업데이트 할 수 있음.

 

6. 샘플 소스

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.json.viewmodel">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

build.gralde (app)

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.json.viewmodel"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    def lifecycle_version = "2.2.0"

    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
    // LiveData
    implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
    // Lifecycles only (without ViewModel or LiveData)
    implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version"

    // Saved state module for ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"

    // Annotation processor
    annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
    // alternately - if using Java8, use the following instead of lifecycle-compiler
    implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/main_master_fragment_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Master Fragment" />

    <Button
        android:id="@+id/main_detail_fragment_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Detail Fragment" />

    <FrameLayout
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

master_fragment.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FAED7D"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="30sp"
        android:gravity="center"
        android:textSize="30sp"
        android:text="Master Fragment"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <Button
            android:id="@+id/master_add_data_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Add Data"/>

        <EditText
            android:id="@+id/master_input_edit_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="Input Text"/>
    </LinearLayout>

    <TextView
        android:id="@+id/master_info_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="N/A"/>

</LinearLayout>

detail_fragment.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#B2CCFF"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="30sp"
        android:gravity="center"
        android:textSize="30sp"
        android:text="Detail Fragment"/>

    <TextView
        android:id="@+id/detail_info_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="N/A"/>

</LinearLayout>

 

MainActivity.java

package com.json.viewmodel;

import android.os.Bundle;
import android.widget.Button;

import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;

/**
 * https://developer.android.com/topic/libraries/architecture/viewmodel#java
 * https://developer.android.com/jetpack/androidx/releases/lifecycle#declaring_dependencies
 */
public class MainActivity extends AppCompatActivity {

    private MasterFragment mMasterFragment;
    private DetailFragment mDetailFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button masterFragmentButton = findViewById(R.id.main_master_fragment_button);
        Button detailFragmentButton = findViewById(R.id.main_detail_fragment_button);
        mMasterFragment = new MasterFragment();
        mDetailFragment = new DetailFragment();

        masterFragmentButton.setOnClickListener(View -> changeFragment(mMasterFragment));
        detailFragmentButton.setOnClickListener(View -> changeFragment(mDetailFragment));

        // 최초 화면에 표시할 Fragment 화면
        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.fragment_container, mMasterFragment, MasterFragment.TAG).commit();
        }
    }

    // 버튼을 누를 경우 선택한 Fragment 변경
    public void changeFragment(Fragment fragment) {
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.fragment_container, fragment).commit();
    }
}

SharedViewModel.java

package com.json.viewmodel;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData<Item> getSelected() {
        return selected;
    }
}

MasterFragment.java

package com.json.viewmodel;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;

public class MasterFragment extends Fragment {
    public static String TAG = "MasterFragment:";

    private SharedViewModel model;

    private Button mAddDataButton;
    private TextView mInfoTextView;
    private TextView mDataEditView;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.master_fragment, container, false);
        mAddDataButton = view.findViewById(R.id.master_add_data_button);
        mInfoTextView = view.findViewById(R.id.master_info_text_view);
        mDataEditView = view.findViewById(R.id.master_input_edit_text);

        return view;
    }

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);

        mAddDataButton.setOnClickListener(item -> {
            Item inputItem = new Item();
            inputItem.setName(mDataEditView.getText().toString());
            model.select(inputItem);
            mDataEditView.setText("");
        });

        model.getSelected().observe(getViewLifecycleOwner(), item -> {
            mInfoTextView.setText(item.getName());
        });
    }
}

DetailFragment.java

package com.json.viewmodel;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;

public class DetailFragment extends Fragment {
    public String TAG = "DetailFragment:";

    private TextView mDetailInfoView;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.detail_fragment, container, false);

        mDetailInfoView = view.findViewById(R.id.detail_info_text_view);

        return view;
    }

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        SharedViewModel model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        model.getSelected().observe(getViewLifecycleOwner(), item -> {
            mDetailInfoView.setText(item.getName());
        });
    }
}

Item.java

package com.json.viewmodel;

public class Item {
    private String mName;
    private String mNumber;

    public void setName(String name) {
        mName = name;
    }
    public void setNumber(String number) {
        mNumber = number;
    }

    public String getName() {
        return mName;
    }
    public String getNumber() {
        return mNumber;
    }
}

 7. 화면 구성

- 메인 화면 실행

- Master Fragment 데이터 입력

- Detail Fragment 화면 이동

> 화면 이동 할 경우 Master Fragment에서 입력한 데이터 갱신

- Master Fragment 돌아가기

> ViewModel 데이터가 삭제되지 않고 유지되어 있음.

 

 

Android 공식 가이드 문서

https://developer.android.com/topic/libraries/architecture/viewmodel#java

 

ViewModel 개요  |  Android 개발자  |  Android Developers

ViewModel을 사용하면 수명 주기를 인식하는 방식으로 UI 데이터를 관리할 수 있습니다.

developer.android.com

반응형