Android内部分享[2]——数据存储和绑定

概述

回顾上一次分享,我们已经对 Android 的整体发展,体系结构,工程搭建,界面布局,组件注册,生命周期有了一定认识,接下来研究一下在 Android 中数据如何存储,如何将数据和界面控件绑定。

Android 中的本地存储主要有三种方式:

  • SharePreference : key-value 形式,主用于数据较少的配置信息的存储。
  • SQLite :一些比较复杂的数据结构,特别适合对象存储。
  • File Save : 比较大的文件(例如日志,图片缓存,apk包等)或者某些特殊的配置文件。

另外还有 ContentProvider 用于进程间数据共享,不是很常用,下面会简要叙述其原理和 FileProvider 的使用。

上面描述的都是数据持久化的方式,在某些特殊情况下我们可能需要用到内存缓存,使用一些弱(WeakReference)软(SoftReference)引用技术和 LruCache 内存缓存类来实现,这里不做讨论。

本次分享的所有主题都有对应的示例代码,可以在这里下载: https://github.com/lxqxsyu/InnerShareCode2

对应分类主题的示例Demo

SharePreference

创建 SharePreference 对象:

private static final String SP_TEST = "sp_test";

private SharedPreferences mSharedPreference;

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

    mSharedPreference = getSharedPreferences(SP_TEST, Context.MODE_PRIVATE);
}

上面的第一个参数是配置文件名称,第二个参数是存储模式,有下面几种:

  • MODE_PRIVATE,则该配置文件只能被自己的应用程序访问。
  • MODE_WORLD_READABLE,则该配置文件除了自己访问外还可以被其它应该程序读取。
  • MODE_WORLD_WRITEABLE,则该配置文件除了自己访问外还可以被其它应该程序读取和写入。
  • MODE_APPEND,检查文件是否存在,存在就往文件追加内容,否则就创建新文件。

存储数据:

private void save(String key, String data){
    SharedPreferences.Editor editor = mSharedPreference.edit();
    editor.putString(key, data);
    editor.commit(); // 或者 editor.apply();
}

存储数据需要获得 Editor 对象,然后使用 Editor 对象添加对应格式数据,最后记得 commit(同步) 或者 apply(异步)。

支持的数据类型如下:

SharePreference支持的数据类型

获取数据:

private String get(String key){
    return mSharedPreference.getString(key, null);
}

根据对应的 key 获取值, 后面第二个参数是默认值(当key没找到时)。

本质上 SharePreference 是基于 xml 格式的一种文件存储,可以在 data/{packagename}/shared_prefs 下找到该文件:

SharePreference的存储文件截图

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="input_data">dfdfdfdfdfdgdgdg</string>
</map>

SQLite数据库

Android 中的数据库是 SQLite3, 可以直接使用 adb 命令来操作该数据,例如:

创建一个名为 test.db 的数据库:

sqlite> sqlite3 test.db

创建一个表:

sqlite> create table mytable(id integer primary key, title text, subtitle text);

插入数据:

sqlite> insert into mytable(id, value) values(1, 'Micheal', 'Sub Micheal');
sqlite> insert into mytable(id, value) values(2, 'Jenny', 'Sub Jenny');
sqlite> insert into mytable(value) values('Francis', 'Sub Francis');
sqlite> insert into mytable(value) values('Kerk', 'Sub Kerk');

Android 中 Google 为我们提供了一个方便操作数据库的类 SQLiteOpenHelper, 我们可以重写它来实现对应的增删改查操作。

首先定义一个表结构, 注意需要实现接口 BaseColumns :

public static class MyTableEntry implements BaseColumns {
    
    public static final String TABLE_NAME = "mytable";      //表名称
    public static final String COLUMN_NAME_TITLE = "title";  //字段一
    public static final String COLUMN_NAME_SUBTITLE = "subtitle";  //字段二

    public MyTableEntry(String title, String subtitle) {
        this.title = title;
        this.subtitle = subtitle;
    }

    public String title;
    public String subtitle;
}

接下来,创建一个 TestDBHelper 类继承自 SQLiteOpenHelper

构造函数:

public class TestDBHelper extends SQLiteOpenHelper {

    public static final String DATABASE_NAME = "test.db";
    public static final int DATABASE_VERSION = 1;

