免费推广自己的网站,还是网站好,培训机构网站建设要求,猎头公司排名前十原文#xff1a;Android Recipes: A Problem-Solution Approach 协议#xff1a;CC BY-NC-SA 4.0 五、持久化数据
即使在将尽可能多的用户数据转移到云中的宏伟架构中#xff0c;移动应用的短暂性也总是要求至少一些用户数据在设备上本地持久存储。这些数据可能包括来自保证… 原文Android Recipes: A Problem-Solution Approach 协议CC BY-NC-SA 4.0 五、持久化数据
即使在将尽可能多的用户数据转移到云中的宏伟架构中移动应用的短暂性也总是要求至少一些用户数据在设备上本地持久存储。这些数据可能包括来自保证离线访问的 web 服务的缓存响应也可能包括用户为特定应用行为设置的首选项。Android 提供了一系列有用的框架来消除使用文件和数据库来保存信息的痛苦。
5–1。制作首选项屏幕
问题
您需要创建一种简单的方法来存储、更改和显示应用中的用户首选项和设置。
解决办法
(API 一级)
使用PreferenceActivity和 XML Preference层次结构一次性提供用户界面、键/值组合和持久性。使用这种方法将创建一个与 Android 设备上的设置应用一致的用户界面保持用户的体验与他们的期望一致。
在 XML 中可以定义一个或多个屏幕的完整集合显示相关的设置并使用PreferenceScreen、PreferenceCategory和相关的Preference元素对其进行分类。然后活动可以使用很少的代码为用户加载这个层次结构。
它是如何工作的
清单 5–1 和 5–2 提供了一个 Android 应用的基本设置示例。XML 定义了两个屏幕其中包含这个框架支持的所有常见首选项类型。请注意一个屏幕嵌套在另一个屏幕中当用户从根屏幕点击相关列表项时将显示内部屏幕。
清单 5–1。??【RES/XML/settings . XML】
?xml version1.0 encodingutf-8? PreferenceScreen xmlns:androidhttp://schemas.android.com/apk/res/android EditTextPreference android:keynamePref android:titleName android:summaryTell Us Your Name android:defaultValueApress / CheckBoxPreference android:keymorePref android:titleEnable More Settings android:defaultValuefalse / PreferenceScreen android:keymoreScreen android:titleMore Settings android:dependencymorePref ListPreference android:keycolorPref android:titleFavorite Color android:summaryChoose your favorite color android:entriesarray/color_names android:entryValuesarray/color_values android:defaultValueGRN / PreferenceCategory android:titleLocation Settings CheckBoxPreference android:keygpsPref android:titleUse GPS Location android:summaryUse GPS to Find You android:defaultValuetrue / CheckBoxPreference android:keynetworkPref android:titleUse Network Location android:summaryUse Network to Find You android:defaultValuetrue / /PreferenceCategory /PreferenceScreen /PreferenceScreen
**清单 5–2。**RES/values/arrays . XML
?xml version1.0 encodingutf-8? resources string-array namecolor_names itemBlack/item itemRed/item itemGreen/item /string-array string-array namecolor_values itemBLK/item itemRED/item itemGRN/item /string-array /resources
首先注意用于创建 XML 文件的约定。虽然这个资源可以从任何目录(比如 res/layout)展开但是惯例是将它们放在项目的一个通用目录中这个目录的名称简单地为“xml”
另外请注意我们为每个Preference对象提供了一个android:key属性而不是android:id。当每个存储的值通过一个SharedPreferences对象在应用的其他地方被引用时将使用键来访问它。此外PreferenceActivity还包含了findPreference()方法用于获取对 Java 代码中一个膨胀的Preference的引用这比使用findViewById();更高效而且findPreference()也将键作为参数。
展开后根首选项屏幕显示一个列表其中包含以下三个选项(按顺序排列):
标题为“名称”的项目 EditTextPreference的实例存储一个字符串值。点击此项将显示一个文本框供用户键入新的首选项值。 标题为“启用更多设置”的项目旁边有一个复选框 CheckBoxPreference 的实例它存储一个布尔值。点击此项将切换复选框的选中状态。 标题为“更多设置”的项目 点击此项将加载另一个包含更多项目的首选项屏幕。
当用户点击“更多设置”项目时第二个屏幕显示三个以上的项目:一个ListPreference项目和另外两个由PreferenceCategory组合在一起的CheckBoxPreference。PreferenceCategory是一种在列表中创建分节符和标题的简单方法用于对实际的首选项进行分组。
ListPreference是示例中使用的最终首选项类型。这个项目需要两个数组参数(尽管它们可以被设置为同一个数组)这两个参数代表用户可以从中选择的一组选项。android:entries数组是要显示的可读项目列表而android:entryValues数组表示要存储的实际值。
所有偏好项也可以选择性地为它们设置默认值。但是该值不会自动加载。当显示PreferenceActivity或调用PreferenceManager.setDefaultValues()时它将第一次加载这个 XML 文件。
现在让我们看看PreferenceActivity将如何加载和管理它。参见清单 5–3。
清单 5–3。 偏好行动中的活动
public class SettingsActivity extends PreferenceActivity {
Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Load preference data from XML addPreferencesFromResource(R.xml.settings); } }
向用户显示首选项并允许他们进行更改所需要的只是调用addPreferencesFromResource()。不需要用PreferenceActivity来调用setContentView()因为addPreferencesFromResource()会放大 XML 并显示它。然而可以提供一个自定义布局只要它包含一个设置了android:idandroid:id/list属性的ListView这是PreferenceActivity将加载首选项的地方。
出于控制访问的唯一目的也可以将偏好项放在列表中。在本例中我们将“启用更多设置”项放在列表中只是为了允许用户启用或禁用对第二个PreferenceScreen的访问。为了实现这一点我们的嵌套PreferenceScreen包含了android:dependency属性该属性将其启用状态链接到另一个首选项的状态。每当引用的首选项未设置或为假时该首选项将被禁用。
当这个活动加载时您会看到类似于 Figure 5–1 的内容。 图 5–1。 首选屏幕在行动
根PreferenceScreen(左)首先显示。如果用户点击“更多设置”将显示第二个屏幕(右侧)。
加载默认值和访问首选项
通常像这样的PreferenceActivity不是应用的根。通常如果设置了默认值在用户访问设置之前应用的其余部分可能需要访问这些值(第一种情况下将加载默认值)。因此在应用中的其他地方调用下面的方法会很有帮助这样可以确保在使用默认值之前将其加载。
PreferenceManager.setDefaultValues(Context context, int resId, boolean readAgain);
这个方法可能会被调用多次默认值将不会被再次加载。它可以放在主活动中以便在第一次启动时调用或者放在一个公共位置在访问共享首选项之前调用。
使用这种机制存储的首选项被放入默认的共享首选项对象中可以使用任何Context指针访问该对象
PreferenceManager.getDefaultSharedPreferences(Context context);
一个示例活动将加载我们上一个示例中设置的默认值并访问一些存储的当前值看起来类似于清单 5–4。
清单 5–4。 活动加载偏好默认值
public class HomeActivity extends Activity {
Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);
//Load the preference defaults PreferenceManager.setDefaultValues(this, R.xml.settings, false); }
Override public void onResume() { super.onResume(); //Access the current settings SharedPreferences settings PreferenceManager.getDefaultSharedPreferences(this);
String name settings.getString(“namePref”, “”); boolean isMoreEnabled settings.getBoolean(“morePref”, false); } }
调用setDefaultValues()将在首选项存储中为 XML 文件中包含android:defaultValue属性的任何项目创建一个值。这将使应用可以访问它们即使用户尚未访问设置屏幕。
然后可以使用一组类型化的访问器函数在SharedPreferences对象上访问这些值。如果 preference key 的值尚不存在这些访问器方法中的每一个都需要返回 preference key 的名称和默认值。
5–2。持久化简单数据
问题
您的应用需要一种简单、低开销的方法来将基本数据(如数字和字符串)存储在持久存储中。
解决办法
(API 一级)
使用SharedPreferences对象应用可以快速创建一个或多个持久存储数据可以保存在这些存储中供以后检索。在底层这些对象实际上作为 XML 文件存储在应用的用户数据区。然而与直接从文件中读写数据不同SharedPreferences为持久化基本数据类型提供了一个有效的框架。
创建多个 SharedPreferences(而不是将所有数据都转储到默认对象中)可能是一个好习惯尤其是当您存储的数据有保质期时。请记住使用 XML 和PreferenceActivity框架存储的所有首选项也存储在默认位置——如果您想存储一组与登录用户相关的项目该怎么办当该用户注销时您将需要删除随之而来的所有持久化数据。如果您将所有数据存储在默认首选项中您很可能需要单独删除每个项目。但是如果您只是为这些设置创建一个首选项对象那么注销就像调用SharedPreferences.clear()一样简单。
它是如何工作的
让我们看一个使用SharedPreferences持久化简单数据的实际例子。清单 5–5 和 5–6 为用户创建一个数据输入表单向远程服务器发送一条简单的消息。为了帮助用户我们将记住他们在每个字段中输入的所有数据直到发出成功的请求。这将允许用户离开屏幕(或被短信或电话打断)而不必再次输入他们的所有信息。
清单 5–5。 res/layout/form.xml
?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:orientationvertical android:layout_widthfill_parent android:layout_heightfill_parent TextView android:layout_widthfill_parent android:layout_heightwrap_content android:textEmail: android:padding5dip / EditText android:idid/email android:layout_widthfill_parent android:layout_heightwrap_content android:singleLinetrue / CheckBox android:idid/age android:layout_widthfill_parent android:layout_heightwrap_content android:textAre You Over 18? / TextView android:layout_widthfill_parent android:layout_heightwrap_content android:textMessage: android:padding5dip / EditText android:idid/message android:layout_widthfill_parent android:layout_heightwrap_content android:minLines3 android:maxLines3 / Button android:idid/submit android:layout_widthfill_parent android:layout_heightwrap_content android:textSubmit / /LinearLayout
清单 5–6。 有持久性的录入表单
public class FormActivity extends Activity implements View.OnClickListener {
EditText email, message; CheckBox age; Button submit;
SharedPreferences formStore;
boolean submitSuccess false;
Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.form);
email (EditText)findViewById(R.id.email); message (EditText)findViewById(R.id.message); age (CheckBox)findViewById(R.id.age);
submit (Button)findViewById(R.id.submit); submit.setOnClickListener(this);
//Retrieve or create the preferences object formStore getPreferences(Activity.MODE_PRIVATE); }
Override public void onResume() { super.onResume(); //Restore the form data email.setText(formStore.getString(“email”, “”)); message.setText(formStore.getString(“message”, “”)); age.setChecked(formStore.getBoolean(“age”, false)); }
Override public void onPause() { super.onPause(); if(submitSuccess) { //Editor calls can be chained together formStore.edit().clear().commit(); } else { //Store the form data SharedPreferences.Editor editor formStore.edit(); editor.putString(“email”, email.getText().toString()); editor.putString(“message”, message.getText().toString()); editor.putBoolean(“age”, age.isChecked()); editor.commit(); } }
Override public void onClick(View v) {
//DO SOME WORK SUBMITTING A MESSAGE
//Mark the operation successful submitSuccess true; //Close finish(); } }
我们从一个典型的用户表单开始两个简单的EditText输入字段和一个CheckBox。当创建活动时我们使用Activity.getPreferences()收集一个SharedPreferences对象这是所有持久化数据将被存储的地方。如果在任何时候活动由于除成功提交之外的原因(由布尔成员控制)而暂停表单的当前状态将被快速加载到首选项中并持久化。
**注意:**当使用Editor将数据保存到SharedPreferences中时务必记住在更改完成后调用commit()或apply()。否则您的更改将不会被保存。
相反每当活动变得可见时onResume()用存储在 preferences 对象中的最新信息加载用户界面。如果不存在首选项因为它们已被清除或从未创建(第一次启动)则表单被设置为空白。
当用户按下 Submit 并且假表单成功提交时随后对onPause()的调用将清除 preferences 中任何存储的表单数据。因为所有这些操作都是在私有首选项对象上完成的所以清除数据不会影响可能已使用其他方式存储的任何用户设置。
注意:Editor调用的方法总是返回同一个Editor对象允许它们在某些地方链接在一起这样做可以让你的代码更具可读性。
共享共享的首选项
前面的例子说明了在单个活动的上下文中使用单个SharedPreferences对象该活动具有从Activity.getPreferences()获得的对象。说实话这个方法实际上只是一个方便的Context.getSharedPreferences()包装器它将活动名作为首选商店名传递。如果您存储的数据最好在两个或多个 Activity 实例之间共享那么调用getSharedPreferences()并传递一个更通用的名称可能更有意义这样就可以很容易地从代码中的不同位置访问它。参见清单 5–7。
清单 5–7。 使用相同偏好的两个活动
public class ActivityOne extends Activity { public static final String PREF_NAME “myPreferences”; private SharedPreferences mPreferences;
Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPreferences getSharedPreferences(PREF_NAME, Activity.MODE_PRIVATE); } }
public class ActivityTwo extends Activity {
private SharedPreferences mPreferences;
Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPreferences getSharedPreferences(ActivityOne.PREF_NAME, Activity.MODE_PRIVATE); }
}
在这个例子中两个活动类使用相同的名称(定义为常量字符串)检索SharedPreferences对象因此它们将访问相同的偏好数据集。此外两个引用甚至指向首选项的同一个实例因为框架为每组SharedPreferences(一组由其名称定义)创建了一个单例对象。这意味着在一方所做的更改会立即反映到另一方。
A Note About Mode
Context.getSharedPreferences()也需要一个模式参数。传递 0 或MODE_PRIVATE提供了默认行为只允许创建首选项的应用(或另一个具有相同用户 ID 的应用)获得读写访问权。此方法支持两个以上的模式参数MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE。这些模式允许其他应用通过对其创建的文件设置适当的用户权限来访问这些首选项。但是外部应用仍然需要一个有效的上下文指向创建首选项文件的包。
例如假设您用包com.examples.myfirstapplication在应用中创建了SharedPreferences with world readable permission。为了从第二个应用访问这些首选项第二个应用将使用以下代码获取它们:
Context otherContext createPackageContext(com.examples.myfirstapplication, 0); SharedPreferences externalPreferences otherContext.getSharedPreferences(PREF_NAME, 0);
**注意:**如果您选择使用 mode 参数来允许外部访问请确保您在调用getSharedPreferences()时提供的模式是一致的。该模式仅在第一次创建首选项文件时使用因此在不同时间使用不同的模式参数调用SharedPreferences只会导致混淆。
5–3 岁。读写文件
问题
您的应用需要从外部文件读入数据或者写出更复杂的数据以实现持久性。
解决办法
(API 一级)
有时使用文件系统是无可替代的。Android 支持所有用于创建、读取、更新和删除(CRUD)操作的标准 Java 文件 I/O以及一些额外的助手使访问特定位置的文件更加方便。应用可以在三个主要位置处理文件:
内存储器 受保护的目录空间来读写文件数据。 外部存储器 用于读写文件数据的外部可安装空间。需要 API 级别 4中的WRITE_EXTERNAL_STORAGE权限。通常这是设备中的物理 SD 卡。 素材 APK 包中受保护的只读空间。对不能/不应该编译的本地资源有好处。
虽然处理文件数据的底层机制保持不变但我们将研究使处理每个目的地略有不同的细节。
它是如何工作的
如前所述传统的 Java FileInputStream和FileOutputStream类构成了访问文件数据的主要方法。事实上您可以随时使用绝对路径位置创建一个File实例并开始传输数据。然而由于不同设备上的根路径不同并且某些目录受到应用的保护我们推荐一些稍微更有效的方法来处理文件。
内部存储
为了创建或修改文件在内存中的位置请使用Context.openFileInput()和Context.openFileOutput()方法。这些方法只需要文件名作为参数而不是整个路径并且将引用与应用的受保护目录空间相关的文件而不考虑特定设备上的确切路径。参见清单 5–8。
清单 5–8。 在内存上 CRUD 一个文件
public class InternalActivity extends Activity {
private static final String FILENAME “data.txt”;
Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView tv new TextView(this); setContentView(tv);
//Create a new file and write some data try { FileOutputStream mOutput openFileOutput(FILENAME, Activity.MODE_PRIVATE); String data “THIS DATA WRITTEN TO A FILE”; mOutput.write(data.getBytes()); mOutput.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
//Read the created file and display to the screen try { FileInputStream mInput openFileInput(FILENAME); byte[] data newbyte[128]; mInput.read(data); mInput.close();
String display new String(data); tv.setText(display.trim()); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
//Delete the created file deleteFile(FILENAME); } }
这个例子使用Context.openFileOutput()将一些简单的字符串数据写到一个文件中。使用此方法时如果文件不存在将会创建该文件。它有两个参数一个文件名和一个操作模式。在这种情况下我们通过将模式定义为MODE_PRIVATE来使用默认操作。这种模式会用每个新的写操作覆盖文件如果你喜欢每次写在现有文件的末尾使用MODE_APPEND。
写操作完成后该示例使用Context.openFileInput()打开 InputStream 并读取文件数据它只需要再次将文件名作为参数。数据被读入一个字节数组并通过 TextView 显示给用户界面。完成操作后Context.deleteFile()用于从存储器中删除文件。
**注意:**数据以字节的形式写入文件流因此更高级别的数据(甚至字符串)必须转换成这种格式或从这种格式转换出来。
这个例子没有留下文件的痕迹但是我们鼓励你尝试同样的例子不要在最后运行deleteFile()来保存文件。将 DDMS 与仿真器或解锁的设备一起使用您可以查看文件系统并可以在相应的应用数据文件夹中找到该应用创建的文件。
因为这些方法是Context的一部分并不绑定到 Activity所以这种类型的文件访问可以发生在应用中你需要的任何地方比如一个BroadcastReceiver或者甚至一个定制类。许多系统构造要么是Context的子类要么在它们的回调中传递对它的引用。这允许在任何地方进行相同的打开/关闭/删除操作。
外部存储
内部存储和外部存储的主要区别在于外部存储是可装载的。这意味着用户可以将他们的设备连接到计算机并可以选择将外部存储作为可移动磁盘安装在 PC 上。通常存储本身是物理可移动的(如 SD 卡)但这不是平台的要求。
**重要提示:**写入设备的外部存储将需要您向应用清单添加一个android.permission.WRITE_EXTERNAL_STORAGE声明。
在外部安装或物理移除设备的外部存储器期间应用无法访问该存储器。因此通过检查Environment.getExternalStorageState()来检查外部存储器是否准备好总是谨慎的。
让我们修改文件示例对设备的外部存储进行同样的操作。参见清单 5–9。
清单 5–9。 在外部存储器上创建一个文件
public class ExternalActivity extends Activity {
private static final String FILENAME “data.txt”;
Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView tv new TextView(this); setContentView(tv);
//Create the file reference File dataFile new File(Environment.getExternalStorageDirectory(), FILENAME);
//Check if external storage is usable if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { Toast.makeText(this, “Cannot use storage.”, Toast.LENGTH_SHORT).show(); finish(); return; }
//Create a new file and write some data try { FileOutputStream mOutput new FileOutputStream(dataFile, false); String data “THIS DATA WRITTEN TO A FILE”; mOutput.write(data.getBytes()); mOutput.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
//Read the created file and display to the screen try { FileInputStream mInput new FileInputStream(dataFile); byte[] data newbyte[128]; mInput.read(data); mInput.close();
String display new String(data); tv.setText(display.trim()); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
//Delete the created file dataFile.delete(); } }
对于外部存储我们利用了更多的传统 Java 文件 I/O。使用外部存储的关键是调用Environment.getExternalStorageDirectory()来检索设备外部存储位置的根路径。
在进行任何操作之前首先用Environment.getExternalStorageState()检查设备外部存储器的状态。如果返回值是除了Environment.MEDIA_MOUNTED之外的任何值我们将不会继续因为存储不能被写入所以活动被关闭。否则可以创建新文件并开始操作。
输入和输出流现在必须使用默认的 Java 构造函数而不是使用Context方便的方法。输出流的默认行为是覆盖当前文件如果当前文件不存在则创建它。如果您的应用每次写入都必须追加到现有文件的末尾请将FileOutputStream构造函数中的布尔参数更改为 true。
通常在外部存储器上为应用文件创建一个特殊的目录是有意义的。我们可以简单地使用更多的 Java 文件 API 来实现这一点。参见清单 5–10。
清单 5–10。 在新目录下 CRUD 一个文件
public class ExternalActivity extends Activity {
private static final String FILENAME “data.txt”; private static final String DNAME “myfiles”;
Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView tv new TextView(this); setContentView(tv);
//Create a new directory on external storage File rootPath new File(Environment.getExternalStorageDirectory(), DNAME); if(!rootPath.exists()) { rootPath.mkdirs(); } //Create the file reference File dataFile new File(rootPath, FILENAME);
//Create a new file and write some data try { FileOutputStream mOutput new FileOutputStream(dataFile, false); String data “THIS DATA WRITTEN TO A FILE”; mOutput.write(data.getBytes()); mOutput.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
//Read the created file and display to the screen try { FileInputStream mInput new FileInputStream(dataFile); byte[] data newbyte[128]; mInput.read(data); mInput.close();
String display new String(data); tv.setText(display.trim()); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
//Delete the created file dataFile.delete(); } }
在本例中我们在外部存储目录中创建新的目录路径并将该新位置用作数据文件的根位置。一旦使用新的目录位置创建了文件引用示例的其余部分是相同的。
5–4 岁。将文件用作资源
问题
您的应用必须利用 Android 无法编译成资源 id 的格式的资源文件。
解决方案
(API 一级)
使用 Assets 目录存放应用需要读取的文件例如本地 HTML、CSV 或专有数据。素材目录是 Android 应用中文件的受保护资源位置。放置在此目录中的文件将与最终的 APK 捆绑在一起但不会被处理或编译。像所有其他应用资源一样素材中的文件是只读的。
它是如何工作的
我们在本书中已经看到了一些具体的例子可以使用素材将内容直接加载到小部件中比如WebView和MediaPlayer。然而在大多数情况下最好使用传统的InputStream来访问素材。清单 5–11 和 5–12 提供了一个从素材中读取私有逗号分隔值(CSV)文件并显示在屏幕上的示例。
清单 5–11。 素材/数据. csv
John,38,Red Sally,42,Blue Rudy,31,Yellow
清单 5–12。 从素材文件中读取
public class AssetActivity extends Activity {
Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView tv new TextView(this); setContentView(tv);
try { //Access application assets AssetManager manager getAssets(); //Open our data file InputStream mInput manager.open(“data.csv”); //Read data in byte[] data newbyte[128]; mInput.read(data); mInput.close();
//Parse the CSV data and display String raw new String(data); ArrayList cooked parse(raw.trim()); StringBuilder builder new StringBuilder(); for(Person piece : cooked) { builder.append(String.format(“%s is %s years old, and likes the color %s”, piece.name, piece.age, piece.color)); builder.append(‘\n’); } tv.setText(builder.toString()); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
}
/* Simple CSV Parser */ private static finalintCOL_NAME 0; private static finalintCOL_AGE 1; private static finalintCOL_COLOR 2;
private ArrayList parse(String raw) { ArrayList results new ArrayList(); Person current null;
StringTokenizer st new StringTokenizer(raw,“,\n”); int state COL_NAME; while(st.hasMoreTokens()) { switch(state) { case COL_NAME: current new Person(); current.name st.nextToken(); state COL_AGE; break; case COL_AGE: current.age st.nextToken(); state COL_COLOR; break; case COL_COLOR: current.color st.nextToken(); results.add(current); state COL_NAME; break; } }
return results; }
privateclass Person { public String name; public String age; public String color;
public Person() { } } }
访问 Assets 中的文件的关键在于使用AssetManager这将允许应用打开当前驻留在 Assets 目录中的任何资源。将我们感兴趣的文件名传递给AssetManager.open()会返回一个 InputStream 供我们读取文件数据。将流读入内存后该示例将原始数据传递给解析例程并在用户界面上显示结果。
解析 CSV
这个例子还展示了一个简单的方法从一个 CSV 文件中获取数据并将其解析成一个模型对象(在本例中称为Person)。这里使用的方法将整个文件读入一个字节数组作为单个字符串进行处理。当要读取的数据量非常大时这种方法不是最有效的内存方法但是对于像这样的小文件这种方法就可以了。
原始字符串被传递到 StringTokenizer 实例中同时传递的还有用作标记断点的必需字符:逗号和换行符。此时可以按顺序处理文件的每个单独的块。使用基本的状态机方法来自每一行的数据被插入到新的Person实例中并被加载到结果列表中。
5–5 岁。管理数据库
问题
您的应用需要持久化数据这些数据可以作为子集或单个记录进行查询或修改。
解决办法
(API 一级)
在一个SQLiteOpenHelper的帮助下创建一个SQLiteDatabase来管理您的数据存储。SQLite 是一种快速、轻量级的数据库技术它利用 SQL 语法来构建查询和管理数据。对 SQLite 的支持内置于 Android SDK 中这使得在应用中设置和使用 SQLite 变得非常容易。
它是如何工作的
定制SQLiteOpenHelper允许您管理数据库模式本身的创建和修改。它也是一个很好的地方可以在创建数据库时将您可能需要的任何初始值或默认值插入到数据库中。清单 5–13 是一个定制助手的例子它创建一个数据库用一个表来存储关于人的基本信息。
清单 5–13。 自定义 SQLiteOpenHelper
public class MyDbHelper extends SQLiteOpenHelper {
private static final String DB_NAME “mydb”; private static final int DB_VERSION 1;
public static final String TABLE_NAME “people”; public static final String COL_NAME “pName”; public static final String COL_DATE “pDate”; private static final String STRING_CREATE “CREATE TABLE “TABLE_NAME” (_id INTEGER PRIMARY KEY AUTOINCREMENT, COL_NAME” TEXT, “COL_DATE” DATE);;
public MyDbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); }
Override public void onCreate(SQLiteDatabase db) { //Create the database table db.execSQL(STRING_CREATE);
//You may also load initial values into the database here ContentValues cv new ContentValues(2); cv.put(COL_NAME, “John Doe”); //Create a formatter for SQL date format SimpleDateFormat dateFormat new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”); cv.put(COL_DATE, dateFormat.format(new Date())); //Insert ‘now’ as the date db.insert(TABLE_NAME, null, cv); }
Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { //For now, clear the database and re-create db.execSQL(DROP TABLE IF EXISTS TABLE_NAME); onCreate(db); } }
数据库需要的关键信息是名称和版本号。创建和升级 SQLiteDatabase 确实需要一点 SQL 知识所以如果您不熟悉一些语法我们建议您浏览一下 SQL 参考资料。助手将在任何时候访问这个特定的数据库时调用onCreate()使用SQLiteOpenHelper.getReadableDatabase()或SQLiteOpenHelper.getWritableDatabase()如果它还不存在的话。
该示例将表名和列名抽象为常量以供外部使用(这是一个很好的习惯)。下面是实际的 SQL 创建字符串它在onCreate()中用来创建我们的表:
CREATE TABLE people (_id INTEGER PRIMARY KEY AUTOINCREMENT, pName TEXT, pAge INTEGER, pDate DATE);
在 Android 中使用 SQLite 时数据库必须进行少量的格式化以便与框架一起正常工作。它的大部分是为您创建的但是您创建的表必须有一部分是用于_id的列。该字符串的其余部分为表中的每条记录再创建两列:
人员姓名的文本字段输入此记录的日期的日期字段
使用ContentValues对象将数据插入数据库。该示例说明了如何在创建数据库时使用ContentValues向数据库中插入一些默认数据。SQLiteDatabase.insert()采用表名、空列 hack 和表示要插入的记录的ContentValues作为参数。
这里没有使用空列黑客但是它有一个可能对您的应用至关重要的目的。SQL 不能将一个完全为空的值插入到数据库中尝试这样做将会导致错误。如果您的实现有可能将一个空的ContentValues传递给insert()则使用空列 hack 来插入一个记录其中引用列的值为空。
关于升级的说明
SQLiteOpenHelper也很好地帮助您在应用的未来版本中迁移数据库模式。每当数据库被访问但是磁盘上的版本与当前版本不匹配(意味着构造函数中传递的版本)就会调用onUpgrade()。
在我们的例子中我们采用了懒人的方式简单地删除现有的数据库并重新创建它。实际上如果数据库包含用户输入的数据这可能不是合适的方法他们可能不会太高兴看到它消失。所以让我们暂时离题看一个可能更有用的onUpgrade()的例子。例如在应用的整个生命周期中使用以下三个数据库:
版本 1:应用的首次发布版本 2:应用升级包括电话号码字段版本 3:应用升级包括插入的日期条目
我们可以利用onUpgrade()来改变现有的数据库而不是删除当前所有的信息。参见清单 5–14。
**清单 5–14。***on upgrade()*的样本
Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { //Upgrade from v1\. Adding phone number if(oldVersion 1) { db.execSQL(ALTER TABLE TABLE_NAME ADD COLUMN phone_number INTEGER;); } //Upgrade from v2\. Add entry date if(oldVersion 2) { db.execSQL(ALTER TABLE TABLE_NAME ADD COLUMN entry_date DATE;); } }
在这个例子中如果用户的现有数据库版本是 1那么这两个语句都将被调用来向数据库添加列。如果他们已经有了版本 2那么只需要调用后面的语句来添加条目日期列。在这两种情况下应用数据库中的任何现有数据都将被保留。
使用数据库
回到我们最初的示例让我们看看一个活动如何利用我们创建的数据库。参见清单 5–15 和清单 5–16。
清单 5–15。 res/layout/main.xml
?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:orientationvertical android:layout_widthfill_parent android:layout_heightfill_parent EditText android:idid/name android:layout_widthfill_parent android:layout_heightwrap_content / Button android:idid/add android:layout_widthfill_parent android:layout_heightwrap_content android:textAdd New Person / ListView android:idid/list android:layout_widthfill_parent android:layout_heightfill_parent / /LinearLayout
清单 5–16。 查看和管理活动数据库
public class DbActivity extends Activity implements View.OnClickListener, AdapterView.OnItemClickListener {
EditText mText; Button mAdd; ListView mList;
MyDbHelper mHelper; SQLiteDatabase mDb; Cursor mCursor; SimpleCursorAdapter mAdapter;
Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);
mText (EditText)findViewById(R.id.name); mAdd (Button)findViewById(R.id.add); mAdd.setOnClickListener(this); mList (ListView)findViewById(R.id.list); mList.setOnItemClickListener(this);
mHelper new MyDbHelper(this); }
Override public void onResume() { super.onResume(); //Open connections to the database mDb mHelper.getWritableDatabase(); String[] columns new String[] {“_id”, MyDbHelper.COL_NAME, MyDbHelper.COL_DATE}; mCursor mDb.query(MyDbHelper.TABLE_NAME, columns, null, null, null, null, null); //Refresh the list String[] headers new String[] {MyDbHelper.COL_NAME, MyDbHelper.COL_DATE}; mAdapter new SimpleCursorAdapter(this, android.R.layout.two_line_list_item, mCursor, headers, newint[]{android.R.id.text1, android.R.id.text2}); mList.setAdapter(mAdapter); }
Override public void onPause() { super.onPause(); //Close all connections mDb.close(); mCursor.close(); }
Override public void onClick(View v) { //Add a new value to the database ContentValues cv new ContentValues(2); cv.put(MyDbHelper.COL_NAME, mText.getText().toString()); //Create a formatter for SQL date format SimpleDateFormat dateFormat new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”); cv.put(MyDbHelper.COL_DATE, dateFormat.format(new Date())); //Insert ‘now’ as the date mDb.insert(MyDbHelper.TABLE_NAME, null, cv); //Refresh the list mCursor.requery(); mAdapter.notifyDataSetChanged(); //Clear the edit field mText.setText(null); }
Override public void onItemClick(AdapterView? parent, View v, int position, long id) { //Delete the item from the database mCursor.moveToPosition(position); //Get the id value of this row String rowId mCursor.getString(0); //Column 0 of the cursor is the id mDb.delete(MyDbHelper.TABLE_NAME, “_id ?”, new String[]{rowId}); //Refresh the list mCursor.requery(); mAdapter.notifyDataSetChanged(); } }
在这个例子中我们利用我们的客户SQLiteOpenHelper来访问数据库实例并将数据库中的每条记录作为列表显示给用户界面。来自数据库的信息如果以Cursor的形式返回这是一个设计用来读取、写入和遍历查询结果的接口。
当活动变得可见时进行数据库查询以返回“people”表中的所有记录。必须将列名数组传递给查询以告诉数据库要返回哪些值。query()的其余参数旨在缩小选择数据集我们将在下一个秘籍中对此进行进一步研究。当不再需要数据库和游标连接时关闭它们是很重要的。在本例中我们在onPause()中这样做此时活动不再处于前台。
SimpleCursorAdapter用于将数据库中的数据映射到标准的 Android 两行列表项目视图中。string 和 int 数组参数构成映射string 数组中每一项的数据都将插入到视图中并在 int 数组中显示相应的 id 值。注意这里传递的列名列表与传递给查询的数组略有不同。这是因为我们将需要知道其他操作的记录 id但是在将数据映射到用户界面时这是不必要的。
用户可以在文本字段中输入姓名然后按“添加新人”按钮来创建新的内容值并将其插入到数据库中。此时为了让 UI 显示更改我们调用了Cursor.requery()和ListAdapter.notifyDataSetChanged()。
相反点击列表中的项目将从数据库中删除该指定项目。为了实现这一点我们必须构造一个简单的 SQL 语句告诉数据库只删除 _id 值与该选择匹配的记录。此时光标和列表适配器再次被刷新。
通过将光标移动到所选位置并调用getString(0)来获得列索引零的值从而获得选择的 _id 值。此请求返回 _id因为在列列表中传递给查询的第一个参数(索引 0)是“_id”delete 语句由两个参数组成:语句字符串和参数。对于字符串中出现的每个问号将在语句中插入传递的数组中的一个参数。
5 至 6 岁。查询数据库
问题
您的应用使用 SQLiteDatabase您需要返回其中包含的数据的特定子集。
解决办法
(API 一级)
使用完全结构化的 SQL 查询为特定数据创建过滤器并从数据库返回这些子集非常简单。有几种重载形式的SQLiteDatabase.query()可以从数据库中收集信息。我们将在这里检查其中最冗长的。
public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)
前两个参数简单地定义了查询数据的表以及我们想要访问的每条记录的列。剩下的参数定义了我们将如何缩小结果的范围。
选择 给定查询的 SQL WHERE 子句。 选择 Args 如果选择了问号则这些项目会填充这些字段。 群组依据 给定查询的 SQL GROUP BY 子句。 拥有 给定查询的 SQL ORDER BY 子句。 排序依据 给定查询的 SQL ORDER BY 子句。 限制 查询返回的最大结果数。
如您所见所有这些参数都旨在为数据库查询提供 SQL 的全部功能。
它是如何工作的
让我们来看一些示例查询可以构造这些查询来完成一些常见的实用查询。
返回值与给定参数匹配的所有行。
String[] COLUMNS new String[] {COL_NAME, COL_DATE}; String selection COL_NAME ?; String[] args new String[] {NAME_TO_MATCH}; Cursor result db.query(TABLE_NAME, COLUMNS, selection, args, null, null, null, null);
这个查询相当简单。selection 语句只是告诉数据库将 name 列中的任何数据与所提供的参数相匹配(该参数是在“”位置插入的)在选择字符串中)。
返回插入数据库的最后 10 行。
String orderBy _id DESC; String limit 10; Cursor result db.query(TABLE_NAME, COLUMNS, null, null, null, null, orderBy, limit);
这个查询没有特殊的选择标准而是告诉数据库按照自动递增的 id 值对结果进行排序最新的(最高的 id)记录排在第一位。limit 子句将返回结果的最大数量设置为 10。
返回日期字段在指定范围内的行(在本例中为 2000 年)。
String[] COLUMNS new String[] {COL_NAME, COL_DATE}; String selection datetime(COL_DATE) datetime(?) AND datetime(COL_DATE) datetime(?); String[] args new String[] {2000-1-1 00:00:00,2000-12-31 23:59:59}; Cursor result db.query(TABLE_NAME, COLUMNS, selection, args, null, null, null, null);
SQLite 没有为日期保留特定的数据类型尽管它们允许在创建表时使用 DATE 作为声明类型。但是标准的 SQL 日期和时间函数可用于创建文本、整数或实数形式的数据表示。这里我们比较数据库中的值和该范围的开始和结束日期的格式化字符串的返回值。
返回整数字段在指定范围内(在本例中为 7 到 10)的行。
String[] COLUMNS new String[] {COL_NAME, COL_AGE}; String selection COL_AGE ? AND COL_AGE ?; String[] args new String[] {7,10}; Cursor result db.query(TABLE_NAME, COLUMNS, selection, args, null, null, null, null);
这与上一个示例类似但没有那么冗长。这里我们只需创建选择语句来返回大于下限但小于上限的值。这两个限制都是作为要插入的参数提供的因此它们可以在应用中动态设置。
5 至 7 岁。备份数据
问题
您的应用将数据保存在设备上当用户更换设备或被迫重新安装应用时您需要为用户提供一种备份和恢复这些数据的方法。
解决办法
(API 一级)
使用设备的外部存储作为安全位置来复制数据库和其他文件。外部存储通常是物理可移动的允许用户将其放在另一个设备中并进行恢复。即使在不可能的情况下当用户将其设备连接到计算机时也可以安装外部存储器从而进行数据传输。
工作原理
清单 5–17 展示了AsyncTask的一个实现它在设备的外部存储器和它在应用数据目录中的位置之间来回复制数据库文件。它还为要实现的活动定义了一个接口以便在操作完成时得到通知。
清单 5–17。 异步请求备份和恢复
public class BackupTask extends AsyncTaskString,Void,Integer {
public interface CompletionListener { void onBackupComplete(); void onRestoreComplete(); void onError(int errorCode); }
public static final int BACKUP_SUCCESS 1; public static final int RESTORE_SUCCESS 2; public static final int BACKUP_ERROR 3; public static final int RESTORE_NOFILEERROR 4;
public static final String COMMAND_BACKUP “backupDatabase”; public static final String COMMAND_RESTORE “restoreDatabase”;
private Context mContext; private CompletionListener listener;
public BackupTask(Context context) { super(); mContext context; }
public void setCompletionListener(CompletionListener aListener) { listener aListener; }
Override protected Integer doInBackground(String… params) {
//Get a reference to the database File dbFile mContext.getDatabasePath(“mydb”); //Get a reference to the directory location for the backup File exportDir new File(Environment.getExternalStorageDirectory(), “myAppBackups”); if (!exportDir.exists()) { exportDir.mkdirs(); } File backup new File(exportDir, dbFile.getName());
//Check the required operation String command params[0]; if(command.equals(COMMAND_BACKUP)) { //Attempt file copy try { backup.createNewFile(); fileCopy(dbFile, backup); returnBACKUP_SUCCESS; } catch (IOException e) { returnBACKUP_ERROR; } } elseif(command.equals(COMMAND_RESTORE)) { //Attempt file copy try { if(!backup.exists()) { returnRESTORE_NOFILEERROR; } dbFile.createNewFile(); fileCopy(backup, dbFile); return RESTORE_SUCCESS; } catch (IOException e) { return BACKUP_ERROR; } } else { return BACKUP_ERROR; } }
Override protected void onPostExecute(Integer result) {
switch(result) { case BACKUP_SUCCESS: if(listener ! null) { listener.onBackupComplete(); } break; case RESTORE_SUCCESS: if(listener ! null) { listener.onRestoreComplete(); } break; case RESTORE_NOFILEERROR: if(listener ! null) { listener.onError(RESTORE_NOFILEERROR); } break; default: if(listener ! null) { listener.onError(BACKUP_ERROR); } } }
private void fileCopy(File source, File dest) throws IOException { FileChannel inChannel new FileInputStream(source).getChannel(); FileChannel outChannel new FileOutputStream(dest).getChannel(); try { inChannel.transferTo(0, inChannel.size(), outChannel); } finally { if (inChannel ! null) inChannel.close(); if (outChannel ! null) outChannel.close(); } } }
如您所见当COMMAND_BACKUP被传递给execute()时BackupTask 将命名数据库的当前版本复制到外部存储中的特定目录当COMMAND_RESTORE被传递时将文件复制回来。
一旦执行任务使用Context.getDatabasePath()来检索我们需要备份的数据库文件的引用。这一行可以很容易地替换为对Context.getFilesDir()的调用访问系统内部存储器上的文件进行备份。还获得了对我们在外部存储器上创建的备份目录的引用。
使用传统的 Java 文件 I/O 复制文件如果一切成功注册的监听器会得到通知。在此过程中任何抛出的异常都会被捕获并向侦听器返回一个错误。现在让我们来看一个利用该任务备份数据库的活动——参见清单 5–18。
清单 5–18。 活动使用备份任务
public class BackupActivity extends Activity implements BackupTask.CompletionListener {
Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //Dummy example database SQLiteDatabase db openOrCreateDatabase(“mydb”, Activity.MODE_PRIVATE, null); db.close(); }
Override public void onResume() { super.onResume(); if( Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ) { BackupTask task new BackupTask(this); task.setCompletionListener(this); task.execute(BackupTask.COMMAND_RESTORE); } }
Override public void onPause() { super.onPause(); if( Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ) { BackupTask task new BackupTask(this); task.execute(BackupTask.COMMAND_BACKUP); } }
Override public void onBackupComplete() { Toast.makeText(this, “Backup Successful”, Toast.LENGTH_SHORT).show(); } Override public void onError(int errorCode) { if(errorCode BackupTask.RESTORE_NOFILEERROR) { Toast.makeText(this, “No Backup Found to Restore”, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, Error During Operation: errorCode, Toast.LENGTH_SHORT).show(); } }
Override public void onRestoreComplete() { Toast.makeText(this, “Restore Successful”, Toast.LENGTH_SHORT).show(); } }
该活动实现了由BackupTask,定义的CompletionListener,因此当操作完成或发生错误时它会得到通知。出于示例的目的在应用的数据库目录中创建了一个虚拟数据库。我们调用openOrCreateDatabase()只是为了允许创建一个文件所以连接在创建之后会立即关闭。在正常情况下这个数据库已经存在这些行是不必要的。
该示例在每次活动恢复时执行一个恢复操作向任务注册自己以便它可以得到通知并向用户提示状态结果。请注意检查外部存储是否可用的任务也落到了活动上如果外部存储不可访问则不会执行任何任务。当活动暂停时执行备份操作这一次不注册回调。这是因为用户对该活动不再感兴趣所以我们将不需要举杯指出操作结果。
额外积分
这个后台任务可以扩展到将数据保存到基于云的服务中以获得最大的安全性和数据可移植性。有许多选项可以实现这一点包括 Google 自己的 web APIs我们建议您尝试一下。
从 API Level 8 开始Android 还包括一个用于将数据备份到云服务的 API。这个 API 可能适合您的目的但是我们不会在这里讨论它。Android 框架不能保证该服务在所有 Android 设备上都可用并且在撰写本文时还没有 API 来确定用户拥有的设备是否支持 Android 备份因此不建议对关键数据使用该服务。
5–8。共享您的数据库
问题
您的应用希望将其维护的数据库内容提供给设备上的其他应用。
解决办法
(API 一级)
创建一个ContentProvider作为应用数据的外部接口。ContentProvider通过类似数据库的接口query()、insert()、update()和delete()向外部请求公开任意一组数据尽管实现者可以自由设计接口如何映射到实际的数据模型。创建一个 ContentProvider 来公开来自SQLiteDatabase的数据简单明了。除了一些小的例外开发人员只需要将调用从提供者传递到数据库。
关于操作哪个数据集的参数通常编码在传递给ContentProvider的 Uri 中。例如发送查询 Uri如
content://com.examples.myprovider/friends
会告诉提供者返回其数据集中“friends”表的信息而
content://com.examples.myprovider/friends/15
将只指示记录 id 15 从查询中返回。应该注意的是这些只是系统其余部分使用的约定您有责任让您创建的ContentProvider以这种方式运行。ContentProvider本身并没有为您提供这种功能。
它是如何工作的
首先要创建一个与数据库交互的ContentProvider,我们必须有一个可以与之交互的数据库。清单 5–19 是一个示例SQLiteOpenHelper实现我们将使用它来创建和访问数据库本身。
清单 5–19。 示例 SQLiteOpenHelper
public class ShareDbHelper extends SQLiteOpenHelper {
private static final String DB_NAME “frienddb”; private static final int DB_VERSION 1;
public static final String TABLE_NAME “friends”; public static final String COL_FIRST “firstName”; public static final String COL_LAST “lastName”; public static final String COL_PHONE “phoneNumber”; private static final String STRING_CREATE “CREATE TABLE “TABLE_NAME” (_id INTEGER PRIMARY KEY AUTOINCREMENT, COL_FIRST” TEXT, “COL_LAST” TEXT, “COL_PHONE” TEXT);;
public ShareDbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); }
Override public void onCreate(SQLiteDatabase db) { //Create the database table db.execSQL(STRING_CREATE);
//Inserting example values into database ContentValues cv new ContentValues(3); cv.put(COL_FIRST, “John”); cv.put(COL_LAST, “Doe”); cv.put(COL_PHONE, “8885551234”); db.insert(TABLE_NAME, null, cv); cv new ContentValues(3); cv.put(COL_FIRST, “Jane”); cv.put(COL_LAST, “Doe”); cv.put(COL_PHONE, “8885552345”); db.insert(TABLE_NAME, null, cv); cv new ContentValues(3); cv.put(COL_FIRST, “Jill”); cv.put(COL_LAST, “Doe”); cv.put(COL_PHONE, “8885553456”); db.insert(TABLE_NAME, null, cv); }
Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { //For now, clear the database and re-create db.execSQL(DROP TABLE IF EXISTS TABLE_NAME); onCreate(db); } }
总的来说这个助手相当简单创建一个表来保存我们的朋友列表其中只有三列用于存放文本数据。出于此示例的目的插入了三个行值。现在让我们来看看将这个数据库暴露给其他应用的ContentProvider——参见清单 5–20 和 5–21。
清单 5–20。 内容提供者的清单声明
manifest xmlns:androidhttp://schemas.android.com/apk/res/android … application … provider android:name.FriendProvider android:authoritiescom.examples.sharedb.friendprovider /provider /application /manifest
清单 5–20。 为一个数据库提供内容
public class FriendProvider extends ContentProvider {
public static final Uri CONTENT_URI Uri.parse(“content://com.examples.sharedb.friendprovider/friends”);
public static finalclass Columns { public static final String _ID “_id”; public static final String FIRST “firstName”; public static final String LAST “lastName”; public static final String PHONE “phoneNumber”; }
/* Uri Matching */ private static final int FRIEND 1; private static final int FRIEND_ID 2;
private static final UriMatcher matcher new UriMatcher(UriMatcher.NO_MATCH); static { matcher.addURI(CONTENT_URI.getAuthority(), “friends”, FRIEND); matcher.addURI(CONTENT_URI.getAuthority(), “friends/#”, FRIEND_ID); }
SQLiteDatabase db;
Override publicint delete(Uri uri, String selection, String[] selectionArgs) { int result matcher.match(uri); switch(result) { case FRIEND: return db.delete(ShareDbHelper.TABLE_NAME, selection, selectionArgs); case FRIEND_ID: return db.delete(ShareDbHelper.TABLE_NAME, “_ID ?”, new String[]{uri.getLastPathSegment()}); default: return 0; } }
Override public String getType(Uri uri) { returnnull; }
Override public Uri insert(Uri uri, ContentValues values) { long id db.insert(ShareDbHelper.TABLE_NAME, null, values); if(id 0) { return Uri.withAppendedPath(uri, String.valueOf(id)); } else { returnnull; } }
Override publicboolean onCreate() { ShareDbHelper helper new ShareDbHelper(getContext()); db helper.getWritableDatabase(); returntrue; }
Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,String sortOrder) { int result matcher.match(uri); switch(result) { case FRIEND: return db.query(ShareDbHelper.TABLE_NAME, projection, selection, selectionArgs,null, null, sortOrder); case FRIEND_ID: return db.query(ShareDbHelper.TABLE_NAME, projection, “_ID ?”, new String[]{uri.getLastPathSegment()}, null, null, sortOrder); default: returnnull; } }
Override publicint update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { int result matcher.match(uri); switch(result) { case FRIEND: return db.update(ShareDbHelper.TABLE_NAME, values, selection, selectionArgs); case FRIEND_ID: return db.update(ShareDbHelper.TABLE_NAME, values, “_ID ?”, new String[]{uri.getLastPathSegment()}); default: return 0; } }
}
ContentProvider 必须在应用的清单中用它所表示的授权字符串来声明。这允许从外部应用访问该提供程序但即使您只在应用内部使用该提供程序这也是必需的。权限是 Android 用来将Uri请求匹配到提供者的因此它应该匹配公共CONTENT_URI的权限部分。
扩展ContentProvider时需要覆盖的六种方法是query()、insert()、update()、delete()、getType()和onCreate()。这些方法中的前四个在SQLiteDatabase中有直接对应的方法所以只需用适当的参数调用数据库方法。这两者之间的主要区别是ContentProvider方法传入了一个Uri提供者应该检查这个方法以确定操作数据库的哪一部分。
当一个活动或其他系统组件调用其内部ContentResolver上的相应方法时(您可以在清单 5–21 中看到这一点)或者在活动的情况下当调用managedQuery()时这四个主要的 CRUD 方法在提供者上被调用。
为了遵守本菜谱第一部分提到的Uri约定insert()返回一个Uri对象它是通过将新创建的记录 id 附加到路径的末尾而创建的。这个Uri应该被它的请求者认为是对刚刚创建的记录的直接引用。
其余的方法(query()、update()和delete())遵循惯例通过检查传入的Uri来查看它是引用特定的记录还是引用整个表。这个任务是在UriMatcher便利类的帮助下完成的。UriMatcher.match()方法将Uri与一组提供的模式进行比较并将匹配的模式作为 int 返回如果没有找到匹配的模式则返回UriMatcher.NO_MATCH。如果一个Uri被附加了一个记录 id那么对数据库的调用将被修改为只影响那个特定的行。
应该通过用UriMatcher.addURI()提供一组模式来初始化一个UriMatcher谷歌建议这一切都在ContentProvider的静态环境中完成。添加的每个模式还被赋予一个常量标识符当进行匹配时该标识符将作为返回值。在提供的模式中可以使用两个通配符:井号(#)将匹配任何数字星号(*)将匹配任何文本。
我们的例子创建了两个匹配的模式。初始模式与提供的CONTENT_URI直接匹配并被用来引用整个数据库表。第二种模式寻找路径的附加数字该数字将被用来引用该 id 处的记录。
通过onCreate()中的ShareDbHelper给出的引用获得对数据库的访问。在决定这种方法是否适用于您的应用时应该考虑所用数据库的大小。我们的数据库在创建时非常小但是更大的数据库可能需要很长时间来创建在这种情况下在这个操作发生时主线程不应该被占用getWritableDatabase()在这些情况下可能需要包装在 AsyncTask 中并在后台完成。现在让我们来看一个访问数据的示例活动——参见清单 5–23 和 5–24。
清单 5–23。 AndroidManifest.xml
?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.examples.sharedb android:versionCode1 android:versionName1.0 uses-sdk android:minSdkVersion1 / application android:icondrawable/icon android:labelstring/app_name activity android:name.ShareActivity android:labelstring/app_name intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity provider android:name.FriendProvider android:authoritiescom.examples.sharedb.friendprovider /provider /application /manifest
清单 5–24。 活动访问内容提供者
public class ShareActivity extends ListActivity implements AdapterView.OnItemClickListener {
Cursor mCursor;
Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
//List of column names to return from the query for each record String[] projection new String[]{FriendProvider.Columns._ID, FriendProvider.Columns.FIRST}; mCursor managedQuery(FriendProvider.CONTENT_URI, projection, null, null, null);
SimpleCursorAdapter adapter new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, mCursor, new String[]{FriendProvider.Columns.FIRST}, newint[]{android.R.id.text1});
ListView list getListView(); list.setOnItemClickListener(this); list.setAdapter(adapter); }
Override public void onItemClick(AdapterView? parent, View v, int position, long id) { mCursor.moveToPosition(position);
Uri uri Uri.withAppendedPath(FriendProvider.CONTENT_URI, mCursor.getString(0)); String[] projection new String[]{FriendProvider.Columns.FIRST, FriendProvider.Columns.LAST, FriendProvider.Columns.PHONE}; //Get the full record Cursor cursor getContentResolver().query(uri, projection, null, null, null); cursor.moveToFirst();
String message String.format(“%s %s, %s”, cursor.getString(0), cursor.getString(1),cursor.getString(2)); Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } }
该示例查询FriendsProvider中的所有记录并将它们放入一个列表中只显示名字列。为了让Cursor正确地适应列表我们的投影必须包括 ID 列即使它没有显示。
如果用户点击列表中的任何一项就会使用在末尾附加了记录 ID 的 Uri 对提供者进行另一次查询迫使提供者只返回那一条记录。此外还提供了一个扩展投影来获取关于这个朋友的所有列数据。
返回的数据被放入一个Toast中并被提交给用户查看。光标中的各个字段通过它们的列索引进行访问对应于传递给查询的投影中的索引。Cursor.getColumnIndex()方法也可以用来查询游标查找与给定列名相关联的索引。
当不再需要某个Cursor时它应该总是被关闭就像我们在用户点击时创建的Cursor一样。成员mCursor从未被显式关闭因为它是由活动管理的。每当使用managedQuery()创建一个Cursor时该活动将打开、关闭和刷新数据以及它自己的正常生命周期。
Figure 5–2 显示了运行此示例以显示提供者内容的结果。 图 5–2。 来自内容提供商的信息
5–9 岁。共享您的其他数据
问题
您希望您的应用将它维护的文件或其他数据提供给设备上的其他应用。
解决方案
(API 三级)
创建一个ContentProvider作为应用数据的外部接口。ContentProvider通过类似数据库的接口query()、insert()、update()和delete()向外部请求公开任意一组数据尽管实现可以自由设计数据如何从这些方法传递到实际模型。
ContentProvider可用于向外部请求公开任何类型的应用数据包括应用的资源和素材。
它是如何工作的
让我们来看一个ContentProvider实现它公开了两个数据源:位于内存中的字符串数组以及存储在应用的 assets 目录中的一系列图像文件。和以前一样我们必须在清单中使用一个provider标签向 Android 系统声明我们的提供者。参见清单 5–25 和清单 5–26。
清单 5–25。 内容提供者的清单声明
?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android … application … provider android:name.ImageProvider android:authoritiescom.examples.share.imageprovider /provider /application /manifest
清单 5–26。 自定义内容提供者公开素材
public class ImageProvider extends ContentProvider {
public static final Uri CONTENT_URI Uri.parse(“content://com.examples.share.imageprovider”);
public static final String COLUMN_NAME “nameString”; public static final String COLUMN_IMAGE “imageUri”;
private String[] mNames;
Override publicint delete(Uri uri, String selection, String[] selectionArgs) { thrownew UnsupportedOperationException(“This ContentProvider is read-only”); }
Override public String getType(Uri uri) { returnnull; }
Override public Uri insert(Uri uri, ContentValues values) { thrownew UnsupportedOperationException(“This ContentProvider is read-only”); }
Override publicboolean onCreate() { mNames new String[] {“John Doe”, “Jane Doe”, “Jill Doe”}; returntrue; }
Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,String sortOrder) { MatrixCursor cursor new MatrixCursor(projection); for(int i 0; i mNames.length; i) { //Insert only the columns they requested MatrixCursor.RowBuilder builder cursor.newRow(); for(String column : projection) { if(column.equals(“_id”)) { //Use the array index as a unique id builder.add(i); } if(column.equals(COLUMN_NAME)) { builder.add(mNames[i]); } if(column.equals(COLUMN_IMAGE)) { builder.add(Uri.withAppendedPath(CONTENT_URI, String.valueOf(i))); } } } return cursor; }
Override publicint update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { thrownew UnsupportedOperationException(“This ContentProvider is read-only”); }
Override public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { int requested Integer.parseInt(uri.getLastPathSegment()); AssetFileDescriptor afd; AssetManager manager getContext().getAssets(); //Return the appropriate asset for the requested item try { switch(requested) { case 0: afd manager.openFd(“logo1.png”); break; case 1: afd manager.openFd(“logo2.png”); break; case 2: afd manager.openFd(“logo3.png”); break; default: afd manager.openFd(“logo1.png”); } return afd; } catch (IOException e) { e.printStackTrace(); returnnull; } } }
正如您可能已经猜到的该示例公开了三个徽标图像素材。我们为此示例选择的图像如图 5–3 所示。 图 5–3。 示例 logo1.png(左)、logo2.png(中)和 logo3.png(右)存储在素材中
首先注意因为我们在素材目录中公开只读内容所以不需要支持继承的方法insert()、update()或delete()所以我们让这些方法简单地抛出一个UnsupportedOperationException。
创建提供者时保存人名的字符串数组被创建onCreate()返回 true 这向系统发出信号表明提供程序已成功创建。提供者为它的Uri和所有可读的列名公开常量。外部应用将使用这些值来请求数据。
此提供程序仅支持对其中所有数据的查询。为了支持对特定记录或所有内容的子集的条件查询应用可以处理传入到query()中的selection和selectionArgs的值。在这个例子中对query()的任何调用都将构建一个包含所有三个元素的游标。
该提供程序中使用的游标实现是一个MatrixCursor它是一个设计用于围绕不在数据库中保存的数据构建的游标。该示例遍历所请求的列列表(投影),并根据所包含的这些列构建每一行。每一行都是通过调用MatrixCursor.newRow()创建的它还返回一个用于添加列数据的Builder实例。应该始终注意将列数据的顺序添加到所请求的投影顺序中。它们应该总是匹配的。
name 列中的值是本地数组中相应的字符串而 _id 值(Android 需要它来利用返回的带有大多数ListAdapter的光标)只是作为数组索引返回。每行的 image 列中显示的数据实际上是代表每行图像文件的内容Uri它是以提供者的内容Uri为基础创建的并附加了数组索引。
当一个外部应用实际上通过ContentResolver.openInputStream()去检索这个内容时将调用openAssetFile()这个调用已经被覆盖以返回一个指向素材目录中的一个图像文件的AssetFileDescriptor。这个实现通过再次解构内容Uri并从末尾检索附加的索引值来确定返回哪个图像文件。
用法举例
让我们看看在 Android 应用的上下文中应该如何实现和访问这个提供者。参见清单 5–27。
清单 5–27。 AndroidManifest.xml
?xml version1.0 encodingutf-8? 要实现这个提供者拥有内容的应用的清单必须声明一个provider标记指出发出请求时要匹配的ContentProvider名称和授权。权限值应该与暴露内容的基本部分相匹配Uri。必须在清单中声明提供程序以便系统可以实例化并运行它即使拥有它的应用没有运行。参见清单 5–28 和清单 5–29。
清单 5–28。 res/layout/main.xml
?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:orientationvertical android:layout_widthfill_parent android:layout_heightfill_parent TextView android:idid/name android:layout_widthwrap_content android:layout_height20dip android:layout_gravitycenter_horizontal / ImageView android:idid/image android:layout_widthwrap_content android:layout_height50dip android:layout_gravitycenter_horizontal / ListView android:idid/list android:layout_widthfill_parent android:layout_heightfill_parent / /LinearLayout
清单 5–29。 从 ImageProvider 读取活动
public class ShareActivity extends Activity implements AdapterView.OnItemClickListener {
Cursor mCursor;
Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);
String[] projection new String[]{“_id”, ImageProvider.COLUMN_NAME, ImageProvider.COLUMN_IMAGE}; mCursor managedQuery(ImageProvider.CONTENT_URI, projection, null, null, null);
SimpleCursorAdapter adapter new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1,mCursor, new String[]{ImageProvider.COLUMN_NAME}, new int[]{android.R.id.text1});
ListView list (ListView)findViewById(R.id.list); list.setOnItemClickListener(this); list.setAdapter(adapter); }
Override public void onItemClick(AdapterView? parent, View v, int position, long id) { //Seek the cursor to the selection mCursor.moveToPosition(position);
//Load the name column into the TextView TextView tv (TextView)findViewById(R.id.name); tv.setText(mCursor.getString(1));
ImageView iv (ImageView)findViewById(R.id.image); try { //Load the content from the image column into the ImageView InputStream in getContentResolver().openInputStream(Uri.parse(mCursor.getString(2))); Bitmap image BitmapFactory.decodeStream(in); iv.setImageBitmap(image); } catch (FileNotFoundException e) { e.printStackTrace(); }
} }
在本例中从自定义的ContentProvider中获得一个托管游标引用数据的公开 Uri 和列名。然后使用一个SimpleCursorAdapter将数据连接到一个ListView,只显示名称值。
当用户点击列表中的任何项目时光标移动到该位置相应的名称和图像显示在上面。这是活动调用ContentResolver.openInputStream()通过存储在列字段中的 Uri 访问素材图像的地方。
Figure 5–4 显示了运行该应用并选择列表中最后一项的结果(Jill Doe)。 图 5–4。 活动从 ContentProvider 提取资源
请注意到Cursor的连接没有被显式关闭因为它是使用managedQuery()创建的这意味着活动将把光标作为其正常生命周期的一部分来管理包括在活动离开前台时关闭它。
需要了解的有用工具:SQLite3
Android 提供了sqlite3工具(在 Android SDK 主目录的tools子目录中),用于在您的托管平台上创建新数据库和管理现有数据库或者(当与 Android 调试桥工具adb结合使用时)在 Android 设备上创建新数据库和管理现有数据库。如果你不熟悉sqlite3将你的浏览器指向[sqlite.org/sqlite.html](http://sqlite.org/sqlite.html)阅读这个命令行工具的简短教程。
您可以用一个数据库文件名参数指定sqlite3(例如sqlite3 employees)来创建数据库文件(如果它不存在的话)或者打开现有的文件并进入这个工具的 shell从这里您可以执行特定于sqlite3的点前缀命令和 SQL 语句。如图 Figure 5–5 所示您也可以不带参数地指定sqlite3并输入 shell。 图 5–5。 调用sqlite3时不带数据库文件名参数
Figure 5–5 揭示了进入sqlite3 shell 后欢迎您的开场白它由您输入命令的sqlite提示符指示。当您键入特定于sqlite3的“.help命令时它还会显示部分帮助文本。
**提示:**您可以在没有参数的情况下指定sqlite3之后创建一个数据库方法是输入适当的 SQL 语句来创建和填充所需的表(并可能创建索引)然后在退出sqlite3之前调用.backup*filename*(其中 filename 标识存储数据库的文件)。
在你的托管平台上创建了数据库之后你会想把它上传到你的 Android 设备上。您可以根据以下命令行语法通过使用push命令调用adb工具来完成这项任务:
adb [-s *serialNumber*] push *local*.db /data/data/*application package*/databases/*remote*.db
该命令将标识为 local.db 的本地托管数据库推送到名为 remote.db 的文件中该文件位于连接的 Android 设备上的/data/data/*application package*/databases目录中。
注意: Local 和 remote 是实际数据库文件名的占位符。按照惯例文件名与一个.db文件扩展名相关联(尽管扩展名不是强制性的)。另外/data/data/*application package*是指应用自己的私有存储区 application package 是指应用唯一的包名。
如果只有一个设备连接到托管平台则不需要-s *serialNumber*本地数据库被推送到该设备上。如果连接了多个设备需要使用-s *serialNumber*来识别特定设备(例如-s emulator-5556)。
或者您可能希望将设备的数据库下载到您的托管平台也许是为了与设备应用的桌面版本一起使用。您可以根据下面的语法通过调用带有pull命令的adb来完成这个任务:
adb [-s *serialNumber*] pull /data/data/*application package*/databases/*remote*.db *local*.db
如果您想使用sqlite3来管理存储在设备上的 SQLite 数据库您需要从该设备的adb远程 shell 中调用这个工具。您可以根据以下语法通过调用adb和sqlite3来完成这项任务:
adb [-s serialNumber] shell
sqlite3 /data/data/application package/databases/remote.db
adb外壳由#提示符指示。输入sqlite3,后跟现有设备托管的数据库文件的路径和名称以操作数据库或创建新的数据库。或者您可以不带参数地输入sqlite3。
sqlite3命令呈现了与您在图 5–1 中看到的相同的序言。输入sqlite3命令发出 SQL 语句来管理 remote.db (或者创建一个新的数据库)然后退出sqlite3 ( .exit或者.quit)再退出adb shell ( exit)。
SQLite3 和 UC
第一章向你介绍了一个名为UC的应用。这个单位转换应用允许你在不同的单位之间进行转换(例如从华氏温度到摄氏温度)。
虽然很有用UC也有缺陷因为每次有新的转换添加到它的转换列表中时都必须重新构建它。我们可以通过在数据库中存储UC的转换来消除这个缺陷这就是我们在本节中要做的。
我们将首先创建一个数据库来存储转换列表。数据库将由一个带有conversion和multiplier列的conversions表组成。此外数据库将存储在一个conversions.db文件中。
Table 5–1 列出了将存储在conversion和multiplier列中的值。
表 5–1。 列Conversion和Multiplier列的值 | **转换** | 乘法器 | | :-- | :-- | | 英亩到平方英里 | 0.0015625 | | 大气压至帕斯卡 | One hundred and one thousand three hundred and twenty-five | | 巴到帕斯卡 | One hundred thousand | | 摄氏温度到华氏温度 | 0(占位符) | | 华氏度到摄氏度 | 0(占位符) | | 达因到牛顿 | 0.00001 | | 英尺/秒到米/秒 | 0.3048 | | 液体盎司(英国)到升 | 0.0284130625 | | 液体盎司(美国)到升 | 0.0295735295625 | | 马力(电力)至瓦特 | Seven hundred and forty-six | | 马力(公制)到瓦特 | Seven hundred and thirty-five point four nine nine | | 千克到吨(英制或长制) | 1/1016.0469088 | | 千克到吨(美制或短制) | 1/907.18474 | | 升到液体盎司(英国) | 1/0.0284130625 | | 升到液体盎司(美国) | 1/0.0295735295625 | | 马赫数到米/秒 | Three hundred and thirty-one point five | | 米/秒到英尺/秒 | 1/0.3048 | | 米/秒到马赫数 | 1/331.5 | | 英里/加仑(英国)到英里/加仑(美国) | Zero point eight three three | | 英里/加仑(美国)到英里/加仑(英国) | 1/0.833 | | 牛顿至达因 | One hundred thousand | | 帕斯卡至大气压 | 1/101325.0 | | 帕斯卡到巴 | 0.00001 | | 平方英里到英亩 | Six hundred and forty | | 吨(英制或长制)到千克 | 1016.0469088 | | 吨(美制或短制)到千克 | 907.18474 | | 瓦特/马力 _ 电动) | 1/746.0 | | 瓦特对马力(公制) | 1/735.499 |
在命令行执行sqlite3 conversions.db创建conversions.db并进入 shell然后执行 SQL 语句create table conversions(conversion varchar(50), mutliplier float);创建该数据库的conversions表。
继续输入一系列 insert 语句将表 5–1 的值行插入到conversions中。例如SQL 语句insert into conversions values(Acres to square miles, 0.0015625);将第一行的值插入到表中。
**注意:**您必须按照它们在表 5–1 中出现的顺序插入行因为Degrees Celsius to Degrees Fahrenheit和Degrees Fahrenheit to Degrees Celsius必须出现在从零开始的位置 3 和 4因为这些位置在UC2.java中是硬编码的。
接下来我们将创建一个类似于UC的UC2应用但是从conversions.db获得它的转换。按照第一章的秘籍 1-10(用 Eclipse 开发UC)中的说明完成这项任务但要做以下更改(参见清单 5–30):
将包名从com.apress.uc更改为com.apress.uc2。忽略arrays.xml文件。UC2不需要这个文件。用清单 5–26 替换框架UC2.java源代码。
清单 5–30。 执行从Conversions.db 获取的单位换算的活动
public class UC2 extends Activity { private int position 0; private String[] conversions; private double[] multipliers;
private class DBHelper extends SQLiteOpenHelper { private final static String DB_PATH “data/data/com.apress.uc2/databases/”; private final static String DB_NAME “conversions.db”; private final static int CONVERSIONS_COLUMN_ID 0; private final static int MULTIPLIERS_COLUMN_ID 1;
private SQLiteDatabase db;
public DBHelper(Context context) { super(context, DB_NAME, null, 1); }
Override public void onCreate(SQLiteDatabase db) { // Do nothing … we don’t create a new database. }
Override public void onUpgrade(SQLiteDatabase db, int oldver, int newver) { // Do nothing … we don’t upgrade a database. }
public boolean populateArrays() { try { String path DB_PATHDB_NAME; db SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY| SQLiteDatabase.NO_LOCALIZED_COLLATORS); Cursor cur db.query(“conversions”, null, null, null, null, null, null); if (cur.getCount() 0) { Toast.makeText(UC2.this, “conversions table is empty”, Toast.LENGTH_LONG).show(); return false; } conversions new String[cur.getCount()]; multipliers new double[cur.getCount()]; int i 0; while (cur.moveToNext()) { conversions[i] cur.getString(CONVERSIONS_COLUMN_ID); multipliers[i] cur.getFloat(MULTIPLIERS_COLUMN_ID); } return true; } catch (SQLException sqle) { Toast.makeText(UC2.this, sqle.getMessage(), Toast.LENGTH_LONG).show(); } finally { if (db ! null) db.close(); } return false; } }
Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);
DBHelper dbh new DBHelper(this); if (!dbh.populateArrays()) finish();
final EditText etUnits (EditText) findViewById(R.id.units);
final Spinner spnConversions (Spinner) findViewById(R.id.conversions); ArrayAdapter aa; aa new ArrayAdapter(this, android.R.layout.simple_spinner_item, conversions); aa.setDropDownViewResource(android.R.layout.simple_spinner_item); spnConversions.setAdapter(aa);
AdapterView.OnItemSelectedListener oisl; oisl new AdapterView.OnItemSelectedListener() { Override public void onItemSelected(AdapterView? parent, View view, int position, long id) { UC2.this.position position; }
Override public void onNothingSelected(AdapterView? parent) { System.out.println(“nothing”); } }; spnConversions.setOnItemSelectedListener(oisl);
final Button btnClear (Button) findViewById(R.id.clear); AdapterView.OnClickListener ocl; ocl new AdapterView.OnClickListener() { Override public void onClick(View v) { etUnits.setText(“”); } }; btnClear.setOnClickListener(ocl); btnClear.setEnabled(false);
final Button btnConvert (Button) findViewById(R.id.convert); ocl new AdapterView.OnClickListener() { Override public void onClick(View v) { String text etUnits.getText().toString(); double input Double.parseDouble(text); double result 0; if (position 3) result input*9.0/5.032; // Celsius to Fahrenheit else if (position 4) result (input-32)5.0/9.0; // Fahrenheit to Celsius else result inputmultipliers[position]; etUnits.setText(“”result); } }; btnConvert.setOnClickListener(ocl); btnConvert.setEnabled(false);
Button btnClose (Button) findViewById(R.id.close); ocl new AdapterView.OnClickListener() { Override public void onClick(View v) { finish(); } }; btnClose.setOnClickListener(ocl);
TextWatcher tw; tw new TextWatcher() { public void afterTextChanged(Editable s) { } public void beforeTextChanged(CharSequence s, int start, int count, int after) { } public void onTextChanged(CharSequence s, int start, int before, int count) { if (etUnits.getText().length() 0) { btnClear.setEnabled(false); btnConvert.setEnabled(false); } else { btnClear.setEnabled(true); btnConvert.setEnabled(true); } } }; etUnits.addTextChangedListener(tw); } }
UC2与UC的不同之处主要在于依靠DBHelper内部类从conversions.db数据库的conversions表中的conversion和multiplier列获取其conversions和multipliers数组的值。
DBHelper扩展android.database.sqlite.SQLiteOpenHelper并覆盖它的抽象onCreate()和onUpgrade()方法。重写方法什么都不做重要的是数据库是否可以打开。
数据库以populateArrays()方法打开。如果成功打开查询conversions表以返回所有行。如果返回的android.database.Cursor对象包含至少一行数组由Cursor值填充。
如果出现问题会显示一条提示消息。虽然对于这个简单的例子来说很方便但是您可能希望显示一个对话框并将其字符串存储在资源文件中。无论是否显示 toast 消息数据库都是关闭的。
UC2与UC的区别还在于它直接实例化了android.widget.ArrayAdapter而不是调用这个类的createFromResource()方法。这样做是为了将字符串名称的conversions数组传递给ArrayAdapter实例。
假设您已经构建了这个应用从 Eclipse 启动它。UC2将短暂显示一个空白屏幕然后在结束前显示一条提示信息。图 5–6 显示这个吐司出现在应用启动屏幕上。 图 5–6。 因为conversions.db数据库还没有出现在设备上所以显示了一个 toast。
出现 toast 是因为在/data/data/com.apress.uc2/databases/路径中不存在conversions.db数据库。我们可以通过将之前创建的conversions.db文件上传到这个路径来纠正这种情况如下所示:
adb push conversions.db /data/data/com.apress.uc2/databases/conversions.db
这一次当您启动该应用时您应该会看到出现在 Figure 5–7 中的屏幕。 图 5–7。 单位转换器的单独屏幕让你执行各种单位转换。
遭受以下一对缺陷的困扰——考虑将修复这些缺陷作为要完成的练习:
由于在UC2.java中对这些位置进行了硬编码因此Degrees Celsius to Degrees Fahrenheit和Degrees Fahrenheit to Degrees Celsius转换必须出现在从零开始的位置 3 和 4。这个硬编码位于下面的摘录中该摘录摘自在清单 5–30 中分配给点击按钮的点击监听器中的onClick()方法:if (position 3) result input*9.0/5.032; // Celsius to Fahrenheit else if (position 4) result (input-32)*5.0/9.0; // Fahrenheit to Celsius else result input*multipliers[position];DBHelper的populateArrays()方法填充应用主线程上的conversions和multipliers数组。这应该不成问题因为conversions表只包含 28 行。然而如果向该表中添加更多的行主线程可能会被占用足够长的时间以至于可怕的应用不响应对话框出现(参见附录 C)。此外这也是为什么 Android 文档指出SQLiteOpenHelper的getReadableDatabase()和getWritableDatabase()方法不应该在主线程上调用的原因。然而对于小型数据库在主线程上调用这些方法应该不成问题。
总结
在这一章中你已经研究了许多在 Android 设备上持久化数据的实用方法。您了解了如何快速创建首选项屏幕以及如何使用首选项和简单的方法来持久化基本数据类型。您已经看到了文件的放置方式和位置既可用于参考也可用于存储。您甚至学习了如何与其他应用共享持久化数据。在下一章中我们将研究如何利用操作系统的服务来进行后台操作和应用之间的通信。