第五章:Reminders 实验:第一部分

优质
小牛编辑
118浏览
2023-12-01

到现在为止你已经熟悉了创建一个新项目,编程,和重构的基本操作了。是时候创建一个Android 应用了,或者说成我们所谓的APP。这章将介绍四个实例项目的第一个。这些实例目的是让你熟悉使用Android Studio开发APP的上下文方面。核心功能是允许你创建和删除一个提醒以及标志那些重要的提醒。重要的条目文体左边将被强调黄色标签。这个APP将合同一个动作条菜单,上下文菜单,一个本地的数据库作存储,如果设备支持的话还有多重选择。

图5-1图示了仿真器上完成的app。这个例子介绍了Android基础并且你也会学到如何用内建的SQLite数据库来保存数据。别担心那些你不熟悉的话题,后续的章节将涵括这些话题大量的细节。

注意:为了一致性,我们建议你用Git克隆这个项目,虽然你将从草稿里用它自身的Git存储库重建这个项目。如果你还没有安装Git,请看第7章。在窗口里打开一个Git-bash会话(在Mac或Linux里的终端)并导航到C:\androidBook\reference\(译者注:在Git-bash控制台里可以使用Dos命令cd c:改当前盘为C;dir查看当前目录文件;cd androidBook进入androidBook子目录;cd reference进入reference子目录)(如果没有这个目录创建它)并提交下面的Git命令:git clone https://bitbucket.org/csgerber/reminders.git Reminders

图5-1 完成的app界面

操作Reminders这个app,你可以用动作条的溢出菜单。点击溢出按钮,在菜单栏右侧,看起来象垂直点,打开一个菜单如图5-2有两个选项:新提醒,和退出。点击新提醒打开一个对话框如图5-3。在这个对话框里,你可以为新提醒加入文本并且点提交加到清单里。点击退出。

图5-2 激活溢出菜单的app界面

图5-3 新提醒对话框

点击清单中的随意一个提醒有两个可选方式,如图5-4:编辑提醒和删除提醒。从上下文菜单中点编辑提醒,弹出对话框如图5-5所示,在那里你可以修改提醒里的文本。点击删除提醒则会删掉这个提醒。

图5-4 上下文菜单

图5-5 编辑提醒对话框

开始一个新项目

在Android Studio开始一个新的项目,用新项目向导如同在第1章里所介绍的。输入Reminders作为项目名,设置公司域名为gerber.apress.com,并选Blank Activity模板。保存到C:\androidBook\Reminders。为了我们例子的一致性,这是一个好主意保存你所有的实验项目到同一个目录,比如C:\androidBook (or use ~/androidBook 对于 Mac/Linux来说)。在向导的下一页,选择电话和掌上设备并设置最低支持SDK到API 8:Android 2.2(冻酸奶)。通过设定最低支持SDK到API 8,你让这个app支持目前市场上99%的设备。点击下一步按钮,选择Blank Activity,再点下一步。设置activity名称为RemindersActivity并点完成,如图5-6所示。

图5-6 输入activity名称

Android Studio在设计模式下显示activity_reminder.xml。这是你的主activity的布局,如图5-7示。如同第1章所讨论的,在这刻项目可以运行在仿真器上或设备上。只要你乐意随便用哪个。

图5-7 activity_reminders的设计模式

初始化Git存储库

建立新项目后的第一步必须是用版本控制来管理源代码。所有这本书的例子都用Git,一个流行的版本控制系统,无缝地协同Android Studio工作并且一直是在线免费的。第7章更彻底地探索了Git和版本控制。

如果你还没安装Git,请参考第7章。从主菜单选择VCS>Import into Version Control>Create Git Repository。(在IOS上,选择VCS >VCS Operations>Create Git Repository)图5-8和5-9展示了这个流程。

图5-8 创建一个Git存储库

图5-9 为Git存储库选择根目录

当提示选择Git的初始目录,确认初始化为项目的根目录(再次,在这个例子里是Reminders)。点击OK。

你将注意到项目工具窗里大部分文件变成棕色的,意味着它们被Git跟踪但还没加入到Git的存储库而且时刻表也没加载。一但你的项目被Gitr控制,Android Studio使用一个色彩策略,随着我们项目的进行将会解释更多的细节。当然你也可能在这: jetbrains.com/idea/help/file-status-highlights.html 得更多的细节,如果你想研究的话。

