Android Architecture Components 整理

Android Architecture Components是谷歌在Google I/O 2017发布一套帮助开发者解决Android架构设计的方案。

里面包含了两大块内容:

  1. 生命周期相关的Lifecycle-aware Components
  2. 数据库解决方案Room

A new collection of libraries that help you design robust, testable, and maintainable apps.

即这是一个帮助构建稳定,易于测试和易于维护的App架构的库。

 

ViewModel

ViewModel是为特定的UI(例如Activity/Fragment)提供数据的类,同时它也承担和数据相关的业务逻辑处理功能:比如根据uid请求网络获取完整的用户信息,或者将用户修改的头像上传到服务器。因为ViewModel是独立于View(例如Activity/Fragment)这一层的,所以并不会被View层的事件影响,比如Activity被回收或者屏幕旋转等并不会造成ViewModel的改变。

 

 

LiveData

LiveData是一个包含可以被观察的数据载体。这么说又有点不好理解了,其实他就是基于观察者模式去做的。当LiveData的数据发生变化时,所有对这个LiveData变化感兴趣的类都会收到变化的更新。并且他们之间并没有类似于接口回调这样的明确的依赖关系。LiveData还能够感知组件(例如activities, fragments, services)的生命周期,防止内存泄漏。其实这和RxJava非常相似,但是LiveData能够在组件生命周期结束后自动阻断数据流的传播,防止产生空指针等意外。这个RxJava是不同的。

 

 

获取数据

用Retrofit的WebService获取数据

public interface Webservice {
    /**
     * @GET declares an HTTP GET request
     * @Path("user") annotation on the userId parameter marks it as a
     * replacement for the {user} placeholder in the @GET path
     */
    @GET("/users/{user}")
    Call getUser(@Path("user") String userId);
}

Repository

Repository类的作用就是获取并提供各种来源的数据(数据库中的数据,网络数据,缓存数据等),并且在来源数据更新时通知数据的获取方。

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData getUser(int userId) {
        // This is not an optimal implementation, we'll fix it below(这不是最好的实现,以后会修复的)
        final MutableLiveData data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) {
                // error case is left out for brevity(为了简单起见,简化了错误处理的情况)
                data.setValue(response.body());
            }
        });
        return data;
    }
}

连接ViewModel和Repository

 

public class UserProfileViewModel extends ViewModel {
    private LiveData user;
    private UserRepository userRepo;

    @Inject // UserRepository parameter is provided by Dagger 2
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(String userId) {
        if (this.user != null) {
            // ViewModel is created per Fragment so
            // we know the userId won't change
            return;
        }
        user = userRepo.getUser(userId);
    }

    public LiveData getUser() {
        return this.user;
    }
}

 

缓存数据

@Singleton  // informs Dagger that this class should be constructed once
public class UserRepository {
    private Webservice webservice;
    // simple in memory cache, details omitted for brevity(简单的内存缓存)
    private UserCache userCache;
    public LiveData getUser(String userId) {
        LiveData cached = userCache.get(userId);
        if (cached != null) {
            return cached;
        }

        final MutableLiveData data = new MutableLiveData<>();
        userCache.put(userId, data);
        // this is still suboptimal but better than before.
        // a complete implementation must also handle the error cases.
        webservice.getUser(userId).enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) {
                data.setValue(response.body());
            }
        });
        return data;
    }
}

持久化数据

Room

Room是一个对象映射库,(姑且按GreenDAO的功能来理解吧)可以在在编译的时候就能检测出SQL语句的异常。还能够在数据库内容发生改变时通过LiveData的形式发出通知

 

  1. 定义User类,并且使用@Entity注解:   

  

@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;
  // getters and setters for fields
}

  2.创建RoomDatabase:

 

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}

3.创建DAO

@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);
    @Query("SELECT * FROM user WHERE id = :userId")
    LiveData load(String userId);
}

4.在RoomDatabase中访问DAO

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

 

把Room和UserRepository结合起来:

@Singleton
public class UserRepository {
    private final Webservice webservice;
    private final UserDao userDao;
    private final Executor executor;

    @Inject
    public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
        this.webservice = webservice;
        this.userDao = userDao;
        this.executor = executor;
    }

    public LiveData getUser(String userId) {
        refreshUser(userId);
        // return a LiveData directly from the database.(从数据库中直接返回LiveData)
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        executor.execute(() -> {
            // running in a background thread(在后台线程运行)
            // check if user was fetched recently(检查数据库中是否有数据)
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // refresh the data
                Response response = webservice.getUser(userId).execute();
                // TODO check for error etc.
                // Update the database.The LiveData will automatically refresh so
                // we don't need to do anything else here besides updating the database(不用其他代码就能实现数据自动更新)
                userDao.save(response.body());
            }
        });
    }
}

数据来源的唯一性

在上面提供的代码中,数据库是App数据的唯一来源。Google推荐采用这种方式。

最终的架构形态

下面这张图展示了使用Android Architecture Components来构建的App整体的架构:

 

 

一些App架构设计的推荐准则

  • 不要把在Manifest中定义的组件作为提供数据的来源(包括Activity、Services、Broadcast Receivers等),因为他们的生命周期相对于App的生命周期是相对短暂的。
  • 严格的限制每个模块的功能。比如上面提到的不要再ViewModel中增加如何获取数据的代码。
  • 每个模块尽可能少的对外暴露方法。
  • 模块中对外暴露的方法要考虑单元测试的方便。
  • 不要重复造轮子,把精力放在能够让App脱颖而出的业务上。
  • 尽可能多的持久化数据,因为这样即使是在网络条件不好的情况下,用户仍然能够使用App
  • 保证数据来源的唯一性(即:提供一个类似UserRepository的类)


Lifecycle

Lifecycle是一个包含组件(Activity或者Fragment)生命周期状态的类,这个类还能够为其他类提供当前的生命周期。

Lifecycle使用两个主要的枚举来跟踪他所关联组件的生命周期。

  • Event 事件 从组件或者Lifecycle类分发出来的生命周期,它们和Activity/Fragment生命周期的事件一一对应。(ON_CREATE,ON_START,ON_RESUME,ON_PAUSE,ON_STOP,ON_DESTROY)
  • State 状态 当前组件的生命周期状态(INITIALIZED,DESTROYED,CREATED,STARTED,RESUMED)


LifecycleOwner

实现LifecycleOwner就表示这是个有生命周期的类,他有一个getLifecycle ()方法是必须实现的。com.android.support:appcompat-v7:26.1.0中的AppCompatActivity已经实现了这个接口,详细的实现可以自行查看代码。

对于前面提到的监听位置的例子。可以把MyLocationListener实现LifecycleObserver,然后在LifecycleActivity/Fragment)的onCreate方法中初始化。这样MyLocationListener就能自行处理生命周期带来的问题。

class MyActivity extends AppCompatActivity {
   private MyLocationListener myLocationListener;

   public void onCreate(...) {
       myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
           // update UI
       });
       Util.checkUserStatus(result -> {
           if (result) {
               myLocationListener.enable();
           }
       });
 }
}

class MyLocationListener implements LifecycleObserver {
   private boolean enabled = false;
   public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
      ...
   }

   @OnLifecycleEvent(Lifecycle.Event.ON_START)
   void start() {
       if (enabled) {
          // connect
       }
   }

   public void enable() {
       enabled = true;
       if (lifecycle.getState().isAtLeast(STARTED)) {
           // connect if not connected
       }
   }

   @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
   void stop() {
       // disconnect if connected
   }
}

