安卓编程初学者手册第三版-八-

180 阅读37分钟

安卓编程初学者手册第三版(八)

原文:zh.annas-archive.org/md5/ceefdd89e585c59c20db6a7760dc11f1

译者:飞龙

协议:CC BY-NC-SA 4.0

第二十六章:使用导航抽屉和片段进行高级 UI

在本章中,我们将看到(可以说是)最先进的 UI。NavigationView小部件或导航抽屉,因为它滑出其内容的方式,可以通过在创建新项目时选择它作为模板来简单地创建。我们将这样做,然后我们将检查自动生成的代码并学习如何与其交互。然后,我们将使用我们对Fragment的所有了解来为每个“抽屉”填充不同的行为和视图。然后在下一章中,我们将学习数据库,为每个Fragment添加一些新功能。

以下是本章我们将要做的事情:

  • 介绍NavigationView

  • 开始使用简单的数据库应用程序

  • 基于自动生成的 Android Studio 模板实现NavigationView项目

  • NavigationView添加多个片段和布局

让我们来看看这个非常酷的 UI 模式。

技术要求

您可以在 GitHub 上找到本章的代码文件github.com/PacktPublishing/Android-Programming-for-Beginners-Third-Edition/tree/main/chapter%2026

介绍 NavigationView

NavigationView有什么好处?嗯,可能会吸引你的第一件事是它可以看起来非常时尚。看看下一个屏幕截图,展示了 Google Play 应用中NavigationView的操作:

图 26.1–NavigationView 在操作中

图 26.1–NavigationView 在操作中

老实说,从一开始,我们的 UI 不会像 Google Play 应用程序中的那样花哨。但是我们的应用程序中将存在相同的功能。

这个 UI 的另一个很棒的地方是它在需要时滑动隐藏/显示自己的方式。正是因为这种行为,它可以是一个相当大的尺寸,使得它在放置选项时非常灵活,当用户完成后,它会完全消失,就像一个抽屉一样。

如果您还没有尝试过,我建议现在尝试一下 Google Play 应用程序,看看它是如何工作的。

您可以从屏幕的左边缘滑动手指,抽屉会慢慢滑出。当然,您也可以以相反的方向将其滑开。

在导航抽屉打开时,屏幕的其余部分会略微变暗(如前一个屏幕截图所示),帮助用户专注于提供的导航选项。

您还可以在打开导航抽屉时在任何地方点击,它会自动滑开,为应用程序的其余部分留出整个屏幕。

抽屉也可以通过点击左上角的菜单图标打开。

我们还可以调整和完善导航抽屉的行为,这是本章末尾我们将看到的。

检查简单的数据库应用程序

在本章中,我们将专注于创建NavigationView并用四个Fragment类实例及其各自的布局填充它。在下一章中,我们将学习并实现数据库功能。

数据库应用程序的屏幕如下。这是我们NavigationView布局的全部荣耀。请注意,当使用NavigationView Activity 模板时,默认情况下提供了许多选项和大部分外观和装饰。

图 26.2–NavigationView 布局

图 26.2–NavigationView 布局

四个主要选项是我们将添加到 UI 中的内容。它们是插入删除搜索结果。布局如下所示,并描述了它们的目的。

插入

第一个屏幕允许用户将人名和他们的年龄插入到数据库中:

图 26.3–插入

图 26.3–插入

这个简单的布局有两个EditText小部件和一个按钮。用户将输入姓名和年龄,然后点击插入按钮将它们添加到数据库中。

删除

这个屏幕更简单。用户将在EditText小部件中输入姓名,然后点击按钮:

图 26.4 – 删除

图 26.4 – 删除

如果输入的姓名在数据库中存在,则该条目(姓名和年龄)将被删除。

搜索

这个布局与上一个布局基本相同,但目的不同:

图 26.5 – 搜索

图 26.5 – 搜索

用户将在EditText小部件中输入姓名,然后点击搜索按钮。如果数据库中存在该姓名,则将显示该姓名以及匹配的年龄。

结果

这个屏幕显示了整个数据库中的所有条目:

图 26.6 – 结果

图 26.6 – 结果

让我们开始使用导航抽屉。

开始简单数据库项目

在 Android Studio 中创建一个新项目。将其命名为Age Database,使用Navigation Drawer Activity模板。在我们做任何其他事情之前,值得在模拟器上运行应用程序,看看作为模板的一部分自动生成了多少内容:

图 26.7 – 主页

图 26.7 – 主页

乍一看,它只是一个普通的布局,带有一个TextView小部件。但是从屏幕左边缘滑动或按菜单按钮,导航抽屉布局就会显现出来:

图 26.8 – 导航页面

图 26.8 – 导航页面

现在我们可以修改选项并为每个选项插入一个带有布局的Fragment。为了理解它是如何工作的,让我们检查一些自动生成的代码。

探索自动生成的代码和资源

打开res/menu文件夹。注意有一个额外的文件名为activity_main_drawer.xml。接下来的代码是从这个文件中摘录出来的,所以我们可以讨论它的内容:

<group android:checkableBehavior="single">
     <item
          android:id="@+id/nav_home"
          android:icon="@drawable/ic_menu_camera"
          android:title="@string/menu_home" />
     <item
          android:id="@+id/nav_gallery"
          android:icon="@drawable/ic_menu_gallery"
          android:title="@string/menu_gallery" />
     <item
          android:id="@+id/nav_slideshow"
          android:icon="@drawable/ic_menu_slideshow"
          android:title="@string/menu_slideshow" />
</group>

注意group标签中有四个item标签。现在注意从上到下的title标签与自动生成的导航抽屉菜单中的三个文本选项完全对应。还要注意,在每个item标签中,有一个id标签,因此我们可以在我们的 Java 代码中引用它们,以及一个icon标签,它对应于drawable文件夹中的一个图标,并且是在导航抽屉中选项旁边显示的图标。

还有一些我们不会使用的自动生成的文件。

让我们编写基于Fragment的类和它们的布局。