点击位于窗口底部边缘的Changes工具按钮切换打开Changes工具窗口并展开叶子标签的未受版本控制文件。这将显示所有被跟踪的文件。为加载它们,选择未受版本控制文件叶子并按Ctrl+Alt+A | Cmd+Alt+A或右击未受版本控制文件叶子并选择Git>Add。棕色文件将会变成绿色,意味着它们在Git中已阶段化而且现在准备被提交了。

Ctrl+K | Cmd+K来调用提交更改对话框。提交文件是Git版本控制系统记录项目更改的一个过程。如图5-10所示,授权者下拉菜单用于重写当前缺省提交者。让这栏空着吧,这样Android Studio将简单地用你安装Git时设的缺省值。去选Before Commit多选框里所有的选项。把下面的信息放入到Commit Message区里:Initial commit using new project wizard。点击提交按钮并在下拉条目里再次选择提交。

图5-10 提交更改到Git

默认情况下,项目工具窗将会打开。项目工具窗以不同的方式组织你的项目,取决于顶部窗口的下拉菜单所选择的示图。缺省地,下拉菜单是Android示图,它按文件目的组织文件而不是按你电脑操作系统组织文件的方式。当你展开项目工具窗,将注意到三个文件夹在app文件夹下:manifests, java, 和 res。Manifests文件夹里有你的Android manifest文件。Java文件夹是存放java 源文件的地方。Res文件夹保存所有你的Android资源文件。在res目录下的资源可能是XML文件,图象,声音,和其他资源用于帮助定义你的app外观和用户体验。一但你有机会展开Android示图,我们推荐切换到Project示图,它更直观因为它直接反映了你电脑上的目录结构。

构建用户界面

默认情况下,Android Studio打开与主activity相关联的XML布局文件在一个新的编辑选项卡里并设置为设计模式,因而通常你在新项目里先看到的是可视化开发器。可视化开发器让你编辑app的可视化布局。在屏幕的中央是预览面板。预览面板是Android设备渲染你当前编辑的布局结果的虚拟展示。这个展示可控于屏幕上方横跨的布局预览控制。这些控制可调整预览并选择不同风格的Android设备,从智能电话到穿戴设备。你也可以改变布局里所描述的相关主题。屏幕的左边,你会发现控件板。包含了众多的控件和widget,它们可被拖放到正虚拟展示的设备平台上。IDE的右侧包含了一个组件树展示了布局里组件的层次关系。布局使用XML文件。当你在这个可视化开发器作出修改时,这些修改将更新于XML中。你可选择Design或Text选项卡来切换可视化或是文本编辑模式。图5-11标识几个可视化开发器关键区域。

图5-11 可视化开发器界面

工作于图形化编辑器

让我们开始创建reminders的列表项吧。在平台上点击Hello World文本框并删除它。找到ListView控件并拖放到平台里。当你拖动时,IDE将显示变化着的尺寸度量以及排列参考来帮助你定们控件,那些当你拖动靠近它们时会企图抓住边缘。拖放listView它会在屏幕顶部排列。你可以定位在顶部的左边或中间。在定位后,找到在编辑器右下侧的属性示图。设置id属性为reminders_list_viewId属性是你可以在JAVA代码里控件编程参考;且这个是我们将如何参考的ListView在之后修改代码时。修改layout:width属性设置为match_parent。这将扩展控件动态地占有尽可能多的父控件宽度。关于布局在第8章里你会学到更多细节。现在,你的布局将装配成图5-12那样。

图5-12 有个ListViewactivity_reminders的布局

在Android里,一个activity定义了控制用户和app交互的逻辑。当第一次学Android时,把activity想象成你的app的一个屏幕是对你有帮助的,虽然activity要比这个复杂得多。布局文件定义为一个XML,但用早先提到的图形化编辑器可以图形化编辑。

编辑布局的原生XML

点击编辑器左下方的文本选项卡,从图形化编辑切换到文本编辑模式。这带来了布局的原生XML示图,右边伴随着一个预览面板。将RelativeLayout的背景色改为黑色,插入这行:android:background="#181818"android:layout_height="match_parent"下面。颜色用十六进制来表达。可以看下第9章关于十六进制颜色的更多信息。注意到你插入那条设置根示图背景色那行后夹缝里的黑灰色的样本。如果你回到设计模式,会观察到整个布局变成黑灰色了。

直接用硬编码一个颜色值到你的XML文件里不是最好的途径。更好的选择是定义一个colors.xml文件到资源文件夹里,并在那定义你的颜色。在XML文件(如colors.xml)里实现值的原因是那些资源被保存和编辑在一个地方并且在在整个项目里可以轻松地参考。