Lifecycles的最佳建议

  • 保持UI Controllers(Activity/Fragment)中代码足够简洁。一定不能包含如何获取数据的代码,要通过ViewModel获取LiveData形式的数据。
  • 用数据驱动UI,UI的职责就是根据数据改变显示的内容,并且把用户操作UI的行为传递给ViewModel。
  • 把业务逻辑相关的代码放到ViewModel中,把ViewModel看成是链接UI和App其他部分的胶水。但ViewModel不能直接获取数据,要通过调用其他类来获取数据。
  • 使用DataBinding来简化View(布局文件)和UI Controllers(Activity/Fragment)之间的代码
  • 如果布局本身太过复杂,可以考虑创建一个Presenter类来处理UI相关的改变。虽然这么做会多写很多代码,但是对于保持UI的简介和可测试性是有帮助的。
  • 不要在ViewModel中持有任何View/Activity的context。否则会造成内存泄露。



LiveData

LiveData是一种持有可被观察数据的类。和其他可被观察的类不同的是,LiveData是有生命周期感知能力的,这意味着它可以在activities, fragments, 或者 services生命周期是活跃状态时更新这些组件。

要想使用LiveData(或者这种有可被观察数据能力的类)就必须配合实现了LifecycleOwner的对象使用。在这种情况下,当对应的生命周期对象DESTORY时,才能移除观察者。这对Activity或者Fragment来说显得尤为重要,因为他们可以在生命周期结束的时候立刻解除对数据的订阅,从而避免内存泄漏等问题。

使用LiveData的优点

  • UI和实时数据保持一致 因为LiveData采用的是观察者模式,这样一来就可以在数据发生改变时获得通知,更新UI。
  • 避免内存泄漏 观察者被绑定到组件的生命周期上,当被绑定的组件销毁(destory)时,观察者会立刻自动清理自身的数据。
  • 不会再产生由于Activity处于stop状态而引起的崩溃 例如:当Activity处于后台状态时,是不会收到LiveData的任何事件的。
  • 不需要再解决生命周期带来的问题 LiveData可以感知被绑定的组件的生命周期,只有在活跃状态才会通知数据变化。
  • 实时数据刷新 当组件处于活跃状态或者从不活跃状态到活跃状态时总是能收到最新的数据
  • 解决Configuration Change问题 在屏幕发生旋转或者被回收再次启动,立刻就能收到最新的数据。
  • 数据共享 如果对应的LiveData是单例的话,就能在app的组件间分享数据。这部分详细的信息可以参考继承LiveData
  •  

使用LiveData

  1. 创建一个持有某种数据类型的LiveData (通常是在ViewModel中)
  2. 创建一个定义了onChange()方法的观察者。这个方法是控制LiveData中数据发生变化时,采取什么措施 (比如更新界面)。通常是在UI Controller (Activity/Fragment) 中创建这个观察者。
  3. 通过 observe()方法连接观察者和LiveData。observe()方法需要携带一个LifecycleOwner类。这样就可以让观察者订阅LiveData中的数据,实现实时更新。

 

创建LiveData对象

LiveData是一个数据的包装。具体的包装对象可以是任何数据,包括集合(比如List)。LiveData通常在ViewModel中创建,然后通过gatter方法获取。具体可以看一下代码:

public class NameViewModel extends ViewModel {

// Create a LiveData with a String 暂时就把MutableLiveData看成是LiveData吧,下面的文章有详细的解释
private MutableLiveData mCurrentName;

    public MutableLiveData getCurrentName() {
        if (mCurrentName == null) {
            mCurrentName = new MutableLiveData();
        }
        return mCurrentName;
    }

// Rest of the ViewModel...
}

观察LiveData中的数据

通常情况下都是在组件的onCreate()方法中开始观察数据,原因有以下两点:

  • 系统会多次调用onResume()方法。
  • 确保Activity/Fragment在处于活跃状态时立刻可以展示数据。

下面的代码展示了如何观察LiveData对象:

public class NameActivity extends AppCompatActivity {

    private NameViewModel mModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Other code to setup the activity...

        // Get the ViewModel.
        mModel = ViewModelProviders.of(this).get(NameViewModel.class);

        // Create the observer which updates the UI.
        final Observer nameObserver = new Observer() {
            @Override
            public void onChanged(@Nullable final String newName) {
                // Update the UI, in this case, a TextView.
                mNameTextView.setText(newName);
            }
        };

        // Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
        mModel.getCurrentName().observe(this, nameObserver);
    }
}

更新LiveData对象

如果想要在UI Controller中改变LiveData中的值呢?(比如点击某个Button把性别从男设置成女)。LiveData并没有提供这样的功能,但是Architecture Component提供了MutableLiveData这样一个类,可以通过setValue(T)postValue(T)方法来修改存储在LiveData中的数据。MutableLiveData是LiveData的一个子类,从名称上也能看出这个类的作用。举个直观点的例子:

 

mButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        String anotherName = "John Doe";
        mModel.getCurrentName().setValue(anotherName);
    }
});

调用setValue()方法就可以把LiveData中的值改为John Doe。同样,通过这种方法修改LiveData中的值同样会触发所有对这个数据感兴趣的类。那么setValue()postValue()有什么不同呢?区别就是setValue()只能在主线程中调用,而postValue()可以在子线程中调用。

Room和LiveData配合使用

Room可以返回LiveData的数据类型。这样对数据库中的任何改动都会被传递出去。这样修改完数据库就能获取最新的数据,减少了主动获取数据的代码。

   public LiveData getUser(String userId) {
        refreshUser(userId);
        // return a LiveData directly from the database.(从数据库中直接返回LiveData)
        return userDao.load(userId);
    }

 

继承LiveData扩展功能

LiveData的活跃状态包括:STARTED或者RESUMED两种状态。那么如何在活跃状态下把数据传递出去呢?

public class StockLiveData extends LiveData {
    private StockManager mStockManager;

    private SimplePriceListener mListener = new SimplePriceListener() {
        @Override
        public void onPriceChanged(BigDecimal price) {
            setValue(price);
        }
    };

    public StockLiveData(String symbol) {
        mStockManager = new StockManager(symbol);
    }

    @Override
    protected void onActive() {
        mStockManager.requestPriceUpdates(mListener);
    }

    @Override
    protected void onInactive() {
        mStockManager.removeUpdates(mListener);
    }
}

可以看到onActive()onInactive()就表示了处于活跃和不活跃状态的回调。

public class MyFragment extends Fragment {
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        LiveData myPriceListener = ...;
        myPriceListener.observe(this, price -> {
            // Update the UI.
        });
    }
}

如果把StockLiveData写成单例模式,那么还可以在不同的组件间共享数据。

public class StockLiveData extends LiveData {
    private static StockLiveData sInstance;
    private StockManager mStockManager;

    private SimplePriceListener mListener = new SimplePriceListener() {
        @Override
        public void onPriceChanged(BigDecimal price) {
            setValue(price);
        }
    };

    @MainThread
    public static StockLiveData get(String symbol) {
        if (sInstance == null) {
            sInstance = new StockLiveData(symbol);
        }
        return sInstance;
    }

    private StockLiveData(String symbol) {
        mStockManager = new StockManager(symbol);
    }

    @Override
    protected void onActive() {
        mStockManager.requestPriceUpdates(mListener);
    }

    @Override
    protected void onInactive() {
        mStockManager.removeUpdates(mListener);
    }
}

转换LiveData中的值(Transform LiveData)

这么说很容易和上文改变LiveData中的值搞混。这里的变换是指在LiveData的数据被分发到各个组件之前转换值的内容,各个组件收到的是转换后的值,但是LiveData里面数据本身的值并没有改变。(和RXJava中map的概念很像)Lifecycle包中提供了Transformations来提供转换的功能。

Transformations.map()

LiveData userLiveData = ...;
LiveData userName = Transformations.map(userLiveData, user -> {
    user.name + " " + user.lastName
});

把原来是包含User的LiveData转换成包含String的LiveData传递出去。

Transformations.switchMap()

private LiveData getUser(String id) {
  ...;
}

LiveData userId = ...;
LiveData user = Transformations.switchMap(userId, id -> getUser(id) );

和上面的map()方法很像。区别在于传递给switchMap()的函数必须返回LiveData对象。
和LiveData一样,Transformation也可以在观察者的整个生命周期中存在。只有在观察者处于观察LiveData状态时,Transformation才会运算。Transformation是延迟运算的(calculated lazily),而生命周期感知的能力确保不会因为延迟发生任何问题。

