Android Architecture Component 学习

之前一直都用 MVC 架构开发 android, 所有的业务逻辑都写在 Activity 中,感觉一般情况下 MVC 就够用了。现在来看一个官方推荐的 AAC 架构。
主要参考: https://developer.android.google.cn/topic/libraries/architecture/guide.html

aac.png

AAC 中主要多了一个 ViewModel, 业务代码不再堆在 Activity (或 Fragment ) 中而是写在 ViewModel 中。ViewModel 负责取数据、更新数据、处理业务逻辑。

下面以一个请求 Github 用户信息显示用户id和用户名的 demo 来看一下 AAC
用户数据: https://api.github.com/users/lesliebeijing

首先引入依赖(基于 Android Studio 3.0 + Java8)

// Java8 support for Lifecycles
implementation "android.arch.lifecycle:common-java8:1.1.0"

// ViewModel and LiveData
implementation "android.arch.lifecycle:extensions:1.1.0"

// Room
implementation "android.arch.persistence.room:runtime:1.0.0"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0"

// Retrofit2
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0'

AS3.0 开始支持 Java8
这里使用 Retrofit2 做网络请求

为了支持 Java8 , gradle 中要加入下面的配置

android {
    // ....
    
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

定义 User 对象

public class User {
    private int id;
    private String name;

    // 省略
}

下面我们要定义一个显示用户信息的 ViewModel

public class UserProfileViewModel extends ViewModel {
    private User user;

    public User getUser() {
        return user;
    }
}

ViewModel 中定义了要显示的用户信息 user

下面要在 Activity (或 Fragment)中实例化 ViewModel

public class MainActivity extends BaseActivity {
    private TextView mUserInfo;
    private UserProfileViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initProcessBar();
        mUserInfo = findViewById(R.id.userInfo);

        viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
    }
}

现在 Activity、View、ViewModel 连接在一起了,当 ViewModel 中获取到用户数据之后怎么更新 UI 呢????这就用到了 LiveData

LiveData 是一个数据容器,实现了观察者模式,对数据进行监听,当数据发生变化时会通知所有的观察者
LiveData 还能感知 Lifecycle ,只有当 LifecycleOwner (Activity 、Fragment、Service) active (OnStart 之后,OnStop 之前) 时才会通知

所以我们把 User 变成 LiveData

public class UserProfileViewModel extends ViewModel {
    private LiveData<User> user;

    public LiveData<User> getUser() {
        return user;
    }
}

这样在 Activity 中就能监听 User 的变化了

viewModel.getUser().observe(this, user -> {
    if (user != null) {
        mUserInfo.setText(user.getId() + "\n" + user.getName());
    }
});

下面请求网络数据

public interface UserService {
    @GET("users/lesliebeijing")
    Call<User> getUser();
}

当然可以直接在 ViewModel 进行网络请求更新数据,但为了更好的隔离这里增加一层 Repository, Repository 负责底层所有的数据交互,提供单一接口,向上层屏蔽底层数据访问细节

public class UserRepository {
    private UserService userService;

    public UserRepository() {
        userService = RetrofitClient.retrofit().create(UserService.class);
    }

    public LiveData<User> getUser() {
        final MutableLiveData<User> user = new MutableLiveData<>();
        userService.getUser().enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                user.setValue(response.body());
            }

            @Override
            public void onFailure(Call<User> call, Throwable t) {

            }
        });
        return user;
    }
}

网络请求成功 setValue 设置新的数据后 activity 中的监听就会执行更新 UI 。
setValue 只能在主线程中执行, 在普通线程中可调用 postValue

下面我们就可以在 ViewModel 中使用 UserRepository

public class UserProfileViewModel extends ViewModel {
    private LiveData<User> user;
    private UserRepository userRepository;

    public UserProfileViewModel() {
        userRepository = new UserRepository();
    }

    public LiveData<User> getUser() {
        user = userRepository.getUser();
        return user;
    }
}

第一阶段结束, 查看源码: https://github.com/lesliebeijing/aac-demo/tree/step1
官方 Demo 中用了 Dagger2 做依赖注入, DI 方便测试,但过多的使用 DI 也会导致代码不清晰,我自己不是很喜欢用 DI, 所有 Demo 中没有使用。


第二阶段看看 Room 持久化
实现一个简单的逻辑:如果数据库中有缓存的用户信息直接显示,否则请求网络获取数据写入数据库

room.png

Room 是一个 ORM ,可以返回 LiveData 对数据库的变化进行监听,也可以返回普通的对象。目前见过的最好用的 ORM 了。

首先为 User 添加注解

@Entity
public class User {
    @PrimaryKey
    private int id;
    private String name;

    // 省略...
}

接着创建 Dao

@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);

    @Query("SELECT * FROM user LIMIT 1")
    LiveData<User> load();

    @Query("SELECT COUNT(*) FROM user")
    int getUserCount();

    default boolean userExists() {
        return getUserCount() > 0;
    }
}

最后创建 Database

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    private static AppDatabase appDb;
    public static final String DB_NAME = "mydb";

    public abstract UserDao userDao();

    public static AppDatabase getInstance(Context context) {
        if (appDb == null) {
            synchronized (AppDatabase.class) {
                if (appDb == null) {
                    appDb = Room.databaseBuilder(context, AppDatabase.class, DB_NAME).build();
                }
            }
        }
        return appDb;
    }
}