编写片段类和它们的布局

我们将创建四个类,包括加载布局的代码以及实际的布局,但在学习了下一章关于 Android 数据库之后,我们不会将任何数据库功能放入 Java 中。

在我们有了四个类和它们的布局之后,我们将看到如何从导航抽屉菜单中加载它们。到本章结束时,我们将拥有一个完全工作的导航抽屉,让用户在片段之间切换,但是片段在下一章之前实际上没有任何功能。

创建类和布局的空文件

通过右键单击layout文件夹并选择content_insert,第二个content_delete,第三个content_search和第四个content_results来创建四个带有垂直LinearLayout作为父视图的布局文件。除了LinearLayout选项和文件名之外,所有选项都可以保持默认值。

现在你应该有四个包含LinearLayout父视图的新布局文件。

让我们编写相关的 Java 类。

编写类

通过右键单击包含MainActivity.java文件的文件夹,并选择InsertFragmentDeleteFragmentSearchFragmentResultsFragment来创建四个新类。从名称上就可以明白哪些片段将显示哪些布局。

为了明确起见,让我们向每个类添加一些代码,使类扩展Fragment并加载其关联的布局。

打开InsertFragment.java并编辑它以包含以下代码:

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
public class InsertFragment extends Fragment {

   @Override
   public View onCreateView(
                LayoutInflater inflater, 
                ViewGroup container, 
                Bundle savedInstanceState) {

          View v = inflater.inflate(
                      R.layout.content_insert, 
                      container, false);

          // Database and UI code goes here in next chapter
          return v;
    }
}

打开DeleteFragment.java并编辑它以包含以下代码:

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
public class DeleteFragment extends Fragment {

   @Override
   public View onCreateView(
                LayoutInflater inflater, 
                ViewGroup container, 
                Bundle savedInstanceState) {

          View v = inflater.inflate(
                      R.layout.content_delete, 
                      container, false);

         // Database and UI code goes here in next chapter

         return v;
    }
}

打开SearchFragment.java并编辑它以包含以下代码:

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
public class SearchFragment extends Fragment{
   @Override
    public View onCreateView(
                LayoutInflater inflater, 
                ViewGroup container, 
                Bundle savedInstanceState) {

           View v = inflater.inflate(
                      R.layout.content_search,
                      container, false);

           // Database and UI code goes here in next 
           chapter

         return v;
    }
}

打开ResultsFragment.java并编辑它以包含以下代码:

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
public class ResultsFragment extends Fragment {
    @Override
    public View onCreateView(
                LayoutInflater inflater, 
                ViewGroup container, 
                Bundle savedInstanceState) {

         View v = inflater.inflate(
                      R.layout.content_results, 
                      container, false);
         // Database and UI code goes here in next chapter

         return v;
    }
}

每个类完全没有功能,除了在onCreateView方法中,从关联的布局文件加载适当的布局。

让我们向之前创建的布局文件添加 UI。

设计布局

正如我们在本章开始时所看到的,所有的布局都很简单。使您的布局与我的完全相同并不是必要的,但是 ID 值必须相同,否则我们在下一章中编写的 Java 代码将无法工作。

设计 content_insert.xml

从调色板的Text类别中拖放两个Plain Text小部件到布局中。请记住,Plain Text小部件是EditText实例。现在在两个Plain Text小部件之后将一个Button小部件拖放到布局中。

根据此表配置小部件:

这是您的布局在 Android Studio 的设计视图中应该是什么样子的:

图 26.9 - 插入布局

图 26.9 - 插入布局

设计 content_delete.xml

Plain Text拖放到布局中,下面是一个Button小部件。根据此表配置小部件:

这是您的布局在 Android Studio 的设计视图中应该是什么样子的:

图 26.10 - 删除布局

图 26.10 - 删除布局

设计 content_search.xml

将一个Plain Text,然后是一个按钮,然后是一个常规的TextView拖放到布局中,然后根据此表配置小部件:

这是您的布局在 Android Studio 的设计视图中应该是什么样子的:

图 26.11 - 搜索布局

图 26.11 - 搜索布局

设计 content_results.xml

将单个TextView小部件(这次不是Plain Text/EditText)拖放到布局中。我们将在下一章中看到如何将整个列表添加到这个单个TextView小部件中。

根据此表配置小部件:

这是您的布局在 Android Studio 的设计视图中应该是什么样子的:

图 26.12 - 结果布局

图 26.12 - 结果布局

现在我们可以使用基于Fragment的类及其布局。

使用 Fragment 类及其布局

这个阶段有三个步骤。首先,我们需要编辑导航抽屉布局的菜单,以反映用户的选项。接下来,我们需要在布局中添加一个View实例,以容纳当前Fragment实例,最后,我们需要在MainActivity.java中添加代码,以在用户点击菜单时在不同的Fragment实例之间切换。

编辑导航抽屉菜单

在项目资源管理器的res/menu文件夹中打开activity_main_drawer.xml文件。编辑我们之前看到的group标签内的代码,以反映我们的菜单选项插入删除搜索结果

<group android:checkableBehavior="single">
   <item
         android:id="@+id/nav_insert"
         android:icon="@drawable/ic_menu_camera"
         android:title="Insert" />
   <item
         android:id="@+id/nav_delete"
         android:icon="@drawable/ic_menu_gallery"
         android:title="Delete" />
   <item
         android:id="@+id/nav_search"
         android:icon="@drawable/ic_menu_slideshow"
         android:title="Search" />
   <item
         android:id="@+id/nav_results"
         android:icon="@drawable/ic_menu_camera"
         android:title="Results" />
</group>

请注意,结果项重用了相机图标。如果您希望添加自己的唯一图标,这是您的挑战。

现在我们可以在主布局中添加一个布局,以容纳当前活动的片段。

向主布局添加一个持有者

打开content_main.xml文件在layout文件夹中。找到以下现有的代码,这是当前不适合我们用途的当前片段持有者:

<fragment
     android:id="@+id/nav_host_fragment"
     android:name="androidx.navigation
          .fragment.NavHostFragment"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     app:defaultNavHost="true"
     app:layout_constraintLeft_toLeftOf="parent"
     app:layout_constraintRight_toRightOf="parent"
     app:layout_constraintTop_toTopOf="parent"
     app:navGraph="@navigation/mobile_navigation" />

删除前面的代码,并在ConstraintLayout的结束标签之前用以下 XML 代码替换它:

    <FrameLayout
        android:id="@+id/fragmentHolder"
        android:layout_width="368dp"
        android:layout_height="495dp"
        tools:layout_editor_absoluteX="8dp"
        tools:layout_editor_absoluteY="8dp">
    </FrameLayout>

切换到设计视图并单击推断约束按钮以固定新布局。

现在我们有一个id属性为fragmentHolderFrameLayout小部件,我们可以获取其引用并加载所有我们的Fragment实例布局。

编写 MainActivity.java 类

用以下内容替换所有现有的import指令:

import android.os.Bundle;
import com.google.android.material.
            floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import android.view.View;
import com.google.android.material.navigation.
        NavigationView;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.FragmentTransaction;
import android.view.MenuItem;

打开MainActivity.java文件并编辑整个代码以匹配以下内容。

注意

最快的方法可能是删除除我们刚刚添加的import指令之外的所有内容。

接下来我们将讨论代码,因此请仔细研究变量名称和各种类及其相关方法。

public class MainActivity extends AppCompatActivity
        implements NavigationView.
OnNavigationItemSelectedListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        FloatingActionButton fab = findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "
                Replace with your own action", 
                Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });
        DrawerLayout drawer = 
        findViewById(R.id.drawer_layout);
        ActionBarDrawerToggle toggle = new 
        ActionBarDrawerToggle(
                this, drawer, toolbar, 
                      R.string.navigation_drawer_open, 
                      R.string.navigation_drawer_close);

        drawer.addDrawerListener(toggle);
        toggle.syncState();
        NavigationView navigationView 
        = findViewById(R.id.nav_view);
        navigationView
        .setNavigationItemSelectedListener(this);
    }
}

在前面的代码中,onCreate方法处理了我们 UI 的一些方面。 代码获取了与我们刚刚看到的布局相对应的DrawerLayout小部件的引用。 代码还创建了一个ActionBarDrawerToggle的新实例,它允许控制/切换抽屉。 接下来,引用被捕获到导航抽屉本身的布局文件(nav_view),代码的最后一行设置了NavigationView上的监听器。

现在按照以下方式添加onBackPressed方法:

@Override
public void onBackPressed() {
     DrawerLayout drawer = 
     findViewById(R.id.drawer_layout);
     if (drawer.isDrawerOpen(GravityCompat.START)) {
          drawer.closeDrawer(GravityCompat.START);
     } else {
          super.onBackPressed();
     }
}

onBackPressed方法是 Activity 的一个重写方法,它处理用户在设备上按返回按钮时发生的情况。 代码关闭抽屉(如果打开),如果没有打开,则简单地调用super.onBackPressed。 这意味着如果抽屉打开,返回按钮将关闭抽屉,如果已经关闭,则具有默认行为。

添加onCreateOptionsMenuonOptionsItemSelected方法,这些方法在此应用程序中并没有真正使用,但将为options按钮添加默认功能:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
     // Inflate the menu; this adds items to the action bar 
     if it is present.
     getMenuInflater().inflate(R.menu.main, menu);
     return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
     // Handle action bar item clicks here. The action bar 
     will
     // automatically handle clicks on the Home/Up button, 
     so long
     // as you specify a parent activity in 
     AndroidManifest.xml.
     int id = item.getItemId();
     //noinspection SimplifiableIfStatement
     if (id == R.id.action_settings) {
          return true;
     }
     return super.onOptionsItemSelected(item);
}

现在添加下面显示的onNavigatioItemSelected方法:

@Override
public boolean onNavigationItemSelected(MenuItem item) {
     // Handle navigation view item clicks here.
     // Create a transaction
     FragmentTransaction transaction = 
          getSupportFragmentManager().beginTransaction();
     int id = item.getItemId();
     if (id == R.id.nav_insert) {
          // Create a new fragment of the appropriate type
          InsertFragment fragment = new InsertFragment();
          // What to do and where to do it
          transaction.replace(R.id.fragmentHolder, 
          fragment);
     } else if (id == R.id.nav_search) {
          SearchFragment fragment = new SearchFragment();
          transaction.replace(R.id.fragmentHolder, 
          fragment);
     } else if (id == R.id.nav_delete) {
          DeleteFragment fragment = new DeleteFragment();
          transaction.replace(R.id.fragmentHolder, 
          fragment);
     }  else if (id == R.id.nav_results) {
          ResultsFragment fragment = new ResultsFragment();
          transaction.replace(R.id.fragmentHolder, 
          fragment);
     }
     // Ask Android to remember which
     // menu options the user has chosen
     transaction.addToBackStack(null);
     // Implement the change
     transaction.commit();
     DrawerLayout drawer = 
     findViewById(R.id.drawer_layout);
     drawer.closeDrawer(GravityCompat.START);
     return true;
}

让我们来看看onNavigationItemSelected方法中的代码。 大部分代码应该看起来很熟悉。 对于我们的每个菜单选项,我们都创建了一个相应类型的新Fragment,并将其插入到具有fragmentHolder属性值的RelativeLayout中。

最后,对于MainActivity.java文件,transaction.addToBackStack方法意味着所选的Fragment实例将被记住,以便与其他实例一起使用。 这样做的结果是,如果用户选择insert片段,然后选择results片段,然后点击返回按钮,那么应用程序将返回用户到insert片段。

现在可以运行应用程序并使用导航抽屉菜单在所有不同的Fragment实例之间切换。 它们看起来就像本章开头的屏幕截图一样,但目前还没有任何功能。

总结