如果在ViewModel对象的内部需要一个Lifecycle对象,那么使用Transformation是一个不错的方法。举个例子:假如有个UI组件接受输入的地址,返回对应的邮政编码。那么可以 实现一个ViewModel和这个组件绑定:

class MyViewModel extends ViewModel {
    private final PostalCodeRepository repository;
    public MyViewModel(PostalCodeRepository repository) {
       this.repository = repository;
    }

    private LiveData getPostalCode(String address) {
       // DON'T DO THIS (不要这么干)
       return repository.getPostCode(address);
    }
}

看代码中的注释,有个// DON'T DO THIS (不要这么干),这是为什么?有一种情况是如果UI组件被回收后又被重新创建,那么又会触发一次 repository.getPostCode(address)查询,而不是重用上次已经获取到的查询。那么应该怎样避免这个问题呢?看一下下面的代码:

class MyViewModel extends ViewModel {
    private final PostalCodeRepository repository;
    private final MutableLiveData addressInput = new MutableLiveData();
    public final LiveData postalCode =
            Transformations.switchMap(addressInput, (address) -> {
                return repository.getPostCode(address);
             });

  public MyViewModel(PostalCodeRepository repository) {
      this.repository = repository
  }

  private void setInput(String address) {
      addressInput.setValue(address);
  }
}

合并多个LiveData中的数据

MediatorLiveData是LiveData的子类,可以通过MediatorLiveData合并多个LiveData来源的数据。同样任意一个来源的LiveData数据发生变化,MediatorLiveData都会通知观察他的对象。说的有点抽象,举个例子。比如UI接收来自本地数据库和网络数据,并更新相应的UI。可以把下面两个LiveData加入到MeidatorLiveData中:

  • 关联数据库的LiveData
  • 关联联网请求的LiveData

相应的UI只需要关注MediatorLiveData就可以在任意数据来源更新时收到通知。

 

ViewModel

ViewModel设计的目的就是存放和处理和UI相关的数据,并且这些数据不受配置变化(Configuration Changes,例如:旋转屏幕,组件被系统回收)的影响。

ViewModel用于为UI组件提供数据,并且能够在旋转屏幕等Configuration Change发生时,仍能保持里面的数据。当UI组件恢复时,可以立刻向UI提供数据。

 

public class MyViewModel extends ViewModel {
    private MutableLiveData> users;
    public LiveData> getUsers() {
        if (users == null) {
            users = new MutableLiveData>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        // do async operation to fetch users
    }
}

Activity访问User List数据:

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}

假如用户按返回键,主动销毁了这个Activity呢?这时系统会调用ViewModel的onCleared()方法,清除ViewModel中的数据。

 

在Fragments间分享数据

使用ViewModel可以很好的解决这个问题。假设有这样两个Fragment,一个Fragment提供一个列表,另一个Fragment提供点击每个item现实的详细信息。

public class SharedViewModel extends ViewModel {
    private final MutableLiveData selected = new MutableLiveData();

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData getSelected() {
        return selected;
    }
}

public class MasterFragment extends Fragment {
    private SharedViewModel model;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends LifecycleFragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, { item ->
           // update UI
        });
    }
}

两个Fragment都是通过getActivity()来获取ViewModelProvider。这意味着两个Activity都是获取的属于同一个Activity的同一个ShareViewModel实例。
这样做优点如下:

  • Activity不需要写任何额外的代码,也不需要关心Fragment之间的通信。
  • Fragment不需要处理除SharedViewModel以外其他的代码。这两个Fragment不需要知道对方是否存在。
  • Fragment的生命周期不会相互影响

ViewModel的生命周期

ViewModel只有在Activity finish或者Fragment detach之后才会销毁。

 

Room

Room在SQLite上提供了一个方便访问的抽象层。App把经常需要访问的数据存储在本地将会大大改善用户的体验。这样用户在网络不好时仍然可以浏览内容。当用户网络可用时,可以更新用户的数据。