Ctrl+X | Cmd+X 或通过主菜单的 Edit ➤ Cut选择十六进制的颜色值#181818并剪切它到粘贴板。输入@color/dark_grey到这个位置。这是一个特殊的语法参考到Android资源文件colors.xml,但这个文件还不存在于你的项目里,Android Studio会高亮红色指出这个错误。按Alt+Enter会提示纠正这个错误的选项。选择第二个选项,创建颜色资源dark_grey,接着把刚才的颜色值粘贴到资源值里:下个出现的对话框值域并点击OK。

新颜色值资源对话框将创建Android资源文件colors.xml并填充十六进制的值。点击OK接着在加入文件到Git对话框里还是点OK,这个新文件加入到版本控制,并确保选择Remember,Don’t Ask Again这个复选框因而这个信息下次不会再打扰你了。图5-13演示了这个流程。

图5-13 析出硬代码颜色值到一个资源文件

在预览模式下这个ListView控件包含的行布局在我们所选的背景色时没有足够的对比度。要改变这项表象,你将在另外一个布局文件里定义该布局。右击资源文件夹里的布局文件夹并选择New ➤ Layout Resource File。在新资源文件对话框里输入reminders_row。在根示图组里用线性布局并让其他的为默认值,如图5-14所示。

图5-14 新资源文件对话框

你现在将创建列表项独立的布局。线性布局的根示图组在布局里是最外层的元素。在预览面板顶部的工具栏控制钮设置其走向为垂直方向。当你用这个控制钮时小心点,因为水平线条表示的是垂直方向,反之亦然。图5-15高亮了这个走向按钮。

图5-15 修改走向按钮

在预览面板的右下方找到属性示图。找到layout:height属性并设为50dp。这个控制控件的高度,dp这个单位参照像素独立密度度量系统。这是公开的,Android允许布局参照屏幕重绘时的密度合适地缩放。你可以点击这个示图里的任意属性并增量键入搜索属性,并按上下键继续搜索。

放一个水平线性布局到垂直线性布局里。拖放一个自定义控件到水平线性控件里并设置它的类属性为android.view.View,来创建一个通用的空示图并付予idrow_tab。在写这篇文章时,Android Studio还是有点局限:它不允许你从控件板里拖放通用示图。当你点击CustomView时,将得到一个有不同选项的对话框,没有一个包括了通用的View类。选择任意一个类并放到你的布局里。为这个限制的情况找到类属性并修改为android.view.View。对照清单5-1看看如何完成它

你将会用到这个通用的View选项卡来标记那些提醒条是很重要的。还在文本编辑模式时,修改你的用户自定义示图的layout:width属性为10dplayout:height属性为match_parent。这里用的match_parent值将让这个示图控件象它的父控件一样高。切换到设计模式并拖放一个大文本控件到水平线性布局的元件树上,并设置宽高属性为match_parent。检查下这个大文本元件定位于用户自定义示图控件的右侧。在元件树里,标志文本视图的元件一定是嵌套在水平线性布局元件里面并且在示图元件下面。如果文本视图在示图元件上面,把它拖下来放到第二(即最后)的位置。给这个文本视图一个id值row_text并设置文字尺寸为18spsp这个单位参照像素独立密度度量系统,象dp那样,但它尊照用户的文本尺寸设置,因而比如:如果用户很难看清并想他的手机上的显示更大的文字,sp将尊照这个设置,然而dp不会。因此,用sp作为文字的尺寸单位总是一个好点子。你将在第8章学到有关屏幕度量的更多内容。

最后,设置文本控件的文本属性为"Reminder Text"。切换到文本编辑模式做更多的修改如清单5-1所示。

清单5-1 reminders_row 布局XML代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:orientation="vertical">
    <LinearLayout
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="48dp">
        <view
        android:layout_width="10dp"
        android:layout_height="match_parent"
        class="android.view.View"
        android:id="@+id/row_tab" />
        <TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:textAppearance="?
        android:attr/textAppearanceLarge"
        android:text="Reminder Text"
        android:id="@+id/row_text"
        android:textSize="18sp" />
    </LinearLayout>
</LinearLayout>

现在要创建一些用户颜色了。切换到设计模式。选择元件树线性布局(垂直方向的)的根。设置android:background属性为@color/dark_grey来重用之前所定义的颜色。在元件树里选择row_tab元件并设置它的android:background属性为@color/green。选择row_text元件并设置它的android:textColor属性为@color/white。在这之前,这些颜色并没设置在colors.xml里,象之前那样定义它们。切换到到文本模式。按F2在这两个错误间重复地跳前和跳后并按Alt_Enter带出智能建议。选择第二个建议并在弹出对话框里用#ffffff#003300修正白色和绿色的问题。修正这些问题后,你可以按Ctrl键和左键点击这些颜色将带到colors.xml文件里,如清单5-2代码所示。

