گوگل به منظور کمک به توسعه دهندگان اندروید و برای اینکه توسعه دهندگان برنامههای قوی تر، قابل آزمایش و قابل اطمینانتری را طراحی کنند، مجموعه ای از کتابخانههای "Android Architecture Components" را منتشر کرد، قابل توجهترین کلاس LiveData و کلاسهای مربوط به lifecycle-aware، کتابخانهی Room persistence و کتابخانهی paging است. در این مقاله ما LiveData در اندروید و مشکلی که میتوانید با آن حل کنید، را بررسی میکنیم. در ادامه همراه ما باشید.
LiveData در اندروید یک کلاسی هست، که دارندهی دیتای قابل مشاهده است. بر خلاف یک مشاهدهی منظم، LiveData از چرخهی حیات آگاه (lifecycle-aware) است، به این معنی که به چرخهی حیات سایر مؤلفههای (components) برنامه مانند "Activities" ، "Fragments" یا "Services" دقت کرده و احترام میگذارد. این آگاهی تضمین میکند که LiveData فقط app component observers را که در حالت فعال چرخه (active lifecycle state) هستند، بروز میکند. چرخه حیات اکتیویتی را در این تصویر مشاهده نمایید (Activity LifeCycle) چرخه حیات فرگمنت را در این تصویر مشاهده نمایید(Fragment LifeCycle)
LiveData یک ناظر را که توسط کلاس Observer نشان داده میشود، در نظر میگیرد، اگر چرخهی حیات (lifecycle) آن در حالت STARTED یا RESUMED باشد، در وضعیت فعال قرار دارد. LiveData فقط ناظران (observers) فعال را دربارهی بروزرسانیها آگاه میکند. ناظران غیرفعال که برای تماشای اشیاء LiveData ثبت شده اند، از تغییرات مطلع نمیشوند.
شما میتوانید یک observer را با یک شیء که رابط LifecycleOwner را پیاده سازی کرده، ثبت کنید. این رابطه اجازه میدهد تا وقتی وضعیت Lifecycle object مربوط به DESTROYED تغییر میکند، observer از بین برود. این به ویژه برای "Activites" و "Fragments" مفید است، زیرا میتوانند اشیاء LiveData را با خیال راحت مشاهده کنند، و نگران leaks_activities و Fragment نباشند، با Unsubscribe کردن چرخهی حیات (LifeCycle) آنها سریع از بین میرود.
استفاده از LiveData مزایای زیر را ارائه میدهد:
LiveData از observer pattern پیروی میکند. LiveData با تغییر وضعیت LifeCycle، اشیاء Observer را مطلع میکند. میتوانید کد خود را برای به روزرسانی UI در اشیاء Observer تلفیق کنید. به جای بروز کردن UI هر بار که دادههای برنامه تغییر میکنند، Observer شما میتواند، هر زمان که تغییر ایجاد میکند، رابط کاربر را بروز کند.
Observerها به LifeCycle Object محدود میشوند و بعد از نابودی (destroyed) چرخهی حیات مرتبط با آنها، خود را پاک میکنند.
به دلیل Stop شدن activities، هیچ crashes وجود ندارد:
اگر چرخه حیات Observer غیرفعال باشد، مانند Activity در back stack، هیچ رویدادی را از LiveData دریافت نمیکند.
مؤلفههای UI فقط دادههای مربوطه را مشاهده میکنند، و مشاهده، متوقف یا دوباره از سر گرفته نمیشوند. LiveData به طور خودکار تمام این موارد را مدیریت میکند، زیرا Observer از تغییرات وضعیت چرخهی حیات مربوطه آگاه است.
اگر چرخهی حیات یا همان LifeCycle غیرفعال شود، پس از فعال شدن دوباره آخرین اطلاعات را دریافت میکند. به عنوان مثال، Activity که در Background بود، آخرین اطلاعات را بلافاصله پس از بازگشت به Foreground دریافت میکند.
اگر یک Activity یا Fragment به دلیل تغییر پیکربندی، مانند چرخش دستگاه (rotation)، دوباره بازیابی شود، بلافاصله آخرین اطلاعات موجود را دریافت میکند.
می توانید با استفاده از الگوی singleton، یک شی LiveData را برای بسته بندی خدمات سیستم گسترش دهید، تا در برنامهی شما به اشتراک گذاشته شود. شی LiveData یک بار به سرویس سیستم متصل میشود، و سپس هر ناظر که به این منبع نیاز داشته باشد، میتواند فقط شیء LiveData را تماشا کند.
LiveData در اندروید تا حدودی شبیه RxJava است، به جز اینکه LiveData از چرخهی حیات آگاه یا lifecycle aware است.
اگر View در پس زمینه (Background) باشد، داده شما در View بروز نمیشود. این به ما کمک میکند تا از exceptions مانند IllegalStateException و غیره جلوگیری کنیم.
هنگامی که Observer را در َActivity خود ثبت میکنیم، باید متد ()onChanged را override کنیم. هر زمان که LiveData تغییر کند، متد ()onChanged شروع به کار میکند. بنابراین در متد ()onChanged میتوانیم LiveData تغییر یافته را روی View بروز کنیم.
LiveData فقط یک نوع داده است، که هر زمان دادهها تغییر میکنند، به ناظر آن اطلاع میدهند. LiveData مانند یک اعلام کننده تغییر داده است.
LiveData با استفاده از ()setValue و ()postValue به Observer اطلاع میدهد.
()setValue روی Thread اصلی (Main Thread) اجرا میشود.
()postValue روی Thread پس زمینه (Background Thread) اجرا میشود.
با مراجعه به ()getValue در نمونه LiveData اطلاعات فعلی را به شما باز میگرداند.
MutableLiveData فقط یک کلاس است که کلاس نوع LiveData را گسترش میدهد.
MutableLiveData معمولاً مورد استفاده قرار میگیرد، زیرا متدهای ()postValue() ،setValue را به صورت عمومی ارائه میدهد، چیزی که کلاس LiveData ارائه نمیدهد.
LiveData / MutableLiveData معمولاً در بروزرسانی دادهها در RecyclerView از نوع مجموعه (لیست، ArrayList و غیره)
استفاده میشود.
در بخش بعد، برنامهای ایجاد خواهیم کرد، که ردیف هایی را در RecyclerView از بانک اطلاعات SQLite اضافه یا حذف میکند.
ما از MutableLiveData استفاده خواهیم کرد، که سوابق RecyclerView را هر زمان که 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
کد طرح 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.sevenlearn.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";
}
}
The code for FavouritesDbHelper.java class is given below:
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.sevenlearn.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.sevenlearn.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 لیستی از Favourite instance objects را در خود نگه میدارد. در ()addFav و ()removeFav تغییرات داده را به Observer اعلام میکنیم، که در MainActivity تعریف شده است.
ما برای مقایسهی نسخه قدیمی و جدید، یک نسخه از ArrayList ایجاد میکنیم.
کد کلاس MainActivity.java در زیر آورده شده است:
package com.sevenlearn.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);
}
});
}
}
}
}
در کد بالا، ما کلاس Adapter را برای RecyclerView در Activity تعریف کردهایم.
;(mFavViewModel.getFavs().observe(this, favsObserver برای تنظیم Observer در MainActivity استفاده میشود، که هر زمان که LiveData بروز شود، از کلاس ViewModel مطلع میشود.
anonymous کلاس favsObserver متشکل از متد ()onChanged است، و آخرین داده هایی را ارائه میدهد، که سپس از طریق RecyclerView بروز میشوند.
خروجی برنامهی فوق در زیر آورده شده است و میتوانید مشاهده کنید. (از این لینک میتوانید برنامه را دانلود کنید)
جمعبندی:
آشنایی با LiveData میتواند خیلی مفید باشد، در این مقاله به توضیح کارایی و ویژگیهای LiveData پرداختیم، به دلیل اینکه LiveData از چرخهی حیات آگاه هست، از خیلی ایرادهای برنامه مثل ارور یا memory leak و ... که بالا اشاره کردیم، جلوگیری میکند. اگر در این مورد سوال یا نظری داشتید خوشحال میشویم که با ما و کاربران سون لرن به اشتراک بگذارید.
منابع:
اگر به یادگیری بیشتر در زمینهی اندروید علاقه داری، با شرکت در دورهی آموزش برنامه نویسی اندروید در کمتر از یکسال به یک توسعهدهنده اندروید همه فن حریف تبدیل میشوی که آمادهی استخدام، دریافت پروژه و حتی پیادهسازی اپلیکیشن خودت هستی.
ممنون از مطلب مفیدتون .لینکی که برای دانلود برنامه گذاشتین کار نمیکنه میشه بررسی کنین و دذستش کنین؟
درود. لینک رو با vpn باز کنید، محتوا دانلود میشه. این مشکل به این دلیل ایجاد شده که ما به دلیل حق کپی رایت باید سورس رو از سایت انگلیسی که منبع این مقاله هست، لینک میکردیم.
با سلام، این بستگی به پروژه و خود شما داره میتونین از هر کدوم که خواستین استفاده کنین، هر کدوم مزیتهای خودشون رو دارن و من به شخصه از RxJava استفاده میکنم