官方介绍
LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。
如果观察者(由 Observer 类表示)的生命周期处于 STARTED 或 RESUMED 状态,则 LiveData 会认为该观察者处于活跃状态。LiveData 只会将更新通知给活跃的观察者。为观察 LiveData 对象而注册的非活跃观察者不会收到更改通知。
您可以注册与实现 LifecycleOwner 接口的对象配对的观察者。有了这种关系,当相应的 Lifecycle 对象的状态变为 DESTROYED 时,便可移除此观察者。 这对于 Activity 和 Fragment 特别有用,因为它们可以放心地观察 LiveData 对象而不必担心泄露(当 Activity 和 Fragment 的生命周期被销毁时,系统会立即退订它们)。
LiveData优势
- 确保界面符合数据状态
LiveData 遵循观察者模式。当生命周期状态发生变化时,LiveData 会通知 Observer 对象。您可以整合代码以在这些 Observer 对象中更新界面。观察者可以在每次发生更改时更新界面,而不是在每次应用数据发生更改时更新界面。
- 不会发生内存泄露
观察者会绑定到 Lifecycle 对象,并在其关联的生命周期遭到销毁后进行自我清理。
- 不会因 Activity 停止而导致崩溃
如果观察者的生命周期处于非活跃状态(如返回栈中的 Activity),则它不会接收任何 LiveData 事件。
- 不再需要手动处理生命周期
界面组件只是观察相关数据,不会停止或恢复观察。LiveData 将自动管理所有这些操作,因为它在观察时可以感知相关的生命周期状态变化。
- 数据始终保持最新状态
如果生命周期变为非活跃状态,它会在再次变为活跃状态时接收最新的数据。例如,曾经在后台的 Activity 会在返回前台后立即接收最新的数据。
- 适当的配置更改
如果由于配置更改(如设备旋转)而重新创建了 Activity 或 Fragment,它会立即接收最新的可用数据。
- 共享资源
您可以使用单一实例模式扩展 LiveData 对象以封装系统服务,以便在应用中共享它们。LiveData 对象连接到系统服务一次,然后需要相应资源的任何观察者只需观察 LiveData 对象。
创建及使用:
LiveData 是一种可用于任何数据的封装容器,其中包括可实现 Collections 的对象,如 List。 LiveData 对象通常存储在 ViewModel 对象中,并可通过 getter 方法进行访问,如以下示例中所示:
public class NameViewModel extends ViewModel {
// Create a LiveData with a String
private MutableLiveData<String> currentName;
public MutableLiveData<String> getCurrentName() {
if (currentName == null) {
currentName = new MutableLiveData<String>();
}
return currentName;
}
// Rest of the ViewModel...
}
Google建议将所有数据的获取放入到ViewModel中进行操作,网络获取数据建议在viewModel的基础上再独立出一个网络仓库,因此LiveData对象也应当放入ViewModel中去创建,而不是Activity或者Fragment。这样数据可以独立于视图控制器存在,不会因为视图控制器的销毁而丢失数据。
LiveData可以封装任何类型的数据,上图代码使用String来举例。
请按照以下步骤使用 LiveData 对象:
- 创建 LiveData 实例以存储某种类型的数据。这通常在 ViewModel 类中完成。
- 创建可定义 onChanged() 方法的 Observer 对象,该方法可以控制当 LiveData 对象存储的数据更改时会发生什么。通常情况下,您可以在界面控制器(如 Activity 或 Fragment)中创建 Observer 对象。
- 使用 observe() 方法将 Observer 对象附加到 LiveData 对象。observe() 方法会采用 LifecycleOwner 对象。这样会使 Observer 对象订阅 LiveData 对象,以使其收到有关更改的通知。通常情况下,您可以在界面控制器(如 Activity 或 Fragment)中附加 Observer 对象。
注意:您可以使用 observeForever(Observer) 方法来注册未关联 LifecycleOwner 对象的观察者。在这种情况下,观察者会被视为始终处于活跃状态,因此它始终会收到关于修改的通知。您可以通过调用 removeObserver(Observer) 方法来移除这些观察者。
当您更新存储在 LiveData 对象中的值时,它会触发所有已注册的观察者(只要附加的 LifecycleOwner 处于活跃状态)。
LiveData 允许界面控制器观察者订阅更新。当 LiveData 对象存储的数据发生更改时,界面会自动更新以做出响应。
为LiveData创建观察者
观察 LiveData 对象
在大多数情况下,应用组件的 onCreate() 方法是开始观察 LiveData 对象的正确着手点,原因如下:
- 确保系统不会从 Activity 或 Fragment 的 onResume() 方法进行冗余调用。
- 确保 Activity 或 Fragment 变为活跃状态后具有可以立即显示的数据。一旦应用组件处于 STARTED 状态,就会从它正在观察的 LiveData 对象接收最新值。只有在设置了要观察的 LiveData 对象时,才会发生这种情况。
- 通常,LiveData 仅在数据发生更改时才发送更新,并且仅发送给活跃观察者。此行为的一种例外情况是,观察者从非活跃状态更改为活跃状态时也会收到更新。此外,如果观察者第二次从非活跃状态更改为活跃状态,则只有在自上次变为活跃状态以来值发生了更改时,它才会收到更新。
以下示例代码说明了如何开始观察 LiveData 对象:
public class NameActivity extends AppCompatActivity {
private NameViewModel model;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Other code to setup the activity...
// Get the ViewModel.
model = ViewModelProviders.of(this).get(NameViewModel.class);
// Create the observer which updates the UI.
final Observer<String> nameObserver = new Observer<String>() {
@Override
public void onChanged(@Nullable final String newName) {
// Update the UI, in this case, a TextView.
nameTextView.setText(newName);
}
};
// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
model.getCurrentName().observe(this, nameObserver);
}
}
在传递 nameObserver 参数的情况下调用 observe() 后,系统会立即调用 onChanged(),从而提供 mCurrentName 中存储的最新值。 如果 LiveData 对象尚未在 mCurrentName 中设置值,则不会调用 onChanged()。
更新 LiveData 对象
LiveData 没有公开可用的方法来更新存储的数据。MutableLiveData 类将公开 setValue(T) 和 postValue(T) 方法,如果您需要修改存储在 LiveData 对象中的值,则必须使用这些方法。通常情况下会在 ViewModel 中使用 MutableLiveData,然后 ViewModel 只会向观察者公开不可变的 LiveData 对象。
设置观察者关系后,您可以更新 LiveData 对象的值(如以下示例中所示),这样当用户点按某个按钮时会触发所有观察者:
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String anotherName = "John Doe";
model.getCurrentName().setValue(anotherName);
}
});
在本例中调用 setValue(T) 导致观察者使用值 John Doe 调用其 onChanged() 方法。本例中演示的是按下按钮的方法,但也可以出于各种各样的原因调用 setValue() 或 postValue() 来更新 mName,这些原因包括响应网络请求或数据库加载完成。在所有情况下,调用 setValue() 或 postValue() 都会触发观察者并更新界面。
注意: 我们知道Android中更新UI需要在主线程中操作,因此LiveData setValue的时候也需要在主线程中。如果想在子线程中改变LiveData的值,可以调用postValue(T) 方法来更新 LiveData 对象。
举例:
下面举一个具体的例子。实现一个列表,每行显示用户的ID和姓名。如果不使用LiveData的话我们需要在List中添加数据的时候将新数据塞给RecycleView的Adapter,然后调用Adapter的刷新方法去刷新RecycleView中的数据。而使用LiveData则只需要在OnChange回调中去调用Adapter去刷新数据就行了,修改List中数据时不需要手动刷新。
注意LiveData对象应该在ViewModel中创建,这里只是示范如何使用LiveData我就偷了个懒放在Activity中了~
创建LiveData:
public class LiveDataActivity extends AppCompatActivity {
private RecyclerView mRv;
private Context mContext;
private RoomRvAdapter mAdapter;
private MutableLiveData<ArrayList<User>> mLiveDataList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_live_data);
mContext = this;
//创建LiveData对象
mLiveDataList = new MutableLiveData<>();
mLiveDataList.setValue(new ArrayList<User>());
mRv = findViewById(R.id.live_data_rv);
mAdapter = new RoomRvAdapter(mLiveDataList.getValue(),mContext);
mRv.setLayoutManager(new LinearLayoutManager(mContext));
mRv.setAdapter(mAdapter);
//给LiveData添加监听
mLiveDataList.observe(this, new Observer<ArrayList<User>>() {
@Override
public void onChanged(ArrayList<User> users) {
//刷新RecycleView中的数据
mAdapter.refreshList(users);
}
});
}
//Button的点击事件,没点击一次随机生成一个User对象添加到List中
public void addOneData(View view) {
User user = new User();
user.setId(new Random().nextLong());
user.setName("王"+new Random().nextInt(10));
ArrayList<User> list = mLiveDataList.getValue();
if (list!=null){
list.add(user);
}
mLiveDataList.setValue(list);
}
}
layout文件很简单,一个button一个RecycleView
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".liveData.LiveDataActivity">
<Button
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加一条数据"
android:onClick="addOneData"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/live_data_rv"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
效果如图所示:
刚开始没有数据
点击按钮添加数据时自动刷新列表
多次点击:
合并多个 LiveData 源
MediatorLiveData 是 LiveData 的子类,允许您合并多个 LiveData 源。只要任何原始的 LiveData 源对象发生更改,就会触发 MediatorLiveData 对象的观察者。
例如,如果界面中有可以从本地数据库或网络更新的 LiveData 对象,则可以向 MediatorLiveData 对象添加以下源:
与存储在数据库中的数据关联的 LiveData 对象。
与从网络访问的数据关联的 LiveData 对象。
您的 Activity 只需观察 MediatorLiveData 对象即可从这两个源接收更新。有关详细示例,请参阅应用架构指南的附录:公开网络状态部分。
LiveData liveData1 = ...;
LiveData liveData2 = ...;
MediatorLiveData liveDataMerger = new MediatorLiveData<>();
liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
liveDataMerger.addSource(liveData1, new Observer() {
private int count = 1;
@Override public void onChanged(@Nullable Integer s) {
count++;
liveDataMerger.setValue(s);
if (count > 10) {
liveDataMerger.removeSource(liveData1);
}
}
});
LiveData结合Room数据库使用以及线程问题
在LiveData的官方文档中有提到LiveData可以和Room数据库一起使用
也就是说Room查询时可以直接返回一个LiveData对象,给这个LiveData对象添加观察者之后只要数据库数据发生改变都可以收到回调。
Room的使用不在这里说了,直接贴上代码
添加依赖:
androidTestImplementation "android.arch.persistence.room:testing:1.1.1"
implementation 'android.arch.persistence.room:runtime:1.1.1'
annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
bean:
@Entity(tableName = "user")
public class User {
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@PrimaryKey
@ColumnInfo(name = "id")
private long id;
@ColumnInfo(name = "name")
private String name;
@ColumnInfo(name = "sex")
private String sex;
@ColumnInfo(name = "age")
private int age;
}
UserDao:
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
LiveData<List<User>> getUserLiveData();//在这里可以直接返回LiveData<>封装的查询结果
@Insert
void insert(User user);
@Delete
void delect(User user);
@Update
void update(User user);
}
UserDatabase
@Database(entities = {User.class},version = 1,exportSchema = false)
public abstract class UserDatabase extends RoomDatabase {
public static final String DB_NAME = "UserDataBase.db";
private static volatile UserDatabase instance;
public static synchronized UserDatabase getInstance(Context context){
if (instance == null){
instance = createDatabase(context);
}
return instance;
}
private static UserDatabase createDatabase(Context context) {
return Room.databaseBuilder(context,UserDatabase.class,DB_NAME).addCallback(new RoomDatabase.Callback(){
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
}
@Override
public void onOpen(@NonNull SupportSQLiteDatabase db) {
super.onOpen(db);
}
}).build();
}
public abstract UserDao getUserDao();
}
在Activity onCreate的时候去通过数据库获取数据,直接返回LiveData<List< User>>类型对象,给其添加观察者,当数据发生改变时刷新RecycleView
public void addObserver() {
UserDatabase.getInstance(mContext).getUserDao()
.getUserLiveData().observe(mContext, new Observer<List<User>>() {
@Override
public void onChanged(List<User> users) {
adapter.refreshList((ArrayList<User>) users);
}
});
}
在布局中添加按钮,点击添加一条新的数据到数据库
public void addData(View view) {
new Thread(new Runnable() {
@Override
public void run() {
User user = new User();
user.setId(new Random().nextLong());
user.setAge(25);
user.setName("老张");
user.setSex("男");
UserDatabase.getInstance(mContext).getUserDao().insert(user);
}
}).start();
}
点击后RecycleView自动添加一条数据。
这里需要注意一点,因为对数据库的操作很可能耗时比较久,所以Room对数据库的操作是需要在子线程中进行的,如果在主线程中操作数据库会报错。
Caused by: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
at androidx.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:209)
而我上面写的这个例子addObserver()中getUserLiveData()的时候并没有开线程,而且运行的时候也没有出现问题。
比较好奇就去看了下RoomDatabase.java中的代码
首先这个报错是对数据库操作的时候会去判断线程
/**
* Asserts that we are not on the main thread.
*
* @hide
*/
@SuppressWarnings("WeakerAccess")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
// used in generated code
public void assertNotMainThread() {
if (mAllowMainThreadQueries) {
return;
}
if (isMainThread()) {
throw new IllegalStateException("Cannot access database on the main thread since"
+ " it may potentially lock the UI for a long period of time.");
}
}
从code中可以看出mAllowMainThreadQueries这个属性可以绕过这个限制。
@NonNull
public Builder<T> allowMainThreadQueries() {
mAllowMainThreadQueries = true;
return this;
}
也就是说在使用Builder创建时只要
Room.databaseBuilder(context,UserDatabase.class,DB_NAME).allowMainThreadQueries()
就可以允许在主线程中操作数据库,但这样肯定是不推荐的,可能造成卡顿甚至ANR,应该是测试的时候使用的。
除此之外我还发现了这个方法,也就是说Room可以自己传线程池去处理数据库耗时操作。
@NonNull
public Builder<T> setQueryExecutor(@NonNull Executor executor) {
mQueryExecutor = executor;
return this;
}
回到之前的问题,为什么使用LiveData作为返回值的时候可以不用自己开线程
这部分逻辑在Room编译时自动生成的类中
UserDao_Impl.java
我写了两个Query方法,一个返回LiveData对象,一个返回List< User>,看一下区别
@Override
public LiveData<List<User>> getUserLiveData() {
final String _sql = "SELECT * FROM user";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
return new ComputableLiveData<List<User>>(__db.getQueryExecutor()) {
private Observer _observer;
@Override
protected List<User> compute() {
if (_observer == null) {
_observer = new Observer("user") {
@Override
public void onInvalidated(@NonNull Set<String> tables) {
invalidate();
}
};
__db.getInvalidationTracker().addWeakObserver(_observer);
}
final Cursor _cursor = __db.query(_statement);
try {
final int _cursorIndexOfId = _cursor.getColumnIndexOrThrow("id");
final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
final int _cursorIndexOfSex = _cursor.getColumnIndexOrThrow("sex");
final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("age");
final List<User> _result = new ArrayList<User>(_cursor.getCount());
while(_cursor.moveToNext()) {
final User _item;
_item = new User();
final long _tmpId;
_tmpId = _cursor.getLong(_cursorIndexOfId);
_item.setId(_tmpId);
final String _tmpName;
_tmpName = _cursor.getString(_cursorIndexOfName);
_item.setName(_tmpName);
final String _tmpSex;
_tmpSex = _cursor.getString(_cursorIndexOfSex);
_item.setSex(_tmpSex);
final int _tmpAge;
_tmpAge = _cursor.getInt(_cursorIndexOfAge);
_item.setAge(_tmpAge);
_result.add(_item);
}
return _result;
} finally {
_cursor.close();
}
}
@Override
protected void finalize() {
_statement.release();
}
}.getLiveData();
}
@Override
public List<User> getAll() {
final String _sql = "SELECT * FROM user";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
final Cursor _cursor = __db.query(_statement);
try {
final int _cursorIndexOfId = _cursor.getColumnIndexOrThrow("id");
final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
final int _cursorIndexOfSex = _cursor.getColumnIndexOrThrow("sex");
final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("age");
final List<User> _result = new ArrayList<User>(_cursor.getCount());
while(_cursor.moveToNext()) {
final User _item;
_item = new User();
final long _tmpId;
_tmpId = _cursor.getLong(_cursorIndexOfId);
_item.setId(_tmpId);
final String _tmpName;
_tmpName = _cursor.getString(_cursorIndexOfName);
_item.setName(_tmpName);
final String _tmpSex;
_tmpSex = _cursor.getString(_cursorIndexOfSex);
_item.setSex(_tmpSex);
final int _tmpAge;
_tmpAge = _cursor.getInt(_cursorIndexOfAge);
_item.setAge(_tmpAge);
_result.add(_item);
}
return _result;
} finally {
_cursor.close();
_statement.release();
}
}
很明显没有使用LiveData的时候直接走RoomDatabase的query方法,是需要做线程检查的
/**
* Wrapper for {@link SupportSQLiteDatabase#query(SupportSQLiteQuery)}.
*
* @param query The Query which includes the SQL and a bind callback for bind arguments.
* @return Result of the query.
*/
public Cursor query(SupportSQLiteQuery query) {
assertNotMainThread();
return mOpenHelper.getWritableDatabase().query(query);
}
而使用LiveData作为返回值时用到了ComputableLiveData类,此类在构造的时候就将RoomDatabase中的mQueryExecutor传入了。如果在构造的时候没有传入自定义的Executor,那么会自动生成一个
if (mQueryExecutor == null) {
mQueryExecutor = ArchTaskExecutor.getIOThreadExecutor();
}
UserDao_Impl.java中复写了compute()方法
在ComputableLiveData.java中mRefreshRunnable 调用了compute() 方法
@VisibleForTesting
final Runnable mRefreshRunnable = new Runnable() {
@WorkerThread
@Override
public void run() {
boolean computed;
do {
computed = false;
// compute can happen only in 1 thread but no reason to lock others.
if (mComputing.compareAndSet(false, true)) {
// as long as it is invalid, keep computing.
try {
T value = null;
while (mInvalid.compareAndSet(true, false)) {
computed = true;
value = compute();
}
if (computed) {
mLiveData.postValue(value);
}
} finally {
// release compute lock
mComputing.set(false);
}
}
// check invalid after releasing compute lock to avoid the following scenario.
// Thread A runs compute()
// Thread A checks invalid, it is false
// Main thread sets invalid to true
// Thread B runs, fails to acquire compute lock and skips
// Thread A releases compute lock
// We've left invalid in set state. The check below recovers.
} while (computed && mInvalid.get());
}
};
ComputableLiveData构造方法中执行了这个runnable对象
/**
* Creates a computable live data that computes values on the specified executor.
*
* @param executor Executor that is used to compute new LiveData values.
*/
@SuppressWarnings("WeakerAccess")
public ComputableLiveData(@NonNull Executor executor) {
mExecutor = executor;
mLiveData = new LiveData<T>() {
@Override
protected void onActive() {
mExecutor.execute(mRefreshRunnable);
}
};
}
OK,看到这里就比较明了了,使用LiveData的时候自动生成的代码会使用子线程操作数据库,而不使用LiveData的时候直接走query方法,需要做线程检查。
Android使用AIDL共享Room数据库
下面我举例使用AIDL实现一个APP访问另外一个APP创建的数据库。
服务端
首先我们创建一个新的项目作为服务端APP,创建一个database,这里我使用Room框架。自定义类型User,直接上代码
build.gradle中引入room支持
androidTestImplementation "android.arch.persistence.room:testing:1.1.1"
implementation 'android.arch.persistence.room:runtime:1.1.1'
annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
实体类User,注意要在AIDL中使用自定义类型,实体类必须实现Parcelable接口来进行序列化。而且这个类必须添加一个无参的构造方法,通常无参的构造方法即使不写编译器也会帮我们自动生成,但是这里必须手动加上。
Entity
package com.example.testapp.database;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "user")
public class User implements Parcelable {
public User() {
//无参构造
}
protected User(Parcel in) {
id = in.readLong();
name = in.readString();
sex = in.readString();
age = in.readInt();
}
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@PrimaryKey
@ColumnInfo(name = "id")
private long id;
@ColumnInfo(name = "name")
private String name;
@ColumnInfo(name = "sex")
private String sex;
@ColumnInfo(name = "age")
private int age;
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(id);
dest.writeString(name);
dest.writeString(sex);
dest.writeInt(age);
}
}
创建UserDao:
package com.example.testapp.database;
import java.util.List;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
LiveData<List<User>> getUserLiveData();
@Query("SELECT * FROM user")
List<User> getAll();
@Insert
void insert(User user);
@Delete
void delect(User user);
@Update
void update(User user);
}
Room数据库支持直接返回LiveData对象的查询结果,AIDL这个例子没有用到。
UserDatabase:
package com.example.testapp.database;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.sqlite.db.SupportSQLiteDatabase;
@Database(entities = {User.class},version = 1,exportSchema = false)
public abstract class UserDatabase extends RoomDatabase {
public static final String DB_NAME = "UserDataBase.db";
private static volatile UserDatabase instance;
public static synchronized UserDatabase getInstance(Context context){
if (instance == null){
instance = createDatabase(context);
}
return instance;
}
private static UserDatabase createDatabase(Context context) {
return Room.databaseBuilder(context,UserDatabase.class,DB_NAME).allowMainThreadQueries().addCallback(new RoomDatabase.Callback(){
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
}
@Override
public void onOpen(@NonNull SupportSQLiteDatabase db) {
super.onOpen(db);
}
}).build();
}
public abstract UserDao getUserDao();
}
数据库相关的逻辑已经编写完毕,可以通过
UserDatabase.getInstance(mContext).getUserDao().insert(user);
来给数据库添加数据。
创建AIDL
将视图切换到Project目录下,右键点击项目创建AIDL文件。
文件名称自定义就好,创建完成后可以看到默认生成的代码。
// DBAidlInterface.aidl
package com.example.testapp.database;
import com.example.testapp.database.User;
// Declare any non-default types here with import statements
interface DBAidlInterface {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
List<User> getUsers();
}
basicTypes这个方法是自动生成的,可以删除,这个方法只是告诉你哪些基本类型可以直接在AIDL中使用。
List getUsers()是我们自己创建的方法,表示需要返回一个List< User>类型的的结果。
注意AIDL文件目前并不能自动导包,所以import信息需要你自己敲上去。
因为我们用到了自定义类型User,所以在这个AIDL文件同包下我们还需要新建一个AIDL文件用来帮助AIDL找到这个User类,这部必不可少,否则会报找不到类的错误。
文件名必须和自定义类型名称相同。这里我们的自定义类型是User,那么这个AIDL文件就需要命名为User.aidl.
内容只需要两行
package com.example.testapp.database;//设置User所在包名
parcelable User;//声明User为parcelable类型
sycn project以下以生成AIDL相关代码。
此时build文件下下就生成了相关文件DBAidlInterface
这个代码都是自动生成的,这里就不贴出来看了。
OK,服务端app的最后一步就是创建Service了,和普通Service类似,只是我们的IBinder对象改为继承刚才生成代码中的内部静态抽象类 Stub。
package com.example.testapp.database;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import java.util.List;
public class DBService extends Service {
public DBService() {
}
@Override
public IBinder onBind(Intent intent) {
return new DBBinder();
}
class DBBinder extends DBAidlInterface.Stub{
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
}
@Override
public List<User> getUsers() throws RemoteException {
//为了方便我的Room配置允许主线程操作,数据很少不会造成ANR
List<User> all = UserDatabase.getInstance(getApplicationContext()).getUserDao().getAll();
return all;
}
}
}
清单文件里注册一下service
<service
android:name=".database.DBService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.testapp.aidl"/>
</intent-filter>
</service>
给service添加一个隐式意图的action,以方便另外一个app可以找到这个service。
到这里服务端APP搞定了,下面编写客户端APP的代码。
客户端
新建工程,将aidl整个包拷贝到新app的对应位置,包名必须一致。同样需要拷贝的还有数据bean User.java。这里同样这个类所在的包必须和服务端User类所在的包名一模一样。比如我服务端User类在com.example.testapp.database包下,那么我就需要在客户端app下也创建一个一样的包把User类放进去。
现在我们就可以去调用服务端的Service了。
简单编写一个Activity,在Activity中使用TextView显示数据库中所有User的name。
package com.example.test2app;
import androidx.appcompat.app.AppCompatActivity;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.example.testapp.database.DBAidlInterface;
import com.example.testapp.database.User;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private DBAidlInterface aidlInterface;
private TextView textView;
private Context mContext;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
aidlInterface = DBAidlInterface.Stub.asInterface(service);
Toast.makeText(mContext,"已连接",Toast.LENGTH_LONG).show();
}
@Override
public void onServiceDisconnected(ComponentName name) {
aidlInterface = null;
Toast.makeText(mContext,"断开连接",Toast.LENGTH_LONG).show();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.text_view);
mContext = this;
}
@Override
protected void onStart() {
super.onStart();
Intent intent =new Intent();
//Android现在对隐式意图管理严格,除了要setAction包名也需要设置,这里包名是服务端app的包名
intent.setAction("com.example.testapp.aidl");
intent.setPackage("com.example.testapp");
//绑定服务
bindService(intent,mConnection,Context.BIND_AUTO_CREATE);
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
//解绑服务
unbindService(mConnection);
}
//Button OnClick方法
public void getData(View view) {
if (aidlInterface!=null){
try {
List<User> users = aidlInterface.getUsers();
String str = "";
for (User user: users){
str = str + user.getName()+" ";
}
textView.setText(str);
} catch (RemoteException e) {
e.printStackTrace();
}
}else {
}
}
}
将两个APP都安装到手机上,在服务端APP上添加数据库数据,切换到客户端app上点击按钮就能拿到最新的数据了。
扩展
想到这个例子主要是之前看Content Provider的时候发现网上操作Content Provider的例子几乎都是使用的原生SQLiteOpenHelper的,并没有结合GreenDao,Room这种sqlite框架或者Realm数据库来使用,使用起来不是很方便。ContentProvider也是Android跨进程数据共享方案,和AIDL一样底层都是Binder机制,就想着能不能用AIDL来跨进程访问数据库,现在看来是可以的。但是有一个问题,客户端在绑定Service的时候需要服务端的进程是活着的。所以在启动前需要先唤醒服务端进程或者给服务端做保活才稳定。还是Content Provider好用~
Room数据库存储自定义数据类和List类型以及碰到的一些坑
Room数据库是Google官方Jetpack框架中推荐的数据库组件,虽然其速度和不使用Sqlite的Realm数据库比还是有一些差距,但是其可以直接返回LiveData对象或者RxJava的Observable对象,使用起来十分方便。
上面的演示都是使用最基本的Room数据类型,下面记录一下复杂类型的使用方法。
1.使用自定义类型
Room如果碰到自定义类型嵌套自定义类型的情况下怎么办?
比如一个User类型中包含有一个Family类型:
data class User(
var id:Long = 0,
var name:String?=null,
var age:Int = 0,
var sex:String?=null
var family:Family ?=null
)
data class Family(
var id:Long = 0,
var father:String?=null,
var mother:String?=null
)
如果在使用Room的时候Entiy中直接使用地定义类型编译时就会直接报错,比如这么写
@Entity(tableName = "user")
data class User(
@PrimaryKey
@ColumnInfo(id= "id")
var id:Long = 0,
@ColumnInfo(name= "name")
var name:String?=null,
@ColumnInfo(age= "age")
var age:Int = 0,
@ColumnInfo(sex= "sex")
var sex:String?=null
@ColumnInfo(family= "family")
var family:Family ?=null
)
报错
cannot figure out how to save this field into database. You can consider adding a type converter for it.
编译器提示我们应该使用一个转换器去转换这个非基本类型。网上搜到的大部分解决方案也是使用Type Converter 来转换类型。这个方法我先不介绍,其实有更加简单的方式来实现。
@Embedded注解
Room提供了一个 @Embedded 注解来帮助我们解决这个问题。
贴一下官方注释:
/**
* Can be used as an annotation on a field of an {@link Entity} or {@code Pojo} to signal that
* nested fields (i.e. fields of the annotated field's class) can be referenced directly in the SQL
* queries.
* <p>
* If the container is an {@link Entity}, these sub fields will be columns in the {@link Entity}'s
* database table.
* <p>
* For example, if you have 2 classes:
* <pre>
* public class Coordinates {
* double latitude;
* double longitude;
* }
* public class Address {
* String street;
* {@literal @}Embedded
* Coordinates coordinates;
* }
* </pre>
* Room will consider {@code latitude} and {@code longitude} as if they are fields of the
* {@code Address} class when mapping an SQLite row to {@code Address}.
* <p>
* So if you have a query that returns {@code street, latitude, longitude}, Room will properly
* construct an {@code Address} class.
* <p>
* If the {@code Address} class is annotated with {@link Entity}, its database table will have 3
* columns: {@code street, latitude, longitude}
* <p>
* If there is a name conflict with the fields of the sub object and the owner object, you can
* specify a {@link #prefix()} for the items of the sub object. Note that prefix is always applied
* to sub fields even if they have a {@link ColumnInfo} with a specific {@code name}.
* <p>
* If sub fields of an embedded field has {@link PrimaryKey} annotation, they <b>will not</b> be
* considered as primary keys in the owner {@link Entity}.
* <p>
* When an embedded field is read, if all fields of the embedded field (and its sub fields) are
* {@code null} in the {@link android.database.Cursor Cursor}, it is set to {@code null}. Otherwise,
* it is constructed.
* <p>
* Note that even if you have {@link TypeConverter}s that convert a {@code null} column into a
* {@code non-null} value, if all columns of the embedded field in the
* {@link android.database.Cursor Cursor} are null, the {@link TypeConverter} will never be called
* and the embedded field will not be constructed.
* <p>
* You can override this behavior by annotating the embedded field with
* {@link androidx.annotation.NonNull}.
*/
简单来说就是使用 @Embedded 标识的嵌套字段可以直接在SQL中引用。
那么之前那个例子可以这么写:
@Entity(tableName = "user")
data class User(
@PrimaryKey
@ColumnInfo(id= "id")
var id:Long = 0,
@ColumnInfo(name= "name")
var name:String?=null,
@ColumnInfo(age= "age")
var age:Int = 0,
@ColumnInfo(sex= "sex")
var sex:String?=null
@Embedded
var family:Family ?=null
)
@Entity(tableName = "family")
data class Family(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "faimly_id")
var id:Long = 0,
@ColumnInfo(name = "faimly_father")
var father:String?=null,
@ColumnInfo(name = "faimly_mother")
var mother:String?=null
)
这样写就没有问题了。
这里我们给Family也定义了一张表,使用 @Embedded后User中嵌套Family类,这其实是把两张表嵌套成了一张,这会带来一些问题,比如字段重名。
User类中有id字段,Family中也有id字段,这里我把Family中的id字段重命名为faimly_id了,如果也写为id和User类中的一样,那么就会报错:
Multiple fields have the same columnName: id. Field names: id, user > id.
除了这种重名情况还有一种坑也会报重名异常
一个类型被多次嵌套的情况:
比如换一种复杂点的情况,有一个User基本类型,还有一个Employee类型与Manager类型
@Entity(tableName = "user")
data class User(
@PrimaryKey
@ColumnInfo(id= "id")
var id:Long = 0,
@ColumnInfo(name= "name")
var name:String?=null,
@ColumnInfo(age= "age")
var age:Int = 0,
@ColumnInfo(sex= "sex")
var sex:String?=null
)
employee中包含manager对象和user对象
@Entity(tableName = "employee")
data class Employee(
@PrimaryKey
@ColumnInfo(id= "employee_id")
var id:Long = 0,
@Embedded
var user:User?=null
@Embedded
var manager:Manager?=null
)
manager中也包含user对象
@Entity(tableName = "manager")
data class Manager(
@PrimaryKey
@ColumnInfo(id= "manager_id")
var id:Long = 0,
@Embedded
var user:User?=null
)
此时在为employee建表的时候,employee包含user类型,为user建立了一张表,employee中还有manager类型,manager中也包含user类型,他又会给user建立一张表。被建立的两张user表是会被嵌套在一起的,这个时候自动生成的代码user中的字段全部都是重名的,编译无法通过。
这种情况下的解决方案有两种,一是为Manager中的User类型重新定义一个Entity类,其中的字段名与Employee中嵌套的字段名不能重名,比如
@Entity(tableName = "manager_user")
data class ManagerUser(
@PrimaryKey
@ColumnInfo(id= "manager_id")
var id:Long = 0,
@ColumnInfo(name= "manager_name")
var name:String?=null,
@ColumnInfo(age= "manager_age")
var age:Int = 0,
@ColumnInfo(sex= "manager_sex")
var sex:String?=null
)
然后再Manager中引用ManagerUser类型而不是User类型。但是这种方法增加了很多重复代码。
第二种方案就是前面说过的Type Converter方式了。
Type Converter
Room中的Type Converter其实就是让使用者自定义的类型转换工具。
像上面这种情况可以把User数据转化为Json数据存储在数据库中。
首先定义Converters类:
class UserConverters {
@TypeConverter
fun stringToUser(value: String):User{
val type = object :TypeToken<User>(){
}.type
return Gson().fromJson(value,type)
}
@TypeConverter
fun userToString(user:User): String {
val gson = Gson()
return gson.toJson(user)
}
}
在其他Entity嵌套User类型的时候就不用使用 @Embedded 注解了,而是在Entity头部使用 @TypeConverters(UserConverters::class) 注解
@Entity(tableName = "employee")
@TypeConverters(UserConverters::class)
data class Employee(
@PrimaryKey
@ColumnInfo(id= "employee_id")
var id:Long = 0,
@ColumnInfo(name = "employee_user")
var user:User?=null
@Embedded
var manager:Manager?=null
)
@Entity(tableName = "manager")
@TypeConverters(UserConverters::class)
data class Manager(
@PrimaryKey
@ColumnInfo(id= "manager_id")
var id:Long = 0,
@ColumnInfo(name = "manager_user")
var user:User?=null
)
这样就OK了,不需要再写一个User类型。
除了使用Json进行转换外任何转换方式都可以自定义,比如将Long类型的时间转化为Date类型
class DateConverter {
@TypeConverter
fun revertDate(value:Long):Date {
return Date(value);
}
@TypeConverter
fun converterDate(value:Date):long {
return value.getTime();
}
}
容器类型的数据存储也是同理,将List数据转化为Json存储
data class PicUrls(
var thumbnail_pic: String? = null
)
class PicUrlsConverters {
@TypeConverter
fun stringToObject(value: String): List<PicUrls> {
val listType = object : TypeToken<List<PicUrls>>() {
}.type
return Gson().fromJson(value, listType)
}
@TypeConverter
fun objectToString(list: List<Any>): String {
val gson = Gson()
return gson.toJson(list)
}
}
顺便提一下这个异常
aused by: java.lang.RuntimeException: cannot find implementation for com.xxx.xxxx.data.database.xxxDataBase.xxxDataBase_Impl does not exist
at androidx.room.Room.getGeneratedImplementation(Room.java:94)
at androidx.room.RoomDatabase$Builder.build(RoomDatabase.java:667)
这种情况通常都是使用kotlin编写项目的时候引入Room依赖时使用的是java的关键字而不是kotlin的关键字导致的。
如果是Java项目,在app 的build.gradle中需要使用
annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
如果你使用的kotlin编写而用了annotationProcessor引入room compiler就会出现上述异常
kotlin项目应该在build.gradle头部先
apply plugin: 'kotlin-kapt'
然后使用kapt引入依赖
kapt 'android.arch.persistence.room:compiler:1.1.1'
这个问题大部分都是没使用kapt导致的,当然也有小部分比如是DataBase的时候忘记了加@Database注解
@Database(entities = [Status::class,User::class,RetweetedStatusBean::class,Visible::class],version = 1,exportSchema = false)
abstract class XXXDataBase : RoomDatabase() {