アンドロイドの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のプロジェクトコードもチェックアウトできます。