在本章中,我们看到了拥有吸引人和令人愉悦的 UI 是多么简单,尽管我们的Fragment实例目前还没有任何功能,但一旦我们学会了数据库,它们就已经准备好了。

在下一章中,我们将学习关于数据库的一般知识,Android 应用程序可以使用的特定数据库,然后我们将为我们的Fragment类添加功能。

第二十七章:Android 数据库

如果我们要制作提供给用户重要功能的应用程序,那么我们几乎肯定需要一种管理、存储和过滤大量数据的方法。

使用 JSON 可以高效地存储大量数据,但当我们需要有选择地使用数据而不仅仅限制于“保存所有”和“加载所有”的选项时,我们需要考虑其他可用的选项。

一门优秀的计算机科学课程可能会教你处理排序和过滤数据所需的算法,但所需的工作量会相当大,我们能否想出与 Android API 提供的解决方案一样好的解决方案的机会有多大呢?

像往常一样,使用 Android API 中提供的解决方案是最合理的。正如我们所见,JSONSharedPreferences类有它们的用途,但在某个时候,我们需要转向使用真正的数据库来解决现实世界的问题。Android 使用 SQLite 数据库管理系统,正如您所期望的那样,有一个 API 可以使其尽可能简单。

在本章中,我们将做以下事情:

  • 确切地了解数据库是什么

  • 了解 SQL 和 SQLite 是什么

  • 学习 SQL 语言的基础知识

  • 看一下 Android SQLite API

  • 编写在上一章开始的 Age Database 应用程序

技术要求

您可以在 GitHub 上找到本章的代码文件github.com/PacktPublishing/Android-Programming-for-Beginners-Third-Edition/tree/main/chapter%2027

数据库 101

让我们回答一大堆与数据库相关的问题,然后我们就可以开始制作使用 SQLite 的应用程序。

什么是数据库?

数据库既是存储的地方,也是检索、存储和操作数据的手段。在学习如何使用之前,能够想象数据库是有帮助的。实际上,数据库内部的结构因所涉及的数据库而异。SQLite 实际上将所有数据存储在一个单个文件中。

然而,如果我们将我们的数据视为电子表格,或者有时是多个电子表格,它会极大地帮助我们理解。我们的数据库,就像电子表格一样,将被分成多个列,代表不同类型的数据,和行,代表数据库的条目。

想象一个具有姓名和考试成绩的数据库。看一下这种数据的可视化表示,并想象它在数据库中会是什么样子:

图 27.1 – 数据库示例

图 27.1 – 数据库示例

然而,请注意,还有一列额外的数据:一个ID列。随着我们的进行,我们将更多地谈论这个。这种类似电子表格的结构称为。如前所述,数据库中可能有多个表。表的每一列都将有一个名称,在与数据库交谈时可以引用该名称。

什么是 SQL?

SQL代表Structured Query Language。这是用于处理数据库的语法。

什么是 SQLite?

SQLite 是 Android 所青睐的数据库系统的名称,并且它有自己的 SQL 版本。SQLite 版本的 SQL 需要稍微不同的原因是数据库具有不同的特性。

接下来的 SQL 语法入门将专注于 SQLite。

SQL 语法入门

在我们学习如何在 Android 中使用 SQLite 之前,我们需要首先学习如何在一般情况下使用 SQLite 的基础知识。

让我们看一些示例 SQL 代码,可以直接在 SQLite 数据库上使用,而不需要任何 Java 或 Android 类;然后我们可以更容易地理解我们的 Java 代码在后面做什么。

SQLite 示例代码

SQL 有关键字,就像 Java 一样,会引起一些事情发生。以下是一些我们很快将要使用的 SQL 关键字的例子:

  • INSERT:允许我们向数据库添加数据

  • DELETE:允许我们从数据库中删除数据

  • SELECT:允许我们从数据库中读取数据

  • WHERE:允许我们指定数据库的部分,匹配特定条件,我们想要在其上使用INSERTDELETESELECT

  • FROM:用于指定数据库中的表或列名

注意

SQLite 的关键字远不止这些;要查看完整的关键字列表,请查看此链接:sqlite.org/lang_keywords.html

除了关键字之外,SQL 还有类型。以下是一些 SQL 类型的示例:

  • 整数:正好适合存储整数

  • 文本:非常适合存储简单的姓名或地址

  • 实数:用于存储大浮点数

注意

SQLite 的类型远不止这些;要查看完整的类型列表,请查看此链接:www.sqlite.org/datatype3.html

让我们看看如何将这些类型与关键字结合起来,使用完整的 SQLite 语句创建表格并添加、删除、修改和读取数据。

创建表格

我们可能会问为什么我们不先创建一个新的数据库。原因是每个 Android 应用程序默认都可以访问一个 SQLite 数据库。该数据库对该应用程序是私有的。以下是我们在该数据库中创建表格的语句。我已经突出显示了一些部分,以便更清楚地理解语句:

create table StudentsAndGrades 
   _ID integer primary key autoincrement not null,
   name text not null,
   score int;

上述代码创建了一个名为StudentsAndGrades的表,其中有一个整数行 ID,每次添加一行数据时都会自动增加(递增)。

该表还将有一个name列,其类型为text,并且不能为空(not null)。

它还将有一个score列,其类型为int。同时,注意语句以分号结束。

向数据库中插入数据

以下是我们如何向数据库插入一行新数据的方式:

INSERT INTO StudentsAndGrades
   (name, score)
   VALUES
   ("Bart", 23);

上述代码向数据库添加了一行。在上述语句之后,数据库将有一个条目,其列(_IDnamescore)的值为(1Bart23)。

以下是我们如何向数据库插入另一行新数据的方式:

INSERT INTO StudentsAndGrades
   (name, score)
   VALUES
   ("Lisa", 100);

上述代码添加了一个新的数据行,其列(_IDnamescore)的值为(2Lisa100)。

我们的类似电子表格的结构现在看起来如下:

图 27.2 - 更新后的电子表格

图 27.2 - 更新后的电子表格

从数据库中检索数据

