原文:http://www.smashingmagazine.com/2011/03/28/get-started-developing-for-android-with-eclipse-reloaded/
在我们教程系列的第一部分中,我们使用Android和Eclipse开发了一个简单的饮茶计时器的应用程序。在第二部分,我们将继续开发这个程序,并给它增加一些其他的额外的功能。在开发的过程中,我们将给你介绍更多重要而强大的Android SDK特性,包括持久化数据存储,Activity和Intent,和共享用户首选项(译者注:类似于windows 的注册表的一种机制)。
跟着本教程,你需要上一篇教程中的代码,如果你想直接使用代码,你可以使用如下的指令从GitHub上check out出tutorial_par_1标记的代码:
$ cd BrewClock
$ git checkout tutorial_part_1
在GitHub中检出了代码后,你需要将代码倒入到Eclipse中的项目中:
- 运行 Eclipse 选择 File → Import…
- 在导入窗口, 选择 “Existing Projects into Workspace”并点击 “Next.”
- 在下一屏,点击 “Browse,”选择你从GitHub上clone出的代码目录。
- 点击“Finish” 将项目导入到Eclipse中。
在导入项目到Eclipse之后,你有可能会看到有如下的警告信息:
Please fix project properties.
如果有这种情况,右键点击“Project Explorer ”中新导入的BrewClock项目,并选择 “Fix Project Properties,” 并重启Eclipse。
数据持久化入门
当前,BrewClock 让用户为他们泡的茶设置一个定时器。这个非常棒的一个工作,但是如果对于不同的茶使用同一个泡茶时间的结果会怎样呢,是不每种茶都应该有自己的一个泡茶时间呢?如果这样,那岂不是所有的用户都需要记下每一类茶所需要泡的时间!这不是一个很好的用户体验。因此,在这篇教程中,我将新增一个功能来为用户每种不同的茶叶存放一个泡茶时间,并当用户想泡茶的时候,可以从茶叶列表中进行选择。
为了实现这个目的,我们得利用Android的丰富的数据持久化的API。Android提供了几种方式来存储数据,本文将要覆盖其中的两种方式。第一种,使用SQLite数据库引擎来为我们存储数据。
SQLite 是一种流行的轻量级SQL数据库引擎,它将数据存在单个文件中。SQLite经常用于桌面或在那些运行不能运行客户端-服务器SQL引擎(例如MySQL或PostgreSQL)的嵌入式的应用上。
每个安装在Android上的应用都可以保存和使用多个SQLite数据库文件(由数据存储容量决定),这些数据由系统自动地进行管理。应用程序的数据是私有并且不能被其他的应用程序所访问。(数据可以通过ContentProvider(译者注:内容提供者类)类进行共享,但是我们不会在本教程中覆盖关于内容提供者的内容)。当数据应用程序被更新时,数据库文件就进行持久化,当应用程序被删除时,数据库文家就被删除。
我们在BrewClock应用使用SQLite数据来维护我们的茶叶列表和泡茶所需要的时间。下面是我们我们将使用的数据表的一个总体介绍。
| Table: teas |
+------------+------------------------+
| Column | Description |
+------------+------------------------+
| _ID | integer , autoincrement |
| name | text , not null |
| brew_time | integer , not null |
+------------+------------------------+
如果以前你使用过SQL,你应该熟悉这些内容。数据表有三个字段,一个唯一标示(_ID),茶叶名称(name)和泡茶时间(brew_time)字段。我们将使用Android提供给我们的API在应用中建立数据表。系统将负责在正确的位置为我们的创建数据库文件。
抽象数据库
为了确保数据库的代码容易被维护,我们用一个单独的类TeaData来抽象所有处理数据库创建,插入,和查询的代码。如果你熟悉模型-试图-控制(译者注:MVC)方法的话,这个你也应该熟悉。所有数据库代码与我们的BrewClockActitvity类隔离开来。Actitvity可以初始化一个新的TeaData实例(这个实例将连接数据库)并完成它所需要的工作。以这种方式工作保证了我们可以方便的更改我们所使用的数据库而不用修改其他那些和数据库不相关部分的代码。
通过菜单File → New → Class.在BrewClock项目中创建一个TeaData的新类。确保TeaData扩展于android.database.sqlite.SQLiteOpenHelper 类,并选中“Constructors from superclass”复选框。
TeaData 类将为你自动地处理SQLite数据库的创建和版本。我们需要增加一些方法来作为其他代码到数据库的接口。
增加两个常量来存储数据库的名字和版本,增加表名和表中列名。我们使用Android提供的常类BaseColumns._ID来做为表的唯一id列:
import android.app.Activity;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.provider.BaseColumns;
public class TeaData extends SQLiteOpenHelper {
private static final String DATABASE_NAME = " teas.db " ;
private static final int DATABASE_VERSION = 1 ;
public static final String TABLE_NAME = " teas " ;
public static final String _ID = BaseColumns._ID;
public static final String NAME = " name " ;
public static final String BREW_TIME = " brew_time " ;
// …
}
为TeaData增加一个构造方法,以数据库名称合版本号为参数调用其父类的构造方法。Android将会自动地打开数据库(如果数据库不存在就自动创建它)。
public TeaData(Context context) {
super (context, DATABASE_NAME, null , DATABASE_VERSION);
}
我们需要重载onCreate方法,并执行一个SQL 串执行创建数据库表的操作。Android将会在数据库文件第一次被创建时调用这个方法。
在启动过程中,Android检查数据库的版本是否我们传入的版本一致。如果版本发生了改变,Android将会调用onUpgrade方法,在这个方法总,你可以编写修改数据库结构的业务逻辑。在本教程中,我们将让Android删除数据库并重建数据库。
在onCreate和onUpgrade中增加如下的代码:
@Override
public void onCreate(SQLiteDatabase db) {
// CREATE TABLE teas (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, brew_time INTEGER);
String sql =
" CREATE TABLE " + TABLE_NAME + " ( "
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ NAME + " TEXT NOT NULL, "
+ BREW_TIME + " INTEGER "
+ " ); " ;
db.execSQL(sql);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL( " DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
下一步,我们需要新增代码让我们方便地在数据库中新增茶叶记录。我们新增一个带茶叶名称和泡茶时间的方法来负责插入记录。Android为了尽量避免开发者使用SQL语句,提供了一堆类来处理向数据库中查入记录。首先,我们创建一个ContentValues集合,并将相关的值插入到这个集合中去。
对于ContentValues集合,我们只要简单地提供一个列名和值来插入就行了。Android负责创建和运行正确的SQL。使用Android的数据类确保了你能写出安全,跨平台的数据库操作代码。
Add a new method, insert(), to the TeaData class:
public void insert(String name, int brewTime) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(NAME, name);
values.put(BREW_TIME, brewTime);
db.insertOrThrow(TABLE_NAME, null , values);
}
查询数据
我们应用程序具有了在数据库中保存数据的能力后,我们同样也需要一种方式将数据取回来。Android提供了游标Cursor接口来完成这件工作。一个游标代表了针对数据库运行一个SQL返回的结果集,游标在这个结果集中维护了一个指针来指向结果集中的一行。这个指针可以向前,向后移动,并返回每一列的值,下面我们用图形来帮助你理解游标:
SQL 查询: SELECT * from teas LIMIT 3;
| _ID | name | brew_time |
+-----------------------------------+
| 1 | Earl Grey | 3 |
| 2 | Green | 1 | < = Cursor
| 3 | Assam | 5 |
+-------+-------------+-------------+
在这个例子中,游标指向了结果集中的第二条记录(绿茶)。我们可以通过调用cursor.moveToPrevious()方法,将游标向前移动,让它指向第一行(Earl Grey),或者调用moveToNext向前移动指向Assam。要取到游标所指向记录的茶叶的名称,我们只要调用cursor.getString(1),1代表我们向提取数据列的下标(注意下标识从0开始的,1代表第二列,依次类推)。
在了解游标后,我们增加一个创建游标对象并返回数据库中所有的茶叶信息。在TeaData中增加all方法:
public Cursor all(Activity activity) {
String[] from = { _ID, NAME, BREW_TIME };
String order = NAME;
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, from, null , null , null , null , order);
activity.startManagingCursor(cursor);
return cursor;
}
因为这个方法乍一看有点古怪,所以让我们先来关心一下这个方法的一些细节。我们没有使用SQL的查询语句,而是使用了Android提供的数据库接口方法。
第一,我们需要告诉Android,我们所关心的列的信息。我们创建了一个字符串数组,数组中存放这TeaData中列的标示信息。我们还设置了我名们期望的结果集按照哪一个列进行排序的列名。
第二,我们使用getReadalbeDatabase()创建了一个到数据库的只读连接,并调用query方法告诉Android我们希望用query方法运行一个查询。query()方法有很多的参数,Android在内部将这些参数转化为一个查询语句。此外,Android的抽象层保证了即使底层数据储存机制发生了变化,我们的应用程序代码也能正确的工作。
由于我们只要返回表中的所有记录,所以我们没有在方法中使用到链接join,过滤filter和分组group(例如:在SQL中的WHERE,JOIN,和GROUP BY)。from和order变量告诉查询数据库需要返回那些列和提取数据时按什么列进行排序。我们使用SQLiteDatabase.query()作为和数据库的人机交互接口。
最后,我们让Activity(在本例中,我们的BrewClockActivity)来管理游标。通常,游标需要人工刷新内容,因此当我们增加一个新茶信息到数据库中时,我们就需要刷新我们的游标。每当我们的应用被挂起和恢复的时候,通过调用startManagingCursor()让Android来帮我们重建结果集。
在TeaData类中增加count方法:
public long count() {
SQLiteDatabase db = getReadableDatabase();
return DatabaseUtils.queryNumEntries(db, TABLE_NAME);
}
保存TeaData类,使用修正没有import 的类(Source → Organize Imports),在完成我们的数据类后,下一步我们将着手修改我们BrewClock的人机界面。
修改BrewClock用户界面,允许进行茶叶选择
持久化茶和泡茶的时间的目的是让用能快速的选择他们所钟爱的预设置的茶。为了完成这个功能,我们需要再BrewClock的主界面上增加一个Spinner(类似于桌面上弹出菜单),生成一个来自于TeaData的茶列表。
和前面的教程一样,我们使用了Eclipse的布局器编辑器在BrewClock的主界面布局XML文件中增加Spinner。在LinearLayout元素下面增加下面这些代码(大约在24行)。如果你打开了可视化的布局编辑器后,你可以点击窗口下面的地”Code View”进行切换。
<!-- Tea Selection -->
< LinearLayout
android:orientation ="vertical"
android:layout_width ="fill_parent"
android:layout_height ="wrap_content" >
< Spinner
android:id ="@+id/tea_spinner"
android:layout_width ="fill_parent"
android:layout_height ="wrap_content" />
</ LinearLayout >
在BrewClockActivity类里面,增加一个成员变量指向Spinner,通过使用findViewById连接界面上的控件:
protected Spinner teaSpinner;
protected TeaData teaData;
// …
public void onCreate(Bundle savedInstanceState) {
// …
teaData = new TeaData( this );
teaSpinner = (Spinner) findViewById(R.id.tea_spinner);
}
运行你的程序以确保新的界面正确地生效。你应该在泡茶计数器下看见一个空白的弹出式菜单(或者是Spinner)。如果点击spinner,Android将显示一个弹出式的菜单并为你提供选择列表。在这时,菜单的内容因该是空的,现在让我们来绑定Spinner和我们的茶叶数据库。
数据绑定
当Android从数据库中查询数据时,它将会返回一个游标Cursor对象。Cursor代表了来自数据库的结果集,并可以移动游标来提取结果中的数据。使用一类Android提供的称为“适配器Adapter”的类,我们很容易将这个结果集绑定到Spinner上。适配器完成了提取数据库结果集中的数据并在界面上显示这些数据等这些复杂而困难工作。
在我们的TeaData.all()方法中已经可以返回一个带有tea表内容的游标,使用这个游标,我们所需要做的工作就是创建一个SimpleCursor适配器来绑定我们的teaSpinner,Android会负责处理将数据显示在spinner的列表中。
通过创建一个SimpleCursorAdapter类来连接Spinner与teaData.all()返回的游标:
public void onCreate(Bundle savedInstanceState) {
// …
Cursor cursor = teaData.all( this );
SimpleCursorAdapter teaCursorAdapter = new SimpleCursorAdapter(
this ,
android.R.layout.simple_spinner_item,
cursor,
new String[] { TeaData.NAME },
new int [] { android.R.id.text1 }
);
teaSpinner.setAdapter(teaCursorAdapter);
teaCursorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
}
注意,我们使用了Android内建的android.R对象。这个对象提供了你的应用程序中的默认资源,例如视图和布局。在我们的代码中,我们使用了android.R.layout.simple_spinner_item,它是简单的文本标签布局。
如果你再次运行的应用程序,你将会看到spinner中仍然是空的!虽然我们已经连接了我们的数据库,但是由于数据库中没有任何记录,所以我们任何看到了空列表。
我们通过在构造方法中增加一些默认记录来让用户可以选择所需要的茶叶,为了避免重复记录,我们只有在数据库中记录为0的情况才增加默认记录。在本教程的代码中,我们使用前面增加的count()来检查数据库中表记录是否为空。
增加当数据库中表为空的默认记录代码。把这些代码增加从数据库提取茶叶数据的前面(译者注:上一段的代码前)。
public void onCreate(Bundle savedInstanceState) {
// …
// Add some default tea data! (Adjust to your preference <img src=" http://coolshell.cn/wp-includes/images/smilies/icon_smile.gif " alt=":)" class="wp-smiley">
if (teaData.count() == 0 ) {
teaData.insert( " Earl Grey " , 3 );
teaData.insert( " Assam " , 3 );
teaData.insert( " Jasmine Green " , 1 );
teaData.insert( " Darjeeling " , 2 );
}
// Code from the previous step:
Cursor cursor = teaData.all( this );
// …
}
现在再次运行你的应用程序。你将会发现茶叶Spinner有了一条选择。点击Spinner让你可以从数据库选择你要的茶叶。
恭喜你!你已经成功关联了你的界面和代码。这是任何软件开发过程中一个非常重要的方面。正如你所看见的,Android将这一步简化的非常容易,但是功能有是非常的NB。使用游标和适配器,你可以将数据源(丛简单的字符串数组到复杂的数据库查询)绑定到任何类型的视图:spinner或列表,设置是类似iTunes cover-flow gallery!
虽然现在已经可以开始泡茶了,但是我们工作还远没有结束。当你从Spinner选择了不同的茶,这个选择却不会发生任何作用。我们需要根据用户所选茶叶的种类取更新我们的泡茶时间。
读取选中茶叶数据并更新泡茶时间
为了能读取用户从数据库中选择茶叶的数据,我们必须增加一个针对此事件的监听器。类似于处理按钮点击事件的OnClickListener监听器一样,我们将实现一个OnItemSelectedListener。当用户从视图中做出一个选择的事件将触发这个监听器,例如从我们的Spinner。
在BrewClockActivity中增加需要实现的接口OnItemSelectedListener。并增加其响应的处理方法onItemSelected()和onNothingSelected():
public class BrewClockActivity extends Activity implements OnClickListener, OnItemSelectedListener {
// …
public void onItemSelected(AdapterView <?> spinner, View view, int position, long id) {
if (spinner == teaSpinner) {
// Update the brew time with the selected tea’s brewtime
Cursor cursor = (Cursor) spinner.getSelectedItem();
setBrewTime(cursor.getInt( 2 ));
}
}
public void onNothingSelected(AdapterView <?> adapterView) {
// Do nothing
}
}
在这里我们要检查是触发的spinner此事件是不是BrewClock的teaSpinner。如果是,我们将提取代表选中记录的游标对象。这些都是由关联teaData和Spinner的SimpleCursorAdapter来提供我们完成的。Android知道哪个查询产生的Spinner数据,也知道用户选择的哪个数据。Android使用游标来返回数据库的一行记录,也代表了用户所选择的茶叶数据。
Cursor的getInt()方法带了一个我们想提取的列的下标为参数。在我们的teaData.all()方法中创建游标的时候,我们读取的列是_ID,NAME和BREW_TIME。假设我们在teaSpinner中选择的是Jasmine Tea,那么将返回我们所选数据所对应的数据库记录。
然后我们再通过传递参数2来选择此记录的第二列的整型值。这个值提供给setBrewTime()方法。这个方法用于更新界面上的泡茶时间。
最后,我们需要告诉teaSpinner BrewClockActivity正在监听OnItemSelected事件。在BrewClockActivity的onCreate方法中增加下面的代码:
public void onCreate() {
// …
teaSpinner.setOnItemSelectedListener( this );
}
大功告成!再次运行你的程序,并从Spinner选择不同的茶叶。每次你所选的茶叶它所对应的泡茶时间都回显示对应的界面上。我们余下的代码中已经可以处理从当前时间开始递减计数。所以在有预先设置的茶叶种类下,我们已经可以完成我们所想要的功能。
你当然可以,回到之前的代码中去增加一些茶叶种类你满足你的口味。但是如果你发布BrewClock程序到Android Market,每当有人向增加新的茶叶数据到数据库中,我就需要去手动的取更新数据中的内容并重新发布它;这样所有的人就必须去更新它,并且所有的人都有一个同样的列表。这听起来非常的不灵活,因此我们还有很多的工作需要完成!
如果用户自己有方法新增茶叶种类到数据库里面,将会非常的不错的做法。因此我们将在下一章继续。。。
Activity 介绍
和你应用程序中每个屏幕关联的代码就是Activity。每次当你从一屏切换到另外一屏,Android就会创建一个新的Activity。在真实世界中,虽然一个应用程序经常由多个屏幕/Activity构成,Andriod却将每个屏幕看作独立的个体。多个Activity工作在一起形成一种关联的体验,这是因为Android让你非常容易地在屏幕/Activity之间传递数据。
在本节最后,你将为你的应用程序新增一个新的Activity(AddTeaActivity)并将它注册到Android系统中。你还需要从最初的BrewClockActivity传递数据到新的Activity中。
首先,我们需要给用户一种方式切换到新的Activity上。我们将使用选项菜单来完成之一步。
选项菜单
当用户他们的设备上的“Menu”按键时,选项菜单以弹出菜单的形式出现。Android负责菜单的自动创建和显示;你只需要告诉Android,菜单显示什么内容和当用户点击菜单时该做什么就行。
然而,最好不要在代码中硬编码菜单的标题,我们可以使用Android的字符串资源。字符串资源是一个独立的文件,在这个文件中你可以维护所有用于用户阅读的字符串和标签资源,并可以在代码调用它们。这就意味着当你在未来需要修改字符串时,你只要修改这一处地方即可。.
在project explorer中导航到“res/values”下,你将会看到string.xml文件已经存在。这个是你再创建新项目的时候由Eclipse创建的,这文件存放着在整个应用程序我们将要使用的字符串。
双击打开strings.xml ,通过窗口底部的选项页切换到XML 视图。
在<resources>…</resources> 元素中增加下面的内容:
< resources >
<!-- … -->
< string name ="add_tea_label" > Add Tea </ string >
</ resources >
我们在这里定义了一个字符串,add_tea_label和它关联的文本,我们可以在整个程序代码中通过add_tea_label来使用其关联的文本。如果标签因为某个原因需要修改,我们只需要在这个文件修改这一个地方就能完成整个程序的修改。
下一步,让我们创建一个新文件完成选项菜单的定义,如果字符串和布局一样,菜单也使用XML来定义。因此我们将在Eclipse中川建一个新的XML文件:
通过选择File → New → Other, 并选择“Android XML File.”在Eclipse中创建一个新的XML文件。
选择资源的类型为 “Menu”,保存文件名为main.xml。Eclipse将为你自动的创建一个目录res/menu, 来存放你的菜单文件。
打开res/menus/main.xml 文件, 通过窗口底部的“main.xml”选项页来切换到XML视图。
增加菜单项, add_tea。
< resources >
<!-- … -->
< string name ="add_tea_label" > Add Tea </ string >
</ resources >
注意android:title 属性被设置为@string/add_tea_label。这告诉Android在我们的strings.xml文件中查找add_tea_label并返回相关联的标签内容。在本列中我们的菜单项的标签时“Add Tea”。
下一步,我们将告诉我们的Activity,当用户点击设备上的“memu”按键时来显示这个选项菜单。
返回BrewClockActivity.java代码, 重载onCreateOptionsMenu 方法,这个方法告诉Android 当用户点击“Menu”按键时,装载我们的菜单:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main, menu);
return true ;
}
当用户点击他设备上的“Menu”按键时,Android将调用onCreateOptionsMenu。在这个方法中,我们创建了一个MenuInflater, 这个对象将从你的应用程序包中装载你的菜单资源。就如同按钮和文本域组成你的应用程序布局一样,main.xml资源也是通过全局对象R来生效的,因此我们将此对象提交给MenuInflater对象。
为了测试菜单,保存并在模拟器中并运行应用程序。当程序运行起来使,点击“Menu”按键,你将会看到一个弹出式的菜单显示了一个“Add Tea”选项。
如果你点击“Add Tea”选项,Android自动地检测到点击并关闭菜单。在后台,Android将会提醒应用程序选项已经被点击。
处理菜单点击
当用户点击 “Add Tea” 菜单选项,我们想要显示一个新的Activity以便我们能进入增加新茶叶种类的界面。通过选择File → New → Class来创建一个的Activiy。
将新类命名为 AddTeaActivity,并确保它继承于android.app.Activity类。这个类也放在com.example.brewclock包中:
package com.example.brewclock;
import android.app.Activity;
import android.os.Bundle;
public class AddTeaActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
}
}
上面样例中的空白Activity将不会完成任何工作。但是通过它,我们已经可以完成选项菜单的功能。
在BrewClockActivity增加一个重载方法onOptionsItemSelected 。当用户点击菜单项时,这个方法被Android调用。 (注意点击的MenuItem为它的接收参数:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.add_tea:
Intent intent = new Intent( this , AddTeaActivity. class );
startActivity(intent);
return true ;
default :
return super .onOptionsItemSelected(item);
}
}
下一步, 增加新方法, saveTea(), 来保存茶叶信息。saveTea 首先从界面上读取茶叶的名称和用户所选的泡茶时间,如果这些输入数据都能通过验证,就将这些数据保存到数据库中:
public boolean saveTea() {
// Read values from the interface
String teaNameText = teaName.getText().toString();
int brewTimeValue = brewTimeSeekBar.getProgress() + 1 ;
// Validate a name has been entered for the tea
if (teaNameText.length() < 2 ) {
AlertDialog.Builder dialog = new AlertDialog.Builder( this );
dialog.setTitle(R.string.invalid_tea_title);
dialog.setMessage(R.string.invalid_tea_no_name);
dialog.show();
return false ;
}
// The tea is valid, so connect to the tea database and insert the tea
TeaData teaData = new TeaData( this );
teaData.insert(teaNameText, brewTimeValue);
teaData.close();
return true ;
}
大段的代码,让我们过一遍这段代码的逻辑。
首先,我们从文本框中读取茶叶名称,从SeekBar读取泡茶时间(记着读的时间要加1以保证时间在1到10分钟之内)。下一步,我们验证茶叶名大于等于2个字符(这是非常简单的验证,如果想做更复杂的验证,那么就使用正则表达式吧)。
如果茶叶名称非法,我们需要让用户知道。我们使用Android提供的工具类,AlertDialog.Biulder类,这个类给我们提供了一个快捷创建和显示模态窗口的方法。在设置完标题和错误信息后,通过调用show方法来显示对话框。这个对话框是模态的modal,因此用户只有按下back按键,这个对话框才会关闭。在这时,我们不想保存任何数据,所以我们的方法返回了false。
如果茶名称合法,我们通过TeaData类创建一个到茶叶数据库的临时连接。这里又一次的显示出把数据库访问抽象成一个独立文件的好处:你可以从任何地方完成对数据库(译者注:其实应该是对TeaData 类)的访问。
当调用完teaData.insert() 来增加记录到数据库后,我们不再需要数据库连接,因此在我们返回成功前,我们关闭了连接。
在模拟器中运行你的程序,按下“Menu”按键,点击屏幕上的“Add Tea”。试图通过在此按下“Menu”和点击屏幕的 “Save Tea.”来保存空茶叶名的茶叶数据。由于是没有茶叶名,一条错误消息将出现在你的面前:
下一步,试着键入你的茶叶名,并选择合适的泡茶时间,再次从菜单选择 “Save Tea” 。这一次,你将不在看到错误的消息。事实上,你什么都看消息不到。
改进用户体验
这样做不是一个很好的用户体验,用户不能知道他的茶叶是否已经成功地保存了。事实上,用户只有从“Add Tea”界面返回,去茶叶列表中查看这一个办法来检查他的是否成功的被保存。这样的做法不好,让用户知道他们的茶叶数据被成功地保存会是更好的一种方式。在茶叶数据被成功保存后,让我们在屏幕上显示一条成功信息。
我们要一条被动的非模态化的信息,因此AlertDialog这次就不能满足我们的需求了。下面我们将要使用另外一个Android的非常流行的特性,Toast。
Toast 在接近屏幕的下方显示一条消息,但是并不会终止用户的操作。Toast经常用于做非重要的的提醒和状态更新。.
在strings.xml 资源文件中新增一个字符串。注意字符串中的%s。我们在下一步中将保存的茶叶名字结合到这个字符串来显示信息。
< string name ="save_tea_success" > %s tea has been saved. </ string >
注意,在onOptionsItemSelected 代码中进行修改,当saveTea返回真时,创建并显示一条弹出式的Toast。第二参数getString()用来连接茶叶名称到Toast信息中。最后,我们需要将茶叶名称清楚,以便用户可以快速增加更多的新茶。
// …
switch (item.getItemId()) {
case R.id.save_tea:
if (saveTea()) {
Toast.makeText( this , getString(R.string.save_tea_success, teaName.getText().toString()), Toast.LENGTH_SHORT).show();
teaName.setText( "" );
}
// …
现在,重新运行应用程序,并增加和保存一些新茶叶。你将会看到弹出式的Toast并让你知道你的茶叶信息已经被保存成功。getString()方法用于连接存在XML文件中的String和茶叶名称,并将%s替换成茶叶的名称。
按下“Back”按键,返回应用程序的主屏幕,点击茶叶spinner。你新增的在数据库中的茶叶已近可以显示在spinner的选项中!
用户首选项
现在BrewClock已经完成了所有的功能。用户可以增加他们喜爱的茶叶和各自不同的泡茶时间到数据库中,并且他们可以快速的从选择他们并开始泡上一杯新茶。任何新增的茶叶信息都被保存在数据库中,因此,即使你退出你的程序,这些茶叶信息在你下次启动程序时仍然可以从spinner列表中找到。
当你重启BrewClock的时候,有一件事你必须注意,就是泡茶计数被清为了0。这使得跟踪我们每天喝了多少茶(一条重要的数据)变得困难。作为最后一个练习,让我们将泡茶计数保存在我们设备上。
我们将不通过增加茶叶数据库的表来完成这个功能,我们将使用Android的“共享首选项Shared Preferences”,一个Android提供给你应用程序用于存储简单数据的数据库(字符串,数字,等等)。例如,优秀的最高分和用户首选项等(译者注:非常类似Windows下的注册表)。
我们首先在BrewClockActivity.java 中增加一堆常量。这些常量用于存放你的共享首选项的名称。我们将使用键的名称来访问泡茶计数。Android负责保存和持久化我们的共享首选项文件。
protected static final String SHARED_PREFS_NAME = " brew_count_preferences " ;
protected static final String BREW_COUNT_SHARED_PREF = " brew_count " ;
下一步,为了我们能在用户首选项中读写泡茶计数,而不是直接的依赖于代码中的初始值,我们将在代码中做一些修改。在BrewClockActivity 的 onCreate 方法中我们将就该setBrewCount附件的代码:
public void onCreate() {
// …
// Set the initial brew values
SharedPreferences sharedPreferences = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
brewCount = sharedPreferences.getInt(BREW_COUNT_SHARED_PREF, 0 );
setBrewCount(brewCount);
// …
}
这里我们将以使用SharedPreference来获取应用程序的共享首选项的实例,并希望得到brew_count键值的值(通过我们之前定义的BREW_COUNT_SHARED_PREF常量来标示)。如果值能获取,这个值将返回给应用程序,如果没有我们使用getInt的第二参数作为默认值返回(在教程中为0)。
现在我们取得存储的泡茶计数值,我们需要确保每当泡茶计数更新的时候,这个值能写回到共享首选项中。
BrewClockActivity的setBrewCount中增加下面的代码:
public void setBrewCount( int count) {
brewCount = count;
brewCountLabel.setText(String.valueOf(brewCount));
// Update the brewCount and write the value to the shared preferences.
SharedPreferences.Editor editor = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE).edit();
editor.putInt(BREW_COUNT_SHARED_PREF, brewCount);
editor.commit();
}
共享首选项不能直接地保存。我们需要使用Android的SharedPreferences.Editor类。调用SharedPreferences的edit方法,返回一个editor实例,这个实例用来保存我们的首选项值。我们只要调用editor实例的commit方法就可以将值保存到共享首选项中。
我们应用程序的所有代码都已完成,现在让我们测试一下我们的程序!
在模拟器中运行应用程序,定一个泡茶时间(这真是一个良好的借口去泡一杯你自己爱喝的茶哦)并退出应用程序,试着运行模拟器上的安装的其他应用程序确保BrewClock被终止。记住,除非这个应用程序已经不在内存中,否则Android不会终止一个Activity。
当你下一次运行你的应用程序时,你将看见之前的泡茶计数已经被维护了。
总结
恭喜!你已经完成了这个应用的程序的所有开发工作,并使用了Android SDK中的数个核心组件。在本教程中,你从中学到了:
- 创建一个简单的SQLite数据库,并保存你的数据;
- 使用Android的数据库类和编写客户化类抽象数据访问;
- 在你的应用程序中增加选项菜单。;
- 在你应用程序中创建并注册新Activity并使用Intent将他们绑定成一组界面;
- 使用内建的“共享首选项”数据库来保存和提取简单用户数据。
无论你要开发神马样类型的应用程序,数据存储和持久化是一个重要的主题。从工具程序和业务工具到3-D游戏,几乎每个应用程序都需要使用到Android提供的数据工具类。
Activities
虽然BrewClock现在在某方面来说已经是个功能完善的应用程序了。但是我们仍然可以在增加一些功能以改进用户体验。例如你可以使用下面的方法来改进你的应用程序:
- 在保存茶叶的时候检查是否存在茶叶名称重名;
- 增加一个菜单选项以将泡茶统计清0;
- 在共享首选项中保存最后所选的泡茶名称和时间以便程序重启时有一个有意义的默认值;
- 增加用户从茶叶数据库中删除记录的选项。
在GitHub库 可以获取到所有的源代码,库中的未来的分支包含着Activitiy的解决方案 你可以通过切换你的本地代码拷贝到tutorial_2分支,下载这个开发教程源代码:
$ cd BrewClock
$ git checkout tutorial_2
我希望你喜欢这个教程,希望这个教程能帮助你设计和开发更棒的Android应用程序。请通过在下面的回复让我知道你的建议和意见,当然我也欢迎你将你建议写在email中并发送给我。
感谢Anselm的建议和反馈!