Listing 5-2. The colors.xml File

<resources>
    <color name="dark_grey">#181818</color>
    <color name="white">#ffffff</color>
    <color name="green">#003300</color>
</resources>

回到activity_reminders.xml布局文件。现连结新的reminders_row布局到这个布局了。切换到文本模式并加入现面的属性到ListView元素里tools:listitem="@layout/reminders_row",如图5-16所示。

加上这个属性在运行时并没改变布局的表象;它只不过改变这个list view的每个条目的预览面板。要让这个新布局有用,你必须用JAVA代码充实它,这个我们将在后续的章节里展示给你。

图5-16 预览面板现正展示一个用户自定义的ListView布局

加入图形增强

刚刚完成一个ListView条目的用户自定义布局,但你不会停止在这里。加入一些视觉增强元素将让你的app与众不同。看一下文本如何显示在屏幕上。精明点的眼球会看到它有点点偏离中心并跑出绿色选项卡的左边。打开reminder_row布局做点小调整。你希望文本重心朝行内垂直方向的中心去点,并给出点边衬这样和边界就有点视觉上的分离。用清单5-3的代码代替你的TextView元素。

Listing 5-3. TextView Additional Attributes

<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="Reminder Text"
android:id="@+id/row_text"
android:textColor="@color/white"
android:textSize="18sp"
android:gravity="center_vertical"
android:padding="10dp"
android:ellipsize="end"
android:maxLines="1"
/>

增加的省略属性将截去过长的文本以适应该条目以一个省略号结束,然而maxLines属性限制了每个条目的行数为1。最后,在内部的线性布局后面但外部线布局结束之前从清单5-4里加上两个更普通的示图对象,来创建条目下的水平规则。外部的线性布局设置高为50dp,内部的则设置为48dp。这两个通用的view对象将占用剩下的垂直的2dp空间来建造一个斜边。清单5-4如下。

Listing 5-4. Extra Generic Views for beveled edge

<LinearLayout>
    <view
    class="android.view.View"
    android:layout_width="fill_parent"
    android:layout_height="1dp"
    android:background="#000"/>
    <view
    class="android.view.View"
    android:layout_width="fill_parent"
    android:layout_height="1dp"
    android:background="#333"/>
</LinearLayout>

<android.view.View
class="android.view.View"
android:layout_width="fill_parent"
android:layout_height="1dp"
android:background="#000"/>
<android.view.View
class="android.view.View"
android:layout_width="fill_parent"
android:layout_height="1dp"
android:background="#333"/>

译者注:上面代码中的控件view会有些问题,无法解析背景色属性。最好改成android.view.View

加载条目到ListView

现在刚修改的布局将改变activity。打开项目工具窗并在JAVA源代码文件夹下找到RemindersActivity文件。它位于com.apress.gerber.reminders包里。找到onCreate()方法。这是你的类里第一个定义的方法。声明一个ListView成员变量取名mListView按清单5-5那样修改onCreate()方法。你将需要解决引入ListViewArrayAdapter

Listing 5-5. Add List Items to the ListView

public class RemindersActivity extends ActionBarActivity {
    private ListView mListView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_reminders);
        mListView = (ListView) findViewById(R.id.reminders_list_view);
        //The arrayAdatper is the controller in our
        //model-view-controller relationship. (controller)
        ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(
            //context
            this,
            //layout (view)
            R.layout.reminders_row,
            //row (view)
            R.id.row_text,
            //data (model) with bogus data to test our listview
            new String[]{"first record", "second record", "third record"});
        mListView.setAdapter(arrayAdapter);
    }
    //Remainder of the class listing omitted for brevity
}

代码里用你早先定义id属性查找ListView控件,并且去除了默认的条目分隔器,这样我们早先自定义的带斜边分隔器将会很好地渲染。这些代码也创建了一个带有几个例子条目的适配器。适配器是一种特别的JAVA类定义于Android SDK里,它的功能作为在SQLite数据库(模型)里模型-示图-控制器关系的控制器,ListView(示图),还有适配器(控制器)。适配器绑定模型到示图上并且处理更新和刷新。AdapterArrayAdapter的超类,用来绑定数组到示图里。在我们的例子里,这个示图是ListViewArrayAdapter的构造函数有三个参数。第一个是上下文对象用来表达当前activity。适配器也需要知道那个布局和布局里的区域或多个区域用于显示行数据。为满足这个要求,你要传送布局和文本示图条目的id到布局里。最后一个参数是一个字符串数组用于表里的每个条目。如果你这时运行项目,将看到给到ArrayAdapter构造器的那些值显示如图5-17。