以下是我们如何从数据库中访问所有的行和列:

SELECT * FROM StudentsAndGrades;

上述代码要求每一行和每一列。*符号可以理解为“所有”。

我们也可以更加有选择性,就像这段代码所示:

SELECT score FROM StudentsAndGrades
     where name = "Lisa";

上述代码只会返回100,这当然是与姓名Lisa相关联的分数。

更新数据库结构

即使在创建表格并添加数据后,我们仍然可以添加新的列。就 SQL 而言,这很简单,但可能会导致已发布应用程序的用户数据出现一些问题。下一个语句添加了一个名为age的新列,其类型为int

ALTER TABLE StudentsAndGrades
     ADD 
     age int;

有许多数据类型、关键字和使用它们的方式,比我们目前所见到的要多。接下来,让我们看一下 Android SQLite API;我们将开始看到如何使用我们的新的 SQLite 技能。

Android SQLite API

Android API 有许多不同的方式,使得使用我们应用程序的数据库变得相当容易。我们需要首先熟悉的是SQLiteOpenHelper类。

SQLiteOpenHelper 和 SQLiteDatabase

SQLiteDatabase类是表示实际数据库的类。然而,SQLiteOpenHelper类是大部分操作发生的地方。这个类将使我们能够访问数据库并初始化SQLiteDatabase的实例。

此外,SQLiteOpenHelper,我们将在我们的 Age Database 应用程序中扩展它,有两个要重写的方法。首先,它有一个onCreate方法,当第一次使用数据库时被调用;因此,我们将把用于创建表结构的 SQL 放在其中是有意义的。

我们必须重写的另一个方法是onUpgrade,你可能已经猜到,它在我们升级数据库时被调用(使用ALTER来改变其结构)。

构建和执行查询

随着我们的数据库结构变得更加复杂,以及我们的 SQL 知识的增长,我们的 SQL 语句会变得非常长和笨拙。出现错误的可能性很高。

我们将帮助解决复杂性问题的方法是将查询从各个部分构建成一个字符串。然后我们可以将该字符串传递给执行查询的方法。

此外,我们将使用final字符串来表示诸如表和列名之类的东西,这样我们就不会与它们搞混。

例如,我们可以声明以下成员,它们将代表之前虚构示例中的表名和列名。请注意,我们还将为数据库本身命名,并为其设置一个字符串:

private static final String DB_NAME = "MyCollegeDB";
private static final String TABLE_S_AND_G = " StudentsAndGrades";
public static final String TABLE_ROW_ID = "_id";
public static final String TABLE_ROW_NAME = "name";
public static final String TABLE_ROW_SCORE = "score";

请注意在前面的代码中,我们将受益于在类外部访问字符串,因为我们将它们声明为public。你可能会认为这违反了封装的规则。的确如此,但当类的意图是尽可能广泛地使用时,这是可以接受的。而且请记住,所有的变量都是 final 的。使用这些字符串变量的外部类不能改变它们或搞乱它们。它们只能引用和使用它们所持有的值。

然后我们可以像下面的示例一样构建一个查询。该示例向我们的假设数据库添加了一个新条目,并将 Java 变量合并到 SQL 语句中:

String name = "Onkar";
int score = 95;
// Add all the details to the table
String query = "INSERT INTO " + TABLE_S_AND_G + " (" +
         TABLE_ROW_NAME + ", " +
         TABLE_ROW_SCORE +
         ") " +
         "VALUES (" +
         "'" + name + "'" + ", " +
         score +
         ");"; 

请注意在前面的代码中,常规的namescore Java 变量被突出显示。之前的名为query的字符串现在是 SQL 语句,与此完全相同:

INSERT INTO StudentsAndGrades (
   name, score)
   VALUES ('Onkar',95);

注意

要学习 Android 编程并不一定要完全掌握前两个代码块。但是,如果你想构建自己的应用程序并构造确切需要的 SQL 语句,理解这些代码块将有所帮助。为什么不学习前两个代码块,以便区分双引号",它们是用+连接在一起的字符串的一部分;单引号',它们是 SQL 语法的一部分;常规的 Java 变量;以及字符串和 Java 中 SQL 语句中的不同分号。

在输入查询时,Android Studio 会提示我们变量的名称,这样错误的几率就会降低,尽管它比简单地输入查询更冗长。

现在我们可以使用之前介绍的类来执行查询:

// This is the actual database
private SQLiteDatabase db;
// Create an instance of our internal CustomSQLiteOpenHelper class
CustomSQLiteOpenHelper helper = new
   CustomSQLiteOpenHelper(context);
// Get a writable database
db = helper.getWritableDatabase();
// Run the query
db.execSQL(query);

在向数据库添加数据时,我们将像前面的代码一样使用execSQL;在从数据库获取数据时,我们将使用rawQuery方法,如下所示:

Cursor c = db.rawQuery(query, null); 

请注意,rawQuery方法返回Cursor类型的对象。

注意

我们可以用几种不同的方式与 SQLite 交互,它们各有优缺点。我们选择使用原始的 SQL 语句,因为这样可以完全透明地展示我们正在做什么,同时加强我们对 SQL 语言的了解。如果你想了解更多,请参阅下一个提示。

数据库游标

除了让我们访问数据库的类和允许我们执行查询的方法之外,还有一个问题,那就是我们从查询中得到的结果如何格式化。

幸运的是,有Cursor类。我们所有的数据库查询都会返回Cursor类型的对象。我们可以使用Cursor类的方法有选择地访问从查询返回的数据,如下所示:

Log.i(c.getString(1), c.getString(2)); 

以前的代码将输出到 logcat 中查询返回的前两列中存储的两个值。决定我们当前正在读取的返回数据的哪一行是Cursor对象本身。

我们可以访问Cursor对象的许多方法,包括moveToNext方法,该方法将Cursor移动到下一行,准备读取:

c.moveToNext();
/*
   This same code now outputs the data in the
   first and second column of the returned 
   data but from the SECOND row.
*/
Log.i(c.getString(1), c.getString(2));