    public TestDBHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
}
  • context : 上下文环境
  • DATABASE_NAME : 数据库名
  • factory : 用来创建对象的Cursor, 一般为 null
  • DATABASE_VERSION : 数据库版本号

定义辅助SQL:

private static final String SQL_CREATE_ENTRIES =
        "CREATE TABLE " + MyTableEntry.TABLE_NAME + " (" +
                MyTableEntry._ID + " INTEGER PRIMARY KEY," +
                MyTableEntry.COLUMN_NAME_TITLE + " TEXT," +
                MyTableEntry.COLUMN_NAME_SUBTITLE + " TEXT)";

private static final String SQL_DELETE_ENTRIES =
        "DROP TABLE IF EXISTS " + MyTableEntry.TABLE_NAME;

重写 SQLiteOpenHelper 的 onCreate, onUpdate(), onDowngrade() 方法:

@Override
public void onCreate(SQLiteDatabase db) {
    db.execSQL(SQL_CREATE_ENTRIES);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    db.execSQL(SQL_DELETE_ENTRIES);
    onCreate(db);
}

@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    onUpgrade(db, oldVersion, newVersion);
}

onCreate() : 该方法是第一个被调用的,一般在其中初始化创建数据库, 也就是说数据库的增删改查操作必须在这个方法执行完之后进行。 onUpdate() : 该方法是当数据库版本发生变化(版本增加)的时候(也就是数据库结构变化后)执行的方法,一般需要在这里重新初始化数据库。 onDowngrade() : 该方法是当数据库版本降级(版本减少)的时候执行。

接下来我们来使用上面的 TestDBHelper 实现数据库增加和查询:

public class SQLiteTestActivity extends BaseActivity {

    private TestDBHelper mDBHelper;
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mDBHelper = new TestDBHelper(this);
    }

    //增加数据
    private void saveData(MyTableEntry entry){
        SQLiteDatabase db = mDBHelper.getWritableDatabase();

        ContentValues values = new ContentValues();
        values.put(MyTableEntry.COLUMN_NAME_TITLE, entry.title);
        values.put(MyTableEntry.COLUMN_NAME_SUBTITLE, entry.subtitle);
        long newRowId = db.insert(MyTableEntry.TABLE_NAME, null, values);
    }

    //根据 title 查询数据
    private List<MyTableEntry> getData(String title){
        SQLiteDatabase db = mDBHelper.getReadableDatabase();
        String[] projection = {
                BaseColumns._ID,
                MyTableEntry.COLUMN_NAME_TITLE,
                MyTableEntry.COLUMN_NAME_SUBTITLE
        };

        String selection = MyTableEntry.COLUMN_NAME_TITLE + " = ?";
        String[] selectionArgs = { title };

        String sortOrder =
                MyTableEntry.COLUMN_NAME_SUBTITLE + " DESC";

        Cursor cursor = db.query(
                MyTableEntry.TABLE_NAME,   // The table to query
                projection,             // The array of columns to return (pass null to get all)
                selection,              // The columns for the WHERE clause
                selectionArgs,          // The values for the WHERE clause
                null,                   // don't group the rows
                null,                   // don't filter by row groups
                sortOrder               // The sort order
        );

        List<MyTableEntry> itemIds = new ArrayList<>();
        while(cursor.moveToNext()) {
            MyTableEntry entry = new MyTableEntry();
            long itemId = cursor.getLong(
                    cursor.getColumnIndexOrThrow(MyTableEntry._ID));
            entry.title = cursor.getString(cursor.getColumnIndexOrThrow(MyTableEntry.COLUMN_NAME_TITLE));
            entry.subtitle = cursor.getString(cursor.getColumnIndexOrThrow(MyTableEntry.COLUMN_NAME_SUBTITLE));
            itemIds.add(entry);
        }
        cursor.close();
        return itemIds;
    }
}

数据库框架

通过上面的体验,是不是感觉操作数据库还是挺麻烦的,如果有多张表工作量看起来还是蛮大的,比较好的是我们可以使用一些第三方封装的对象映射框架来直接通过操作对象来操作数据库。

