웹사이트 검색

Android LiveData


이 자습서에서는 Android 애플리케이션의 LiveData 아키텍처 구성 요소에 대해 설명합니다. 이 튜토리얼을 더 잘 이해하려면 Android ViewModel로 빠르게 우회하세요.

Android LiveData

LiveData는 아키텍처 패턴의 일부입니다. 기본적으로 프리미티브/컬렉션 유형을 포함하는 데이터 홀더입니다. 보기의 변경 사항을 관찰하고 ACTIVE일 때 보기를 업데이트하는 데 사용됩니다. 따라서 LiveData는 수명 주기를 인식합니다. 우리는 ViewModel이 데이터를 View에 전달하는 데 사용된다는 것을 알고 있습니다. 데이터가 뷰를 변경해야 할 때마다 여러 번 호출해야 하므로 ViewModel만 사용하는 것은 지루하고 비용이 많이 드는 작업이 될 수 있습니다. 또한 데이터 모델을 다른 위치에 저장해야 합니다. LiveData는 ArrayList, ViewModel을 기반으로 합니다.

Android LiveData 대 RxJava

Android LiveData는 LiveData가 수명 주기를 인식한다는 점을 제외하면 RxJava와 다소 유사합니다. 보기가 백그라운드에 있는 경우 보기에서 데이터를 업데이트하지 않습니다. 이것은 IllegalStateException 등과 같은 예외를 피하는 데 도움이 됩니다. ViewModel의 LiveData는 활동을 어떻게 업데이트합니까? 활동에 관찰자를 등록할 때 onChanged() 메서드를 재정의해야 합니다. onChanged() 메서드는 LiveData가 변경될 때마다 트리거됩니다. 따라서 onChanged()에서 변경된 LiveData를 View에 업데이트할 수 있습니다.

LiveData는 데이터가 변경될 때마다 관찰자에게 알리는 데이터 유형일 뿐입니다. LiveData는 데이터 변경 알리미와 같습니다.

LiveData는 setValue()postValue()를 사용하여 관찰자에게 알립니다. setValue()는 메인 스레드에서 실행됩니다. postValue()는 백그라운드 스레드에서 실행됩니다. LiveData 유형 인스턴스에서 getValue()를 호출하면 현재 데이터가 반환됩니다.

MutableLiveData

MutableLiveData는 LiveData 유형 클래스를 확장한 클래스일 뿐입니다. MutableLiveData는 LiveData 클래스가 제공하지 않는 postValue(), setValue() 메서드를 공개적으로 제공하므로 일반적으로 사용됩니다. LiveData/MutableLiveData는 이전 ArrayList와 새 ArrayList를 비교하여 RecyclerView 행의 최소 수를 업데이트하기 위해 DiffUtil에서 데이터를 업데이트하는 데 일반적으로 사용됩니다.

Android LiveData 예제 프로젝트 구조

implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'android.arch.lifecycle:extensions:1.1.1'

Android LiveData 코드

activity_main.xml 레이아웃의 코드는 다음과 같습니다.

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:app="https://schemas.android.com/apk/res-auto"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </RelativeLayout>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        app:srcCompat="@android:drawable/ic_input_add" />

</android.support.design.widget.CoordinatorLayout>

list_item_row.xml 레이아웃의 코드는 다음과 같습니다.

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="https://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:gravity="center_vertical">

        <TextView
            android:id="@+id/tvUrl"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentStart="true"
            android:autoLink="web"
            android:padding="8dp"
            android:textColor="@android:color/black"
            android:textSize="20sp" />

        <TextView
            android:id="@+id/tvDate"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_below="@+id/tvUrl" />

        <ImageButton
            android:id="@+id/btnDelete"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true"
            android:layout_centerVertical="true"
            android:src="@android:drawable/ic_menu_delete" />


    </RelativeLayout>


</android.support.v7.widget.CardView>

DbSettings.java 클래스의 코드는 다음과 같습니다.

package com.journaldev.androidlivedata.db;

import android.provider.BaseColumns;

public class DbSettings {

    public static final String DB_NAME = "favourites.db";
    public static final int DB_VERSION = 1;

    public class DBEntry implements BaseColumns {

        public static final String TABLE = "fav";
        public static final String COL_FAV_URL = "url";
        public static final String COL_FAV_DATE = "date";

    }
}

FavouritesDbHelper.java 클래스의 코드는 다음과 같습니다.