在某些情况下,我们将能够将Cursor绑定到我们 UI 的一部分(例如RecyclerView),就像我们在 Note to Self 应用程序中使用ArrayList实例一样,然后将一切留给 Android API。

Cursor类还有许多有用的方法,其中一些我们很快就会看到。

注意

这是对 Android SQLite API 的介绍实际上只是触及了它的能力表面。随着我们进一步进行,我们将遇到更多的方法和类。然而,如果您的应用想法需要复杂的数据管理,进一步研究是值得的。

现在我们可以看到所有这些理论是如何结合在一起的,以及我们将如何在 Age Database 应用程序中构建我们的数据库代码结构。

编写数据库类

在这里,我们将实践我们迄今为止学到的一切,并完成编写 Age Database 应用程序。在我们之前的部分的Fragment类可以与共享数据库进行交互之前,我们需要一个类来处理与数据库的交互和创建。

我们将创建一个通过使用SQLiteOpenHelper类来管理我们的数据库的类。它还将定义一些final字符串来表示表的名称和其列。此外,它将提供一堆我们可以调用的辅助方法来执行所有必要的查询。在必要时,这些辅助方法将返回一个Cursor对象,我们可以用来显示我们检索到的数据。如果我们的应用程序需要发展,添加新的辅助方法将是微不足道的。

创建一个名为DataManager的新类,并添加以下成员变量:

import android.database.sqlite.SQLiteDatabase;
public class DataManager {
    // This is the actual database
    private SQLiteDatabase db;
    /*
        Next we have a public static final string for
        each row/table that we need to refer to both
        inside and outside this class
    */
    public static final String TABLE_ROW_ID = "_id";
    public static final String TABLE_ROW_NAME = "name";
    public static final String TABLE_ROW_AGE = "age";
    /*
        Next we have a private static final strings for
        each row/table that we need to refer to just
        inside this class
    */
    private static final String DB_NAME = "name_age_db";
    private static final int DB_VERSION = 1;
    private static final String TABLE_N_AND_A = 
                                   "name_and_age";
}

接下来,我们添加一个构造函数,它将创建我们的自定义版本的SQLiteOpenHelper的实例。我们很快将实现这个类作为一个内部类。构造函数还初始化了我们的SQLiteDatabase引用db成员。

将我们刚刚讨论过的以下构造函数添加到DataManager类中:

public DataManager(Context context) {
   // Create an instance of our internal 
   CustomSQLiteOpenHelper 

   CustomSQLiteOpenHelper helper = new 
      CustomSQLiteOpenHelper(context);
   // Get a writable database
   db = helper.getWritableDatabase();
}

现在我们可以添加我们将从 Fragment 类中访问的辅助方法。从insert方法开始,它根据传入方法的nameage参数执行INSERT SQL 查询。

insert方法添加到DataManager类中:

// Here are all our helper methods
// Insert a record
public void insert(String name, String age){
   // Add all the details to the table
   String query = "INSERT INTO " + TABLE_N_AND_A + " (" +
                  TABLE_ROW_NAME + ", " +
                  TABLE_ROW_AGE +
                  ") " +
                  "VALUES (" +
                  "'" + name + "'" + ", " +
                  "'" + age + "'" +
                  ");";
   Log.i("insert() = ", query);
   db.execSQL(query);
}

下一个名为delete的方法将从数据库中删除一条记录,如果它在名称列中具有与传入的name参数匹配的值。它使用 SQLDELETE关键字来实现这一点。

delete方法添加到DataManager类中:

// Delete a record
public void delete(String name){
   // Delete the details from the table if already exists
   String query = "DELETE FROM " + TABLE_N_AND_A +
                  " WHERE " + TABLE_ROW_NAME +
                  " = '" + name + "';";
   Log.i("delete() = ", query);
   db.execSQL(query);
}

接下来,我们有selectAll方法,它也如其名称所示。它使用SELECT查询并使用*参数来实现这一点,该参数相当于单独指定所有列。还要注意,该方法返回一个Cursor实例,我们将在一些Fragment类中使用。

selectAll方法添加到DataManager类中:

// Get all the records
public Cursor selectAll() {
   Cursor c = db.rawQuery("SELECT *" +" from " +
                TABLE_N_AND_A, null);
   return c;
}

现在我们添加一个searchName方法,该方法具有一个String参数,用于用户想要搜索的名称。它还返回一个包含找到的所有条目的Cursor实例。请注意,SQL 语句使用SELECTFROMWHERE来实现这一点:

// Find a specific record
public Cursor searchName(String name) {
   String query = "SELECT " +
                  TABLE_ROW_ID + ", " +
                  TABLE_ROW_NAME +
                  ", " + TABLE_ROW_AGE +
                  " from " +
                  TABLE_N_AND_A + " WHERE " +
                  TABLE_ROW_NAME + " = '" + name + "';";
   Log.i("searchName() = ", query);
   Cursor c = db.rawQuery(query, null);
   return c;
}

最后,对于DataManager类,我们创建一个内部类,它将是我们的SQLiteOpenHelper的实现。这是一个最基本的实现。

我们有一个构造函数,接收一个Context对象,数据库名称和数据库版本。

我们还重写了onCreate方法,其中包含创建具有_IDnameage列的数据库表的 SQL 语句。

onUpgrade方法在此应用程序中被故意留空。

将内部的CustomSQLiteOpenHelper类添加到DataManager类中:

// This class is created when our DataManager is initialized
private class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
   public CustomSQLiteOpenHelper(Context context) {
         super(context, DB_NAME, null, DB_VERSION);
   }
   // This runs the first time the database is created
   @Override
   public void onCreate(SQLiteDatabase db) {
         // Create a table for photos and all their details
         String newTableQueryString = "create table "
                      + TABLE_N_AND_A + " ("
                      + TABLE_ROW_ID
                      + " integer primary key 
                      autoincrement not null,"
                      + TABLE_ROW_NAME
                      + " text not null,"
                      + TABLE_ROW_AGE
                      + " text not null);";
         db.execSQL(newTableQueryString);

   }
   // This method only runs when we increment DB_VERSION
   @Override
   public void onUpgrade(SQLiteDatabase db, 
int oldVersion, int newVersion) {
// Not needed in this app
// but we must still override it
   }
}

