浅谈Android Architecture Components

浅谈Android Architecture Components


简介

Google IO 2017发布Android Architecture Components,自己先研究了一下,蛮有意思的,特来记录一下。本文内容主要是参考官方文档以及自己的理解,如有错误之处,恳请指出。

Android Architecture Components

Android Architecture Components是Google发布的一套新的架构组件,使App的架构更加健壮,后面简称AAC。

AAC主要提供了Lifecycle,ViewModel,LiveData,Room等功能,下面依次说明:

  • Lifecycle

生命周期管理,把原先Android生命周期的中的代码抽取出来,如将原先需要在onStart()等生命周期中执行的代码分离到Activity或者Fragment之外。

  • LiveData

一个数据持有类,持有数据并且这个数据可以被观察被监听,和其他Observer不同的是,它是和Lifecycle是绑定的,在生命周期内使用有效,减少内存泄露和引用问题。

  • ViewModel

用于实现架构中的ViewModel,同时是与Lifecycle绑定的,使用者无需担心生命周期。可以在多个Fragment之间共享数据,比如旋转屏幕后Activity会重新create,这时候使用ViewModel还是之前的数据,不需要再次请求网络数据。

  • Room

谷歌推出的一个Sqlite ORM库,不过使用起来还不错,使用注解,极大简化数据库的操作,有点类似Retrofit的风格。

AAC的架构是这样的:

新的架构
  • Activity/Fragment

UI层,通常是Activity/Fragment等,监听ViewModel,当VIewModel数据更新时刷新UI,监听用户事件反馈到ViewModel,主流的数据驱动界面。

  • ViewModel

持有或保存数据,向Repository中获取数据,响应UI层的事件,执行响应的操作,响应数据变化并通知到UI层。

  • Repository

App的完全的数据模型,ViewModel交互的对象,提供简单的数据修改和获取的接口,配合好网络层数据的更新与本地持久化数据的更新,同步等

  • Data Source

包含本地的数据库等,网络api等,这些基本上和现有的一些MVVM,以及Clean架构的组合比较相似

Gradle 集成

根目录gradle文件中添加Google Maven Repository

allprojects {
    repositories {
        jcenter()
        maven { url 'https://maven.google.com' }
    }
}

在模块中添加对应的依赖

如使用Lifecycle,LiveData、ViewModel,添加如下依赖。

compile "android.arch.lifecycle:runtime:1.0.0-alpha1"
compile "android.arch.lifecycle:extensions:1.0.0-alpha1"
annotationProcessor "android.arch.lifecycle:compiler:1.0.0-alpha1"

如使用Room功能,添加如下依赖。

compile "android.arch.persistence.room:runtime:1.0.0-alpha1"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha1"

// For testing Room migrations, add:
testCompile "android.arch.persistence.room:testing:1.0.0-alpha1"

// For Room RxJava support, add:
compile "android.arch.persistence.room:rxjava2:1.0.0-alpha1"

LifeCycles

Android开发中,经常需要管理生命周期。举个栗子,我们需要获取用户的地址位置,当这个Activity在显示的时候,我们开启定位功能,然后实时获取到定位信息,当页面被销毁的时候,需要关闭定位功能。

下面是简单的示例代码。

class MyLocationListener {
    public MyLocationListener(Context context, Callback callback) {
        // ...
    }

    void start() {
        // connect to system location service
    }

    void stop() {
        // disconnect from system location service
    }
}

class MyActivity extends AppCompatActivity {

    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, (location) -> {
            // update UI
        });
    }

    public void onStart() {
        super.onStart();
        myLocationListener.start();
    }

    public void onStop() {
        super.onStop();
        myLocationListener.stop();
    }
}

上面只是一个简单的场景,我们来个复杂一点的场景。当定位功能需要满足一些条件下才开启,那么会变得复杂多了。可能在执行Activity的stop方法时,定位的start方法才刚刚开始执行,比如如下代码,这样生命周期管理就变得很麻烦了。

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, location -> {
            // update UI
        });
    }

    public void onStart() {
        super.onStart();
        Util.checkUserStatus(result -> {
            // what if this callback is invoked AFTER activity is stopped?
            if (result) {
                myLocationListener.start();
            }
        });
    }

    public void onStop() {
        super.onStop();
        myLocationListener.stop();
    }
}