Android 中数据库对象映射框架很多,例如:LitePal, Room, Realm, ObjectBox等, 下面我们就来看一下最新的 ObjectBox 框架的使用。

第一步,引入库

打开工程根目录下的 build.gradle 文件,添加如下配置:

buildscript {

    ext.objectboxVersion = '2.1.0'

    dependencies {
        classpath "io.objectbox:objectbox-gradle-plugin:$objectboxVersion"
    }
}

接着在 app/build.gradledependencies { } 中添加如下代码:

dependencies {
    debugImplementation "io.objectbox:objectbox-android-objectbrowser:$objectboxVersion"
    releaseImplementation "io.objectbox:objectbox-android:$objectboxVersion"
}

最后在 app/build.gradle 最后一行添加如下插件使用代码:

apply plugin: 'io.objectbox'

第二步,定义映射类

ObjectBox 同主流的 DB 库一样,采用注解来标注实体类,然后编译生成 DAO 相关类。使用 @Entity 标注需要存取的实体类。@Id 标记用于 ObjectBox 进行 ID 自增。

@Entity
public class MyBoxTable {
    @Id
    public long id;
    public String title;
    public String subtitle;
}

第三步,获取 BoxStore 对象

在 Application 中创建 BoxStore 并向外暴露对应的 MyBoxTable 的 Box 对象,后面可以使用该对象操作数据库。

public class App extends Application {

    private static App mInstance;
    private BoxStore mBoxStore;

    @Override
    public void onCreate() {
        super.onCreate();
        mInstance = this;
        mBoxStore = MyObjectBox.builder().androidContext(this).build();
    }

    public static App getInstance(){
        return mInstance;
    }

    public Box<MyBoxTable> getMyTableBox(){
        return mBoxStore.boxFor(MyBoxTable.class);
    }
}

第四步,操作数据库

增加数据:

App.getInstance().getMyTableBox().put(new MyBoxTable(title, subtitle));

查询数据:

List<MyBoxTable> entrys = App.getInstance().getMyTableBox().query().equal(MyBoxTable_.title, key).build().find();

文件存储

Android 系统自带存储空间,也可以通过 sd 卡来扩展存储,它们之间的关系就好比电脑的硬盘和移动硬盘的关系。 前者空间较小,后者空间大,但后者不一定可用。 开发应用,处理本地数据存取时,可能会遇到这些问题:

  1. 需要判断sd卡是否可用: 占用过多机身内部存储,容易招致用户反感,优先将数据存放于sd卡。
  2. 应用数据存放路径,同其他应用应该保持一致,应用卸载时,清除数据,不然会招致用户反感。
  3. 需要判断两者的可用空间: sd卡存在时,可用空间反而小于机身内部存储,这时应该选用机身存储。
  4. 数据安全性,本应用数据不愿意被其他应用读写。
  5. 图片缓存等,不应该被扫描加入到用户相册等媒体库中去。

内部存储方式存储的文件属于其所创建的应用程序私有,其他应用程序无权进行操作。当创建的应用程序被卸载时,其内部存储的文件也随之被删除。当内部存储器的存储空间不足时,缓存文件可能会被删除以释放空间。因此,缓存文件是不可靠的。当使用缓存文件时,自己应该维护好缓存文件,并且将缓存文件限制在特定大小之内。

第一步: 通过 Context.openFileOutput(String name, int mode) 方法打开文件并设定读写方式,返回 FileOutputStream