现在我们可以在我们的Fragment类中添加代码来使用我们的新的DataManager类。

编写 Fragment 类以使用 DataManager 类

将这段突出显示的代码添加到InsertFragment类中以更新onCreateView方法:

View v = inflater.inflate(R.layout.content_insert, 
   container, false);
final DataManager dm = 
   new DataManager(getActivity());
Button btnInsert = 
   v.findViewById(R.id.btnInsert);

final EditText editName = 
   v.findViewById(R.id.editName);

final EditText editAge = 
   v.findViewById(R.id.editAge);
btnInsert.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
          dm.insert(editName.getText().toString(),
                       editAge.getText().toString());
   }
});
return v;

在代码中,我们获取了我们的DataManager类的实例和对每个 UI 小部件的引用。然后,在onClick方法中,我们使用insert方法向数据库添加新的姓名和年龄。要插入的值来自两个EditText小部件。

将这段突出显示的代码添加到DeleteFragment类中以更新onCreateView方法:

View v = inflater.inflate(R.layout.content_delete, 
   container, false);
final DataManager dm = 
   new DataManager(getActivity());
Button btnDelete = 
   v.findViewById(R.id.btnDelete);

final EditText editDelete = 
   v.findViewById(R.id.editDelete);
btnDelete.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
          dm.delete(editDelete.getText().toString());
   }
});
return v;

DeleteFragment类中,我们创建了我们的DataManager类的实例,然后从我们的布局中获取了EditTextButton小部件的引用。当按钮被点击时,将调用delete方法,传入用户输入的EditText小部件中的任何文本的值。delete方法搜索我们的数据库是否有匹配项,如果找到,则删除它。

将这段突出显示的代码添加到SearchFragment类中以更新onCreateView方法:

View v = inflater.inflate(R.layout.content_search,
   container,false);
Button btnSearch = 
   v.findViewById(R.id.btnSearch);

final EditText editSearch = 
   v.findViewById(R.id.editSearch);

final TextView textResult = 
   v.findViewById(R.id.textResult);
// This is our DataManager instance
final DataManager dm = 
   new DataManager(getActivity());
btnSearch.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
          Cursor c = dm.searchName(
                     editSearch.getText().toString());
// Make sure a result was found before using the 
          Cursor
          if(c.getCount() > 0) {
                 c.moveToNext();
textResult.setText("Result = " + 
c.getString(1) + " - " + 
                     c.getString(2));
          }
   }
});
return v;

与我们所有不同的Fragment类一样,我们创建了DataManager类的实例,并获取了布局中所有不同 UI 小部件的引用。在onClick方法中,使用searchName方法,传入EditText小部件的值。如果数据库在Cursor实例中返回结果,那么TextView小部件使用其setText方法输出结果。

将这段突出显示的代码添加到ResultsFragment类中以更新onCreateView方法:

View v = inflater.inflate(R.layout.content_results, 
   container, false);
// Create an instance of our DataManager
DataManager dm = 
   new DataManager(getActivity());
// Get a reference to the TextView to show the results
TextView textResults = 
   v.findViewById(R.id.textResults);
// Create and initialize a Cursor with all the results
Cursor c = dm.selectAll();
// A String to hold all the text
String list = "";
// Loop through the results in the Cursor
while (c.moveToNext()){
   // Add the results to the String
   // with a little formatting
   list+=(c.getString(1) + " - " + c.getString(2) + "\n");
}
// Display the String in the TextView
textResults.setText(list);
return v;

在这个类中,Cursor实例在任何交互发生之前使用selectAll方法加载数据。然后通过连接结果将Cursor的内容输出到TextView小部件中。在连接中的\n是在Cursor实例中的每个结果之间创建新行的。

运行 Age Database 应用程序

让我们运行一些我们应用程序的功能,以确保它按预期工作。

首先,我使用插入菜单选项向数据库添加了一个新的名字:

图 27.3 – 插入菜单

图 27.3 – 插入菜单

然后我通过查看结果选项确认它确实存在:

图 27.4 – 结果选项

图 27.4 – 结果选项

之后,我添加了一些更多的姓名和年龄,只是为了填充数据库:

图 27.5 – 填充数据库

图 27.5 – 填充数据库

然后我使用了删除菜单选项,再次查看结果选项,以确保我选择的名字确实被删除了。

图 27.6 – 删除菜单

图 27.6 – 删除菜单

然后我搜索了一个我知道存在的名字来测试搜索菜单选项:

图 27.7 – 搜索菜单

图 27.7 – 搜索菜单

让我们回顾一下本章我们所做的事情。

摘要

在本章中,我们涵盖了很多内容。我们学习了关于数据库,特别是 Android 应用程序使用的数据库 SQLite。我们练习了使用 SQL 语言与数据库进行通信的基础知识。

我们已经看到了 Android API 如何帮助我们使用 SQLite 数据库,并实现了我们的第一个使用数据库的工作应用程序。

你已经走了很长的路,已经到达了书的尽头。让我们谈谈接下来可能会发生什么。

第二十八章:在你离开之前快速聊一下

我们的旅程就快结束了。这一章提供了一些想法和指针,你可能在匆忙制作自己的应用之前想要看一看:

  • 发布

  • 制作你的第一个应用

  • 继续学习

  • 谢谢

发布

你已经足够了解如何设计你自己的应用。你甚至可以对本书中的应用进行一些修改。

我决定不提供在 Google Play 商店上发布的逐步指南,因为这些步骤并不复杂。然而,它们相当深入和有点费力。大部分步骤涉及输入关于你和你的应用的个人信息以及图片。这样的教程可能会是这样的:

  1. 填写这个文本框。

  2. 现在填写这个文本框。

  3. 上传这张图片。

  4. 等等。