AAC中提供了Lifecycle,用来帮助我们解决这样的问题。LifeCycle使用2个枚举类来解决生命周期管理问题。一个是事件,一个是状态。

事件:

生命周期事件由系统来分发,这些事件对于与Activity和Fragment的生命周期函数。

状态:

Lifecycle的状态,用于追踪中Lifecycle对象,如下图所示。

Lifecycle的状态

上面的定位功能代码,使用LifeCycle实现以后是这样的,实现一个LifecycleObserver接口,然后用注解标注状态,最后在LifecycleOwner中添加监听。

public class MyObserver implements LifecycleObserver {

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    public void onResume() {
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    public void onPause() {
    }
}

aLifecycleOwner.getLifecycle().addObserver(new MyObserver());

上面代码中用到了aLifecycleOwnerLifecycleOwner接口对象,LifecycleOwner是一个只有一个方法的接口getLifecycle(),需要由子类来实现。

在Lib中已经有实现好的子类,我们可以直接拿来使用。比如LifecycleActivityLifecycleFragment,我们只需要继承此类就行了。

当然实际开发的时候我们会自己定义BaseActivity,Java是单继承模式,那么需要自己实现LifecycleRegistryOwner接口。

如下所示即可,代码很近简单

public class BaseFragment extends Fragment implements LifecycleRegistryOwner {

    LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);

    @Override
    public LifecycleRegistry getLifecycle() {
        return lifecycleRegistry;
    }
}

LiveData

LiveData 是一个 Data Holder 类,可以持有数据,同时这个数据可以被监听的,当数据改变的时候,可以触发回调。但是又不像普通的Observable,LiveData绑定了App的组件,LiveData可以指定在LifeCycle的某个状态被触发。比如LiveData可以指定在LifeCycle的 STARTED 或 RESUMED状体被触发。

public class LocationLiveData extends LiveData<Location> {

    private LocationManager locationManager;

    private SimpleLocationListener listener = new SimpleLocationListener() {
        @Override
        public void onLocationChanged(Location location) {
            setValue(location);
        }
    };

    public LocationLiveData(Context context) {
        locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
    }

    @Override
    protected void onActive() {
        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);
    }

    @Override
    protected void onInactive() {
        locationManager.removeUpdates(listener);
    }
}
  • onActive()

这个方法在LiveData在被激活的状态下执行,我们可以开始执行一些操作。

  • onActive()

这个方法在LiveData在的失去活性状态下执行,我们可以结束执行一些操作。

  • setValue()

执行这个方法的时候,LiveData可以触发它的回调。

LocationLiveData可以这样使用。

public class MyFragment extends LifecycleFragment {

    public void onActivityCreated (Bundle savedInstanceState) {
        LiveData<Location> myLocationListener = ...;
        Util.checkUserStatus(result -> {
            if (result) {
                myLocationListener.addObserver(this, location -> {
                    // update UI
                });
            }
        });
    }
}

注意,上面的addObserver方法,必须传LifecycleOwner对象,也就是说添加的对象必须是可以被LifeCycle管理的。

如果LifeCycle没有触发对对应的状态(STARTED or RESUMED),它的值被改变了,那么Observe就不会被执行,

如果LifeCycle被销毁了,那么Observe将自动被删除。

实际上LiveData就提供一种新的供数据共享方式。可以用它在多个Activity、Fragment等其他有生命周期管理的类中实现数据共享。

还是上面的定位例子。

public class LocationLiveData extends LiveData<Location> {

    private static LocationLiveData sInstance;

    private LocationManager locationManager;

    @MainThread
    public static LocationLiveData get(Context context) {
        if (sInstance == null) {
            sInstance = new LocationLiveData(context.getApplicationContext());
        }
        return sInstance;
    }

