项目地址 https://github.com/jiyarong/recyle_example

完成一个完整的列表一般包含这几个步骤

  • 拉取数据,一般是从网络
  • 解析数据,映射到列表项上
  • 翻页,在网页上一般是点击下一页,App里一般是向下滚动
  • 下拉刷新
  • 给列表项增加点击事件,跳转至详情

刚接触安卓的我完全没想到安卓里要完成这件事,要比在web端复杂很多

目标

做一个列表,调用本博客的api,在安卓里显示一个列表

建立数据的模型类

我们的目标是拉取本博客的posts列表,所以为了之后方便解析,先把这个模型建了

com.xxx.model下Post.java

package com.jikabao.recycleexample.model;

import java.util.List;

import lombok.Data;

@Data
public class Post {
    String id;
    String title;
    String updated_at;
    String content;
    List<Tag> tags;
}

同文件夹下建立Tag.java

package com.jikabao.recycleexample.model;

import lombok.Data;

@Data
public class Tag {
    String id;
    String name;
}

lombok的@Data修饰符在这里的作用是,自动生成setName(), getName()之类的方法,详细的看这里

引入http框架 retrofit2

app/build.gradle

...
    implementation 'com.squareup.retrofit2:retrofit:2.6.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
...

com.xxx.util下新建两个文件 ApiFactory.java以及Api.interface
ApiFactory.java

package com.jikabao.recycleexample.util;

import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class ApiFactory {
    static API instance;

    public static API getInstance() {
        if (instance == null) {
            Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl("https://vblog.peterji.cn/")
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();

            instance = retrofit.create(API.class);

        }

        return instance;
    }
}

Api.interface

package com.jikabao.recycleexample.util;
import com.jikabao.recycleexample.model.Post;
import com.jikabao.recycleexample.model.PostList;

import java.util.List;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.Query;

public interface API {
    @GET(value = "api/posts")
    Call<PostList> PostList(@Query("page") Integer page);

    @GET(value = "api/posts/{id}")
    Call<Post> PostDetail(@Path("id") String id);
}

对于post列表,还需要建一个模型来解析,因为api返回的数据一般是有层级的,而不是直接就是一组数据
PostList.java

package com.jikabao.recycleexample.model;

import com.google.gson.annotations.SerializedName;

import java.util.List;

import lombok.Data;

@Data
public class PostList {
    @SerializedName("posts")
    private List<Post> posts;

    @SerializedName("last_page")
    private boolean lastPage;
}

到此拉远程数据的步骤就结束了,具体用法会在下面出现

RecyclerView

安卓官方推荐的列表组件,优点是性能上有保证,会自动回收可视范围外的列表项,从而节省内存,官方介绍

安装RecyclerView

app/build.gradle

...
implementation 'com.android.support:recyclerview-v7:28.0.0'
...

在你activity关联的xml中加入

...
<android.support.v7.widget.RecyclerView
    android:id="@+id/recycler_view"
    android:scrollbars="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
...

在activity里

package com.jikabao.recycleexample;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

public class MainActivity extends AppCompatActivity {
    private RecyclerView recycle_view;
    private ActivityAdapter adapter;
    private LinearLayoutManager layoutManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //找到xml里对应的recycle_view
        recycle_view = (RecyclerView) findViewById(R.id.recycler_view);
        //定义一个layoutManager,这是recycle_view的必选项
        layoutManager = new LinearLayoutManager(this);
        //实例化一个adapter,负责管理列表的数据和渲染,也是必选项,adapter会在之后设置
        adapter = new ActivityAdapter();

        recycle_view.setLayoutManager(layoutManager);
        recycle_view.setAdapter(adapter);
    }
}

配置adapter

新建 activityAdapter.java

package com.jikabao.recycleexample;

import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;

public class ActivityAdapter extends RecyclerView.Adapter<ActivityAdapter.CustomViewHolder> {
    @Override
    public CustomViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        
    }
    
    @Override
    public void onBindViewHolder (@NonNull CustomViewHolder holder, int position) {
        
    }
    
    @Override
    public int getItemCount () {
        
    }

    class CustomViewHolder extends RecyclerView.ViewHolder {
        
        public CustomViewHolder(View itemView) {
            super(itemView);
        }
    }

}

adapter需要实现三个方法和一个匿名子类,从上往下分别是 更新列表时的回调,渲染列表项的回调,列表当前的列表项数量,以及一个继承自RecyclerView.ViewHolder的ViewHolder

接着给这个adapter一些基本的属性

public class ActivityAdapter extends RecyclerView.Adapter<ActivityAdapter.CustomViewHolder> {

    private static final Integer VIEW_TYPE_ITEM = 1;
    private static final Integer VIEW_TYPE_LOADING = 2;
    private Context context;
    View.OnClickListener onItemClick;

    private List<Post> dataList;

