这次来实现一个简单的评论应用,主要使用到 Android 中的存储、数据库的操作以及相册、通讯录数据的读取。
SharedPreferences
这里密码的存储使用到了SharedPreferences
这个东西,这个东西用于保存和检索原始数据类型的一些永久性键值对。使用起来也是非常的简单的。
- 检索
1 | SharedPreferences settings = mContext.getSharedPreferences(SHAREDPREFERENCE_NAME, 0); |
只需要提供文件的名字以及所需要的键值对的名字,就可以读取指定类型的数据。
- 保存
1 | SharedPreferences sharedPreferences = mContext.getSharedPreferences(SHAREDPREFERENCE_NAME, Context.MODE_PRIVATE); |
保存也是十分地简单,还是提供同样的文件名字和键值对名字,然后获取一个Editor
对象,然后使用apply()
方法保存。
这里没有使用commit()
是因为Android Studio
给了我如下的提示,然后就劝我改成apply()
,然后我就改了。
Consider using apply() instead of commit on shared preferences. Whereas commit blocks and writes its data to persistent storage immediately, apply will handle it in the background.
然后运行程序,通过Android Studio
的Device File Explorer
,找到我们的应用的data
目录,就可以看到在应用下的shared_prefs
文件夹里面生成了一个我们之前指定的名字的xml
,里面就存储着我们密码的键值对。
内部存储
内部存储是应用的私有文件,一般保存在data/data/应用包名/files
文件夹里面,只有应用本身才可以访问,而其他应用不能访问,当应用被卸载的时候,这些文件也会一同被移除。
- 写入
1 | try (FileOutputStream fos = mContext.openFileOutput(FILE_NAME, Context.MODE_PRIVATE)) { |
这里使用了try-catch
来捕获保存失败时候抛出的异常,在 try 的括号里面打开FileOutputStream
,就可以不用再去手动close
,把这个变量的作用域限定在这个 try 里面。
只需要指定文件名,和打开的方式(这里使用了私有模式,还有MODE_APPEND
可以选择)就可以在内存存储的专属目录下写入一个文件。
然后同样使用Device File Explorer
查看,可以发现指定名字和内容的文件已经成功创建了。
- 读取
1 | try (FileInputStream fis = mContext.openFileInput(FILE_NAME)) { |
读取来说也是相对简单的,和写入大同小异,使用FileInputStream
打开指定名字的文件,如果抛出异常就说明文件不存在或者读取失败,这时就可以提醒用户。当读取成功的时候,使用read
方法将文件内存读取到一个byte
数组里面,然后转换成String
设置到EditText
控件当中。
外部存储
除了内部存储之外,还有缓存和外部存储。缓存可以使用getCacheDir()
方法来操作,是应用的临时文件,而外部存储的文件是全局可读取文件,其他具有存储访问权限的应用或者连接计算机都可以读取这些文件。
在Android 7.0
中,我们也可以使用作用域来访问外部存储中的特定目录,比如相册。
如果需要读写外部存储,那么就需要先取得权限WRITE_EXTERNAL_STORAGE
,而在现在主流的 Android 版本之中,还需要显式地先用户请求该权限。
当然,应用也可以使用外部存储存放一些私有文件,比如一些大型游戏,通常需要很大的空间存放应用的数据资源。那么可以通过getExternalFilesDir()
来获取外部存储中的私有存储目录,也是其他应用不可以访问的。
相对于内部存储,外部存储可以存放一些公共的可以与其他应用共享的文件(然而内部存储使用内容提供器同样可以实现),或者说是一些通用的媒体文件(如音乐,照片等,可以存放在公共的存储区域以便同一管理),也可以存储一些体积相对较大的资源文件(如游戏资源包)
简单的论坛应用
登陆界面:
注册界面:
评论界面:
举报评论:
删除评论:
删除后评论:
查看电话号码(不存在):
查看电话号码(存在):
界面
这次的实验一共有两个界面,一个登陆注册的页面,另一个是评论页面。
登陆注册页面外层使用了一个约束布局,里面放一个RadioGroup
和LinearLayout
前者是登陆/注册的单选按钮,后者就是登陆注册的LinearLayout
RadioGroup
通过onCheckedChanged
事件控制两个layout
的visibility
的属性,选择显示登陆页面还是注册页面:
1 | public void onCheckedChanged(RadioGroup group, int checkedId) { |
评论页面比较简单,就是一个LinearLayout
里面加一个ListView
和一个LinearLayout
(发送评论)。
通过layout_weight
设置ListView
占满屏幕上面的位置,EditText
占满下面左边的位置。
从相册选择图片
只需要发送一个特殊的Intent
,就可以跳转到系统的相册选择工具选择图片
1 | Intent intent = new Intent(Intent.ACTION_PICK, null); |
这里指定了image/*
的类型,那么系统就会跳转到默认的图片选择应用选择图片,不需要我们再设计界面和获取存储来让用户选择,这样会使得 Android 更加统一和安全。
然后在Activity
里面重载 onActivityResult
,获取用户选择的照片的Uri
1 | // MainActivity |
使用MediaStore
从Uri
里面获取Bitmap
,然后再填充到ImageView
里面,这里使用createScaledBitmap
的方法,将图片按比例缩放到宽度为 150 的大小,避免了OOM
的出现。
数据库操作
这里我使用了GreenDAO
来操作数据库。
使用前准备
首先,在build.gradle(Project)
里面添加repositories
和 dependencies
:
1 | buildscript { |
然后在build.gradle(Module)
里面添加依赖以及设置
1 | implementation 'org.greenrobot:greendao:3.2.2' // add library |
1 | greendao { |
开始使用
接下来,就可以创建一个数据库的Model
类:
这里先建立一个User
类
1 |
|
使用@Entity
标记需要生成数据库表的类
使用@Id
标记作为主键的属性
这里我设置了一个属性like
,用于存储用户点赞过的评论 ID,由于数据库里面不支持List
类型,因此我们需要再写一个类手动将他转换为String
模式
使用@Convert
为他设置目标类型以及转换器
1 | public class StringConverter implements PropertyConverter<List<Long>, String> { |
转换器中需要实现两个重载函数,将两种类型转换,一般的做法就是使用分割符(比如,
)来分开列表项。
做完上面的事情之后,就可以执行Build
-Make Project
,然后GreenDAO
就会为我们生成好SQL
的操作类:DAOMaster
, DAOSession
, UserDAO
最后我们写一个Manager
来实现单例模式操作数据库
1 | public class GreenDaoManager { |
然后在Application
的创建的时候,初始数据库。
1 | GreenDaoManager.init(this) |
使用方法
GreenDAO
的使用十分简单:
查询:
1 | UserDao userDao = GreenDaoManager.getInstance().getSession().getUserDao(); |
增删改可以使用insert
, delete
, update
等方法操作。
由于我们需要将用户的头像存放到数据库当中,因此需要转换为byte[]
类型,下面给出转换的方法:
1 | // Image to byte[] |
可以看到,在应用的 data 目录下,可以找到生成的sqlite
数据库文件
数据库设计
这次实验有一个点赞的数据需要存储,既需要保证用户对于某一条评论的点赞是唯一的(不能重复点赞两次),还需要记录每条评论的点赞数。因此这个数据就有必要分开来存储。
对于用户,我存储了以下的数据:
1 |
|
首先是以用户名为主键,存储用户的一些基本信息,然后用一个List
存储用户点赞过的评论的id
,然后再转换成String
存放到数据库中。这样一来,当用户登录的时候,我们只需要获取当前用户点赞过的评论,那么就可以将用户点赞过的评论的点赞图片效果换成已经点赞。
通过GreenDAO
生成的代码,我们可以发现他已经为我们生成了一个 SQL 语句
1 | db.execSQL("CREATE TABLE " + constraint + "\"COMMENT\" (" + // |
而不同的属性也生成好名字
1 | public final static Property Id = new Property(0, Long.class, "id", true, "_id"); |
再来看看评论的存储
1 | true) (autoincrement = |
除了存储一些基本信息外,还需要记录每条评论的点赞数。
读取通讯录
首先,再Manifest.xml
里面表明权限
1 | <uses-permission android:name="android.permission.READ_CONTACTS"/> |
通讯录权限是Android
中的危险权限,声明之后还必须要动态获取才能得到。
1 | if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED){ |
在Activity
里面重载onResquestPermissionsResult
判断用户是否以及授权
1 |
|
然后我们使用ContentResolver
来获取我们需要的数据,我们可以把它当作数据库一样使用,指定查询的内容和条件,就可以获得我们需要的数据。
1 | Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI; |
使用projection
限定查询的数据为手机号码,用selection
过滤查询的内容,选择我们需要的数据。然后执行query
获取数据。
1 | if (cursor != null && cursor.moveToFirst()) { |
然后判断cursor
里面是否存在数据,添加到我们的对话框中。
最后,项目的目录结构如下:
遇到的困难以及解决思路
这次的内容比上次多,期间遇到了不少问题,但是通过Google
查找资料都很快就可以解决。
比如动态权限获取问题,之前做 Android 应用的时候就遇到过这种问题,必须要显式向用户请求权限才能获取。
还有GreenDAO
的model
类,他的自增长 ID 必须是Long
类型的,而long
类型是没有效的,这个又是一个小坑。
还有读取通讯录的时候一开始照着tutorial
的内容做,但是发现并不能运行,仔细研究发现使用projection
参数更容易操作数据,因此换了种写法就可以了。
还有各种Image
to byte[]
的方法,也是通过stackoverflow找到解决方案