package com.journaldev.androidlivedata.db;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class FavouritesDBHelper extends SQLiteOpenHelper {

    public FavouritesDBHelper(Context context) {
        super(context, DbSettings.DB_NAME, null, DbSettings.DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        String createTable = "CREATE TABLE " + DbSettings.DBEntry.TABLE + " ( " +
                DbSettings.DBEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                DbSettings.DBEntry.COL_FAV_URL + " TEXT NOT NULL, " +
                DbSettings.DBEntry.COL_FAV_DATE + " INTEGER NOT NULL);";
        db.execSQL(createTable);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("DROP TABLE IF EXISTS " + DbSettings.DBEntry.TABLE);
        onCreate(db);
    }

}

Favourites.java 모델 클래스의 코드는 다음과 같습니다.

package com.journaldev.androidlivedata;

public class Favourites {

    public long mId;
    public String mUrl;
    public long mDate;

    public Favourites(long id, String name, long date) {
        mId = id;
        mUrl = name;
        mDate = date;
    }

    public Favourites(Favourites favourites) {
        mId = favourites.mId;
        mUrl = favourites.mUrl;
        mDate = favourites.mDate;
    }

}

따라서 SQLite 데이터베이스에서 ID, URL, DATE의 세 가지 레코드가 있는 테이블을 만듭니다. FavouritesViewModel.java 클래스의 코드는 다음과 같습니다.

package com.journaldev.androidlivedata;

import android.app.Application;
import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.MutableLiveData;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;


import com.journaldev.androidlivedata.db.DbSettings;
import com.journaldev.androidlivedata.db.FavouritesDBHelper;
import java.util.ArrayList;
import java.util.List;

public class FavouritesViewModel extends AndroidViewModel {

    private FavouritesDBHelper mFavHelper;
    private MutableLiveData<List<Favourites>> mFavs;

    FavouritesViewModel(Application application) {
        super(application);
        mFavHelper = new FavouritesDBHelper(application);
    }

    public MutableLiveData<List<Favourites>> getFavs() {
        if (mFavs == null) {
            mFavs = new MutableLiveData<>();
            loadFavs();
        }

        return mFavs;
    }

    private void loadFavs() {
        List<Favourites> newFavs = new ArrayList<>();
        SQLiteDatabase db = mFavHelper.getReadableDatabase();
        Cursor cursor = db.query(DbSettings.DBEntry.TABLE,
                new String[]{
                        DbSettings.DBEntry._ID,
                        DbSettings.DBEntry.COL_FAV_URL,
                        DbSettings.DBEntry.COL_FAV_DATE
                },
                null, null, null, null, null);
        while (cursor.moveToNext()) {
            int idxId = cursor.getColumnIndex(DbSettings.DBEntry._ID);
            int idxUrl = cursor.getColumnIndex(DbSettings.DBEntry.COL_FAV_URL);
            int idxDate = cursor.getColumnIndex(DbSettings.DBEntry.COL_FAV_DATE);
            newFavs.add(new Favourites(cursor.getLong(idxId), cursor.getString(idxUrl), cursor.getLong(idxDate)));
        }

        cursor.close();
        db.close();
        mFavs.setValue(newFavs);
    }


    public void addFav(String url, long date) {

        SQLiteDatabase db = mFavHelper.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put(DbSettings.DBEntry.COL_FAV_URL, url);
        values.put(DbSettings.DBEntry.COL_FAV_DATE, date);
        long id = db.insertWithOnConflict(DbSettings.DBEntry.TABLE,
                null,
                values,
                SQLiteDatabase.CONFLICT_REPLACE);
        db.close();


        List<Favourites> favourites = mFavs.getValue();

        ArrayList<Favourites> clonedFavs;
        if (favourites == null) {
            clonedFavs = new ArrayList<>();
        } else {
            clonedFavs = new ArrayList<>(favourites.size());
            for (int i = 0; i < favourites.size(); i++) {
                clonedFavs.add(new Favourites(favourites.get(i)));
            }
        }

        Favourites fav = new Favourites(id, url, date);
        clonedFavs.add(fav);
        mFavs.setValue(clonedFavs);
    }

    public void removeFav(long id) {
        SQLiteDatabase db = mFavHelper.getWritableDatabase();
        db.delete(
                DbSettings.DBEntry.TABLE,
                DbSettings.DBEntry._ID + " = ?",
                new String[]{Long.toString(id)}
        );
        db.close();

        List<Favourites> favs = mFavs.getValue();
        ArrayList<Favourites> clonedFavs = new ArrayList<>(favs.size());
        for (int i = 0; i < favs.size(); i++) {
            clonedFavs.add(new Favourites(favs.get(i)));
        }

        int index = -1;
        for (int i = 0; i < clonedFavs.size(); i++) {
            Favourites favourites = clonedFavs.get(i);
            if (favourites.mId == id) {
                index = i;
            }
        }
        if (index != -1) {
            clonedFavs.remove(index);
        }
        mFavs.setValue(clonedFavs);
    }

}

MutableLiveData는 즐겨찾기 인스턴스 객체 목록을 보유합니다. addFav()removeFav()에서 MainActivity에 정의된 Observer에 데이터 변경 사항을 알립니다. 이전 것과 새 것을 비교하기 위해 ArrayList의 복사본을 만듭니다. MainActivity.java 클래스의 코드는 다음과 같습니다.

package com.journaldev.androidlivedata;

import android.arch.lifecycle.Observer;
import android.arch.lifecycle.ViewModelProviders;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;

import java.util.Date;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    private FavAdapter mFavAdapter;
    private FavouritesViewModel mFavViewModel;
    private List<Favourites> mFav;
    FloatingActionButton fab;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        fab = findViewById(R.id.fab);
        final RecyclerView recyclerView = findViewById(R.id.recyclerView);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        mFavViewModel = ViewModelProviders.of(this).get(FavouritesViewModel.class);
        final Observer<List<Favourites>> favsObserver = new Observer<List<Favourites>>() {
            @Override
            public void onChanged(@Nullable final List<Favourites> updatedList) {
                if (mFav == null) {
                    mFav = updatedList;
                    mFavAdapter = new FavAdapter();
                    recyclerView.setAdapter(mFavAdapter);
                } else {
                    DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {

                        @Override
                        public int getOldListSize() {
                            return mFav.size();
                        }

                        @Override
                        public int getNewListSize() {
                            return updatedList.size();
                        }

                        @Override
                        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
                            return mFav.get(oldItemPosition).mId ==
                                    updatedList.get(newItemPosition).mId;
                        }

                        @Override
                        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                            Favourites oldFav = mFav.get(oldItemPosition);
                            Favourites newFav = updatedList.get(newItemPosition);
                            return oldFav.equals(newFav);
                        }
                    });
                    result.dispatchUpdatesTo(mFavAdapter);
                    mFav = updatedList;
                }
            }
        };

        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                final EditText inUrl = new EditText(MainActivity.this);
                AlertDialog dialog = new AlertDialog.Builder(MainActivity.this)
                        .setTitle("New favourite")
                        .setMessage("Add a url link below")
                        .setView(inUrl)
                        .setPositiveButton("Add", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                String url = String.valueOf(inUrl.getText());
                                long date = (new Date()).getTime();

                                mFavViewModel.addFav(url, date);
                            }
                        })
                        .setNegativeButton("Cancel", null)
                        .create();
                dialog.show();
            }
        });

        mFavViewModel.getFavs().observe(this, favsObserver);
    }


    public class FavAdapter extends RecyclerView.Adapter<FavAdapter.FavViewHolder> {

        @Override
        public FavViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_row, parent, false);
            return new FavViewHolder(itemView);
        }

        @Override
        public void onBindViewHolder(FavViewHolder holder, int position) {
            Favourites favourites = mFav.get(position);
            holder.mTxtUrl.setText(favourites.mUrl);
            holder.mTxtDate.setText((new Date(favourites.mDate).toString()));
        }

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

        class FavViewHolder extends RecyclerView.ViewHolder {

            TextView mTxtUrl;
            TextView mTxtDate;

            FavViewHolder(View itemView) {
                super(itemView);
                mTxtUrl = itemView.findViewById(R.id.tvUrl);
                mTxtDate = itemView.findViewById(R.id.tvDate);
                ImageButton btnDelete = itemView.findViewById(R.id.btnDelete);
                btnDelete.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        int pos = getAdapterPosition();
                        Favourites favourites = mFav.get(pos);
                        mFavViewModel.removeFav(favourites.mId);
                    }
                });
            }
        }
    }

}

AndroidLiveData 프로젝트를 다운로드합니다.

GitHub 리포지토리에서 Android Studio 프로젝트 코드를 체크아웃할 수도 있습니다.