图5-17 ListView例子

Ctrl+K | Cmd+K提交更改到Git并填入Adds ListView with custom colors到提交信息里。当你工作于这个项目里,用提交信息描述每次提交的追加/移除/更改来执行增加提交到Git是一个不错的体验。为将来的合作者及用户保持这个习惯来会让事情变得容易点,来鉴别各次单独的提交以及以后的构建记录。

设置溢出菜单的行为条

Android使用一个共同的可视化元素叫做行为条。行为条用于多数app定位导航和其他选项以让用户执行重要的任务。这时运行这个app,你可能注意到一个菜单图标象三个竖直点的那个。这些点被叫做溢出菜单。点击这个溢出菜单图标会产生只有一个叫setting菜单项的菜单。这个菜单项放在那是新项目向导模板的一部分并且本质上是一个占位没有任何执行动作。RemindersActivity装载了menu_reminders.xml文件,可以在res/menu文件夹下找到。让我们象清单5-6那样对这个文件做点改变加上一些新的菜单项。

Listing 5-6. New Menu Items

<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.apress.gerber.reminders.app.RemindersActivity" >
    <item android:id="@+id/action_new"
    android:title="new Reminder"
    android:orderInCategory="100"
    app:showAsAction="never" />
    <item android:id="@+id/action_exit"
    android:title="exit"
    android:orderInCategory="200"
    app:showAsAction="never" />
</menu>

在前述的代码清单里,title属性关联于菜单项的显示文本。因为我们用硬代码设置这些属性值,Andorid Studio将会标记警告。按F2在这些警告间跳跃并按Alt+Enter拉出智能建议。你只需简单地按回车接受第一个建议,输入新字符串资源的名称,接着马上弹出对话框,再按回车接受命名的资源。用new_reminder作为第一个条目的名称,第二个叫exit

打开RemindersActivity并用清单5-7的代码代替onOptionItemSelected()方法。你还需要解决Log类的引入。当你点击app上的一个菜单项时,实时调用这个方法,传入那个被点击的菜单项的引用。Switch语句用MenuItemitemId,执行一个log语句或终结这个activity,取决于哪个菜单项被点击了。这个例子用Log.d()方法写入文本到Android的调试日志里。如果你的app包括多个activity并且这些activity比当前activity先显示,那么调用finish()将简单地把当前activity推出栈并且把控制交给下面的activity。因为RemindersActivity是这个app的唯一activityfinish()方法推出它将导致这个app的终止。

Listing 5-7. onOptionsItemSelected( ) Method Definition

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case R.id.action_new:
            //create new Reminder
            Log.d(getLocalClassName(),"create new Reminder");
            return true;
        case R.id.action_exit:
            finish();
            return true;
        default:
            return false;
    }
}

运行这个app并测试新的菜单选项。点击new Reminder菜单项并观察Android日志里出现的信息。Android DDMS(Dalvik调试监控服务)窗将与仿真器或设备运行app的同时打开,并且你将需要选择调试选项低于Log级别才可看到调试日志。运行你的app并与这些菜单项互动。注意在你点击New Reminder菜单项时Android DDMS出来的日志。最后,按Ctrl+K | Cmd+K并提交你的代码到Git,用Adds new reminder and exit menu options作为提交信息。

保持提醒

因为Reminders这个app需要保存提醒清单,你将需要一个保存策略。Android SDK和运行时系统提供了一个内建的数据库引擎叫SQLite,它设计为有限记忆体环境并很适合于移动设备。这节涵括了SQLite数据库并浏览怎么保存提醒清单。我们的策略将包括一个数据模型,一个数据库代理类,还有一个游标适配器(CursorAdapter)。这个模型将维持数据读写到数据库。代理将是一个适配器类将把从app简单调用转换为到数据库API调用。最后,游标适配器将扩展为一个标准的Android类用于以抽象方式处理数据访问。

数据模型

让我们开始创建数据模型。右击com.apress.gerber.reminders包并选择New ➤ Java Class。命名这个类为Reminder并按回车。如清单5-8那样装裱这个类。这是一个简单的POJO(简单的老JAVA对象)定义了几个实例变量和相应的gettersetter方法。Remider类包含了和个整型的ID,字符串变量,和数值化的重要值。ID是用于标记每个提醒的唯一数字。字符串则保存了提醒的文本。重要值是一个数值化的指示器用来标志一个独立的提醒是否重要(1=重要,0=不重要)。我们更愿意用整型而不是布尔值是因为SQLite数据库没有布尔数据类型。

