アンドロイドのLiveData

このチュートリアルでは、AndroidアプリケーションにおけるLiveDataアーキテクチャコンポーネントについて説明します。このチュートリアルをより理解するために、Android ViewModelに一度ふらっと立ち寄ってみてください。

以下は日本語訳の一例です。Android LiveData

LiveDataはアーキテクチャのパターンの一部です。基本的にはプリミティブ/コレクション型のデータを保持するデータホルダーです。LiveDataはビューの変更を観察し、アクティブな場合にビューを更新するために使用されます。したがって、LiveDataはライフサイクルに対応しています。私たちはViewModelがViewにデータを伝達するために使用されることを知っています。ViewModelのみを使用すると、データがビューに反映されるたびに複数の呼び出しを行う必要があり、手間とコストがかかります。さらに、データモデルを異なる場所に格納する必要があります。LiveDataはObserverパターンに基づいており、ViewModelとViewの間の通信を容易にします。データの変更を観察し、複数の場所からのデータ参照の追加や削除に複数の呼び出しを行う代わりに、データを自動的に更新します(たとえばSQLite、ArrayList、ViewModelなどの場所)。

Android LiveData と RxJava の比較

Android LiveDataは、RxJavaに似たものですが、LiveDataはライフサイクルに対応しています。もしViewがバックグラウンドにある場合、LiveDataはView内のデータを更新しません。これにより、IllegalStateExceptionなどの例外を回避することができます。ViewModelでのLiveDataがActivityにどのように更新されるのかはどうでしょうか?ActivityでObserverを登録する際には、onChanged()メソッドをオーバーライドする必要があります。onChanged()メソッドは、LiveDataが変更されるたびにトリガーされます。したがって、onChanged()内で変更されたLiveDataをViewに更新することができます。

LiveDataは、データが変更される度にその監視者に通知するデータ型です。LiveDataは、データ変更の通知者のようなものです。

LiveDataはsetValue()とpostValue()を使ってObserverに通知します。setValue()はメインスレッド上で実行されます。postValue()はバックグラウンドスレッド上で実行されます。LiveData型のインスタンスでgetValue()を呼び出すと、現在のデータが返されます。

ミュータブルデータ

MutableLiveDataは、LiveData型のクラスを拡張したクラスです。MutableLiveDataは、LiveDataクラスが提供しないpostValue()メソッドとsetValue()メソッドを公開しているため、一般的によく使用されます。LiveData/MutableLiveDataは、コレクション型(List、ArrayListなど)からRecyclerViewのデータを更新する際によく使用されます。次のセクションでは、SQLiteデータベースからRecyclerViewに行を追加/削除するアプリケーションを作成します。LiveDataの変更時にRecyclerViewのレコードを更新するためにMutableLiveDataを使用します。古いArrayListと新しいArrayListを比較して、最小限のRecyclerView行を更新するためにDiffUtilを使用します。

Android LiveDataのプロジェクト構造の例

次の内容をあなたのbuild.gradleファイルに追加してください。

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'

アンドロイドの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.scdev.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.scdev.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.scdev.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の3つのレコードを持つテーブルを作成します。FavouritesViewModel.javaクラスのコードは以下の通りです:

package com.scdev.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.scdev.androidlivedata.db.DbSettings;
import com.scdev.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は、Favouriteインスタンスオブジェクトのリストを保持しています。addFav()とremoveFav()では、MainActivityで定義されているObserverにデータの変更を通知します。古いリストと新しいリストを比較するために、ArrayListのコピーを作成します。MainActivity.javaクラスのコードは以下の通りです。

package com.scdev.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);
                    }
                });
            }
        }
    }

}

上記のコードでは、活動の中でReyclerViewのアダプタークラスが定義されています。mFavViewModel.getFavs().observe(this, favsObserver);は、LiveDataが更新されるたびにViewModelクラスから通知されるMainActivityにObserverを設定するために使用されます。favsObserver匿名クラスにはonChanged()メソッドが含まれており、最新のデータが提供され、それがRecyclerViewで更新されます。上記のアプリケーションの出力は以下の通りです。これでAndroid LiveDataチュートリアルは終了です。プロジェクトは以下のリンクからダウンロードすることができます。

「AndroidLiveDataプロジェクトをダウンロードしてください。」

私たちのGitHubリポジトリからAndroid Studioのプロジェクトコードもチェックアウトできます。

コメントを残す 0

Your email address will not be published. Required fields are marked *