Android Jetpack框架-LiveData和Room使用

官方介绍

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() {

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值