Listing 5-8. Reminder Class Definition

public class Reminder {
    private int mId;
    private String mContent;
    private int mImportant;
    public Reminder(int id, String content, int important) {
        mId = id;
    mImportant = important;
        mContent = content;
    }
    public int getId() {
        return mId;
    }
    public void setId(int id) {
        mId = id;
    }
    public int getImportant() {
        return mImportant;
    }
    public void setImportant(int important) {
        mImportant = important;
    }
    public String getContent() {
        return mContent;
    }
    public void setContent(String content) {
        mContent = content;
    }
}

现在你将创建一个数据代理。再次,这个代理将转换简单的应用调用为低级别的SQLite API调用。在com.apress.gerber.reminders包里新建一个类叫RemindersDbAdapter。把清单5-9的代码直接加入到这个新类里。当你解析导入时,发现DatabaseHelper并没在Android SDK里。我们将在后面的步骤里定义DatabaseHelper。这些代码定义了列名称和索引;一个TAG作日志;两个上下文对外对象;和一个SQL语句用于创建数据库。

Listing 5-9. Code to be placed inside the RemindersDbAdapter class

//these are the column names
public static final String COL_ID = "_id";
public static final String COL_CONTENT = "content";
public static final String COL_IMPORTANT = "important";
//these are the corresponding indices
public static final int INDEX_ID = 0;
public static final int INDEX_CONTENT = INDEX_ID + 1;
public static final int INDEX_IMPORTANT = INDEX_ID + 2;
//used for logging
private static final String TAG = "RemindersDbAdapter";
private DatabaseHelper mDbHelper;
private SQLiteDatabase mDb;
private static final String DATABASE_NAME = "dba_remdrs";
private static final String TABLE_NAME = "tbl_remdrs";
private static final int DATABASE_VERSION = 1;
private final Context mCtx;
//SQL statement used to create the database
private static final String DATABASE_CREATE =
    "CREATE TABLE if not exists " + TABLE_NAME + " ( " +
    COL_ID + " INTEGER PRIMARY KEY autoincrement, " +
    COL_CONTENT + " TEXT, " +
    COL_IMPORTANT + " INTEGER );";

SQLite API

DatabaseHelper是一个SQLite API类,用于打开和关闭数据库。它用到了上下文Context,这是一个抽象的Android类以提供到Android操作系统的访问。DatabaseHelper是一个用户自定义类,且必须由你来定义。用清单5-10实施DatabaseHelper类,作为RemindersDbAdapter的内部类。把这些代码放到RemindersDbAdapter后段,但在类结束的花括号的前面。

Listing 5-10. RemindersDbAdapter

private static class DatabaseHelper extends SQLiteOpenHelper {
    DatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    @Override
    public void onCreate(SQLiteDatabase db) {
        Log.w(TAG, DATABASE_CREATE);
        db.execSQL(DATABASE_CREATE);
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
            + newVersion + ", which will destroy all old data");
        db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
        onCreate(db);
    }
}

DatabaseHelper继承于QLiteOpenHelper,用于以特殊的回调方法来维护数据库。回调方法是运行时环境在应用的整个生命周期内都调用的方法,并且它们用到SQLiteDatabase db变量提供SQL命令的执行。在构造器里数据库就被初始化了。构造器传递数据库名和版本到它的父类;然后父类做数据库的设置繁杂工作。当需要建立数据库时onCreate()方法被运行时自动调用。这个操作只运行一次,当app首次启动并且数据库还未创建时。onUpgrade()方法是在数据库更新时被调用,例如假如开发者改变纲要。如果你改变数据库的纲要,确保给DATABASE_VERSION增加1,然后onUpgrade()会处理剩下的事情。如果你忘了给DATABASE_VERSION加1,你的app将崩溃即便在调试构建模式下。在前述的代码里,在运行onCreate()方法之前我们运行一个SQL命令来移除数据库里唯一的表单,以方便重建表单。

清单5-11演示了用DatabaseHelper来打开和关闭数据库。构造器保存了上下文的一个实例,它会被传送到DatabaseHelperopen()方法初始化helper并用它得到一个数据库的实例,直到close()方法用于关闭数据库。把这些代码加入到RemindersDbAdapter类里,在所有成员变量之后和DatabaseHelper内部类之前。当你解析导入时,用android.database.SQLException类。