其中,参数 mode 取值为:

  • MODE_PRIVATE:默认访问方式,文件仅能被创建应用程序访问。
  • MODE_APPEND:若文件已经存在,则在文件末尾继续写入数据,而不抹掉文件原有内容。
  • MODE_WORLD_READABLE:允许该文件被其他应用程序执行读取内容操作。
  • MODE_WORLD_WRITEABLE:允许该文件被其他应用程序执行写操作。

    private void writeFile(String message){
    try {
        FileOutputStream fos = openFileOutput(FILE_NAME, Context.MODE_PRIVATE);
        fos.write(message.getBytes());
        fos.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    }
    

第二步:通过 Context.openFileInput(String name) 方法读取文件内容:

private String readFile(){
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try {
        FileInputStream fis = openFileInput(FILE_NAME);
        byte[] buffer = new byte[1024];
        int length;
        while ((length = fis.read(buffer)) != -1){
            baos.write(buffer, 0, length);
        }
        return baos.toString("UTF-8");
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

我们上面设置的路径 FILE_NAME 如果是这样定义的:

public static final String FILE_NAME = "test_write_read.txt";

则文件会存放在 data/{packagename}/files/ 目录下:

文件存放结构

如果我们要存储到 sd 卡就需要先判断 sd 卡是否可用,然后再进行存储:

public static boolean hasSDCardMounted() {
    String state = Environment.getExternalStorageState();
    if (state != null && state.equals(Environment.MEDIA_MOUNTED)) {
        return true;
    } else {
        return false;
    }
}

在 API 19 / Andorid 4.4 / KITKAT 之前读写 sd 卡是需要主动声明下面两个权限的:

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

路径规律: 一般地,通过 ContextEnvironment 相关的方法获取文件存取的路径。通过这两个类可获取各种路径,如下:

    ($rootDir)
+- /data                -> Environment.getDataDirectory()
|   |
|   |   ($appDataDir)
|   +- data/com.myapp
|       |
|       |   ($filesDir)
|       +- files           -> Context.getFilesDir() /Context.getFileStreamPath("")
|       |       |
|       |       +- file1    -> Context.getFileStreamPath("file1")
|       |   ($cacheDir)
|       +- cache            -> Context.getCacheDir()
|       |
|       +- app_$name        -> Context.getDir(String name, int mode)
|
|   ($rootDir)
+- /storage/sdcard0     -> Environment.getExternalStorageDirectory()
    |                       / Environment.getExternalStoragePublicDirectory("")
    |
    +- dir1             -> Environment.getExternalStoragePublicDirectory("dir1")
    |
    |   ($appDataDir)
    +- Andorid/data/com.myapp
        |
        |   ($filesDir)
        +- files        -> Context.getExternalFilesDir("")
        |   |
        |   +- file1    -> Context.getExternalFilesDir("file1")
        |   +- Music    -> Context.getExternalFilesDir(Environment.Music);
        |   +- Picture  -> ... Environment.Picture
        |   +- ...
        |
        |   ($cacheDir)
        +- cache        -> Context.getExternalCacheDir()
        |
        +- ???

根目录:

Environment.getDataDirectory():  /data

Environment.getExternalStorageDirectory():  /storage/sdcard0

内部(相对apk)存储目录:

//内部
Context.getFilesDir() / Context.getFileStreamPath("")
Context.getCacheDir()
Context.getDir(String name, int mode)

//外部
Context.getExternalFilesDir()
Context.getExternalCacheDir()

/data/{packagename} 和外部存储目录 /storage/sdcard0/Android/data/{packagename} 下存储的数据在 apk 卸载后会被系统删除,我们应将应用的数据放于这两个目录中。

注意上面 getFilesDir() 和 getExternalFilesDir() 所对应的内部和外部存储的区别,机身内存不足时,文件会被删除,外部存储没有实时监控,当空间不足时,文件不会实时被删除,可能返回空对象。

外部存储公开目录:

Environment.getExternalStorageDirectory():   /storage/sdcard0

Environment.getExternalStoragePublicDirectory(""):  /storage/sdcard0

Environment.getExternalStoragePublicDirectory("folder1"):  /storage/sdcard0/folder1

外部存储中,公开的数据目录。 这些目录将不会随着应用的删除而被系统删除,请斟酌使用。

FileProvider

上面文件的存储目录如果我们选用类似 getCacheDir() 这种只可以 apk 内部访问,如果我们要暴露给外部可以使用类似 Environment.getExternalStorageDirectory() 完全暴露给了外部所有 apk, 这种显然是不够安全的,有的时候我们只需要暴露给特定的 apk, 这个时候就需要用到 FileProvider 来实现了。

FileProvider 是 ContentProvider 的一个特殊子类,它通过创建一个 content:// Uri 来使应用程序相关的文件实现安全共享: file:/// Uri.

ContentProvider中的URL有固定格式

ContentProvider 提供了对底层数据存储方式的抽象,如下图所示如果我们将 SQLite 换成 MongoDB 对于上层 apk 而言没有什么变化。

ContentProvider数据存储方式抽象

但是最主要的特点还是 ContentProvider 为应用间的数据交互提供了一个安全的环境,它准许你把自己的应用数据根据需求开放给其他应用进行增、删、改、查,而不用担心直接开放数据库权限而带来的安全问题。

使用FileProvider的大致步骤如下(了解即可):

第一步:在manifest清单文件中注册provider

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.jph.takephoto.fileprovider"
    android:grantUriPermissions="true"
    android:exported="false">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

第二步:指定共享的目录, 在资源(res)目录下创建一个xml目录,然后创建一个名为 file_paths 的资源文件。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <paths>
        <external-path path="" name="camera_photos" />
    </paths>
</resources>

这里的 <paths> 有很多形式, <external-path> 对应的是 Environment.getExternalStorageDirectory() 目录。

这个name名字可以随便起,替代path的一个显式称谓,这个值能起到隐藏共享目录的名称的作用。这个path是要共享的子目录路径,这个值只能是一个目录路径而不能是一个文件的路径。

第三步:使用 FileProvider

上述准备工作做完之后,现在我们就可以使用FileProvider了,以调用系统相机拍照为例:

File file=new File(Environment.getExternalStorageDirectory(), 
        "/temp/"+System.currentTimeMillis() + ".jpg");
if (!file.getParentFile().exists())file.getParentFile().mkdirs();
Uri imageUri = FileProvider.getUriForFile(context, 
        "com.jph.takephoto.fileprovider", file);//通过FileProvider创建一个content类型的Uri
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //添加这一句表示对目标应用临时授权该Uri所代表的文件
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);//设置Action为拍照
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);//将拍取的照片保存到指定URI
startActivityForResult(intent,1006);

有关 FileProvider 和 ContentProvider 详细解读可以阅读我的另一篇博文:https://dp2px.com/2017/12/06/fileprovider/

第四步:显示图片到 ImageView

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if(requestCode == REQUEST_CAMERA && resultCode == RESULT_OK){
        mPhotoImage.setImageURI(mImageUri);
    }
}