使用原始的SQLite可以提供这样的功能,但是有以下两个缺点:

  • 没有编译时SQL语句的检查。尤其是当你的数据库表发生变化时,需要手动的更新相关代码,这会花费相当多的时间并且容易出错。
  • 编写大量SQL语句和Java对象之间相互转化的代码。

Room包含以下三个重要组成部分:

  • Database 创建数据库。

  • Entities 数据库表中对应的Java对象

  • DAOs 访问数据库

 

 

User.java

@Entity
public class User {
    @PrimaryKey
    private int uid;

    @ColumnInfo(name = "first_name")
    private String firstName;

    @ColumnInfo(name = "last_name")
    private String lastName;

    // Getters and setters are ignored for brevity, 
    // but they're required for Room to work.
    //Getters和setters为了简单起见就省略了,但是对Room来说是必须的
}

UserDao.java

@Dao
public interface UserDao {
    @Query("SELECT * FROM user")
    List getAll();

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List loadAllByIds(int[] userIds);

    @Query("SELECT * FROM user WHERE first_name LIKE :first AND "
           + "last_name LIKE :last LIMIT 1")
    User findByName(String first, String last);

    @Insert
    void insertAll(User... users);

    @Delete
    void delete(User user);
}

AppDatabase.java

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

在创建了上面三个文件后,就可以通过如下代码创建数据库了:

AppDatabase db = Room.databaseBuilder(getApplicationContext(),
        AppDatabase.class, "database-name").build();

Entities

@Entity
如果上面的User类中包含一个字段是不希望存放到数据库中的,那么可以用@Ignore注解这个字段:

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;

    //不需要被存放到数据库中
    @Ignore
    Bitmap picture;
}

Room持久化一个类的field必须要求这个field是可以访问的。可以把这个field设为public或者设置setter和getter。

Primary Key 主键

每个Entity都必须定义一个field为主键,即使是这个Entity只有一个field。如果想要Room生成自动的primary key,可以使用@PrimaryKey的autoGenerate属性。如果Entity的primary key是多个Field的复合Key,可以向下面这样设置:

@Entity(primaryKeys = {"firstName", "lastName"})
class User {
    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
}

在默认情况下Room使用类名作为数据库表的名称。如果想要设置不同的名称,可以参考下面的代码,设置表名tableName为users:

@Entity(tableName = "users")
class User {
    ...
}

和设置tableName相似,Room默认使用field的名称作为表的列名。如果想要使用不同的名称,可以通过@ColumnInfo(name = "first_name")设置,代码如下:

@Entity(tableName = "users")
class User {
    @PrimaryKey
    public int id;

    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    public String lastName;

    @Ignore
    Bitmap picture;
}

和设置tableName相似,Room默认使用field的名称作为表的列名。如果想要使用不同的名称,可以通过@ColumnInfo(name = "first_name")设置,代码如下:

@Entity(tableName = "users")
class User {
    @PrimaryKey
    public int id;

    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    public String lastName;

    @Ignore
    Bitmap picture;
}

索引和唯一性

外键

例如,有一个Pet类需要和User类建立关系,可以通过@ForeignKey来达到这个目的,代码如下:

@Entity(foreignKeys = @ForeignKey(entity = User.class,
                                  parentColumns = "id",
                                  childColumns = "user_id"))
class Pet {
    @PrimaryKey
    public int petId;

    public String name;

    @ColumnInfo(name = "user_id")
    public int userId;
}

对象嵌套对象

class Address {
    public String street;
    public String state;
    public String city;

    @ColumnInfo(name = "post_code")
    public int postCode;
}

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;

    @Embedded
    public Address address;
}

 

Data Access Objects(DAOs)

DAOs是数据库访问的抽象层。
Dao可以是一个接口也可以是一个抽象类。如果是抽象类,那么它可以接受一个RoomDatabase作为构造器的唯一参数。
Room不允许在主线程中访问数据库,除非在builder里面调用allowMainThreadQueries() 。因为访问数据库是耗时的,可能阻塞主线程,引起UI卡顿。

 

