第5章Android数据存储与访问 5.1简 单 存 储 5.1.1SharedPreferences SharedPreferences是Android中最容易理解的数据存储技术,常用来存储一些轻量级的数据,采用keyvalue(键值对)的方式保存数据,类似于Web程序的Cookie,通常用来保存一些配置文件数据、用户名及密码等。 SharedPreferences不仅能够保存数据,还能实现不同应用程序间的数据共享,支持三种访问模式: 私有(MODE_PRIVATE)、全局读(MODE_WORLD_READABLE)、全局写(MODE_WORLD_WRITEABLE)。其中MODE_PRIVATE是默认模式,该模式下的配置文件只允许本程序和享有本程序ID的程序访问; MODE_WORLD_READABLE模式允许其他应用程序读文件; MODE_WORLD_WRITEABLE模式允许其他应用程序写文件。如果既要全局读又要全局写,可将访问模式设置为MODE_WORLD_READABLE +MODE_WORLD_WRITEABLE。 除了定义SharedPreferences的访问模式,还要定义SharedPreferences的名称,该名称是SharedPreferences在Android文件系统中保存的文件名称,一般声明为常量字符串,以方便在代码中多次使用,如: SharedPreferences sharedPreferences = getSharedPreferences("filename", MODE_PRIVATE); 其中,getSharedPreferences()为Android系统函数,通过它可获得SharedPreferences实例。 获取SharedPreferences实例后,通过SharedPreferences.Editor类对SharedPreferences实例进行修改,完成数据设置,最后调用commit()函数保存数据。SharedPreferences广泛支持各种基本数据类型,包括整型、布尔型、浮点型和长整型等,如: SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString("Name", "Tom"); editor.putFloat("Height", 1.78f); editor.commit(); 如果需要从已保存的SharedPreferences中读取数据,同样调用getSharedPreferences()函数,并在函数的第1个参数中指明需要访问的SharedPreferences名称,然后通过get()函数获取保存在SharedPreferences中的键值对,如: SharedPreferences mySdPferences = getSharedPreferences("filename", MODE_PRIVATE); String name = mySdPferences.getString("Name", "Default Name"); float height = mySdPferences.getFloat("Height", 1.70f); 其中,get()函数的第1个参数是键值对的键名,第2个参数是无法获取键值时的默认值。 Android移动网络程序设计案例教程(Android Studio版·第2版·微课视频版) 第 5 章Android数据存储与访问 Android系统为每个应用程序建立了与包同名的目录,用来保存应用程序产生的数据文件,包括普通文件、SharedPreferences文件和数据库文件等。SharedPreferences产生的文件就保存在/data/data//shared_prefs目录下。 5.1.2使用SharedPreferences存储用户登录信息 SharedPreferences使用方法比较简单,下面以一个例子来讲解SharedPreferences的用法。 视频讲解 图5.1SharedPreferencesDemo程序界面 【例51】演示使用SharedPreferences保存用户名和密码的方法。 程序SharedPreferencesDemo演示了如何使用SharedPreferences保存用户名和密码。用户输入用户名和密码后单击“保存”按钮,数据被保存在SharedPreferences文件中。以后每次程序重新启动后,会将保存的用户登录信息从SharedPreferences文件中读出并显示在输入框中,界面效果如图5.1所示。 该程序的MainActivity.java文件内容如下。 package edu.cqut.sharedpreferencesdemo; import android.os.Bundle; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; public class MainActivity extends Activity { SharedPreferences mySharedPreferences; Button saveButton; EditText editName,editPswrod; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); editName=(EditText)findViewById(R.id.editName); editPswrod=(EditText)findViewById(R.id.editPassword); saveButton=(Button)findViewById(R.id.button1); saveButton.setOnClickListener(new Button.OnClickListener() { @Override public void onClick(View v) { mySharedPreferences=getSharedPreferences("userInfo", Context.MODE_PRIVATE); SharedPreferences.Editor editor = mySharedPreferences.edit(); editor.putString("username", editName.getText().toString()); editor.putString("password", editPswrod.getText().toString()); editor.commit(); Toast.makeText(MainActivity.this, "写入Sharedpreferences成功!", Toast.LENGTH_LONG).show(); } }); mySharedPreferences=getSharedPreferences("userInfo", Context.MODE_PRIVATE); String usename =mySharedPreferences.getString("username", ""); String password =mySharedPreferences.getString("password", ""); editName.setText(usename); editPswrod.setText(password); } } 在本程序中,shared_prefs目录中生成了一个名为userInfo.xml的文件。可以使用Android Device Monitor查看文件的位置。虽然Android Studio 3.1以后Google公司放弃了Android Device Monitor,并将它从开发环境的菜单中取消了,但仍然可以通过Windows的资源文件管理器运行该设备监视器程序。为了运行Android Device Monitor,通过图1.19的Android SDK Manager界面找到Android SDK的位置,然后关闭Android Studio和Android模拟器,在Android SDK目录的tools文件夹下找到monitor.bat,双击该文件运行,则弹出如图5.2所示的Android Device Monitor界面,这时再打开Android Studio。Android 7.0以上的模拟器默认情况下Android Device Monitor无法查看data目录,这里使用Android 6.0模拟器,在Android Studio中运行程序后,在Android Device Monitor中切换到File Explorer页面,可以看到userInfo.xml保存在/data/data/edu.cqut.sharepreferencesdemo/shared_prefs目录下,文件大小为152字节,在Linux下的权限为rwrw。 图5.2userInfo.xml文件 Linux系统中文件权限分别描述了创建者、同组用户和其他用户对文件的操作限制。x表示可执行,r表示可读,w表示可写,d表示目录,表示普通文件,图5.2中的“edu.cqut.sharedpreferencesdemo”的权限为drwxrxx,表示目录、可被创建者读写及执行、被同组用户读及执行、其他用户只能执行; 由于设置mySharedPreferences实例的权限为MODE_PRIVATE,因此userInfo.xml的权限为仅创建者和同组用户具有读写文件的权限。 5.2文 件 存 储 5.2.1内部存储 除了使用SharedPreferences存取少量数据外,更多的是使用文件系统进行数据的存取。Android系统允许应用程序创建仅能够自身访问的私有文件,文件保存在设备的内部存储器上,位于系统的/data/data//files目录中。Android使用Linux的文件系统,支持标准Java的I/O类和方法,存取文件主要使用数据流方式。 流是一个可被顺序访问的数据序列,是对计算机输入数据和输出数据的抽象,有输入流和输出流之分,输入流用来读取数据,输出流则相反,用来写入数据。常用的文件字节数据流有如下两种。 (1) FileInputStream: 文件字节输入流。 (2) FileOutputStream: 文件字节输出流。 为了能使用字节流,可以使用openFileOutputStream()、openFileOutput()和openFileInput()等函数。openFileOutputStream()的语法格式为: public FileOutputStream openFileOutput(String name, int mode) 其中,第1个参数是文件名称,不可以包含描述路径的斜杠; 第2个参数是操作模式。Android系统支持4种文件操作模式,如表5.1所示。函数的返回值是FileOutputStream类型。 表5.1文件操作模式 模式说明 MODE_PRIVATE 私有模式,默认模式,文件仅能够被创建文件的程序或具有相同UID的程序访问 MODE_APPEND追加模式,如果文件已存在,则在文件的结尾添加新数据 MODE_WORLD_READABLE全局读模式,允许任何程序读取私有文件 MODE_WORLD_WRITEABLE全局写模式,允许任何程序写入私有文件 使用openFileOutput()函数输出数据示例代码如下。 FileOutputStream fos = openFileOutput("fileDemo.txt", MODE_PRIVATE); String text = "Some data"; fos.write(text.getBytes()); fos.flush(); fos.close(); 由于FileOutputStream是字节流,因此对于字符串数据,需要先将其转换为字节数组,再使用write()方法写入。如果写入的数据量较小,系统会把数据保存在数据缓冲区中,等数据量积累到一定程度后再一次性写入文件。因此,在调用close()函数关闭文件前,务必使用flush()函数将缓冲区内的所有数据写入文件,否则可能导致部分数据丢失。 openFileInput()函数语法格式为: public FileInputStream openFileInput(String name) 参数为文件名,同样不允许包含描述路径的斜杠。使用openFileInput()打开已有文件,并以二进制方式读取数据的示例代码如下。 FileInputStream fis = openFileInput("fileDemo.txt"); byte[] readBytes = new byte[fis.available()]; while (fis.read(readBytes) != -1){} 由于文件操作可能会遇到各种问题而导致操作失败,因此在代码中应使用try/catch捕获可能产生的异常。 5.2.2外部存储 Android外部存储设备一般指Micro SD卡,存储在SD卡上的文件称为外部文件。SD卡使用FAT(File Allocation Table)文件系统,不支持访问模式和权限控制。Android模拟器支持SD卡的模拟,可以设置模拟器中SD卡的容量,模拟器启动后会自动加载SD卡。正确加载SD卡后,SD卡中的目录和文件被映射到storage/emulated/D目录下。因为用户可以加载或者卸载SD卡,因此编程访问SD卡前要检测SD卡目录是否可用; 如果不可用,说明设备中SD卡已经被卸载; 如果可用,则直接使用标准的java.io.File类进行访问。 使用SD卡存取文件,需要在程序的AndroidManifest.xml中注册用户对SD卡的权限,分别是加载/卸载文件系统权限和向外部存储器写入数据的权限,如: 对SD卡进行读写操作可以用环境变量访问类Environment的下面两个方法。 (1) getExternalStorageState(): 获取当前存储设备的状态。 (2) getExternalStorageDirectory(): 获取SD卡的根目录。 示例代码如下。 if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { File path = Environment.getExternalStorageDirectory(); //获取SD卡目录路径 File sdfile = new File(path, "filename.txt");//指定文件filename.txt在SD卡中的位置 //读写操作 … } 上面代码中常量Environment.MEDIA_MOUNTED表示对SD卡具有读写权限。 使用Android API 23(Android 6.0)及以上版本的设备,在操作SD卡时不仅要在AndroidManifest.xml文件中设置SD卡的读取权限,还要在对SD卡有读写操作的地方授权才能访问SD卡。授权只需一次即可,只要不卸载应用程序,权限就仍然存在。下面代码展示了向用户申请SD卡写入授权以及响应的操作。 import android.Manifest; import android.app.Activity; import android.content.pm.PackageManager; import.androidx.core.app.ActivityCompat; public class PermisionUtils extends Activity { private static final int REQUEST_EXTERNAL_STORAGE = 1; //存储权限申请码 private static String[] PERMISSIONS_STORAGE = { //存储权限名 Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}; //检查App是否有权限写入设备存储器,如果没有,提示用户需要获得该授权 public static boolean verifyStoragePermissions(Activity activity) {//检查是否有写权限 int permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); if (permission != PackageManager.PERMISSION_GRANTED) { //App没有权限,提示用户给予该权限 ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE); return false; } return true; } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (requestCode == REQUEST_EXTERNAL_STORAGE) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED && permissions[0].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { ... //用户同意授权,做相应处理 } else { ... //用户拒绝授权,做相应处理 } } } } 在Activity页面需要授权写入SD卡的地方调用: verifyStoragePermissions(this); this指当前的Activity的上下文环境,程序运行该方法后会出现申请授权对话框,用户单击ALLOW后,程序获取SD卡读写权限。 Google对Android API 29(Android 10.0)版本的设备的外部存储做了新的要求,使用了作用域存储适配功能,对SD卡的使用做了较大限制。从Android 10开始,每个应用程序只能在自己的外置存储空间关联目录下读取和创建文件,使用context.getExternalFileDir()方法获取该关联目录。这个目录下的文件会被计入应用程序的占用空间中,同时也会随着应用程序的卸载而被删除。 目前Android 10系统对于作用域存储适配的要求还不是非常严格,如果不想进行作用域存储的适配,可以在AndroidManifest.xml中加入以下配置。 ... 下面以一个示例说明如何使用存储器存取文件。 视频讲解 【例52】演示使用输入/输出流存储文件。 程序FileStorageDemo使用FileOutputStream和FileInputStream存取用户编写的字符串,其运行界面如图5.3所示。由于需要访问SD卡,程序首次运行时会弹出如图5.3(a)所示的申请访问外部设备对话框,用户单击ALLOW后出现如图5.3(b)所示的运行界面,用户在编辑框中输入相应文字后单击相应按钮完成文字的保存和存储。 图5.3文件存储程序运行界面 import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import android.app.Activity; import android.os.Bundle; import android.os.Environment; import android.content.Context; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import android.Manifest; import android.content.pm.PackageManager; public class MainActivity extends AppCompatActivity { //存储权限 private static final int REQUEST_EXTERNAL_STORAGE = 1; private static String[] PERMISSIONS_STORAGE = { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}; EditText editText; //接收用户输入的字符串 Button btnSave,btnRead,btnSaveSD,btnReadSD; String fileName="test.txt"; //文件名称 String str; //要存取的字符串 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); editText=(EditText)findViewById(R.id.editText); btnSave=(Button)findViewById(R.id.btnSave); btnSave.setOnClickListener(new mClick()); btnRead=(Button)findViewById(R.id.btnRead); btnRead.setOnClickListener(new mClick()); btnSaveSD=(Button)findViewById(R.id.btnSaveSD); btnSaveSD.setOnClickListener(new mClick()); btnReadSD=(Button)findViewById(R.id.btnReadSD); btnReadSD.setOnClickListener(new mClick()); //确认SD卡权限是否已开启,如果没有开启,提示用户开启 verifyStoragePermissions(MainActivity.this); } class mClick implements Button.OnClickListener{ @Override public void onClick(View arg0) { if(arg0==btnSave) //存储文件到内部存储器 savefile(); else if(arg0==btnRead) //从内部存储器读取文件 readfile(fileName); else if(arg0==btnSaveSD)//存储文件到SD卡中 saveSDfile(); else if(arg0==btnReadSD)//从SD卡中读取文件 readSDfile(fileName); } } void savefile() {str = editText.getText().toString(); try{ FileOutputStream f_out = openFileOutput(fileName, Context.MODE_PRIVATE); f_out.write(str.getBytes()); f_out.flush(); f_out.close(); Toast.makeText(MainActivity.this,"文件保存成功", Toast.LENGTH_LONG).show(); } catch(FileNotFoundException e){ e.printStackTrace(); } catch(IOException e){ e.printStackTrace(); } } void readfile(String fileName) {byte[] buffer = new byte[1024]; FileInputStream in_file=null; try{ in_file = openFileInput(fileName); int bytes = in_file.read(buffer); str = new String (buffer,0,bytes); Toast.makeText(MainActivity.this, "文件内容:"+str, Toast.LENGTH_SHORT).show(); } catch (FileNotFoundException e) { java.lang.System.out.print("文件不存在!"); } catch (IOException e) { java.lang.System.out.print("IO流错误"); } } void saveSDfile() {str = editText.getText().toString(); Toast.makeText(MainActivity.this, "文件内容:"+str, Toast.LENGTH_LONG).show(); //判断SD卡是否允许读写操作 if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {File path = Environment.getExternalStorageDirectory(); //获取SD卡目录路径 String strPath = path.getAbsolutePath(); File sdfile = new File(strPath+"/"+ fileName); try{ Toast.makeText(MainActivity.this, sdfile.toString(), Toast.LENGTH_LONG).show(); FileOutputStream f_out = new FileOutputStream(sdfile); f_out.write(str.getBytes()); f_out.flush(); f_out.close(); Toast.makeText(MainActivity.this,"文件保存成功", Toast.LENGTH_LONG).show(); } catch (FileNotFoundException e){ Toast.makeText(MainActivity.this, "文件未找到异常:"+str, Toast.LENGTH_LONG).show(); } catch (Exception e) { Toast.makeText(MainActivity.this, "其他异常:"+str, Toast.LENGTH_LONG).show(); } } else Toast.makeText(MainActivity.this, "SD卡被禁止读写:"+str, Toast.LENGTH_LONG).show(); } void readSDfile(String fileName) { if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { File path = Environment.getExternalStorageDirectory(); //获取SD卡目录路径 File sdfile = new File(path, fileName); try{ FileInputStream in_file = new FileInputStream(sdfile); byte[] buffer = new byte[1024]; int bytes = in_file.read(buffer); str = new String(buffer,0,bytes); Toast.makeText(MainActivity.this, "文件内容:"+str, Toast.LENGTH_LONG).show(); } catch(FileNotFoundException e){ java.lang.System.out.print("文件不存在"); } catch (Exception e) { java.lang.System.out.print("IO流错误"); } } } //检查App是否有权限写入设备存储器,如果没有,给出提示获得该权限 public static void verifyStoragePermissions(Activity activity) {//检查是否有写权限 int permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); if (permission != PackageManager.PERMISSION_GRANTED) { //App没有权限,提示用户给予该权限 ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE); } } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (requestCode == REQUEST_EXTERNAL_STORAGE) { if (!(grantResults[0] == PackageManager.PERMISSION_GRANTED)) { Toast.makeText(MainActivity.this, "没有权限访问SD卡", Toast.LENGTH_LONG).show(); } } } } 为了保证程序正常运行,最后不要忘了在AndroidManifest.xml中注册用户对SD卡的权限。 5.2.3编写一个文件存储访问类 文件存储是很多程序经常用到的功能,这里编写一个文件存储访问类——DataFileAccess类,该类囊括了文件操作的常用功能,可以将它用于程序中,从而简化开发流程。 import android.content.Context; import android.content.res.Resources; import android.os.Environment; import android.os.StatFs; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; public class DataFileAccess { private Context mContext; private String mSDPath; //SD卡路径 DataFileAccess(Context cont) { mContext = cont; mSDPath = Environment.getExternalStorageDirectory().getPath() + "/"; } /**判断SD卡是否存在?是否可以进行读写*/ public boolean SDCardState() { if(Environment.getExternalStorageState().equals ( Environment.MEDIA_MOUNTED)) //表示SD卡存在并且可以读写 { return true; } else { return false; } } /**获取SD卡文件路径*/ public String SDCardPath() { if(SDCardState()){//如果SD卡存在并且可以读写 mSDPath = Environment.getExternalStorageDirectory().getPath(); return mSDPath; } else{ return null; } } /**获取SD卡可用容量大小(MB)*/ public long SDCardFree() { if(null!=SDCardPath()){ StatFs statfs = new StatFs(SDCardPath()); //获取SD卡的Block可用数 long availaBlocks = statfs.getAvailableBlocks(); //获取每个Block的大小 long blockSize = statfs.getBlockSize(); //计算SD卡可用容量大小MB long SDFreeSize = availaBlocks*blockSize/1024/1024; return SDFreeSize; } else{ return 0; } } /** * 在SD卡上创建目录及子目录 * @param dirName: 要创建的目录及子目录名(目录路径) * @return boolean: true表示创建成功, false表示创建失败 */ public boolean createSDDir(String dirName) { String [] strSubDir = dirName.split("/"); String strCurrentPath = mSDPath; for (int i=0; i 最后,为了兼容Android 6.0以上版本的设备,在MainActivity类中增加向用户申请SD卡读写权的代码。如果用户同意授权,则在onRequestPermissionsResult()方法中调用CopyDishImagesFromRawToSD()方法完成图像复制任务。 public class MainActivity extends AppCompatActivity { //存储权限 private static final int REQUEST_EXTERNAL_STORAGE = 1; private static String[] PERMISSIONS_STORAGE = { Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}; @Override protected void onCreate(Bundle savedInstanceState) { ... mAppInstance.g_orders = new ArrayList(); //创建订单列表 verifyStoragePermissions(MainActivity.this); ... } //检查App是否有权限写入设备存储器,如果没有给出提示获得该权限 public static boolean verifyStoragePermissions(Activity activity) { //检查是否有写权限 int permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); if (permission != PackageManager.PERMISSION_GRANTED) { //App没有权限,提示用户给予该权限 ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE); return false; } return true; } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (requestCode == REQUEST_EXTERNAL_STORAGE) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED && permissions[0].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) CopyDishImagesFromRawToSD(); //将raw文件夹中的菜品图像复制到SD卡中 } } } 5.3数据库存储 5.3.1SQLite简介 SQLite是一款轻型的关系数据库,是由D.RichardHipp发布的开源嵌入式数据库,支持跨平台,最大支持2048GB数据,可被所有主流编程语言支持,目前已经在很多嵌入式产品中使用。它占用资源非常低,在嵌入式设备中,可能只需要几百KB的内存就够了。 SQLite数据库管理工具很多,比较常用的有SQLite Expert Professional,其强大的功能几乎可以在可视化环境下完成所有的数据库操作。另外,Mozilla Firefox——火狐浏览器的免费插件SQLite Manager也支持SQLite的可视化操作,这两个软件的运行界面如图5.4所示。 SQLite的核心大约有3万行标准C代码,模块化的设计使这些代码非常易于理解。Android集成了SQLite数据库,每个Android应用程序都可以使用该数据库。对于熟悉SQL语言的开发人员来说,在Android中使用SQLite也很简单。 Android系统中,每个应用程序的SQLite数据库保存在各自的/data/data// databases目录中,默认情况下,所有数据库都是私有的,仅允许创建数据库的应用程序访问。 图5.4两款SQLite数据库可视化管理工具 图5.4(续) 5.3.2管理和操作SQLite数据库的对象 Android提供了一个名为SQLiteDatebase的类,该类封装了一些数据库的API,使用它可以对数据库进行添加(Create)、查询(Retrieve)、更新(Update)和删除(Delete)。表5.2列出了SQLiteDatebase类的常用方法。 表5.2SQLiteDatebase类常用方法 方法说明 openOrCreateDatabase(String path, SQLiteDatabase.CursorFactory factory)打开或创建数据库 openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags)打开指定的数据库 delete(String table, String whereClause, String[] whereArgs)删除一条记录 insert(String table,String nullColumnHack,ContentValues values)插入一条记录query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy)查询一条记录 update(String table, ContentValues values, String whereClause, String[] whereArgs)修改一条记录 execSQL(String sql)执行一条SQL语句 close()关闭数据库 除了SQLiteDatebase,还有一个类SQLiteOpenHelper,是SQLiteDatebase的辅助类,主要用于创建数据库,并对数据库的版本进行管理。该类是一个抽象类,使用时一般是定义一个类继承SQLiteOpenHelper,并实现两个回调方法: OnCreate(SQLiteDatabase db)和onUpgrade(SQLiteDatabse, int oldVersion, int newVersion)来创建和更新数据库。SQLiteOpenHelper的方法如表5.3所示。 表5.3SQLiteOpenHelper类的常用方法 方法说明 onCreate(SQLiteDatabase db)首次生成数据库时调用该方法 onOpen(SQLiteDatabase db)调用已经打开的数据库 onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)升级数据库时调用 getWritableDatabase()得到可写的数据库,返回SQLiteDatabase对象,然后通过对象进行写入数据库操作 getReadableDatabase()得到可读的数据库,返回SQLiteDatabase对象,然后通过对象进行数据库读取操作 close()关闭数据库,需要强调的是,在每次打开数据库后停止使用时调用,否则会造成数据泄露 5.3.3数据操作 数据操作包括3个层次,依次如下。  数据库操作: 建立或删除数据库。  数据表操作: 建立、修改及删除数据库中的数据表。  数据记录操作: 对数据表中的数据记录执行添加、删除、修改和查询等操作。 为了便于理解,我们通过一个例子来说明SQLite的数据操作。 图5.5SQLiteDemo运行效果 视频讲解 【例53】编写程序演示SQLite数据库操作。 建立SQLiteDemo的Android程序。在该程序中建立一个people数据库,该数据库中有一个peopleinfo的数据表,该表拥有4个字段,分别是ID(整型、主键)、姓名(字符串型)、年龄(整型)和身高(浮点型)。用户在程序中可以完成对数据库的常用操作,如图5.5所示。 为了实现以上功能,需要编写一个类DBAdapter完成数据库及表的建立、更新、删除操作,以及对表中记录的插入、更新、删除、查询操作。 首先定义一个People类如下。 public class People { public int ID = -1; public String Name; public int Age; public float Height; } 然后,定义一个数据库类如下。 import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteDatabase.CursorFactory; public class DBAdapter {private static final String DB_NAME = "people.db";//数据库名称 private static final String DB_TABLE = "peopleinfo";//数据表名称 private static final int DB_VERSION = 1;//数据库版本号 public static final String KEY_ID = "_id";//ID字段名称 public static final String NAME = "name";//姓名字段名称 public static final String AGE = "age";//年龄字段名称 public static final String HEIGHT = "height";//身高字段名称 private SQLiteDatabase db;//people数据库 private final Context context; } 1. 创建、更新及删除数据表 创建数据库及其数据表有多种方法,可以应用SQLiteDatabase对象的openDatabase()方法及openOrCreateDatabase()方法,也可以使用SQLiteHelper的子类创建数据库,该方法示例如下。 /** 静态Helper类,用于建立、更新和打开数据库*/ /*DBOpenHelper作为访问SQLite的助手类,提供两方面功能: (1)getReadableDatabase(),getWritableDatabase()可以获得SQLiteDatabase对象,通过该对象对数据库进行操作 (2)提供onCreate()和onUpgrade()两个回调函数,允许我们在创建和升级数据库时,进行自己的操作*/ private static class DBOpenHelper extends SQLiteOpenHelper { //在SQLiteOpenHelper的子类中必须有该构造函数 public DBOpenHelper(Context context, String name, CursorFactory factory, int version) { super(context, name, factory, version); } //创建数据表的SQL语句 private static final String DB_CREATE = "create table " + DB_TABLE //创建的数据表名称 + " (" + KEY_ID + " integer primary key autoincrement, " //ID号:整型主键字 //段,自动递增 + NAME+ " text not null, " //姓名:字符串字段,不能为空 + AGE+ " integer," //年龄:整型字段 + HEIGHT + " float);"; //身高:浮点型字段 @Override //该函数在第一次创建数据库时候执行,实际上是第一次得到SQLiteDatabase对象的时候,才 //会调用 public void onCreate(SQLiteDatabase _db) { _db.execSQL(DB_CREATE); } @Override //更新数据表 public void onUpgrade(SQLiteDatabase _db, int _oldVersion, int _newVersion) { _db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE); onCreate(_db); } /**创建数据表*/ public void create_table(SQLiteDatabase _db, String createTableSql) throws SQLiteException { _db.execSQL(createTableSql); } /**删除数据表*/ public void delete_table(SQLiteDatabase _db, String tableName) throws SQLiteException { _db.execSQL("DROP TABLE IF EXISTS " + tableName); } } 在DBAdapter类中添加DBOpenHelper类的成员变量及执行数据库创建、打开、关闭操作的函数。 public class DBAdapter { ... private DBOpenHelper dbOpenHelper; public DBAdapter(Context _context) { context = _context; } /**关闭数据库 */ public void close() { if (db != null){ db.close(); db = null; } } /** 创建及打开数据库 */ public void open() throws SQLiteException { //创建一个DatabaseHelper对象 dbOpenHelper = new DBOpenHelper(context, DB_NAME, null, DB_VERSION); //只有调用了DatabaseHelper对象的getReadableDatabase()或者getWritableDatabase() //方法才会调用DBOpenHelper的onCreate()方法 try { db = dbOpenHelper.getWritableDatabase(); }catch (SQLiteException ex) { db = dbOpenHelper.getReadableDatabase(); } } /**删除数据库*/ public void delete() throws SQLiteException { context.deleteDatabase(DB_NAME); } 2. 数据记录操作 数据表中的列称为字段,每一行称为记录。对数据表中的数据进行操作处理,主要是对其记录进行操作处理。 对数据记录处理有两种方法,一种是编写一条对记录进行增、删、改、查的SQL语句,通过execSQL()方法来执行。另一种是使用Android系统的SQLiteDatabase对象的相应方法进行操作。前者容易掌握,下面只介绍使用SQLiteDatabase对象操作数据记录的方法。 1) 增加记录 新增记录使用SQLiteDatabase对象的insert()方法实现,方法原型为: long insert(String table, String nullColumnHack, ContentValues values) 其参数含义如下。  table: 增加记录的数据表。  nullColumnHack: 空列的默认值,通常为null。  values: 为ContentValues对象,即键值对的字段名称,键名为表中字段名,键值为要增加的记录数据值。通过ContentValues对象的put()方法将数据存放到ContentValues对象中。  返回值: 返回插入记录所在行的行号,如果插入失败返回-1。 下面是DBAdapter类中的插入记录的方法。 public long insertRecord(People people) {//生成ContentValues对象 ContentValues newValues = new ContentValues(); //向该对象当中插入键值对,其中键是列名,值是希望插入到这一列的值,值必须和键的类型匹配 newValues.put(NAME, people.Name); newValues.put(AGE, people.Age); newValues.put(HEIGHT, people.Height); return db.insert(DB_TABLE, null, newValues); } 2) 修改记录 修改记录使用SQLiteDatabase对象的update()方法,方法原型为: int update(String table, ContentValues values, String whereClause, String[] whereArgs) 其参数含义如下。  table: 修改记录的数据表。  values: ContentValues对象,存放已修改数据的对象。  whereClause: 修改数据的条件,相当于SQL语句的where子句,null表示更新所有记录。  whereArgs: 修改数据值的数组,null表示更新整行。  返回值: 返回修改记录个数,修改失败返回0。 下面是使用update()函数修改记录的方法。 //相当于执行SQL语句中的update语句: update table_name SET XXCOL=XXX WHERE XXCOL=XX... public long updateOneRecord(long id , People people) {ContentValues updateValues = new ContentValues(); updateValues.put(NAME, people.Name); updateValues.put(AGE, people.Age); updateValues.put(HEIGHT, people.Height); return db.update(DB_TABLE, updateValues, KEY_ID + "=" + id, null); } 3) 删除记录 删除记录使用SQLiteDatabase对象的delete()方法,方法原型为: int delete(String table, String whereClause, String[] whereArgs) 其参数含义如下。  table: 删除记录的数据表。  whereClause: 删除数据的条件,相当于SQL语句的where子句,null表示删除所有记录。  whereArgs: 删除条件的数组。  返回值: 返回删除记录的个数,删除失败返回0。 下面是DBAdapter类中的删除记录的方法。 public long deleteAllRecords() {return db.delete(DB_TABLE, null, null); } public long deleteOneRecord(long id) {return db.delete(DB_TABLE, KEY_ID + "=" + id, null); } 4) 查询记录 查询记录使用SQLiteDatabase对象的query()方法,方法原型为: Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) 其参数含义如下。  table: 查询记录的数据表。  columns: 查询的字段,如为null表示所有字段。  selection: 查询条件,可以使用通配符“?”。  selectionArgs: 参数数组,用于替换查询条件中的“?”。  groupBy: 查询结果,按指定字段分组。  having: 限定分组的条件。  orderBy: 查询结果的排序条件。  返回值: 返回查询结果,如果没有找到返回null。 用query()方法查询的数据均封装在查询结果Cursor对象中,Cursor相当于SQL语句中的resultSet结果集上的一个游标,可以在结果集中向前、向后移动,并能够获取结果集的属性名称和序号,具体的方法如表5.4所示。 表5.4Cursor类的公有方法 方法说明 moveToFirst()将游标移动到结果集的第一行记录 moveToLast()将游标移动到结果集的最后一行记录 moveToNext()将游标移动到结果集的下一行记录 moveToPrevious()将游标移动到结果集的上一行记录 moveToPosition(int position)将游标移动到结果集的指定位置 int getCount()获得结果集的记录数 int getPosition()获得游标的当前位置 int getColumnIndexOrThrow(String columnName) 返回指定属性名称的序号,如果属性不存在,则产生异常 String getColumnName(int columnIndex)返回指定序号的属性名称 String\[\] getColumnNames()返回属性名称的字符串数组 int getColumnIndex(String columnName)返回指定属性名称的序号 下面是DBAdapter类中的查询记录的方法。 public People[] queryAllRecords() {Cursor results = db.query(DB_TABLE, new String[] { KEY_ID, NAME, AGE, HEIGHT}, null, null, null, null, null); return ConvertToPeople(results); } public People[] queryOneRecord(long id) {Cursor results = db.query(DB_TABLE, new String[] { KEY_ID, NAME, AGE, HEIGHT}, KEY_ID + "=" + id, null, null, null, null); return ConvertToPeople(results); } private People[] ConvertToPeople(Cursor cursor) {int resultCounts = cursor.getCount(); if (resultCounts == 0 || !cursor.moveToFirst()) return null; People[] peoples = new People[resultCounts]; for (int i = 0 ; i0?"成功":"失败"); labelView.setText(msg); } }; OnClickListener updateButtonListener = new OnClickListener() { @Override public void onClick(View v) { People people = new People(); people.Name = nameText.getText().toString(); people.Age = Integer.parseInt(ageText.getText().toString()); people.Height = Float.parseFloat(heightText.getText().toString()); long id = Integer.parseInt(idEntry.getText().toString()); long count = dbAdapter.updateOneRecord(id, people); if (count == 0 ){ labelView.setText("更新错误!"); } else { labelView.setText("更新成功,更新数据"+String.valueOf(count)+"条"); } } }; } 5.3.4用数据库管理“移动点餐系统”中的菜单 “移动点餐系统”中的SQLite菜单数据库名称为dishes.db,其中包含一个数据表dishinfo存储菜品信息,该表拥有4个字段,分别是_id(菜品ID,整型,主键)、name(菜名,字符串型)、imgname(菜品图片文件名,字符串型)、price(价格,浮点型)。注意,数据库中保存的不是菜品图片,而是菜品图片的文件名,图片仍然保存在SD卡中,访问图片时根据程序中设置的图片目录及数据库中的文件名进行查找。 为了在“移动点餐系统”中使用SQLite数据库存储菜单,采用以下步骤修改程序,粗体部分为增加或者修改内容。 (1) 在Dish类中增加一个成员变量保存图片文件的名称。 public class Dish { public int mId = -1; //菜品ID public String mName;//菜名 public int mImage; //菜品图片 public String mImageName;//菜品图片的文件名 public float mPrice;//价格 } (2) 修改主页面MainActivity类中的FillDishesList()方法,在输出的ArrayList列表的各Dish元素中增加存储菜品图片文件名的部分。 private ArrayList FillDishesList() { String imgPath = mDFA.SDCardPath() + "/" + mAppInstance.g_imgDishImgPath + "/"; ArrayList theDishesList = new ArrayList(); Dish theDish = new Dish(); //添加菜品 theDish.mId = 1001; theDish.mName = "宫保鸡丁"; theDish.mPrice = (float) 20.0; theDish.mImage = (R.raw.food01gongbaojiding); theDish.mImageName = imgPath + "food01gongbaojiding.jpg"; theDishesList.add(theDish); theDish = new Dish(); theDish.mId = 1002; theDish.mName = "椒盐玉米"; theDish.mPrice = (float) 24.0; theDish.mImage = (R.raw.food02jiaoyanyumi); theDish.mImageName = imgPath + "food02jiaoyanyumi.jpg"; theDishesList.add(theDish); theDish = new Dish(); theDish.mId = 1003; theDish.mName = "清蒸武昌鱼"; theDish.mPrice = (float) 48.0; theDish.mImage = (R.raw.food03qingzhengwuchangyu); theDish.mImageName = imgPath + "food03qingzhengwuchangyu.jpg"; theDishesList.add(theDish); theDish = new Dish(); theDish.mId = 1004; theDish.mName = "鱼香肉丝"; theDish.mPrice = (float) 20.0; theDish.mImage = (R.raw.food04yuxiangrousi); theDish.mImageName = imgPath + "food04yuxiangrousi.jpg"; theDishesList.add(theDish); return theDishesList; } (3) 将前面的数据库管理类DBAdapter添加到本程序中,根据dishes.db内容对该类进行修改,使之适合菜品数据库。 public class DBAdapter { private static final String DB_NAME = "dishes.db"; private static final String DB_TABLE = "dishinfo"; private static final int DB_VERSION = 1; public static final String KEY_ID = "_id"; public static final String KEY_NAME = "name"; public static final String KEY_IMGNAME = "imgname"; public static final String KEY_PRICE = "price"; private SQLiteDatabase db; private final Context context; private DBOpenHelper dbOpenHelper; public DBAdapter(Context _context) { context = _context; } /** Close the database */ public void close() {if (db != null) {db.close(); db = null; } } /** Open the database */ public void open() throws SQLiteException {dbOpenHelper = new DBOpenHelper(context, DB_NAME, null, DB_VERSION); try { db = dbOpenHelper.getWritableDatabase(); }catch (SQLiteException ex) { db = dbOpenHelper.getReadableDatabase(); } } public long insert(Dish dish) { ContentValues newValues = new ContentValues(); newValues.put(KEY_ID, dish.mId); newValues.put(KEY_NAME, dish.mName); newValues.put(KEY_IMGNAME, dish.mImageName); newValues.put(KEY_PRICE, dish.mPrice); return db.insert(DB_TABLE, null, newValues); } public ArrayList queryAllData() {Cursor results = db.query(DB_TABLE, new String[] { KEY_ID, KEY_NAME, KEY_IMGNAME, KEY_PRICE}, null, null, null, null, null); return ConvertToDishes(results); } public Dish queryOneData(long id) {Cursor results = db.query(DB_TABLE, new String[] { KEY_ID, KEY_NAME, KEY_IMGNAME, KEY_PRICE}, KEY_ID+"="+id, null, null, null, null); return ConertToDish(results); } private Dish ConertToDish(Cursor cursor) { int resultCounts = cursor.getCount(); if (resultCounts == 0 || !cursor.moveToFirst()){ return null; } Dish theDish = new Dish(); theDish.mId = cursor.getInt(0); theDish.mName = cursor.getString(cursor.getColumnIndex(KEY_NAME)); theDish.mImageName = cursor.getString(cursor.getColumnIndex(KEY_IMGNAME)); theDish.mPrice = cursor.getFloat(cursor.getColumnIndex(KEY_PRICE)); return theDish; } private ArrayList ConvertToDishes(Cursor cursor) {int resultCounts = cursor.getCount(); if (resultCounts == 0 || !cursor.moveToFirst()){ return null; } ArrayList dishes = new ArrayList(); for (int i = 0 ; i dishes) { int s = dishes.size();//得到列表元素个数 for (int i=0; i>格式的数据 public ArrayList> getDishData() { //将菜品从数据库中填充进ArrayList列表 ArrayList dishes = queryAllData(); ArrayList> fooddata=new ArrayList>(); //再将菜品从ArrayList中填充进foodinfo列表 int s = dishes.size();//得到菜品数量 for (int i=0; i map = new HashMap(); map.put("dishid", theDish.mId); map.put("image", theDish.mImageName); map.put("title", theDish.mName); map.put("price", theDish.mPrice); fooddata.add(map); } return fooddata; } } 然后在MyApplication类中添加一个数据库管理的成员变量。 public class MyApplication extends Application//该类用于保存全局变量 { … public DBAdapter g_dbAdepter = null;//数据库辅助对象 } (4) 在MainActivity类的onCreate()方法中添加将菜单保存进dishes.db数据库中的代码。为了便于和前面的代码衔接,这里没有采用直接将菜品信息填充进数据库的方法,而是仍沿袭以前代码将菜品保存在ArrayList列表中,然后再由ArrayList列表填充进数据库中。 @Override protected void onCreate(Bundle savedInstanceState) { … mAppInstance.g_orders = new ArrayList(); //创建订单列表 CopyDishImagesFromRawToSD();//将raw文件夹中的菜品图像复制到 //SD卡的指定文件夹中 mAppInstance.g_dbAdepter = new DBAdapter(this); mAppInstance.g_dbAdepter.open(); mAppInstance.g_dbAdepter.deleteAllData(); //清除原有菜品数据 ArrayList dishes = FillDishesList();//将菜品列表保存在内存dishes表中 //将菜品从dishes表中填充进数据库 mAppInstance.g_dbAdepter.FillDishTable(dishes); } (5) 修改程序的CaipinActivity类的onCreate()方法,让菜单列表的加载、菜品的查询等操作都在菜品数据库中进行。 protected void onCreate(Bundle savedInstanceState) { ... final MyApplication appInstance = (MyApplication)getApplication(); mfoodinfo = appInstance.g_dbAdepter.getDishData(); ... //设置ListView选项单击监听器 this.mlistview.setOnItemClickListener(new OnItemClickListener(){ @Override public void onItemClick(AdapterView arg0, //选项所属的ListView View arg1, //被选中的控件,即ListView中被选中的子项 int arg2, //被选中子项在ListView中的位置 long arg3) //被选中子项的行号 { ListView templist = (ListView)arg0; View mView = templist.getChildAt(arg2); //选中子项(即item)在ListView中的位置 final TextView tvId = (TextView)mView.findViewById(R.id.dishid); final TextView tvTitle = (TextView)mView.findViewById(R.id.title); ... //对话框销毁时的响应事件 orderDlg.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { if (orderDlg.mBtnClicked == OrderOneDialog.ButtonID.BUTTON_OK) { //将菜品加入购物车中 long dishId = Long.parseLong(tvId.getText().toString()); Dish newDish = appInstance.g_dbAdepter.queryOneData(dishId); appInstance.g_cart.AddOneOrderItem(newDish, orderDlg.mNum); ... } } }); } }); } (6) 修改OrderedActivity类的onCreate()方法中的this.mlistview.setOnItemClickListener()方法中的代码,在其onItemClick()方法中找到orderDlg.setOnDismissListener()方法,修改其onDismiss()方法如下。 @Override public void onDismiss(DialogInterface dialog) { if (orderDlg.mBtnClicked == OrderOneDialog.ButtonID.BUTTON_OK) { //修改购物车中的已点菜品 MyApplication appInstance = (MyApplication)getApplication(); String dishName = tvTitle.getText().toString(); if (orderDlg.mNum <= 0) //如果该菜品数量为0,则将该菜品从已点菜单中删除 appInstance.g_cart.DeleteOneOrderItem(dishName); else //修改购物车中该菜品的数量 appInstance.g_cart.ModifyOneOrderItem(dishName, orderDlg.mNum); Toast.makeText(OrderedActivity.this, tvTitle.getText().toString()+":"+orderDlg.mNum,Toast.LENGTH_LONG).show(); //将购物车中已点菜品列表(重新)填充进mlistItemAdapter适配器中,更新显示列表,再次 //计算价格 UpdateOrderList(); } }