调用系统相机拍照

上面只是简化步骤,实际上的获取系统相机拍照还需要动态申请系统权限(7.0 上下兼容),这部分代码在示例代码中有(文章末尾下载), 另外还存在裁切和压缩的过程,还有部分手机的兼容性处理等问题。

private void checkPermission(){
    if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
        requestSystemCamera();
    }else {
        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
            requestSystemCamera();
        } else {
            ActivityCompat.requestPermissions(this, new String[]{
                    Manifest.permission.CAMERA},
                    REQUEST_PERMISSON_CAMERA);
        }
    }
}

数据传递

利用 Intent 携带简单数据:

Intent intent = new Intent(this, TestTurnActivity.class);

intent.putExtra("title", "LI-XIAO-QIANG");
intent.putExtra("subtitle", "SUB-LXQ");

/*Bundle bundle = intent.getExtras();
bundle.putString("title", "LI-XIAO-QIANG");
bundle.putString("subtitle", "SUB-LXQ");*/

startActivity(intent);

Serializable 序列化对象传递对象:

public class TestSerializObj implements Serializable {
    public String title;
    public String subtitle;
}
TestSerializObj tso = new TestSerializObj();
tso.title = TITLE;
tso.subtitle = SUB_TITLE;

Intent intent = new Intent(this, TestTurnActivity.class);
intent.putExtra("serialObject", tso);
startActivity(intent);

实现 Parcelable 接口传递对象:

public class TestParcelableObj implements Parcelable {

    public String title;
    public String subtitle;

    public TestParcelableObj() {
    }