单例模式

下面改造 Repository

public class UserRepository {
    private UserService userService;
    private UserDao userDao;

    public UserRepository(Context context) {
        userService = RetrofitClient.retrofit().create(UserService.class);
        userDao = AppDatabase.getInstance(context).userDao();
    }

    public LiveData<User> getUser() {
        Executors.newSingleThreadExecutor().execute(() -> {
            if (!userDao.userExists()) {
                Log.d("leslie", "not exists fetch from network");
                try {
                    Response<User> response = userService.getUser().execute();
                    userDao.save(response.body());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
        return userDao.load();
    }
}

getUser 直接返回 userDao.load(),如果用户信息不存在会请求网络获取数据后写入数据库,Room 会监听数据库的变化通过 LiveData 回调到 UI层

Database 的实例化需要 Context 对象,让 UserProfileViewModel 改为继承 AndroidViewModel 就可以获取到 Context 对象

public class UserProfileViewModel extends AndroidViewModel {
    private UserRepository userRepository;

    public UserProfileViewModel(@NonNull Application application) {
        super(application);
        userRepository = new UserRepository(getApplication());
    }
}

第二阶段结束,查看源码 https://github.com/lesliebeijing/aac-demo/tree/step2


第三阶段,看一下 Loading 状态和错误信息处理
要监听请求状态和错误可以把用户数据和请求状态包装在一起返回

定义状态

public enum Status {
    SUCCESS,
    ERROR,
    LOADING
}

定义一个 Resource 类

public class Resource<T> {
    public final Status status;
    public final T data;
    public final String message;

    private Resource(Status status, T data, String message) {
        this.status = status;
        this.data = data;
        this.message = message;
    }

    public static <T> Resource<T> success(T data) {
        return new Resource<>(Status.SUCCESS, data, null);
    }

    public static <T> Resource<T> error(String message, T data) {
        return new Resource<>(Status.ERROR, data, message);
    }

    public static <T> Resource<T> loading(T data) {
        return new Resource<>(Status.LOADING, data, null);
    }
}

下面我们实现一种比较常见的场景,如果数据库有数据先显示,然后根据情况决定是否请求数据,请求数据成功后把新数据写入数据库

封装一个通用的类来处理这种场景

public abstract class NetworkBoundResource<T> {
    private Executor executor;
    private MediatorLiveData<Resource<T>> result = new MediatorLiveData<>();

    public NetworkBoundResource() {
        executor = Executors.newSingleThreadExecutor();
        result.setValue(Resource.loading(null));
        final LiveData<T> dbSource = loadFromDb();
        result.addSource(dbSource, data -> {
            result.removeSource(dbSource);
            if (shouldFetch()) {
                fetchFromNetwork(dbSource);
            } else {
                result.setValue(Resource.success(data));
            }
        });
    }

    private void fetchFromNetwork(final LiveData<T> dbSource) {
        result.addSource(dbSource, data -> result.setValue(Resource.loading(data)));
        loadData().enqueue(new Callback<T>() {
            @Override
            public void onResponse(Call<T> call, final Response<T> response) {
                result.removeSource(dbSource);
                if (response.isSuccessful()) {
                    executor.execute(() -> {
                        saveToDb(response.body());
                        result.addSource(loadFromDb(), data -> result.setValue(Resource.success(data)));
                    });
                } else {
                    String message = null;
                    if (response.errorBody() != null) {
                        try {
                            message = response.errorBody().string();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    final String errorMessage = message;
                    result.addSource(dbSource, data -> result.setValue(Resource.error(errorMessage, data)));
                }
            }

            @Override
            public void onFailure(Call<T> call, Throwable t) {
                result.removeSource(dbSource);
                result.addSource(dbSource, data -> result.setValue(Resource.error(t.getMessage(), data)));
            }
        });
    }

    protected abstract Call<T> loadData();

    // 默认 true ,子类可复写
    protected boolean shouldFetch() {
        return true;
    }

    protected abstract void saveToDb(T data);

    protected abstract LiveData<T> loadFromDb();

    public LiveData<Resource<T>> getResult() {
        return result;
    }
}

MediatorLiveData 可以监听多个数据源,并把多个数据源的事件合并在一起发送

改造 UserRepository

public LiveData<Resource<User>> getUser() {
    return new NetworkBoundResource<User>() {

        @Override
        protected Call<User> loadData() {
            return userService.getUser();
        }

        @Override
        protected void saveToDb(User user) {
            userDao.save(user);
        }

        @Override
        protected LiveData<User> loadFromDb() {
            return userDao.load();
        }
    }.getResult();
}

注意现在返回 Resource<User> 了

最后前台就可以根据状态显示 loading 和错误信息了

viewModel.getUser().observe(this, resource -> {
    Log.d("leslie", "observe " + resource.status);
    Status status = resource.status;
    if (status == LOADING) {
        showProcessBar();
        setData(resource.data);
    } else {
        hideProcessBar();
        if (status == SUCCESS) {
            setData(resource.data);
        } else {
            showToast(resource.message);
        }
    }
});

官方 Demo 添加了 Retrofit adapter 让请求可以返回 LiveData, 这里没有这么做

查看源码 https://github.com/lesliebeijing/aac-demo

官方 sample: https://github.com/googlesamples/android-architecture-components

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