Listing 5-11. Database Open and Close Methods

public RemindersDbAdapter(Context ctx) {
    this.mCtx = ctx;
}
//open
public void open() throws SQLException {
    mDbHelper = new DatabaseHelper(mCtx);
    mDb = mDbHelper.getWritableDatabase();
}
//close
public void close() {
    if (mDbHelper != null) {
        mDbHelper.close();
    }
}

清单5-12包含了所有处理在tbl_remdrs表单里创建,读取,更新,和删除Reminder对象的操作。通常参考CRUD操作;CRUD即代表创建,读取,更新,删除。加入这些代码到RemindersDbAdapter类的close()方法之后。

Listing 5-12. Database CRUD Operations

//CREATE
//note that the id will be created for you automatically
public void createReminder(String name, boolean important) {
    ContentValues values = new ContentValues();
    values.put(COL_CONTENT, name);
    values.put(COL_IMPORTANT, important ? 1 : 0);
    mDb.insert(TABLE_NAME, null, values);
}
//overloaded to take a reminder
public long createReminder(Reminder reminder) {
    ContentValues values = new ContentValues();
    values.put(COL_CONTENT, reminder.getContent()); // Contact Name
    values.put(COL_IMPORTANT, reminder.getImportant()); // Contact Phone Number
    // Inserting Row
    return mDb.insert(TABLE_NAME, null, values);
}
//READ
public Reminder fetchReminderById(int id) {
    Cursor cursor = mDb.query(TABLE_NAME, new String[]{COL_ID,
        COL_CONTENT, COL_IMPORTANT}, COL_ID + "=?",
        new String[]{String.valueOf(id)}, null, null, null, null
    );
    if (cursor != null)
        cursor.moveToFirst();
    return new Reminder(
        cursor.getInt(INDEX_ID),
        cursor.getString(INDEX_CONTENT),
        cursor.getInt(INDEX_IMPORTANT)
    );
}
public Cursor fetchAllReminders() {
    Cursor mCursor = mDb.query(TABLE_NAME, new String[]{COL_ID,
        COL_CONTENT, COL_IMPORTANT},
        null, null, null, null, null
    );
    if (mCursor != null) {
        mCursor.moveToFirst();
    }
    return mCursor;
}
//UPDATE
public void updateReminder(Reminder reminder) {
    ContentValues values = new ContentValues();
    values.put(COL_CONTENT, reminder.getContent());
    values.put(COL_IMPORTANT, reminder.getImportant());
    mDb.update(TABLE_NAME, values,
        COL_ID + "=?", new String[]{String.valueOf(reminder.getId())});
}
//DELETE
public void deleteReminderById(int nId) {
    mDb.delete(TABLE_NAME, COL_ID + "=?", new String[]{String.valueOf(nId)});
}
public void deleteAllReminders() {
    mDb.delete(TABLE_NAME, null, null);
}

每个方法都用到了SQLiteDatabase mDb变量来生成和执行SQL语句。如果你熟悉SQL,你会猜到这些SQL语句将由INSERT, SELECT, UPDATE, 或 DELETE组成。

两个创建方法用到了特别的ContentValues对象,这个是数据载体用于在insert方法里传送数据值到数据库对象。在insert语句里数据库最终将转换这些对象到数据库。有两个读方法,一个用于引出单个提醒另一个则引出一个游标以遍历所有的提醒。晚点在一个特别的Adapter类里你将用到游标。

更新方法象第二个创建方法。无论怎样,这个方法调用了底层的数据库对象的更新方法,那个将生成并执行一个更新SQL语句而不是一个插入语句。

最后,有两个删除方法。第一个针对特定的提醒用 id参数和数据库对象来生成和执行一条删除语句。第二个方法需要数据库生成并执行一条删除语句来移除所有表单里的提醒。

最后,有两个删除方法。第一个针对特定的提醒用 id参数和数据库对象来生成和执行一条删除语句。第二个方法需要数据库生成并执行一条删除语句来移除所有表单里的提醒。

这时,你需要一个手段从数据库提出提醒并放入到ListView。清单5-13演示了必要的逻辑,通过继承之前你看到的特别的Adapter Android类来绑定数据库值到单独的行对象。创建一个新类RemindersSimpleCursorAdaptercom.apress.gerber.reminders包下,并完成处理代码。当解析导入类时,使用android.support.v4.widget.SimpleCursorAdapter类。

Listing 5-13. RemindersSimpleCursorAdapter Code