    public CustomListAdapter(List<Post> dataList) {
        this.dataList = dataList;
    }

...

解释一下这两个常量,作用是用来通过view_type来实现当列表在加载数据的时候,需要显示不一样的画面,比如一个正在加载并伴随一个在转的图标,数据加载完成了,去掉这个画面并显示新数据

因此我们需要创建两个不同的ViewHolder,他们都继承自CustomViewHolder

public class ActivityAdapter extends RecyclerView.Adapter<ActivityAdapter.CustomViewHolder> {
...
    class DataViewHolder extends CustomViewHolder {
        public DataViewHolder(View itemView) {
            super(itemView);
            title = itemView.findViewById(R.id.title);
            updated_at = itemView.findViewById(R.id.updated_at);
            tags_container = itemView.findViewById(R.id.tags);
        }

    }

    class ProgressViewHolder extends CustomViewHolder {
        public ProgressViewHolder(View itemView) {
            super(itemView);
        }
    }

    class CustomViewHolder extends RecyclerView.ViewHolder {
        public TextView title;
        public TextView updated_at;
        public LinearLayout tags_container;

        public CustomViewHolder(View itemView) {
            super(itemView);
        }
    }

接下来新建一个xml,作为list_item的样板

activity_list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/item_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginHorizontal="5dp"
    android:layout_marginVertical="5dp">

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="100dp"
        app:cardBackgroundColor="#f5f5dc"
        app:cardCornerRadius="5dp"
        app:cardElevation="4dp"
        app:cardMaxElevation="6dp"
        app:contentPadding="3dp">

        <android.support.constraint.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:id="@+id/title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@android:color/transparent"
                android:textAlignment="viewStart"
                android:textColor="#000000"
                android:textSize="18sp"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <LinearLayout
                android:id="@+id/tags"
                app:layout_constraintTop_toBottomOf="@id/title"
                app:layout_constraintLeft_toLeftOf="parent"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="horizontal"/>

            <TextView
                android:id="@+id/updated_at"
                android:layout_width="0dp"
                android:layout_height="20dp"
                android:background="@android:color/transparent"
                android:textAlignment="center"
                android:textColor="@android:color/darker_gray"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toBottomOf="@id/tags" />

        </android.support.constraint.ConstraintLayout>

    </android.support.v7.widget.CardView>

</RelativeLayout>

开始实现那三个需要实现的方法,外加一个额外的判断列表状态的方法getItemViewType

public class ActivityAdapter extends RecyclerView.Adapter<ActivityAdapter.CustomViewHolder> {

...
    @Override
    public CustomViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View root = null;
        context = parent.getContext();
        if (viewType == VIEW_TYPE_ITEM) {
            root = LayoutInflater.from(context).inflate(R.layout.activity_list_item, parent, false);
            if (onItemClick != null) {
                root.setOnClickListener(onItemClick);
            }
            return new DataViewHolder(root);
        } else {
            root = LayoutInflater.from(context).inflate(R.layout.row_process, parent, false);
            return new ProgressViewHolder(root);
        }
    }

    @Override
    public void onBindViewHolder (@NonNull CustomViewHolder holder, int position) {
        if (holder instanceof DataViewHolder) {
            if (dataList != null) {
                Post post = dataList.get(position);
                holder.title.setText(post.getTitle());
                holder.updated_at.setText(post.getUpdated_at());
                List<Tag> tags = post.getTags();
                holder.tags_container.removeAllViews();
                for (int i = 0; i < tags.size(); i++) {
                    Tag t = tags.get(i);
                    Button child = new Button(context);
                    child.setText(t.getName());
                    holder.tags_container.addView(child);
                }
            }
        }else{
            //Do whatever you want. Or nothing !!
        }
    }

    @Override
    public int getItemCount () {
        if (dataList == null) {
            return 0;
        } else {
            return dataList.size();
        }
    }

    @Override
    public int getItemViewType(int position) {
        if (dataList.get(position) != null)
            return VIEW_TYPE_ITEM;
        else
            return VIEW_TYPE_LOADING;
    }

还有一个row_process.xml需要写一下,这是加载中的页面

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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">

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

发起网络请求

在发起网络请求之前,首先要在AndroidManifest.xml的application节点之前加入权限

 <uses-permission android:name="android.permission.INTERNET" />

MainActivity加入几个属性

public class MainActivity extends AppCompatActivity {
    private RecyclerView recycle_view;
    private ActivityAdapter adapter;
    private LinearLayoutManager layoutManager;