这样做不太有趣,也不太有用。

要开始,你只需要访问play.google.com/apps/publish并支付一次性的适度费用(大约 25 美元,根据你所在地区的货币而定)。这样你就可以终身发布游戏。

注意

如果你想要一个发布的清单,可以查看这个链接,developer.android.com/distribute/…

制作一个应用!

如果你只是把这一件事付诸实践,你就可以忽略这一章中的其他一切:

不要等到你成为专家才开始制作应用!

开始构建你的梦想应用,一个拥有所有功能的应用,将会在 Google Play 上风靡一时。然而,一个简单的建议是:先做一些规划!但不要太多;然后开始吧。

在一旁有一些更小、更容易实现的项目:你可以向朋友和家人展示这些项目,并探索你还不熟悉的 Android 领域。如果你对这些应用有信心,你可以将它们上传到 Google Play。如果你担心它们会被评论员接受,那就把它们免费发布,并在描述中注明“只是一个原型”或类似的内容。

如果你的经历和我的一样,你会发现当你阅读、学习和制作应用时,你会发现你的梦想应用可以在很多方面得到改进,你可能会被激发重新设计它,甚至重新开始。

如果你这样做,我可以保证下一次构建应用时,你会用一半的时间做出两倍好的成果,至少是这样!

继续学习

如果你觉得自己已经走了很长的路,那么你是对的。然而,总是有更多东西需要学习。

继续阅读

你会发现,当你制作你的第一个应用时,你会突然意识到你的知识中存在一个需要填补的空白,以使某个功能得以实现。这是正常的,也是可以预料的,所以不要让它吓到你。想一想如何描述这个问题,并在谷歌上搜索解决方案。

你可能会发现项目中的特定类会变得超出实际和可维护的范围。这表明有更好的方式来构建结构,并且可能有一个现成的设计模式可以让你的生活更轻松。

为了预防这几乎是不可避免的,为什么不立即学习一些模式呢?一个很好的来源是Head First: Java Design Patterns,可以从所有好的书店购买。

GitHub

GitHub 允许你搜索和浏览其他人编写的代码,并查看他们是如何解决问题的。这很有用,因为查看类的文件结构,然后经常深入研究它们,通常可以显示如何从一开始规划你的应用程序,并防止你走上错误的道路。你甚至可以获得一个 GitHub 应用程序,让你可以在手机或平板电脑上舒适地进行这些操作。或者,你可以配置 Android Studio 来保存和分享你的项目到 GitHub。例如,在主页www.github.com上搜索“Android 片段”,你将看到超过 1,000 个相关项目,你可以浏览:

图 28.1 – Android 片段结果

图 28.1 – Android 片段结果

Stack Overflow

如果你遇到困难,遇到奇怪的错误,或者遇到无法解释的崩溃,通常最好的去处是谷歌。这样做,你会惊讶地发现 Stack Overflow 似乎经常出现在搜索结果中,并且有充分的理由。

Stack Overflow 允许用户发布他们问题的描述以及示例代码,以便社区可以回答。然而,根据我的经验,很少有必要发布问题,因为几乎总会有人遇到完全相同的问题。

Stack Overflow 特别适合处理最前沿的问题。如果新的 Android Studio 版本有 bug,或者新版本的 Android API 似乎没有做应该做的事情,那么你几乎可以肯定,全世界成千上万的其他开发者也遇到了和你一样的问题。然后,一些聪明的编程人员,通常来自 Android 开发团队本身,会提供答案。

Stack Overflow 也适合进行一些轻松的阅读。前往主页www.stackoverflow.com,在搜索框中输入Android,你将看到 Stack Overflow 社区最新问题的列表:

图 28.2 – Android 列表

图 28.2 – Android 列表

我并不是建议你立即投入并开始尝试回答所有问题,但阅读问题和建议会教会你很多东西,你可能会发现,你比你期望的更经常地有解决方案,或者至少有解决方案的想法。

Android 用户论坛

此外,值得注册一些 Android 论坛并偶尔访问它们,以了解用户的视角下的热门话题和趋势。我不会在这里列出任何论坛,因为只需要快速搜索即可。

如果你对这个话题很认真,那么你可以参加一些 Android 会议,在那里你可以与成千上万的其他开发者交流并参加讲座。如果你对此感兴趣,可以在网上搜索 droidcon、Android Developer Days 和 GDG DevFest。

更高级的学习

你现在可以阅读更多其他 Android 书籍。我在本书开头提到,几乎没有书籍,甚至可以说没有一本书,教会读者如何在没有 Java 经验的情况下学习 Android 编程。这就是我写这本书的原因。

现在你已经对面向对象编程和 Java 有了很好的理解,还对应用程序设计和 Android API 有了简要介绍,你现在可以阅读针对已经了解如何在 Java 中编程的人的 Android“初学者”书籍了,就像你现在所做的那样。

这些书籍充满了很好的例子,你可以构建或仅仅阅读,以巩固你在本书中学到的知识,以不同的方式使用你的知识,当然,也学到一些全新的东西。

也许值得进一步阅读一些纯 Java 书籍。也许很难相信,在刚刚浏览了大约 750 页之后,Java 还有很多内容没有时间在这里涵盖。

我可以列举一些书名,但在亚马逊上拥有最多积极评价的书籍往往是值得探索的书籍。

我的其他渠道

请保持联系!

再见,谢谢你

我写这本书的时候非常开心。我知道这是陈词滥调,但也是真的。然而,最重要的是,我希望你能从中获益,并将其作为你未来编程之路的垫脚石。

也许你正在阅读这本书是为了一点乐趣或者发布一个应用程序的荣誉,或者作为编程工作的垫脚石,或者你真的会开发一个在 Google Play 上风靡一时的应用程序。

无论如何,我非常感谢你购买了这本书,我祝愿你未来的努力一切顺利。

我认为每个人都有一个应用程序的潜力,你只需要付出足够的努力将它发挥出来。