    private SimpleLocationListener listener = new SimpleLocationListener() {
        @Override
        public void onLocationChanged(Location location) {
            setValue(location);
        }
    };

    private LocationLiveData(Context context) {
        locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
    }

    @Override
    protected void onActive() {
        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);
    }

    @Override
    protected void onInactive() {
        locationManager.removeUpdates(listener);
    }
}

然后在Fragment中调用。

public class MyFragment extends LifecycleFragment {

    public void onActivityCreated (Bundle savedInstanceState) {

        Util.checkUserStatus(result -> {
            if (result) {
                LocationLiveData.get(getActivity()).observe(this, location -> {
                   // update UI
                });
            }
        });
  }
}

从上面的示例,可以得到使用LiveData优点:

  • 没有内存泄露的风险,全部绑定到对应的生命周期,当LifeCycle被销毁的时候,它们也自动被移除

  • 降低Crash,当Activity被销毁的时候,LiveData的Observer自动被删除,然后UI就不会再接受到通知

  • 实时数据,因为LiveData是持有真正的数据的,所以当生命周期又重新开始的时候,又可以重新拿到数据

  • 正常配置改变,当Activity或者Fragment重新创建的时候,可以从LiveData中获取上一次有用的数据

  • 不再需要手动的管理生命周期

Transformations

有时候需要对一个LiveData做Observer,但是这个LiveData是依赖另外一个LiveData,有点类似于RxJava中的操作符,我们可以这样做。

  • Transformations.map()

用于事件流的传递,用于触发下游数据。

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

这个和map类似,只不过这个是用来触发上游数据。

private LiveData<User> getUser(String id) {
  // ...
}

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

ViewModel

ViewModel是用来存储UI层的数据,以及管理对应的数据,当数据修改的时候,可以马上刷新UI。

Android系统提供控件,比如Activity和Fragment,这些控件都是具有生命周期方法,这些生命周期方法被系统调用。

当这些控件被销毁或者被重建的时候,如果数据保存在这些对象中,那么数据就会丢失。比如在一个界面,保存了一些用户信息,当界面重新创建的时候,就需要重新去获取数据。当然了也可以使用控件自动再带的方法,在onSaveInstanceState方法中保存数据,在onCreate中重新获得数据,但这仅仅在数据量比较小的情况下。如果数据量很大,这种方法就不能适用了。

另外一个问题就是,经常需要在Activity中加载数据,这些数据可能是异步的,因为获取数据需要花费很长的时间。那么Activity就需要管理这些数据调用,否则很有可能会产生内存泄露问题。最后需要做很多额外的操作,来保证程序的正常运行。

同时Activity不仅仅只是用来加载数据的,还要加载其他资源,做其他的操作,最后Activity类变大,就是我们常讲的上帝类。也有不少架构是把一些操作放到单独的类中,比如MVP就是这样,创建相同类似于生命周期的函数做代理,这样可以减少Activity的代码量,但是这样就会变得很复杂,同时也难以测试。

AAC中提供ViewModel可以很方便的用来管理数据。我们可以利用它来管理UI组件与数据的绑定关系。ViewModel提供自动绑定的形式,当数据源有更新的时候,可以自动立即的更新UI。

下面是一个简单的代码示例。

public class MyViewModel extends ViewModel {

    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<Users>>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        // do async operation to fetch users
    }
}
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
        });
    }
}

当我们获取ViewModel实例的时候,ViewModel是通过ViewModelProvider保存在LifeCycle中,ViewModel会一直保存在LifeCycle中,直到Activity或者Fragment销毁了,触发LifeCycle被销毁,那么ViewModel也会被销毁的。下面是ViewModel的生命周期图。

Room

Room是一个持久化工具,和ormlite、greenDao类似,都是ORM工具。在开发中我们可以利用Room来操作sqlite数据库。

Room主要分为三个部分:

  • Database

使用注解申明一个类,注解中包含若干个Entity类,这个Database类主要负责创建数据库以及获取数据对象的。

  • Entity

表示每个数据库的总的一个表结构,同样也是使用注解表示,类中的每个字段都对应表中的一列。

  • DAO