public class RemindersSimpleCursorAdapter extends SimpleCursorAdapter {
    public RemindersSimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags) {
        super(context, layout, c, from, to, flags);
    }
    //to use a viewholder, you must override the following two methods and define a ViewHolder class
    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        return super.newView(context, cursor, parent);
    }
    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        super.bindView(view, context, cursor);
        ViewHolder holder = (ViewHolder) view.getTag();
        if (holder == null) {
            holder = new ViewHolder();
            holder.colImp = cursor.getColumnIndexOrThrow(RemindersDbAdapter.COL_IMPORTANT);
            holder.listTab = view.findViewById(R.id.row_tab);
            view.setTag(holder);
        }
        if (cursor.getInt(holder.colImp) > 0) {
            holder.listTab.setBackgroundColor(context.getResources().getColor(R.color.orange));
        } else {
            holder.listTab.setBackgroundColor(context.getResources().getColor(R.color.green));
        }
    }
    static class ViewHolder {
        //store the column index
        int colImp;
        //store the view
        View listTab;
    }
}

我们用适配器把所有提醒登记到ListView。在运行时中,ListView将重复调用在适配器里的bindView()方法,以单独的示图对象作为用户装载器,且在清单里滚动。填入这些清单条目到示图里是适配器的工作。在这个例子代码里,我们使用了适配器的子类SimpleCursorAdapter。这个类用了一个游标对象,它保存着跟踪表单里的行轨迹。

这里你看到了一个ViewHolder模式的例子。这是一个容易认识的Android模式,每个ViewHolder对象含有一个示图的标签。用数据源(在这个例子里是Cursor)的值,这个对象加载清单里的示图对象。ViewHolder定义为一个内部静态类,有两个实例变量,一个用于索引重要的表单项另一个用于在布局里定义的row_tab示图。

bindView()方法开始于父类在示图里从游标到元素的map值方法的调用。然后检查看(holder)是否附有一个标签还是有必要创建一个新的(holder)。然后bindView()方法用重要列索引和早先定义的row_tab配置容器的实例变量。在容器被发现或配置后,从当前提醒的COL_IMPORTANT常量秋决定row_tab用什么颜色。这个例子用了新的橙色,那个你要加到colors.xml: <color name="orange">#ffff381a</color>

早先你用了ArrayAdapter来管理模型和示图之间的关系。SimpleCursorAdapter也是用同样的模式,虽然它的模型是SQLite数据库。将清单5-14更改到你的RemindersDbAdaperRemindersSimpleCursorAdapter类里。

Listing 5-14. RemindersActivity Code

public class RemindersActivity extends ActionBarActivity {
    private ListView mListView;
    private RemindersDbAdapter mDbAdapter;
    private RemindersSimpleCursorAdapter mCursorAdapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_reminders);
        mListView = (ListView) findViewById(R.id.reminders_list_view);
        mListView.setDivider(null);
        mDbAdapter = new RemindersDbAdapter(this);
        mDbAdapter.open();
        Cursor cursor = mDbAdapter.fetchAllReminders();
        //from columns defined in the db
        String[] from = new String[]{
            RemindersDbAdapter.COL_CONTENT
        };
        //to the ids of views in the layout
        int[] to = new int[]{
            R.id.row_text
        };
        mCursorAdapter = new RemindersSimpleCursorAdapter(
            //context
            RemindersActivity.this,
            //the layout of the row
            R.layout.reminders_row,
            //cursor
            cursor,
            //from columns defined in the db
            from,
            //to the ids of views in the layout
            to,
            //flag - not used
        0);
        // the cursorAdapter (controller) is now updating the listView (view)
        //with data from the db (model)
        mListView.setAdapter(mCursorAdapter);
    }
    //Abbreviated for brevity
}

这时候如果你运行app,你还是看不到清单里有任何东西;屏幕完全是空的,因为你最后的修改在例子的数据部分插入了数据库功能。按Ctrl+K | Cmd+K并提交的修改,提交信息:Adds SQLite database persistence for reminders and a new color for important reminders。聪明点的话,你可能想弄清楚怎么用新的RemindersDbAdaper把例子里的条目加回来。这将在下章描述,你可以继续看下去并检查下作业了。

小结

致此,你有了个成熟的Android应用。在这章,你学会了怎样设置第一个Android项目并用Git控制代码。你也探索了怎么编辑Android布局,用设计或文本模式。你也看到建立一个在动作条里的溢出菜单。这章的最后探索了ListViewAdapter,以及绑定数据到内建的SQLite数据库。在接下来的章节里,增加创建和编辑提醒的功能,你将完成这个app。