添加方便使用的方法

Insert

使用 @Insert注解的方法,Room将会生成插入的代码。

@Dao
public interface MyDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public void insertUsers(User... users);

    @Insert
    public void insertBothUsers(User user1, User user2);

    @Insert
    public void insertUsersAndFriends(User user, List friends);
}

如果@Insert 方法只接受一个参数,那么将返回一个long,对应着插入的rowId。如果接受多个参数,或者数组,或者集合,那么就会返回一个long的数组或者list。

Update

@Dao
public interface MyDao {
    @Update
    public void updateUsers(User... users);
}

也可以让update方法返回一个int型的整数,代表被update的行号。

@Dao
public interface MyDao {
    @Delete
    public void deleteUsers(User... users);
}

 

Paging Library

Paging Library(分页加载库)用于逐步从数据源加载信息,而不会耗费过多的设备资源或者等待太长的时间。

总体概览

一个常见的需求是获取很多数据,但是同时也只展示其中的一小部分数据。这就会引起很多问题,比如浪费资源和流量等。
现有的Android APIs可以提供分页加载的功能,但是也带来了显著的限制和缺点:

  • CursorAdapter,使得从数据库加载数据到ListView变得非常容易。但是这是在主线程中查询数据库,并且分页的内容使用低效的 Cursor返回。更多使用CursorAdapter带来的问题参考Large Database Queries on Android。
  • AsyncListUtil提供基于位置的( position-based)分页加载到 RecyclerView中,但是无法使用不基于位置(non-positional)的分页加载,而且还强制把null作为占位符。

提供的类

DataSource
数据源。根据你想要访问数据的方式,可以有两种子类可供选择:

  • KeyedDataSource用于加载从第N到N+1数据。
  • TiledDataSource 用于从任意位置的分页数据。

例如使用 Room persistence library 就可以自动创建返回 TiledDataSource类型的数据:

@Query("select * from users WHERE age > :age order by name DESC, id ASC")
TiledDataSource usersOlderThan(int age);

 

@Dao
interface UserDao {
    @Query("SELECT * FROM user ORDER BY lastName ASC")
    public abstract LivePagedListProvider usersByLastName();
}

class MyViewModel extends ViewModel {
    public final LiveData> usersList;
    public MyViewModel(UserDao userDao) {
        usersList = userDao.usersByLastName().create(
                /* initial load position */ 0,
                new PagedList.Config.Builder()
                        .setPageSize(50)
                        .setPrefetchDistance(50)
                        .build());
    }
}

class MyActivity extends AppCompatActivity {
    @Override
    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.user_list);
        UserAdapter adapter = new UserAdapter();
        viewModel.usersList.observe(this, pagedList -> adapter.setList(pagedList));
        recyclerView.setAdapter(adapter);
    }
}

class UserAdapter extends PagedListAdapter {
    public UserAdapter() {
        super(DIFF_CALLBACK);
    }
    @Override
    public void onBindViewHolder(UserViewHolder holder, int position) {
        User user = getItem(position);
        if (user != null) {
            holder.bindTo(user);
        } else {
            // Null defines a placeholder item - PagedListAdapter will automatically invalidate
            // this row when the actual object is loaded from the database
            holder.clear();
        }
    }
    public static final DiffCallback DIFF_CALLBACK = new DiffCallback() {
        @Override
        public boolean areItemsTheSame(@NonNull User oldUser, @NonNull User newUser) {
            // User properties may have changed if reloaded from the DB, but ID is fixed
            return oldUser.getId() == newUser.getId();
        }
        @Override
        public boolean areContentsTheSame(@NonNull User oldUser, @NonNull User newUser) {
            // NOTE: if you use equals, your object must properly override Object#equals()
            // Incorrectly returning false here will result in too many animations.
            return oldUser.equals(newUser);
        }
    }
}

 

 

 

 

你可能感兴趣的:(Android基础)