DAO是 Data Access Object的缩写,表示从从代码中直接访问数据库,屏蔽sql语句。

下面是官方给的结构图。

代码示例:

// 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.
}

// UserDao.java
@Dao
public interface UserDao {
    @Query("SELECT * FROM user")
    List<User> getAll();

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List<User> 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();
}

最后在代码中调用Database对象

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

注意: Database最好设计成单利模式,否则对象太多会有性能的影响。

Entities

@Entity
class User {
    @PrimaryKey
    public int id;

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

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

    @Ignore
    Bitmap picture;
}

@Entity用来注解一个实体类,对应数据库一张表。默认情况下,Room为实体中定义的每个成员变量在数据中创建对应的字段,我们可能不想保存到数据库的字段,这时候就要用道@Ignore注解。

注意: 为了保存每一个字段,这个字段需要有可以访问的gettter/setter方法或者是public的属性

Entity的参数 primaryKeys

每个实体必须至少定义1个字段作为主键,即使只有一个成员变量,除了使用@PrimaryKey 将字段标记为主键的方式之外,还可以通过在@Entity注解中指定参数的形式

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

    @Ignore
    Bitmap picture;
}

Entity的参数 tableName

默认情况下,Room使用类名作为数据库表名。如果你想表都有一个不同的名称,就可以在@Entity中使用tableName参数指定

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

和tableName作用类似; @ColumnInfo注解是改变成员变量对应的数据库的字段名称。

Entity的参数 indices

indices的参数值是@Index的数组,在某些情况写为了加快查询速度我们可以需要加入索引

@Entity(indices = {@Index("name"), @Index("last_name", "address")})
class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String address;

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

    @Ignore
    Bitmap picture;
}

有时,数据库中某些字段或字段组必须是唯一的。通过将@Index的unique 设置为true,可以强制执行此唯一性属性。

下面的代码示例防止表有两行包含FirstName和LastName列值相同的一组:

@Entity(indices = {@Index(value = {"first_name", "last_name"}, unique = true)})
class User {
    @PrimaryKey
    public int id;

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

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

    @Ignore
    Bitmap picture;
}

Entity的参数 foreignKeys

因为SQLite是一个关系型数据库,你可以指定对象之间的关系。尽管大多数ORM库允许实体对象相互引用,但Room明确禁止。实体之间没有对象引用。

不能使用直接关系,所以就要用到foreignKeys(外键)。

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

    public String title;

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

外键是非常强大的,因为它允许指定引用实体更新时发生的操作。例如,级联删除,你可以告诉SQLite删除所有书籍的用户如果用户对应的实例是由包括OnDelete =CASCADE在@ForeignKey注释。ON_CONFLICT : @Insert(onConflict=REPLACE) REMOVE 或者 REPLACE

有时候可能还需要对象嵌套这时候可以用@Embedded注解

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;
}

Dao

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

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List<User> 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);
}

数据访问对象Data Access Objects (DAOs)是一种抽象访问数据库的干净的方式。

DAO的Insert 操作

当创建DAO方法并用@Insert注释它时,生成一个实现时会在一个事务中完成插入所有参数到数据库。

@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<User> friends);
}

DAO的Update、Delete操作

与上面类似

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

    @Delete
    public void deleteUsers(User... users);
}

DAO的Query 操作

一个简单查询示例

@Dao
public interface MyDao {
    @Query("SELECT * FROM user")
    public User[] loadAllUsers();
}

稍微复杂的,带参数的查询操作

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge")
    public User[] loadAllUsersOlderThan(int minAge);
}

也可以带多个参数

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
    public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

    @Query("SELECT * FROM user WHERE first_name LIKE :search OR last_name LIKE :search")
    public List<User> findUserWithName(String search);
}

返回子集

上面示例都是查询一个表中的所有字段,结果用对应的Entity即可,但是如果我只要其中的几个字段,那么该怎么使用呢?

比如上面的User,我只需要firstName和lastName,首先定义一个子集,然后结果改成对应子集即可。

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

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

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user")
    public List<NameTuple> loadFullName();
}