    List<Post> data =  new ArrayList<>();
    API api = ApiFactory.getInstance();
    Integer currentPage = 1;
    boolean loading = true;
    boolean lastPage = false;
...

接着开始发起网络请求,以填充数据,在activity里增加一个方法

...
void fetchData () {
        api.PostList(this.currentPage).enqueue(new Callback<PostList>() {
            @Override
            public void onResponse(Call<PostList> call, Response<PostList> response) {
                PostList postList = response.body();
                lastPage = postList.isLastPage();
                adapter.addData(postList.getPosts());
                currentPage += 1;
            }

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

            }
        });
    };
...

这里要说下的是,adapter不是数据绑定的渲染方式,任何数据更新需要显式声明notifyDataSetChanged,因此在adapter类里封装一个方法

    public void addData(List<Post> data) {
        dataList.addAll(data);
        notifyDataSetChanged();
    }

接着在activity的onCreate回调中调用这个方法

...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //找到xml里对应的recycle_view
        recycle_view = (RecyclerView) findViewById(R.id.recycler_view);
        //定义一个layoutManager,这是recycle_view的必选项
        layoutManager = new LinearLayoutManager(this);
        //实例化一个adapter,负责管理列表的数据和渲染,也是必选项,adapter会在之后设置
        adapter = new ActivityAdapter(data);

        recycle_view.setLayoutManager(layoutManager);
        recycle_view.setAdapter(adapter);

        fetchData();
    }
...

到此为止应该能正常看到一个列表了,接下来再添加一些额外的功能,下拉和滚动

下拉刷新

下拉比较简单,你只要将你的recycle_view包裹在SwipeRefreshLayout里面就行了,并且这个组件也是官方的
activity_main.xml

...
    <android.support.v4.widget.SwipeRefreshLayout
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/swipeLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:scrollbars="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
        
    </android.support.v4.widget.SwipeRefreshLayout>
...

之后在main activity里面增加一些代码

public class MainActivity extends AppCompatActivity {
   ...
    private SwipeRefreshLayout swipeRefreshLayout;

   ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ...
        swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeLayout);

        swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                refresh();
            }
        });
       ...
    }


    void refresh() {
        data.removeAll(data);
        currentPage = 1;
        adapter.notifyDataSetChanged();
        fetchData();
    }

顺便还要在拉取成功的事件里设置一下swiper的状态,用.setRefreshing(false);

滚动翻页

新建一个类InfiniteScrollListener,继承自RecyclerView.OnScrollListener
代码如下

package com.jikabao.recycleexample;

import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

public class InfiniteScrollListener extends RecyclerView.OnScrollListener  {
    private final static int VISIBLE_THRESHOLD = 2;
    private LinearLayoutManager linearLayoutManager;
    private boolean loading; // LOAD MORE Progress dialog
    private OnLoadMoreListener listener;
    private boolean pauseListening = false;


    private boolean END_OF_FEED_ADDED = false;
    private int NUM_LOAD_ITEMS = 10;

    public InfiniteScrollListener(LinearLayoutManager linearLayoutManager, OnLoadMoreListener listener) {
        this.linearLayoutManager = linearLayoutManager;
        this.listener = listener;
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        if (dx == 0 && dy == 0)
            return;
        int totalItemCount = linearLayoutManager.getItemCount();
        int lastVisibleItem = linearLayoutManager.findLastVisibleItemPosition();
        if (!loading && totalItemCount <= lastVisibleItem + VISIBLE_THRESHOLD && totalItemCount != 0 && !END_OF_FEED_ADDED && !pauseListening) {
            if (listener != null) {
                listener.onLoadMore();
            }
            loading = true;
        }
    }

    public void setLoaded() {
        loading = false;
    }

    public interface OnLoadMoreListener {
        void onLoadMore();
    }

    public void addEndOfRequests() {
        this.END_OF_FEED_ADDED = true;
    }

    public void reStartRequest() {
        this.END_OF_FEED_ADDED = false;
    }

    public void pauseScrollListener(boolean pauseListening) {
        this.pauseListening = pauseListening;
    }
}

main activity增加代码

public class MainActivity extends AppCompatActivity implements InfiniteScrollListener.OnLoadMoreListener {
    ...
    InfiniteScrollListener infiniteScrollListener;
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ...
        layoutManager = new LinearLayoutManager(this);
        infiniteScrollListener = new InfiniteScrollListener(layoutManager, this);
        infiniteScrollListener.setLoaded();

        recycle_view.addOnScrollListener(infiniteScrollListener);

...



    @Override
    public void onLoadMore() {
        fetchData();
    }

增加点击事件

最后给列表项增加点击事件

adapter.setOnItemClick(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int itemPosition = recycle_view.getChildLayoutPosition(view);
                Post post = data.get(itemPosition);
                Intent intent = new Intent(context, PostDetailActivity.class);
                String message = post.getId();
                intent.putExtra("PostId", message);
                startActivity(intent);
            }
        });

PostDetailActivity是详情页的activity,新建一个就好了,到此为止列表功能就全部完成了

项目地址 https://github.com/jiyarong/recyle_example