簡介
背景
這幾年 MVP 架構在安卓界非常流行原探,幾乎已經成為主流框架几颜,它讓業(yè)務邏輯 和 UI操作相對獨立,使得代碼結構更清晰逗爹。
MVVM 在前端火得一塌糊涂,而在安卓這邊卻基本沒見到幾個人在用嚎于,看到介紹 MVVM 也最多是講 DataBinding 或 介紹思想的掘而。偶爾看到幾篇提到應用的,還是對谷歌官網的Architecture Components 文章的翻譯于购。
相信大家看別人博客或官方文檔的時候袍睡,總會碰到一些坑。要么入門教程寫得太復雜(無力吐槽肋僧,前面寫一堆原理斑胜,各種高大上的圖,然并卵嫌吠,到實踐部分一筆帶過止潘,你確定真的是入門教程嗎)。要么就是簡單得就是一個 hello world辫诅,然后就沒有下文了(看了想罵人)凭戴。
實在看不下去的我,決定插手你的人生炕矮。
目錄
《安卓-深入淺出MVVM教程》大致分兩部分:應用篇么夫、原理篇。
采用循序漸進方式肤视,內容深入淺出魏割,符合人類學習規(guī)律,希望大家用最少時間掌握 MVVM钢颂。
應用篇:
01 Hello MVVM (快速入門)
02 Repository (數據倉庫)
03 Cache (本地緩存)
04 State Lcee (加載/空/錯誤/內容視圖)
05 Simple Data Source (簡單的數據源)
06 Load More (加載更多)
07 DataBinding (數據與視圖綁定)
08 RxJava2
09 Dragger2
10 Abstract (抽象)
11 Demo (例子)
12-n 待定(歡迎 github 提建議)
原理篇
01 MyLiveData(最簡單的LiveData)
02-n 待定(并不是解讀源碼,那樣太無聊了拜银,打算帶你從0擼一個 Architecture)
關于提問
本人水平和精力有限殊鞭,如果有大佬發(fā)現哪里寫錯了或有好的建議遭垛,歡迎在本教程附帶的 github倉庫 提issue盔夜。
What段誊?為什么不在博客留言?考慮到國內轉載基本無視版權的情況铜异,一般來說你都不是在源出處看到這篇文章趾盐,所以留言我也一般是看不到的庶喜。
教程附帶代碼
https://github.com/ittianyu/MVVM
應用篇放在 app 模塊下,原理篇放在 implementation 模塊下救鲤。
每一節(jié)代碼采用不同包名久窟,相互獨立。
前言
上一節(jié)是 04 的變體本缠,這一節(jié)因為改動太大斥扛,所以從 0 開始。
這次的案例是從 github 上獲取倉庫列表丹锹,并展示倉庫名 和 star 數量稀颁。
環(huán)境
root build.gradle
allprojects {
repositories {
jcenter()
maven { url 'https://maven.google.com' }
}
}
ext {
lifecycle = '1.0.0-rc1'
lifecycleRuntime = '1.0.3'
room = '1.0.0-rc1'
supportLibraryVersion = '26.+'
}
app bulid.gradle
dependencies {
...
// Lifecycles, LiveData and ViewModel
compile "android.arch.lifecycle:runtime:$rootProject.lifecycleRuntime"
compile "android.arch.lifecycle:extensions:$rootProject.lifecycle"
annotationProcessor "android.arch.lifecycle:compiler:$rootProject.lifecycle"
// room
compile "android.arch.persistence.room:runtime:$rootProject.room"
annotationProcessor "android.arch.persistence.room:compiler:$rootProject.room"
// retrofit
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
}
manifest
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
Model
Bean
先把之前寫過的一些實體類定義出來
Lcee
public class Lcee<T> {
public final Status status;
public final T data;
public final Throwable error;
private Lcee(Status status, T data, Throwable error) {
this.status = status;
this.data = data;
this.error = error;
}
public static <T> Lcee<T> content(T data) {
return new Lcee<>(Status.Content, data, null);
}
public static <T> Lcee<T> error(T data, Throwable error) {
return new Lcee<>(Status.Error, data, error);
}
public static <T> Lcee<T> error(Throwable error) {
return error(null, error);
}
public static <T> Lcee<T> empty(T data) {
return new Lcee<>(Status.Empty, data, null);
}
public static <T> Lcee<T> empty() {
return empty(null);
}
public static <T> Lcee<T> loading(T data) {
return new Lcee<>(Status.Loading, data, null);
}
public static <T> Lcee<T> loading() {
return loading(null);
}
}
Status
public enum Status {
Loading,
Content,
Empty,
Error,
}
api
https://api.github.com/search/repositories?q=tetris+language:assembly&sort=stars&order=desc
返回數據就不寫了(太長了),直接訪問一下就能看到楣黍。
Bean
根據 Api匾灶,用 GsonFormatter 生成實體類,或者自己手寫也行租漂。我這里之后又把內部類提出來了阶女。
Projects
public class Projects implements Serializable {
private int total_count;
private boolean incomplete_results;
private List<ProjectItem> items;
... getter and setter ...
}
ProjectItem
public class ProjectItem {
private int id;
private String name;
private String full_name;
private ProjectOwner owner;
@SerializedName("private")
private boolean privateX;
private String html_url;
private String description;
private boolean fork;
private String url;
private String forks_url;
private String keys_url;
private String collaborators_url;
private String teams_url;
private String hooks_url;
private String issue_events_url;
private String events_url;
private String assignees_url;
private String branches_url;
private String tags_url;
private String blobs_url;
private String git_tags_url;
private String git_refs_url;
private String trees_url;
private String statuses_url;
private String languages_url;
private String stargazers_url;
private String contributors_url;
private String subscribers_url;
private String subscription_url;
private String commits_url;
private String git_commits_url;
private String comments_url;
private String issue_comment_url;
private String contents_url;
private String compare_url;
private String merges_url;
private String archive_url;
private String downloads_url;
private String issues_url;
private String pulls_url;
private String milestones_url;
private String notifications_url;
private String labels_url;
private String releases_url;
private String deployments_url;
private String created_at;
private String updated_at;
private String pushed_at;
private String git_url;
private String ssh_url;
private String clone_url;
private String svn_url;
private String homepage;
private int size;
private int stargazers_count;
private int watchers_count;
private String language;
private boolean has_issues;
private boolean has_projects;
private boolean has_downloads;
private boolean has_wiki;
private boolean has_pages;
private int forks_count;
private String mirror_url;
private int open_issues_count;
private int forks;
private int open_issues;
private int watchers;
private String default_branch;
private double score;
... getter and setter ...
}
ProjectOwner
public class ProjectOwner {
private String login;
private int id;
private String avatar_url;
private String gravatar_id;
private String url;
private String html_url;
private String followers_url;
private String following_url;
private String gists_url;
private String starred_url;
private String subscriptions_url;
private String organizations_url;
private String repos_url;
private String events_url;
private String received_events_url;
private String type;
private boolean site_admin;
... getter and setter ...
}
View
有了數據模型,接下來要展示 View 了窜锯。
事實上下面寫的這些都不是本節(jié)的重點张肾,但又不得不寫,所以不做過多解釋锚扎。
xml
和之前一樣吞瞪,4 種 視圖,內容視圖為 SwipeRefreshLayout
包裹 RecyclerView
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/v_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/v_content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/srl"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>
</FrameLayout>
<FrameLayout
android:id="@+id/v_error"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="Network error, click to reload" />
</FrameLayout>
<FrameLayout
android:id="@+id/v_empty"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="User not exist" />
</FrameLayout>
<FrameLayout
android:id="@+id/v_loading"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
</FrameLayout>
ProjectsActivity
先初始化 View驾孔,并綁定 UI 事件芍秆。
先定義兩個空方法 reload
loadMore
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.f_activity_projects);
initView();
initData();
initEvent();
}
private void initView() {
vContent = findViewById(R.id.v_content);
vError = findViewById(R.id.v_error);
vLoading = findViewById(R.id.v_loading);
vEmpty = findViewById(R.id.v_empty);
srl = (SwipeRefreshLayout) findViewById(R.id.srl);
rv = (RecyclerView) findViewById(R.id.rv);
rv.setLayoutManager(new LinearLayoutManager(this));
projectsAdapter = new ProjectsAdapter();
rv.setAdapter(projectsAdapter);
}
private void initData() {
}
private void initEvent() {
View.OnClickListener reloadClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
reload();
}
};
vError.setOnClickListener(reloadClickListener);
vEmpty.setOnClickListener(reloadClickListener);
// refresh
srl.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
if (isLoading()) {
srl.setRefreshing(false);
return;
}
reload();
}
});
// load more
rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
public int mLastVisibleItemPosition;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof LinearLayoutManager) {
mLastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
}
if (projectsAdapter == null)
return;
if (newState == RecyclerView.SCROLL_STATE_IDLE
&& mLastVisibleItemPosition + 1 == projectsAdapter.getItemCount()) {
if (isLoading())
return;
loadMore();
}
}
});
}
private void reload() {
}
private void loadMore() {
}
private void isLoading() {
}
ProjectsAdapter
上面初始化 rv 時用到了 ProjectsAdapter
,其實就是每一行展示 name 和 star 數量翠勉。
public class ProjectsAdapter extends RecyclerView.Adapter<ProjectsAdapter.ProjectViewHolder> {
private List<ProjectItem> data = new ArrayList<>();
public ProjectsAdapter() {
}
public ProjectsAdapter(List<ProjectItem> data) {
this.data = data;
}
@Override
public ProjectViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.f_item_project, parent, false);
return new ProjectViewHolder(view);
}
@Override
public void onBindViewHolder(ProjectViewHolder holder, int position) {
ProjectItem item = getData().get(position);
holder.tvName.setText(item.getFull_name());
holder.tvStar.setText(item.getStargazers_count() + " star");
}
@Override
public int getItemCount() {
return data.size();
}
public void setData(List<ProjectItem> data) {
if (null != data)
this.data = data;
else
this.data.clear();
notifyDataSetChanged();
}
public void addData(List<ProjectItem> data) {
if (null == data || data.size() == 0)
return;
int index = getItemCount();
this.data.addAll(data);
notifyItemRangeInserted(index, data.size());
}
public static class ProjectViewHolder extends RecyclerView.ViewHolder {
TextView tvName;
TextView tvStar;
public ProjectViewHolder(View itemView) {
super(itemView);
tvName = itemView.findViewById(R.id.tv_name);
tvStar = itemView.findViewById(R.id.tv_star);
}
}
public List<ProjectItem> getData() {
return data;
}
}
f_item_project.xml
list 的 item 非常簡單妖啥,就是水平的兩個 TextView
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingBottom="8dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="8dp">
<TextView
android:id="@+id/tv_name"
android:text="name/project"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:maxLines="1"
android:layout_marginRight="8dp"
android:textColor="@android:color/black"
android:textSize="18sp" />
<TextView
android:id="@+id/tv_star"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="99999" />
</LinearLayout>
Model DataSource
UI 的基本代碼已經有了,就差和 ViewModel 綁定了对碌。
為了便于 ViewModel 調用數據荆虱,我們還需要寫 DataSource
而 DataSource 前面幾節(jié)已經詳細的講過了,這里不過多解釋。
ProjectDataSource
public interface ProjectDataSource {
LiveData<Lcee<Projects>> queryProjects(int page);
}
LocalProjectDataSource
表
在實體類上加上注解怀读,但實際上還會面臨兩個問題
- 找不到合適的主鍵诉位。
仔細想一下,如果直接存Projects
菜枷,最關鍵的還是 page苍糠,所以這里加入一個 page 作為主鍵。 -
List<ProjectItem>
不容易存儲啤誊。
使用外鍵來關聯太麻煩了岳瞭,所以這里為了方便,直接轉化為 json 后再存儲蚊锹。也就是 Ignore 這個字段瞳筏,然后加一個private String itemsJson;
實際上,完全有其他選擇枫耳,比如存 ProjectItem
而不是 Projects
乏矾。但這里為了簡單的演示 LoadMore,就不在這里大做文章迁杨。
@Entity(tableName = "projects")
public class Projects implements Serializable {
...
@Ignore
private List<ProjectItem> items;
// used for local cache
@PrimaryKey
private int page;
@ColumnInfo(name = "items")
private String itemsJson;
...
}
Dao
@Dao
public interface ProjectsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)// cache need update
Long add(Projects projects);
@Query("select * from projects where page=:page")
LiveData<Projects> queryProjects(int page);
}
庫
@Database(entities = {Projects.class}, version = 1, exportSchema = false)
public abstract class DB extends RoomDatabase {
public abstract ProjectsDao getProjectsDao();
}
DBHelper
public class DBHelper {
private static final DBHelper instance = new DBHelper();
private static final String DATABASE_NAME = "f_load_more";
private DBHelper() {
}
public static DBHelper getInstance() {
return instance;
}
private DB db;
public void init(Context context) {
db = Room.databaseBuilder(context.getApplicationContext(), DB.class, DATABASE_NAME).build();
}
public DB getDb() {
return db;
}
}
Service
接口
public interface ProjectsService {
LiveData<Long> add(Projects projects);
/**
*
* @param page start from 1
* @return
*/
LiveData<Projects> queryProjects(int page);
}
實現
public class ProjectsServiceImpl implements ProjectsService {
private static final ProjectsServiceImpl instance = new ProjectsServiceImpl();
private ProjectsServiceImpl() {
}
public static ProjectsServiceImpl getInstance() {
return instance;
}
private ProjectsDao projectsDao = DBHelper.getInstance().getDb().getProjectsDao();
@Override
public LiveData<Long> add(final Projects projects) {
// transfer long to LiveData<Long>
final MutableLiveData<Long> data = new MutableLiveData<>();
new AsyncTask<Void, Void, Long>() {
@Override
protected Long doInBackground(Void... voids) {
return projectsDao.add(projects);
}
@Override
protected void onPostExecute(Long rowId) {
data.setValue(rowId);
}
}.execute();
return data;
}
@Override
public LiveData<Projects> queryProjects(int page) {
return projectsDao.queryProjects(page);
}
}
LocalProjectDataSource
注意钻心,因為數據庫存儲的關系,在存之前先轉化為 json projects.itemsToJson();
在取的時候铅协,要從 json 轉化為 bean
projects.itemsFromJson();
public class LocalProjectDataSource implements ProjectDataSource {
private static final LocalProjectDataSource instance = new LocalProjectDataSource();
private LocalProjectDataSource() {
}
public static LocalProjectDataSource getInstance() {
return instance;
}
private ProjectsService projectsService = ProjectsServiceImpl.getInstance();
public LiveData<Long> addProjects(Projects projects) {
// format items to json
projects.itemsToJson();
return projectsService.add(projects);
}
@Override
public LiveData<Lcee<Projects>> queryProjects(int page) {
final MediatorLiveData<Lcee<Projects>> data = new MediatorLiveData<>();
data.setValue(Lcee.<Projects>loading());
data.addSource(projectsService.queryProjects(page), new Observer<Projects>() {
@Override
public void onChanged(@Nullable Projects projects) {
if (null == projects) {
data.setValue(Lcee.<Projects>empty());
} else {
// parse items from json
projects.itemsFromJson();
data.setValue(Lcee.content(projects));
}
}
});
return data;
}
}
所以在 Projects
加上這兩個方法
public void itemsToJson() {
if (items == null)
items = Collections.emptyList();
itemsJson = new Gson().toJson(items);
}
public void itemsFromJson() {
if (TextUtils.isEmpty(itemsJson))
return;
items = new Gson().fromJson(itemsJson, new TypeToken<List<ProjectItem>>(){}.getType());
}
RemoteProjectDataSource
RetrofitFactory
public class RetrofitFactory {
private static OkHttpClient client;
private static Retrofit retrofit;
private static final String HOST = "https://api.github.com";
static {
client = new OkHttpClient.Builder()
.connectTimeout(9, TimeUnit.SECONDS)
.build();
retrofit = new Retrofit.Builder()
.baseUrl(HOST)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
public static Retrofit getInstance() {
return retrofit;
}
}
ProjectApi
public interface ProjectApi {
@GET("/search/repositories?q=tetris+language:assembly&sort=stars&order=desc")
Call<Projects> queryProjects(@Query("page") int page);
}
RemoteProjectDataSource
這里需要注意捷沸,在存到本地源的時候,要先 projects.setPage(page);
設置 page狐史,也就是主鍵
public class RemoteProjectDataSource implements ProjectDataSource {
private static final RemoteProjectDataSource instance = new RemoteProjectDataSource();
private RemoteProjectDataSource() {
}
public static RemoteProjectDataSource getInstance() {
return instance;
}
private ProjectApi projectApi = RetrofitFactory.getInstance().create(ProjectApi.class);
@Override
public LiveData<Lcee<Projects>> queryProjects(final int page) {
final MutableLiveData<Lcee<Projects>> data = new MutableLiveData<>();
data.setValue(Lcee.<Projects>loading());
projectApi.queryProjects(page)
.enqueue(new Callback<Projects>() {
@Override
public void onResponse(Call<Projects> call, Response<Projects> response) {
Projects projects = response.body();
if (null == projects) {
data.setValue(Lcee.<Projects>empty());
return;
}
data.setValue(Lcee.content(projects));
// update cache
projects.setPage(page);// must set page before save to db
LocalProjectDataSource.getInstance().addProjects(projects);
}
@Override
public void onFailure(Call<Projects> call, Throwable t) {
t.printStackTrace();
data.setValue(Lcee.<Projects>error(t));
}
});
return data;
}
}
ProjectsRepository
public class ProjectsRepository {
private static final ProjectsRepository instance = new ProjectsRepository();
private ProjectsRepository() {
}
public static ProjectsRepository getInstance() {
return instance;
}
private Context context;
private ProjectDataSource remoteProjectDataSource = RemoteProjectDataSource.getInstance();
private ProjectDataSource localProjectDataSource = LocalProjectDataSource.getInstance();
public void init(Context context) {
this.context = context.getApplicationContext();
}
public LiveData<Lcee<Projects>> getProjects(int page) {
if (NetworkUtils.isConnected(context)) {
return remoteProjectDataSource.queryProjects(page);
} else {
return localProjectDataSource.queryProjects(page);
}
}
}
ViewModel
ProjectsViewModel
public class ProjectsViewModel extends ViewModel {
private ProjectsRepository projectsRepository = ProjectsRepository.getInstance();
private MutableLiveData<Integer> ldPage;;
private LiveData<Lcee<Projects>> ldProjects;
public LiveData<Lcee<Projects>> getProjects() {
if (null == ldProjects) {
ldPage = new MutableLiveData<>();
ldProjects = Transformations.switchMap(ldPage, new Function<Integer, LiveData<Lcee<Projects>>>() {
@Override
public LiveData<Lcee<Projects>> apply(Integer page) {
return projectsRepository.getProjects(page);
}
});
}
return ldProjects;
}
public void reload() {
ldPage.setValue(1);
}
public void loadMore(int currentCount) {
ldPage.setValue(PageUtils.getPage(currentCount));
}
}
PageUtils
public class PageUtils {
private static final int COUNT_PER_PAGE = 30;
public static int getPage(int count) {
int page = count / COUNT_PER_PAGE + 1;
if (count % COUNT_PER_PAGE > 0)
page++;
return page;
}
}
View Bind With ViewModel
private void initData() {
DBHelper.getInstance().init(this);
ProjectsRepository.getInstance().init(this);
projectsViewModel = ViewModelProviders.of(this).get(ProjectsViewModel.class);
projectsViewModel.getProjects().observe(this, new Observer<Lcee<Projects>>() {
@Override
public void onChanged(@Nullable Lcee<Projects> data) {
updateView(data);
}
});
reload();
}
private void reload() {
projectsViewModel.reload();
}
private void loadMore() {
projectsViewModel.loadMore(projectsAdapter.getItemCount());
}
private void updateView(Lcee<Projects> lcee) {
status = lcee.status;
switch (lcee.status) {
case Content: {
updateContentView(lcee.data);
break;
}
case Empty: {
updateEmptyView();
break;
}
case Error: {
updateErrorView();
break;
}
case Loading: {
updateLoadingView();
break;
}
}
}
到這里為止痒给,似乎和之前沒什么不同。
Loading Status
接下來就是本節(jié)的重點了骏全。
Loading State 還可以再分
仔細想一下苍柏,加載狀態(tài)其實是可以分為 刷新、加載更多姜贡、初始化加載
Loading State 的區(qū)別是 UI 操作
但需要注意的是试吁,這三種加載狀態(tài),對于數據本身來說楼咳,是很難區(qū)分的熄捍,也就是說都是加載狀態(tài),數據本身怎么會知道是刷新還是加載更多呢母怜。
而區(qū)分這幾種加載狀態(tài)的是 UI 操作余耽,比如用戶下拉刷新,所以加載狀態(tài)就是 Refreshing苹熏,而用戶上滑加載更多則是 LoadMore碟贾。
非要把三種加載狀態(tài)給數據本身币喧,就需要請求時把加載類型作為參數傳給 ViewModel,這顯然是破壞性行為缕陕。
定義獨立的 Load Status
因為這幾種狀態(tài)多見于 List 中粱锐,所以姑且命名為 ListStatus
public enum ListStatus {
Refreshing,
LoadingMore,
Content,
}
記錄 ListStatus
在刷新操作 和 加載更多操作中,記錄當前的 ListStatus
listStatus = ListStatus.Refreshing;
listStatus = ListStatus.LoadingMore;
// refresh
srl.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
if (isLoading()) {
srl.setRefreshing(false);
return;
}
listStatus = ListStatus.Refreshing;
reload();
}
});
// load more
rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
public int mLastVisibleItemPosition;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof LinearLayoutManager) {
mLastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
}
if (projectsAdapter == null)
return;
if (newState == RecyclerView.SCROLL_STATE_IDLE
&& mLastVisibleItemPosition + 1 == projectsAdapter.getItemCount()) {
if (isLoading())
return;
listStatus = ListStatus.LoadingMore;
loadMore();
}
}
});
updateContentView
因為有了獨立的 ListStatus扛邑,所以在渲染 Content 時,也要考慮铐然。
如果是 LoadingMore
蔬崩,則往 adapter 中加入數據。
如果是 Refreshing
搀暑,則停止 下拉刷新UI
否則 給 adapter 重新設置數據
private void updateContentView(Projects projects) {
switch (listStatus) {
case LoadingMore: {
projectsAdapter.addData(projects.getItems());
// todo load more complete
break;
}
case Refreshing: {
srl.setRefreshing(false);
break;
}
default: {
showContent();
projectsAdapter.setData(projects.getItems());
break;
}
}
}
updateEmptyView
空視圖也是一樣沥阳,如果下拉刷新得到結果為空,則停止刷新自点,并顯示空視圖桐罕,如果是加載更多返回空,則說明沒有更多數據了桂敛,應該更新 List 的 Footer(這里省事我沒加 Footer)
private void updateEmptyView() {
switch (listStatus) {
case LoadingMore: {
// todo no more data
break;
}
case Refreshing: {
srl.setRefreshing(false);
showEmpty();
break;
}
default: {
showEmpty();
break;
}
}
}
updateErrorView
private void updateErrorView() {
switch (listStatus) {
case LoadingMore: {
// todo load more error
break;
}
case Refreshing: {
srl.setRefreshing(false);
Toast.makeText(this, "Refresh failed", Toast.LENGTH_SHORT).show();
break;
}
default: {
showError();
break;
}
}
}
updateLoadingView
private void updateLoadingView() {
switch (listStatus) {
case LoadingMore: {
// todo show loading more view in list footer
break;
}
case Refreshing: {
break;
}
default: {
showLoading();
break;
}
}
}
其他
private void showContent() {
vContent.setVisibility(View.VISIBLE);
vEmpty.setVisibility(View.GONE);
vError.setVisibility(View.GONE);
vLoading.setVisibility(View.GONE);
}
private void showEmpty() {
vContent.setVisibility(View.GONE);
vEmpty.setVisibility(View.VISIBLE);
vError.setVisibility(View.GONE);
vLoading.setVisibility(View.GONE);
}
private void showError() {
vContent.setVisibility(View.GONE);
vEmpty.setVisibility(View.GONE);
vError.setVisibility(View.VISIBLE);
vLoading.setVisibility(View.GONE);
}
private void showLoading() {
vContent.setVisibility(View.GONE);
vEmpty.setVisibility(View.GONE);
vError.setVisibility(View.GONE);
vLoading.setVisibility(View.VISIBLE);
}
private boolean isLoading() {
return status == Status.Loading;
}
總結
倒吸一口冷氣功炮,這篇幅有點長,因為是從 0 開始术唬,很多代碼都是大家熟悉的薪伏。
重點要掌握的是,View 層里面的 ListStatus 狀態(tài)粗仓,以及根據 ListStatus 和 Status 來渲染界面嫁怀。
到這里大家可以放輕松了,因為本系列教程中最復雜的 UI 邏輯已經搞定了借浊。
但別高興得太早塘淑,老司機要開始飚車了,大家坐穩(wěn)扶好蚂斤。