支持集合参数

有个这样一个查询需求,比如要查询某两个地区的所有用户,直接用sql中的in即可,但是如果这个地区是程序指定的,个数不确定,那么改怎么办?

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public List<NameTuple> loadUsersFromRegions(List<String> regions);
}

支持Observable

前面提到了LiveData,可以异步的获取数据,那么我们的Room也是支持异步查询的。

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
}

支持RxJava

RxJava是另外一个异步操作库,同样也是支持的。

@Dao
public interface MyDao {
    @Query("SELECT * from user where id = :id LIMIT 1")
    public Flowable<User> loadUserById(int id);
}

支持Cursor

原始的Android系统查询结果是通过Cursor来获取的,同样也支持。

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
    public Cursor loadRawUsersOlderThan(int minAge);
}

多表查询

有时候数据库存在范式相关,数据拆到了多个表中,那么就需要关联多个表进行查询,如果结果只是一个表的数据,那么很简单,直接用Entity定义的类型即可。

@Dao
public interface MyDao {
    @Query("SELECT * FROM book "
           + "INNER JOIN loan ON loan.book_id = book.id "
           + "INNER JOIN user ON user.id = loan.user_id "
           + "WHERE user.name LIKE :userName")
   public List<Book> findBooksBorrowedByNameSync(String userName);
}

如果结果是部分字段,同上面一样,需要单独定义一个POJO,来接受数据。

public class UserPet {
   public String userName;
   public String petName;
}

@Dao
public interface MyDao {
   @Query("SELECT user.name AS userName, pet.name AS petName "
          + "FROM user, pet "
          + "WHERE user.id = pet.user_id")
   public LiveData<List<UserPet>> loadUserAndPetNames();
}

类型转换

有时候Java定义的数据类型和数据库中存储的数据类型是不一样的,Room提供类型转换,这样在操作数据库的时候,可以自动转换。

比如在Java中,时间用Date表示,但是在数据库中类型确实long,这样有利于存储。

public class Converters {
    @TypeConverter
    public static Date fromTimestamp(Long value) {
        return value == null ? null : new Date(value);
    }

    @TypeConverter
    public static Long dateToTimestamp(Date date) {
        return date == null ? null : date.getTime();
    }
}

定义数据库时候需要指定类型转换,同时定义好Entity和Dao。

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

@Entity
public class User {
    ...
    private Date birthday;
}

@Dao
public interface UserDao {
    ...
    @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
    List<User> findUsersBornBetweenDates(Date from, Date to);
}

数据库升级

版本迭代中,我们不可避免的会遇到数据库升级问题,Room也为我们提供了数据库升级的处理接口。

Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name").addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();

static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, PRIMARY KEY(`id`))");
    }
};

static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER");
    }
};

迁移过程结束后,Room将验证架构以确保迁移正确发生。如果Room发现问题,则抛出包含不匹配信息的异常。

警告: 如果不提供必要的迁移,Room会重新构建数据库,这意味着将丢失数据库中的所有数据。

输出模式

可以在gradle中设置开启输出模式,便于我们调试,查看数据库表情况,以及做数据库迁移。

android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation":
                             "$projectDir/schemas".toString()]
            }
        }
    }
}

Sample

这里是官方示例,本人自己参考官方文档和示例Android Architecture Components samples后,也写出了一个类似的示例项目XiaoxiaZhihu_AAC,还请多多指教。

总结

原先IO是在5月底已经结束,本来想尽快参照官方文档和示例,把API撸一遍,然后写个Demo和文章来介绍一下。代码示例早已经写出来了,但6月分工作太忙,然后又出差到北京,最后等到了6月底了,才把这篇文章给写出来了。中间可能有内容以及改变,如果有发现稳重有错误,请及时指出,不理赐教。同时以后做事一定要有始有终,确定的事一定要坚持。

相关资料

Android Architecture Components

简单聊聊Android Architecture Componets

Android Architecture Components samples

XiaoxiaZhihu_AAC

浅谈Android Architecture Components

Room ORM 数据库框架

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容