    public static final Creator<TestParcelableObj> CREATOR = new Creator<TestParcelableObj>() {
        @Override
        public TestParcelableObj createFromParcel(Parcel in) {
            TestParcelableObj tpo = new TestParcelableObj();
            tpo.title = in.readString();
            tpo.subtitle = in.readString();
            return tpo;
        }

        @Override
        public TestParcelableObj[] newArray(int size) {
            return new TestParcelableObj[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(title);
        dest.writeString(subtitle);
    }
}
TestParcelableObj tpo = new TestParcelableObj();
tpo.title = TITLE;
tpo.subtitle = SUB_TITLE;

Intent intent = new Intent(this, TestTurnActivity.class);
intent.putExtra("parcelableObject", tpo);
intent.putExtra("type", 2);
startActivity(intent);

接收数据:

String title = getIntent().getStringExtra("title");
String subtitle = getIntent().getStringExtra("subtitle");
TestSerializObj tso = (TestSerializObj) getIntent().getSerializableExtra("serialObject");
TestParcelableObj tpo = getIntent().getParcelableExtra("parcelableObject");

数据绑定

上面我们已经看到如何给 TextView 设置数据, Google 官方发布的 DataBinding 使用标签的方式可以实现类似 Vue 的数据双向绑定,一般情况下用不到,至少我很少这样用。况且 kotlin 替代 java 开发 Android 的趋势愈加明显,这里我们简单了解一下 kotlin 中的扩展库 kotlin-android-extensions

还记得上面使用的 findViewById() 吗?通过它可以获得 xml 中定义的对象, 在 kotlin 中会抛弃这么繁琐的用法,直接可以操作 id 来赋值。

<?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"
    tools:context=".main.MainActivity">

    <TextView
        android:id="@+id/hellotext"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
class MainActivity: BaseActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main);

        hellotext.setText(R.string.string_use_kotlin_ext)
    }

    companion object {
        fun newIntent(context: Context): Intent{
            return Intent(context, MainActivity::class.java)
        }
    }
}

而用 java 开发的过程中常常使用 butterknife 来简化索取对象的过程,例如:

@BindView(R.id.tv_title)
TextView mTitle;
@BindView(R.id.tv_subtitle)
TextView mSubTitle;

适配器和列表

Android 中的列表和适配器经历了从 ListView, GrideView 到 RecyclerView 的过程,至少现在来说没有必要知道 ListView 和 GridView 的用法,因为它们已经完全被 RecyclerView 替代。

app/build.gradle 文件的 dependencies 中添加:

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

为了简化对适配器的操作,这里我们使用一个开源框架 BaseRecyclerViewAdapterHelper 官网地址: http://www.recyclerview.org/

引入框架:

implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.30'

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/recyclerview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

适配器:

public class ListAdapter extends BaseQuickAdapter<ListEntry, BaseViewHolder> {

    public ListAdapter() {
        super(R.layout.item_adapter_list_view);
    }

    @Override
    protected void convert(BaseViewHolder helper, ListEntry item) {
        helper.setText(R.id.item_title, item.title);
    }
}

将 RecyclerView 绑定到适配器:

mRecyclerView = findViewById(R.id.recyclerview);
//设置为线性布局
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mVerticalGap = ConvertUtil.dip2px(this, 6);
//设置item间隙
mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
                                @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.bottom = mVerticalGap;
    }
});
mListAdapter = new ListAdapter();
mListAdapter.openLoadAnimation();
mRecyclerView.setAdapter(mListAdapter);

绑定数据到适配器:

List<ListEntry> datas = new ArrayList<>();
for(int i = 0; i < 50; i++){
    ListEntry entry = new ListEntry();
    entry.title = "这个是列表item" + i;
    datas.add(entry);
}
mListAdapter.setNewData(datas); //绑定数据

线性列表效果

如果我们要更改为网格的形式,只需要修改上面的 setLayoutManager:

mRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));

然后我们再调整一下格子之间的间隙:

mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
                                @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.bottom = mVerticalGap / 2;
        outRect.left = mVerticalGap / 2;
        outRect.right = mVerticalGap / 2;
        outRect.top = mVerticalGap / 2;
    }
});

网格列表效果

最后我们给每个格子(item)添加一个点击事件:

mListAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() {
    @Override
    public void onItemClick(BaseQuickAdapter adapter, View view, int position) {
        ListEntry entry = (ListEntry) adapter.getData().get(position);
        ToastUtil.showToast(GrideAdapterTestActivity.this, "你点击了:" + entry.title);
    }
});

当然除了能实现上面的列表和网格,我们也可以通过 RecyclerView 实现瀑布流样式和不规则的其他样式。

本文源码GitHub地址: https://github.com/lxqxsyu/InnerShareCode2

本文PDF格式下载: https://dp2px.com/2019/07/31/android-train2/android-train2.pdf