安卓编程初学者手册第三版(五)
原文:
zh.annas-archive.org/md5/ceefdd89e585c59c20db6a7760dc11f1译者:飞龙
第十五章:数组、映射和随机数
在本章中,我们将学习 Java 数组,它允许我们以有组织和高效的方式操纵大量数据。我们还将使用与数组有密切关系的 Java 类ArrayList,并研究它们之间的区别。
一旦我们熟悉处理大量数据,我们将看看 Android API 提供了什么帮助,让我们轻松地将我们新发现的数据处理技能与用户界面连接起来,而不费吹灰之力。
本章的主题包括以下内容:
-
Random类 -
使用数组处理数据
-
数组小应用程序
-
包括一个小型应用程序的动态数组
-
包括一个小型应用程序的多维数组
-
ArrayList类 -
增强型
for循环 -
Java HashMap
首先,让我们了解一下Random类。
技术要求
您可以在 GitHub 上找到本章的代码文件,网址为github.com/PacktPublishing/Android-Programming-for-Beginners-Third-Edition/tree/main/chapter%2015。
一个随机的转移
有时在我们的应用程序中,我们会需要一个随机数,而 Java 为我们提供了Random类来满足这些需求。这个类有许多可能的用途。例如,也许我们的应用程序想要显示每日随机提示,或者是一个需要在不同情景之间选择的游戏,或者是一个随机提问的测验。
Random类是 Java API 的一部分,完全兼容我们的 Android 应用程序。
让我们看看如何创建随机数,然后在本章中我们将把它应用到实际中。所有的工作都由Random类为我们完成。首先,我们需要创建一个Random类型的对象:
Random randGenerator = new Random();
然后我们使用我们新对象的nextInt方法在一定范围内生成一个随机数。下一行代码使用我们的Random对象生成随机数,并将结果存储在ourRandomNumber变量中:
int ourRandomNumber = randGenerator.nextInt(10);
我们输入的范围开始于零。因此,前一行将生成一个介于 0 和 9 之间的随机数。如果我们想要一个介于 1 和 10 之间的随机数,我们只需这样做:
ourRandomNumber ++;
我们还可以使用Random对象使用nextLong、nextFloat和nextDouble方法获取其他类型的随机数。
我们将在本章后面用一个快速地理测验应用程序实际使用Random类。
使用数组处理大量数据
也许您会想知道当我们有很多变量需要跟踪时会发生什么。我们的自我备忘录应用程序有 100 条备忘录,或者游戏中有前 100 名得分的高分表怎么办?我们可以像这样声明和初始化 100 个单独的变量:
Note note1;
Note note2;
Note note3;
// 96 more lines like the above
Note note100;
或者我们可以这样做:
int topScore1;
int topScore2;
int topScore3;
// 96 more lines like the above
int topScore100;
一开始,这可能看起来笨拙,但是当有人获得新的最高分或者我们想让用户对他们的笔记显示顺序进行排序时怎么办?使用高分情景,我们必须将每个变量中的分数下移一个位置。噩梦开始了:
topScore100 = topScore99;
topScore99 = topScore98;
topScore98 = topScore97;
// 96 more lines like the above
topScore1 = score;
一定有更好的方法。当我们有一整个数组的变量时,我们需要的是一个 Java 数组。数组是一个引用变量,最多可以容纳预先确定的固定数量的元素。每个元素都是一个具有一致类型的变量。
以下代码声明了一个可以容纳int类型变量的数组,可能是一个高分表或一系列考试成绩:
int [] intArray;
我们还可以声明其他类型的数组,包括Note等类,如下所示:
String [] classNames;
boolean [] bankOfSwitches;
float [] closingBalancesInMarch;
Note [] notes;
在使用之前,这些数组中的每一个都需要分配固定的最大存储空间。就像其他对象一样,我们必须在使用数组之前对其进行初始化:
intArray = new int [100];
上面的代码分配了最多 100 个int大小的存储空间。想象一下我们的内存仓库中有 100 个连续的存储空间。这些空间可能被标记为intArray[0],intArray[1],intArray[2]等等,每个空间都保存一个单独的int值。也许稍微令人惊讶的是,存储空间从零开始,而不是 1。因此,在一个 100 宽的数组中,存储空间将从 0 到 99。
我们可以像这样初始化一些存储空间中的值:
intArray[0] = 5;
intArray[1] = 6;
intArray[2] = 7;
但请注意,我们只能将预先声明的类型放入数组中,数组保存的类型永远不会改变:
intArray[3]= "John Carmack"; // Won't compile String not int
因此,当我们有一个int类型的数组时,每个这些int变量被称为什么?这些变量的名称是什么,我们如何访问其中存储的值?数组表示法语法替换了变量名称。我们可以对数组中的变量进行与常规变量相同的操作:
intArray [3] = 123;
上面的代码将值123分配给intArray的第四个位置。这里是另一个例子:
intArray[10] = intArray[9] - intArray[4];
上面的代码从intArray的第五个位置减去第十个位置的值,并将答案存储在第十一个位置。我们还可以将数组中的值分配给相同类型的常规变量,如下所示:
int myNamedInt = intArray [3];
但请注意,myNamedInt是一个单独且独立的原始变量,对它的任何更改都不会影响存储在intArray引用中的值。它在仓库中有自己的空间,并且与数组没有关联。更具体地说,数组在堆上,而int变量在栈上。
数组是对象
我们说过数组是引用变量。将数组变量视为给定类型的一组变量的地址。也许,使用仓库的类比,someArray是一个过道编号。因此,someArray[0],someArray[1]等等是过道编号,后面是过道中的位置编号。
数组也是对象;也就是说,它们有我们可以使用的方法和属性。例如:
int lengthOfSomeArray = someArray.length;
在上面的代码中,我们将someArray的长度分配给名为lengthOfSomeArray的int变量。
我们甚至可以声明一个数组的数组。这是一个数组,其中每个元素中都隐藏着另一个数组,如下所示:
String[][] countriesAndCities;
在前面的数组中,我们可以在每个国家中保存一个城市列表。不过,现在先不要疯狂地使用数组。只需记住,数组最多可以容纳预定数量的任意类型的变量,并且可以使用以下语法访问这些值:
someArray[someLocation];
让我们在一个真实的应用程序中使用一些数组,试着理解如何在真实代码中使用它们以及我们可能用它们做什么。
简单数组示例迷你应用程序
让我们制作一个简单的工作数组示例。您可以在可下载的代码包中找到此示例的完整代码。它在第十五章/Simple Array Example/MainActivity.java。
使用空活动模板创建一个项目,并将其命名为Simple Array Example。
首先,我们声明我们的数组,分配五个空间,并为每个元素初始化值。然后我们将每个值输出到 logcat 控制台。将此代码添加到onCreate方法中,就在调用setContentView之后:
// Declaring an array
int[] ourArray;
// Allocate memory for a maximum size of 5 elements
ourArray = new int[5];
// Initialize ourArray with values
// The values are arbitrary, but they must be int
// The indexes are not arbitrary. 0 through 4 or crash!
ourArray[0] = 25;
ourArray[1] = 50;
ourArray[2] = 125;
ourArray[3] = 68;
ourArray[4] = 47;
//Output all the stored values
Log.i("info", "Here is ourArray:");
Log.i("info", "[0] = "+ourArray[0]);
Log.i("info", "[1] = "+ourArray[1]);
Log.i("info", "[2] = "+ourArray[2]);
Log.i("info", "[3] = "+ourArray[3]);
Log.i("info", "[4] = "+ourArray[4]);
接下来,我们将数组的每个元素相加,就像我们可以对常规的int类型变量一样。请注意,当我们将数组元素相加时,我们是在多行上这样做的。这没问题,因为我们省略了分号,直到最后一个操作,所以 Java 编译器将这些行视为一个语句。将我们刚刚讨论的代码添加到MainActivity.java:
/*
We can do any calculation with an array element
provided it is appropriate to the contained type
Like this:
*/
int answer = ourArray[0] +
ourArray[1] +
ourArray[2] +
ourArray[3] +
ourArray[4];
Log.i("info", "Answer = "+ answer);
运行示例并在 logcat 窗口中查看输出。
请记住,模拟器显示不会发生任何事情,因为所有输出都将发送到我们在 Android Studio 中的 logcat 控制台窗口。这是输出:
info﹕ Here is ourArray:
info﹕ [0] = 25
info﹕ [1] = 50
info﹕ [2] = 125
info﹕ [3] = 68
info﹕ [4] = 47
info﹕ Answer = 315
我们声明了一个名为ourArray的数组来保存int变量,然后为该类型的最多五个变量分配了空间。
接下来,我们为数组中的五个空间分配了一个值。请记住,第一个空间是ourArray[0],最后一个空间是ourArray[4]。
接下来,我们简单地将每个数组位置的值打印到控制台上,从输出中我们可以看到它们保存了我们在上一步中初始化的值。然后我们将ourArray中的每个元素相加,并将它们的值初始化为answer变量。然后我们将answer打印到控制台上,我们可以看到确实所有的值都被相加了,就像它们是普通的int类型一样,它们确实是,只是以不同的方式存储和访问。
使用数组进行动态处理
正如我们在所有这些数组内容开始时讨论的,如果我们需要单独声明和初始化数组的每个元素,那么数组与常规变量相比并没有太大的好处。让我们看一个动态声明和初始化数组的例子。
动态数组示例
让我们做一个简单的动态数组示例。您可以在下载包中找到此示例的工作项目。它在第十五章/Dynamic Array Example/MainActivity.java中。
使用空活动模板创建一个项目,并将其命名为“动态数组示例”。
在onCreate方法中的setContentView方法调用之后,输入以下代码。在我们讨论和分析代码之前,看看您能否猜出输出会是什么:
// Declaring and allocating in one step
int[] ourArray = new int[1000];
// Let's initialize ourArray using a for loop
// Because more than a few variables is allot of typing!
for(int i = 0; i < 1000; i++){
// Put the value of our value into ourArray
// At the position decided by i.
ourArray[i] = i*5;
//Output what is going on
Log.i("info", "i = " + i);
Log.i("info", "ourArray[i] = " + ourArray[i]);
}
运行示例应用程序,记住屏幕上不会发生任何事情,因为所有输出都将发送到我们在 Android Studio 中的 logcat 控制台窗口。以下是输出:
info﹕ i = 0
info﹕ ourArray[i] = 0
info﹕ i = 1
info﹕ ourArray[i] = 5
info﹕ i = 2
info﹕ ourArray[i] = 10
... 994 iterations of the loop removed for brevity.
info﹕ ourArray[i] = 4985
info﹕ i = 998
info﹕ ourArray[i] = 4990
info﹕ i = 999
info﹕ ourArray[i] = 4995
首先,我们声明并分配了一个名为ourArray的数组,用于保存最多 1,000 个int值。请注意,这次我们在一行代码中完成了两个步骤:
int[] ourArray = new int[1000];
然后我们使用了一个设置为循环 1,000 次的for循环:
(int i = 0; i < 1000; i++){
我们用值为i乘以 5 来初始化数组中的空间,从 0 到 999,就像这样:
ourArray[i] = i*5;
然后,为了演示i的值以及数组中每个位置保存的值,我们输出了i的值,然后是数组中相应位置保存的值,就像这样:
Log.i("info", "i = " + i);
Log.i("info", "ourArray[i] = " + ourArray[i]);
所有这些都发生了 1,000 次,产生了我们所看到的输出。当然,我们还没有在真实世界的应用程序中使用这种技术,但我们很快将使用它来使我们的“备忘录”应用程序保存几乎无限数量的备忘录。
使用数组进入 n 维
我们非常简要地提到了数组甚至可以在每个位置上保存其他数组。但是,如果一个数组保存了很多保存了其他某种类型的数组的数组,我们如何访问包含数组中的值呢?无论如何,我们为什么需要这个呢?看看下一个示例,多维数组在哪里可以派上用场。
多维数组迷你应用程序
让我们做一个简单的多维数组示例。您可以在下载包中找到此示例的工作项目。它在第十五章/Multidimensional Array Example/MainActivity.java中。
使用空活动模板创建一个项目,并将其命名为“多维数组示例”。
在onCreate中的setContentView调用之后,添加以下代码,包括声明和初始化一个二维数组(已突出显示):
// Random object for generating question numbers
Random randInt = new Random();
// a variable to hold the random value generated
int questionNumber;
// declare and allocate in separate stages for clarity
// but we don't have to
String[][] countriesAndCities;
// Now we have a 2 dimensional array
countriesAndCities = new String[5][2];
// 5 arrays with 2 elements each
// Perfect for 5 "What's the capital city" questions
// Now we load the questions and answers into our arrays
// You could do this with less questions to save typing
// But don't do more or you will get an exception
countriesAndCities [0][0] = "United Kingdom";
countriesAndCities [0][1] = "London";
countriesAndCities [1][0] = "USA";
countriesAndCities [1][1] = "Washington";
countriesAndCities [2][0] = "India";
countriesAndCities [2][1] = "New Delhi";
countriesAndCities [3][0] = "Brazil";
countriesAndCities [3][1] = "Brasilia";
countriesAndCities [4][0] = "Kenya";
countriesAndCities [4][1] = "Nairobi";
现在我们使用for循环和我们的Random对象输出数组的内容。请注意,尽管问题是随机的,但我们始终可以选择正确的答案。在上一个代码之后添加以下代码:
/*
Now we know that the country is stored at element 0
The matching capital at element 1
Here are two variables that reflect this
*/
int country = 0;
int capital = 1;
// A quick for loop to ask 3 questions
for(int i = 0; i < 3; i++){
// get a random question number between 0 and 4
questionNumber = randInt.nextInt(5);
// and ask the question and in this case just
// give the answer for the sake of brevity
Log.i("info", "The capital of "
+countriesAndCities[questionNumber][country]);
Log.i("info", "is "
+countriesAndCities[questionNumber][capital]);
} // end of for loop
运行示例,记住屏幕上不会发生任何事情,因为所有输出都将发送到我们在 Android Studio 中的 logcat 控制台窗口。以下是输出:
info﹕ The capital of USA
info﹕ is Washington
info﹕ The capital of India
info﹕ is New Delhi
info﹕ The capital of United Kingdom
info﹕ is London
刚才发生了什么?让我们一块一块地过一遍,这样我们就知道到底发生了什么。
我们创建一个名为randInt的Random类型的新对象,准备在程序后面生成随机数:
Random randInt = new Random();
有一个简单的int变量来保存一个问题编号:
int questionNumber;
在这里我们声明了一个名为countriesAndCities的数组数组。外部数组保存数组:
String[][] countriesAndCities;
现在我们在数组中分配空间。第一个外部数组现在可以保存五个数组,每个内部数组可以保存两个字符串:
countriesAndCities = new String[5][2];
现在我们初始化我们的数组来保存国家和它们对应的首都。注意到每一对初始化的外部数组编号保持不变,表明每个国家/首都对在一个内部数组中,一个String数组。当然,每个内部数组都保存在外部数组的一个元素中(保存数组的数组):
countriesAndCities [0][0] = "United Kingdom";
countriesAndCities [0][1] = "London";
countriesAndCities [1][0] = "USA";
countriesAndCities [1][1] = "Washington";
countriesAndCities [2][0] = "India";
countriesAndCities [2][1] = "New Delhi";
countriesAndCities [3][0] = "Brazil";
countriesAndCities [3][1] = "Brasilia";
countriesAndCities [4][0] = "Kenya";
countriesAndCities [4][1] = "Nairobi";
为了使即将到来的for循环更清晰,我们声明和初始化int变量来表示我们数组中的国家和首都。如果你回顾一下数组初始化,所有的国家都保存在内部数组的位置0,所有对应的首都都在位置1:
int country = 0;
int capital = 1;
现在我们设置一个for循环运行三次。请注意,这不仅仅是访问我们数组的前三个元素;它只是确定我们循环的次数。我们可以让它循环一次或一千次;示例仍然有效:
for(int i = 0; i < 3; i++){
接下来,我们确定要提问的问题 - 或者更具体地说,我们的外部数组的哪个元素。记住,randInt.nextInt(5)返回一个 0 到 4 之间的数字 - 这正是我们需要的,因为我们有一个包含五个元素的外部数组,从 0 到 4:
questionNumber = randInt.nextInt(5);
现在我们可以通过输出内部数组中保存的字符串来提问,而这些内部数组又是由前一行随机生成的外部数组保存的:
Log.i("info", "The capital of "
+countriesAndCities[questionNumber][country]);
Log.i("info", "is "
+countriesAndCities[questionNumber][capital]);
}//end of for loop
值得一提的是,在本书的其余部分我们将不再使用多维数组。所以,如果对这些数组内部的数组还有一点模糊,那没关系。你知道它们存在,知道它们能做什么,如果有必要的话可以重新学习。
数组越界异常
当我们尝试访问一个不存在的数组元素时,就会发生数组越界异常。有时编译器会为我们捕捉到它,以防止错误进入工作中的应用程序。例如,看看这段代码:
int[] ourArray = new int[1000];
int someValue = 1; // Arbitrary value
ourArray[1000] = someValue;
// Won't compile as compiler knows this won't work.
// Only locations 0 through 999 are valid
但如果我们做这样的事情呢:
int[] ourArray = new int[1000];
int someValue = 1;// Arbitrary value
int x = 999;
if(userDoesSomething){
x++; // x now equals 1000
}
ourArray[x] = someValue;
// Array out of bounds exception if userDoesSomething
// evaluates to true! This is because we end up referencing
// position 1000 when the array only has positions 0
// through 999
// Compiler can't spot it. App will crash!
我们唯一能避免这个问题的方法是知道这个规则:数组从零开始,到它们的长度-1。我们还可以使用清晰、可读的代码,在这种代码中更容易评估我们所做的事情,并更容易发现问题。
ArrayList
ArrayList 就像是一个增强版的普通 Java 数组。它克服了数组的一些缺点,比如需要预先确定大小。它添加了一些有用的方法来使数据易于管理,并且使用了一个更清晰的增强版for循环,比普通的for循环更容易使用。
让我们看一些使用ArrayList实例的代码:
// Declare a new ArrayList called myList to hold int variables
ArrayList<int> myList;
// Initialize the myList ready for use
myList = new ArrayList<int>();
在前面的代码中,我们声明并初始化了一个名为myList的新ArrayList。我们也可以像这段代码所示的那样一步完成:
ArrayList<int> myList = new ArrayList<int>();
到目前为止还没有特别有趣的东西,所以让我们看看我们实际上可以用ArrayList做些什么。这次我们使用一个String ArrayList实例:
// declare and initialize a new ArrayList
ArrayList<String> myList = new ArrayList<String>();
// Add a new String to myList in the next available location
myList.add("Donald Knuth");
// And another
myList.add("Rasmus Lerdorf");
// And another
myList.add("Richard Stallman");
// We can also choose 'where' to add an entry
myList.add(1, "James Gosling");
// Is there anything in our ArrayList?
if(myList.isEmpty()){
// Nothing to see here
}else{
// Do something with the data
}
// How many items in our ArrayList?
int numItems = myList.size();
// Now where did I put James Gosling?
int position = myList.indexOf("James Gosling");
在前面的代码中,我们看到我们可以在ArrayList对象上使用ArrayList类的一些非常有用的方法;这些方法列在下面:
-
我们可以添加一个项目(
myList.add)。 -
在特定位置添加(
myList.add(x, value))。 -
检查
ArrayList是否为空(myList.isEmpty)。 -
看看它有多少元素(
myList.size())。 -
获取给定项目的当前位置(
myList.indexOf)。
注意
ArrayList类中甚至还有更多的方法,你可以在这里阅读:docs.oracle.com/javase/7/do…
有了所有这些功能,我们现在只需要一种方法来动态处理ArrayList实例。
增强型 for 循环
这是增强型for循环的条件:
for (String s : myList)
前面的例子会逐个遍历myList中的所有项目。在每一步中,s会保存当前的String值。
因此,这段代码将在控制台上打印出我们上一节ArrayList代码示例中的所有杰出程序员:
for (String s : myList){
Log.i("Programmer: ","" + s);
}
我们也可以使用增强型for循环来处理常规数组:
int [] anArray = new int [];
// We can initialize arrays quickly like this
anArray {0, 1, 2, 3, 4, 5}
for (int s : anArray){
Log.i("Contents = ","" + s);
}
还有一个即将到来的新闻快讯!
数组和 ArrayList 实例是多态的
我们已经知道我们可以将对象放入数组和ArrayList中。但是多态意味着它们可以处理多个不同类型的对象,只要它们有一个共同的父类型,都可以放在同一个数组或ArrayList中。
在第十章**,面向对象编程中,我们学到多态意味着不同的形式。但在数组和ArrayList的上下文中,这对我们意味着什么呢?
简化到最简单的形式:任何子类都可以作为使用超类的代码的一部分。
例如,如果我们有一个Animal实例的数组,我们可以将任何一个是Animal子类的对象放入Animal数组中 - 也许是Cat和Dog实例。
这意味着我们可以编写更简单、更易理解、更易更改的代码:
// This code assumes we have an Animal class
// And we have a Cat and Dog class that extends Animal
Animal myAnimal = new Animal();
Dog myDog = new Dog();
Cat myCat = new Cat();
Animal [] myAnimals = new Animal[10];
myAnimals[0] = myAnimal; // As expected
myAnimals[1] = myDog; // This is OK too
myAnimals[2] = myCat; // And this is fine as well
此外,我们可以为超类编写代码,并依赖于这样一个事实,即使它被子类化多少次,在一定的参数范围内,代码仍然可以工作。让我们继续我们之前的例子:
// 6 months later we need elephants
// with its own unique aspects
// If it extends Animal we can still do this
Elephant myElephant = new Elephant();
myAnimals[3] = myElephant; // And this is fine as well
但是当我们从多态数组中移除一个对象时,我们必须记得将其转换为我们想要的类型:
Cat newCat = (Cat) myAnimals[2];
我们刚刚讨论的对ArrayList也适用。有了这些新的工具包,包括数组、ArrayList,以及它们的多态性,我们可以继续学习一些更多的 Android 类,这些类很快就会用到我们的 Note to Self 应用程序中。
更多的 Java 集合 - 了解 Java HashMap
Java 的HashMap很棒。它是 Java 集合框架的一部分,也是我们在下一章中将在 Note to Self 项目中使用的ArrayList类的一种近亲。它们基本上封装了一些有用的数据存储技术,否则对我们来说可能会相当技术性。
我觉得值得先看一下HashMap。假设我们想要存储角色扮演游戏中许多角色的数据,每个不同的角色都由Character类型的对象表示。
我们可以使用一些我们已经了解的 Java 工具,比如数组或ArrayList。Java 的HashMap也类似于这些东西,但是使用HashMap,我们可以为每个Character对象提供一个唯一的键/标识符,并使用该键/标识符访问任何这样的对象。
哈希术语来自于将我们选择的键/标识符转换为HashMap类内部使用的东西的过程。这个过程被称为哈希。
我们选择的键/标识符可以访问任何我们的Character实例。在Character类的情况下,一个好的键/标识符候选者可能是角色的名字。
每个键/标识符都有一个相应的对象;在这种情况下,它是Character类型的。这被称为键值对。
我们只需给HashMap一个键,它就会给我们相应的对象。不需要担心我们存储角色的索引是哪个,无论是 Geralt、Ciri 还是 Triss;只需将名字传递给HashMap,它就会为我们完成工作。
让我们看一些例子。你不需要输入任何代码 - 只需熟悉它的工作原理。
我们可以声明一个新的HashMap来保存键和Character实例,就像这样的代码:
Map<String, Character> characterMap;
前面的代码假设我们已经编写了一个名为Character的类。
我们可以像这样初始化HashMap:
characterMap = new HashMap();
我们可以像这样添加一个新的键和其关联的对象:
characterMap.put("Geralt", new Character());
我们也可以使用这个:
characterMap.put("Ciri", new Character());
我们也可以使用这个:
characterMap.put("Triss", new Character());
注意
所有示例代码都假设我们可以在其他地方赋予Character实例它们独特的属性,以反映它们的内部差异。
我们可以像这样从HashMap中检索条目:
Character ciri = characterMap.get("Ciri");
或者我们可以直接使用Character类的方法,就像这样:
characterMap.get("Geralt").drawSilverSword();
// Or maybe call some other hypothetical method
characterMap.get("Triss").openFastTravelPortal("Kaer Morhen");
先前的代码调用了假设的Character类上的drawSilverSword和openFastTravelPortal方法。
注意
HashMap类也有很多有用的方法,就像ArrayList一样。在这里查看HashMap的官方 Java 页面:docs.oracle.com/javase/tutorial/collections/interfaces/map.html。
让我们谈谈“Note to Self”应用程序。
“Note to Self”应用程序
尽管我们学到了很多,但我们还没有准备好为“Note to Self”应用程序应用解决方案。我们可以更新我们的代码,将大量的笔记存储在ArrayList实例中,但在这之前,我们还需要一种方法来在 UI 中显示ArrayList的内容。例如,将整个ArrayList的内容放入TextView小部件中看起来并不好。
解决方案是适配器和一个名为RecyclerView的特殊 UI 布局。我们将在下一章中介绍它们。
常见问题
- 一个只能进行真实计算的计算机如何可能生成真正的随机数?
实际上,计算机无法创建真正随机的数字,但Random类使用一个种子来产生一个在严格的统计检验下会被认为是真正随机的数字。要了解更多关于种子和生成随机数的信息,请查看这篇文章:en.wikipedia.org/wiki/Random_number_generation。
总结
在本章中,我们看了如何使用简单的 Java 数组来存储大量数据,只要它们是相同类型的。我们还使用了ArrayList类,它类似于一个带有大量额外功能的数组。此外,我们发现数组和ArrayList实例都是多态的,这意味着一个数组(或ArrayList)可以容纳多个不同的对象,只要它们都是从同一个父类派生的。
此外,我们还了解了HashMap类,它也是一种数据存储解决方案,但允许以不同的方式访问。
在下一章中,我们将学习Adapter和RecyclerView类,将我们的理论付诸实践,并增强“Note to Self”应用程序。
第十六章:适配器和回收器
在这个简短的章节中,我们将取得很大进展。我们将首先学习适配器和列表的理论 - 如何在 Java 代码中扩展RecyclerAdapter类并添加一个作为列表的RecyclerView实例到我们的 UI - 然后通过 Android API 的明显魔术将它们绑定在一起,以便RecyclerView显示RecyclerAdapter的内容并允许用户滚动内容。你可能已经猜到,我们将使用这种技术来显示我们的 Note to Self 应用程序中的笔记列表。
在本章中,我们将涵盖以下内容:
-
研究适配器的理论并将其绑定到我们的 UI
-
使用
RecyclerView实现布局 -
为在
RecyclerView中使用的列表项布局 -
使用
RecyclerAdapter实现适配器 -
将适配器绑定到
RecyclerView -
将笔记存储在
ArrayList中,并在RecyclerView中显示它们 -
讨论如何进一步改进 Note to Self 应用程序
很快我们将拥有一个自我管理的布局,用来保存和显示所有的笔记,所以让我们开始吧。
技术要求
您可以在 GitHub 上找到本章的代码文件,网址为github.com/PacktPublishing/Android-Programming-for-Beginners-Third-Edition/tree/main/chapter%2016.
RecyclerView 和 RecyclerAdapter
在第五章,使用 CardView 和 ScrollView 创建美丽的布局,我们使用了ScrollView,并用一些CardView小部件填充它,以便我们可以看到它滚动。我们可以利用我们刚学到的关于数组和ArrayList的知识,创建一个TextView小部件数组,用它们填充一个ScrollView,并在每个TextView中放置一个笔记的标题。这听起来像是在 Note to Self 应用程序中显示每个笔记的完美解决方案。
我们可以在 Java 代码中动态创建TextView小部件,将它们的text属性设置为笔记的标题,然后将TextView小部件添加到ScrollView中包含的LinearLayout中。然而,这并不完美。
显示大量小部件的问题
这可能看起来不错,但如果有数十、数百甚至数千个笔记怎么办?我们不能在内存中拥有数千个TextView小部件,因为 Android 设备可能会因为尝试处理如此大量的小部件和它们的数据而耗尽内存,或者至少变得非常缓慢。
现在,想象一下我们(我们确实想要)希望ScrollView中的每个笔记显示它是重要的、待办事项还是想法。以及笔记文本的简短片段呢?
我们需要设计一些聪明的代码,从多个ArrayList实例中加载和销毁Note对象和每个笔记的多个TextView小部件。这是可以做到的,但要高效地完成远非易事。
显示大量小部件的问题的解决方案
幸运的是,这是移动开发人员如此常见的问题,以至于 Android API 内置了解决方案。
我们可以在我们的 UI 布局中添加一个名为RecyclerView的单个小部件(类似于环保的ScrollView,但也带有助推器)。RecyclerView类被设计为解决我们讨论的问题的解决方案。此外,我们需要使用一种特殊类型的类与RecyclerView进行交互,该类了解RecyclerView的工作原理。
我们将使用适配器与之交互。我们将使用RecyclerAdapter类,扩展它,自定义它,然后使用它来控制我们的ArrayList实例(其中将保存我们的Note实例)的数据并在RecyclerView中显示它。
让我们更多地了解一下RecyclerView和RecyclerAdapter类的工作原理。
如何使用 RecyclerView 和 RecyclerAdapter
我们已经知道如何存储几乎无限的笔记;我们可以在ArrayList实例中这样做,尽管我们还没有实现它。我们也知道有一个名为RecyclerView的 UI 布局,专门设计用于从ArrayList实例中显示潜在的长列表数据。我们只需要看看如何将所有这些付诸实践。
要向我们的布局添加RecyclerView小部件,我们可以简单地从调色板上拖放它到我们的 UI 上,以通常的方式。现在不要这样做。让我们先讨论一下。
如果您在content_main.xml中的按钮下面添加了一个RecyclerView小部件,那么 UI 设计师中的RecyclerView小部件将如下所示:
图 16.1 - RecyclerView 小部件
然而,这种外观更多地代表了可能性,而不是应用程序的实际外观。如果我们在添加RecyclerView小部件后立即运行应用程序,我们只会得到一个RecyclerView小部件所在的空白区域。
要实际使用RecyclerView,我们需要做的第一件事是决定列表中的每个项目的外观。它可以是一个单独的TextView小部件,也可以是一个完整的布局。我们将使用LinearLayout。为了清晰和具体,我们将使用一个LinearLayout,它包含我们的RecyclerView中每个项目的三个TextView小部件。这将允许我们显示笔记状态(重要、想法或待办事项)、笔记标题以及来自实际笔记内容的短片段文本。
需要在其自己的 XML 文件中定义列表项,然后RecyclerView可以容纳此列表项布局的多个实例。
当然,这些都没有解释我们如何克服管理显示在哪个列表项中的数据的复杂性,以及如何从ArrayList中检索数据。
这些数据处理由我们自己定制的RecyclerAdapter类来处理。RecyclerAdapter类实现了Adapter接口。我们不需要知道Adapter内部是如何工作的;我们只需要重写必要的方法,然后RecyclerAdapter将负责与我们的RecyclerView小部件进行通信。
将RecyclerAdapter的实现连接到RecyclerView肯定比将 20 个TextView实例拖放到ScrollView上要复杂得多 - 但一旦完成,我们就可以忘记它,它将继续工作并自行管理,而不管我们向ArrayList中添加了多少笔记。它还具有处理诸如整洁格式和检测列表中点击了哪个项目等功能。
我们需要重写RecyclerAdapter类的一些方法,并添加一些我们自己的代码。
我们将如何使用 RecyclerAdapter 和笔记的 ArrayList 设置 RecyclerView
让我们先看一下所需步骤的概要,以便知道可以期望什么。为了使整个过程正常运行,我们将执行以下操作:
-
删除临时按钮和相关代码,然后向我们的布局添加一个具有特定
id属性的RecyclerView小部件。 -
创建一个 XML 布局来表示列表中的每个项目。我们已经提到列表中的每个项目将是一个包含三个
TextView小部件的LinearLayout。 -
创建一个扩展
RecyclerAdapter的新类,并向多个重写的方法添加代码,以控制其外观和行为,包括使用我们的列表项布局和充满Note实例的ArrayList。 -
向
MainActivity类添加代码,以使用RecyclerAdapter类和RecyclerView小部件,并将其绑定到我们的ArrayList实例。 -
向
MainActivity添加一个ArrayList以保存我们所有的笔记,并更新createNewNote方法,以将在DialogNewNote类中创建的任何新笔记添加到这个ArrayList中。
让我们详细地走一遍每一步。
向 Note to Self 项目添加 RecyclerView、RecyclerAdapter 和 ArrayList
打开“Note to Self”项目。作为提醒,如果您想要查看完成本章后的代码和工作中的应用程序,可以在第十六章/Note to self文件夹中找到。
注意
由于本章中所需的操作在不同的文件、类和方法之间跳转,我鼓励您通过在首选文本编辑器中保持打开以供参考的下载包中的文件来跟随。
删除临时的“显示笔记”按钮并添加 RecyclerView
接下来的几个步骤将清除我们在第十四章**Android 对话框窗口中添加的临时代码,并设置我们的RecyclerView小部件,以便在本章后面绑定到RecyclerAdapter:
-
在
content_main.xml文件中,删除之前为测试目的添加的 ID 为button的临时Button。 -
在
MainActivity.java文件的onCreate方法中,删除Button实例声明和初始化以及处理其点击的匿名类,因为此代码现在会创建错误。我们将在本章后面删除一些临时代码。删除以下代码:
// Temporary code
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Create a new DialogShowNote called dialog
DialogShowNote dialog = new DialogShowNote();
// Send the note via the sendNoteSelected
method
dialog.sendNoteSelected(mTempNote);
// Create the dialog
dialog.show(getSupportFragmentManager(),
"123");
}
});
-
现在切换回设计视图中的
content_main.xml文件,并从调色板的容器类别中拖放一个RecyclerView小部件到布局中。 -
将其
id属性设置为recyclerView。
现在我们已经从项目中删除了临时的 UI 方面,并且我们有了一个完整的RecyclerView小部件,具有一个准备从我们的 Java 代码中引用的唯一id值。
为 RecyclerView 创建列表项
接下来,我们需要一个布局来表示RecyclerView小部件中的每个项目。如前所述,我们将使用一个包含三个TextView小部件的LinearLayout。
使用以下步骤创建一个用于在我们的RecyclerView中使用的列表项:
-
右键单击
LinearLayout中的listitem。 -
确保您在
orientation属性上选择了vertical。 -
查看下一个屏幕截图,以了解我们在本节剩余步骤中要实现的内容。我已经对其进行了注释,以显示完成应用程序中每个部分的内容:
图 16.2 - 用于在我们的 RecyclerView 中使用的列表项
-
将三个
TextView小部件拖放到布局中,依次排列。第一个(顶部)将保存笔记状态/类型(想法、重要或待办事项)。第二个(中间)将保存笔记标题,第三个(底部)将保存笔记本身的文本片段。 -
根据以下表格配置
LinearLayout和TextView小部件的各种属性:
现在我们有了一个用于主布局的RecylerView小部件和一个用于RecyclerView列表中每个项目的布局。我们可以继续编写我们的RecyclerAdapter类实现。
编写 RecyclerAdapter 类的代码
现在我们将创建并编写一个全新的类。让我们称之为我们的新类NoteAdapter。在与MainActivity类(以及所有其他类)相同的文件夹中创建一个名为NoteAdapter的新类。
通过添加这些import语句并将其扩展为RecyclerView.Adapter类来编辑NoteAdapter类的代码,然后添加这两个重要的成员变量。编辑NoteAdapter类,使其与我们刚刚讨论过的以下代码相同:
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
public class NoteAdapter extends
RecyclerView.Adapter<NoteAdapter.ListItemHolder> {
private List<Note> mNoteList;
private MainActivity mMainActivity;
}
注意到类声明被红色下划线标出,显示我们的代码中有错误。错误是因为我们需要重写RecylerView.Adapter类(我们正在扩展的类)的一些抽象方法。
注意
我们在第十一章中讨论了抽象类及其方法,更多面向对象的编程。
这样做的最快方法是点击类声明,按住Alt键,然后点击Enter键。选择实现方法,如下面的截图所示:
图 16.3 - 选择实现方法
然后,点击确定以让 Android Studio 自动生成所需的方法。
这个过程添加了以下三个方法:
-
onCreateViewHolder方法,在需要列表项布局时调用。 -
onBindViewHolder方法,在将RecyclerAdapter绑定到布局中的RecyclerView时调用。 -
getItemCount方法,将用于返回ArrayList中Note实例的数量。现在它只返回0。
很快我们将为这些方法中的每一个添加代码,以在指定的时间做必要的工作。
但是,请注意,我们的代码中仍然存在多个错误,包括自动生成的方法以及类声明。此时代码编辑器的截图可能会有用:
图 16.4 - 我们代码中的多个错误
错误是因为NoteAdapter.ListItemHolder类不存在。当我们扩展NoteAdapter时,我们添加了ListItemHolder。这是我们选择的类类型,将用作每个列表项的持有者。目前它不存在,因此出现错误。当我们要求 Android Studio 实现缺失的方法时,也会自动生成具有相同原因的两个方法。
让我们通过开始编写所需的ListItemHolder类来解决问题。如果ListItemHolder实例与NoteAdapter共享数据/变量对我们有用,因此我们将ListItemHolder创建为内部类。
点击类声明中的错误,然后选择创建类'ListItemHolder',如下截图所示:
图 16.5 - 选择创建类'ListItemHolder'
以下代码已添加到NoteAdapter类中:
public class ListItemHolder {
}
但是类声明仍然有一个错误,如下截图所示:
图 16.6 - 类声明错误
错误消息显示ListItemHolder,但ListItemHolder必须扩展RecyclerView.ViewHolder才能用作参数化类型。
修改ListItemHolder类的声明以匹配此代码:
public class ListItemHolder extends
RecyclerView.ViewHolder
implements View.OnClickListener {
}
现在NoteAdapter类声明中的错误已经消失,但因为我们实现了View.OnClickListener,我们需要实现onClick方法。此外,ViewHolder没有提供默认构造函数,所以我们需要添加。将以下onClick方法(暂时为空)和此构造方法(暂时为空)添加到ListItemHolder类中:
public ListItemHolder(View view) {
super(view);
}
@Override
public void onClick(View view) {
}
注意
确保您将代码添加到内部的ListItemHolder类而不是NoteAdapter类。
经过多次调整和自动生成,我们最终拥有了一个无错误的NoteAdapter类,其中包括重写的方法和一个内部类,我们可以编写代码来使我们的RecyclerAdapter类工作。此外,我们可以编写代码来响应每个ListItemHolder实例的点击(在onClick方法中)。
编写 NoteAdapter 构造函数
接下来,我们将编写NoteAdapter构造方法,该方法将初始化NoteAdapter类的成员。将此构造方法添加到NoteAdapter类中:
public NoteAdapter(MainActivity mainActivity,
List<Note> noteList) {
mMainActivity = mainActivity;
mNoteList = noteList;
}
首先,注意构造函数的参数。它接收一个MainActivity实例以及一个List。这意味着当我们使用这个类时,我们需要发送一个对这个应用程序的主要活动(MainActivity)的引用,以及一个List/ArrayList。我们很快就会看到我们很快会在MainActivity类中编写的Note实例的ArrayList引用。然后,NoteAdapter将永久持有对所有用户笔记的引用。
编写 onCreateViewHolder 方法
接下来,我们将调整自动生成的onCreateViewHolder方法。将两行代码添加到onCreateViewHolder方法中,并研究自动生成的参数:
@NonNull
@Override
public NoteAdapter.ListItemHolder onCreateViewHolder(
@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.listitem, parent,
false);
return new ListItemHolder(itemView);
}
此代码通过使用LayoutInflater和我们新设计的listitem布局来初始化itemView。然后返回一个新的ListItemHolder实例,其中包含一个已经膨胀并且可以立即使用的布局。
编写 onBindViewHolder 方法
接下来,我们将调整onBindViewHolder方法。添加突出显示的代码使方法与此代码相同,并确保也研究方法的参数:
@Override
public void onBindViewHolder(
@NonNull NoteAdapter.ListItemHolder holder, int position) {
Note note = mNoteList.get(position);
holder.mTitle.setText(note.getTitle());
// Show the first 15 characters of the actual note
// Unless a short note then show half
if(note.getDescription().length() > 15) {
holder.mDescription.setText(note.getDescription()
.substring(0, 15));
}
else{
holder.mDescription.setText(note.getDescription()
.substring(0, note.getDescription().length() /2 ));
}
// What is the status of the note?
if(note.isIdea()){
holder.mStatus.setText(R.string.idea_text);
}
else if(note.isImportant()){
holder.mStatus.setText(R.string.important_text);
}
else if(note.isTodo()){
holder.mStatus.setText(R.string.todo_text);
}
}
首先,代码检查笔记是否超过 15 个字符,如果是,则将其截断,以便在列表中看起来合理。
然后,它检查笔记的类型(想法、待办事项或重要)并从字符串资源中分配适当的标签。
这段新代码在holder.mTitle、holder.mDescription和holder.mStatus变量中留下了一些错误,因为我们需要将它们添加到我们的ListItemHolder内部类中。我们很快就会做到这一点。
编写 getItemCount
修改此自动生成方法中的return语句,使其与下一行显示的突出显示的代码相同:
@Override
public int getItemCount() {
return mNoteList.size();
}
此代码在类内部使用,并提供ArrayList中当前项目的数量。
编写 ListItemHolder 内部类
现在我们可以转向内部类ListItemHolder。通过添加以下突出显示的代码来调整ListItemHolder内部类:
public class ListItemHolder
extends RecyclerView.ViewHolder
implements View.OnClickListener {
TextView mTitle;
TextView mDescription;
TextView mStatus;
public ListItemHolder(View view) {
super(view);
mTitle =
view.findViewById(R.id.textViewTitle);
mDescription =
view.findViewById(R.id.textViewDescription);
mStatus =
view.findViewById(R.id.textViewStatus);
view.setClickable(true);
view.setOnClickListener(this);
}
@Override
public void onClick(View view) {
mMainActivity.showNote(getAdapterPosition());
}
}
ListItemHolder构造函数只是获取布局中每个TextView小部件的引用。最后两行代码将整个视图设置为可点击,以便操作系统在点击持有者时调用我们讨论的下一个方法onClick。
在onClick方法中,mMainActivity.showNote方法的调用存在错误,因为该方法尚不存在,但我们将在下一节中修复这个问题。该调用将在适当的DialogFragment实例中显示被点击的笔记。
编写 MainActivity 以使用 RecyclerView 和 RecyclerAdapter 类
现在,切换到编辑窗口中的MainActivity类。将这三个新成员添加到MainActivity类中,并删除下面注释掉的临时代码:
// Temporary code
//Note mTempNote = new Note();
private List<Note> noteList = new ArrayList<>();
private RecyclerView recyclerView;
private NoteAdapter mAdapter;
如果尚未添加,请添加以下import指令:
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
这三个成员是我们所有Note实例的ArrayList,我们的RecyclerView实例以及我们的类NoteAdapter的一个实例。
向“onCreate”方法添加代码
在处理浮动操作按钮的代码之后,将以下突出显示的代码添加到onCreate方法中(为了上下文再次显示):
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
DialogNewNote dialog = new DialogNewNote();
dialog.show(getSupportFragmentManager(), "");
}
});
recyclerView =
findViewById(R.id.recyclerView);
mAdapter = new NoteAdapter(this, noteList);
RecyclerView.LayoutManager mLayoutManager =
new LinearLayoutManager(getApplicationContext());
recyclerView.setLayoutManager(mLayoutManager);
recyclerView.setItemAnimator(new DefaultItemAnimator());
// Add a neat dividing line between items in the list
recyclerView.addItemDecoration(
new DividerItemDecoration(this, LinearLayoutManager.VERTICAL));
// set the adapter
recyclerView.setAdapter(mAdapter);
前面的代码将需要以下三个import指令:
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.DividerItemDecoration;
在前面的代码中,我们使用布局中的RecyclerView实例初始化recyclerView引用。通过调用我们编写的构造函数来初始化我们的NoteAdapter(mAdapter)并注意到对MainActivity(this)和ArrayList实例的引用被传递进来,正如我们之前编写的类所要求的那样。
接下来,我们创建一个新对象LayoutManager。在下一行代码中,我们在recyclerView上调用setLayoutManager并传入这个新的LayoutManager实例。现在我们可以配置recyclerView的一些属性。
setItemAnimator和addItemDecoration方法使每个列表项在列表中的每个项目之间都有一个分隔线,视觉上更加美观。稍后,当我们构建设置屏幕时,我们将让用户选择添加和删除这个分隔线。
我们要做的最后一件事是调用setAdapter方法,将我们的适配器与我们的视图结合起来。
现在我们将对addNote方法进行一些更改。
修改 addNote 方法
在addNote方法中,删除我们在第十四章**Android 对话框窗口中添加的临时代码(显示为注释),并添加下面显示的新突出显示的代码:
public void createNewNote(Note n){
// Temporary code
//mTempNote = n;
noteList.add(n);
mAdapter.notifyDataSetChanged();
}
新突出显示的代码将一个笔记添加到ArrayList中,而不仅仅是初始化一个孤立的Note对象,现在已经被注释掉。然后,我们需要调用notifyDataSetChanged方法,让我们的适配器知道已经添加了一个新的笔记。
编写 showNote 方法
添加这个新方法,它是从NoteAdapter类中使用传递给NoteAdapter构造函数的对该类的引用来调用的。更具体地说,当用户点击RecyclerView中的项目时,它是从ListerItemHolder内部类中调用的。将showNote方法添加到MainActivity类中:
public void showNote(int noteToShow){
DialogShowNote dialog = new DialogShowNote();
dialog.sendNoteSelected(noteList.get(noteToShow));
dialog.show(getSupportFragmentManager(), "");
}
注意
NoteAdapter.java文件中的所有错误现在都已经消失。
刚刚添加的代码将启动一个新的DialogShowNote实例,并传入由noteToShow指向的特定所需的笔记。
运行应用程序
您现在可以运行应用程序,并输入一个新的笔记,如下面的截图所示:
图 16.7 – 添加新笔记
当您输入了几种类型的笔记后,列表(RecyclerView)将看起来像这样:
图 16.8 – 笔记列表
读者挑战
我们本可以花更多时间来格式化我们的两个对话框窗口的布局。为什么不参考第五章**使用 CardView 和 ScrollView 创建美丽的布局,以及 Material Design 网站,做得比我们迄今为止做得更好呢?此外,您可以通过使用CardView而不是LinearLayout来增强RecyclerView类/笔记列表。
不要花太长时间添加新的笔记,因为有一个小问题。关闭并重新启动应用程序。哦,所有的笔记都消失了!
常见问题解答
- 我仍然不明白
RecyclerAdapter是如何工作的 – 为什么?
这是因为我们实际上没有讨论它。我们没有讨论背后的细节的原因是我们不需要知道它们。如果我们重写了所需的方法,就像我们刚刚看到的那样,一切都会正常工作。这就是RecyclerAdapter和我们使用的大多数其他类的意图:隐藏实现,公开方法来暴露必要的功能。
- 我需要了解
RecyclerAdapter类和其他类的内部情况吗?
确实,关于RecyclerAdapter(以及我们在本书中使用的几乎每个类)还有更多细节,我们没有空间来讨论。阅读您使用的类的官方文档是一个好习惯。您可以在这里阅读有关 Android API 的所有类的更多信息:developer.android.com/reference。
总结
现在我们已经添加了保存多个笔记的能力,并实现了显示它们的能力。
我们通过学习和使用RecyclerAdapter类来实现了这一点,该类实现了Adapter接口,允许我们将RecyclerView和ArrayList绑定在一起,无需我们(程序员)担心这些类的复杂代码,而我们甚至看不到。
在下一章中,我们将开始让用户的笔记在他们退出应用程序或关闭设备时保持。此外,我们将创建一个设置屏幕,并看看我们如何使设置也持久。我们将使用不同的技术来实现这些目标。
第十七章:数据持久性和共享
在本章中,我们将看一下将数据保存到 Android 设备的永久存储的几种不同方法。此外,我们将首次向我们的应用程序添加第二个Activity。当在我们的应用程序中实现一个单独的“屏幕”,比如设置屏幕时,将其放在一个新的Activity中通常是有意义的。我们可以费力地隐藏原始 UI,然后显示新的 UI,但这很快会导致令人困惑和容易出错的代码。因此,我们将看看如何添加一个Activity类并在它们之间导航用户。
总之,在本章中,我们将做以下事情:
-
学习如何使用 Android 意图在
Activity类之间切换并传递数据 -
为“Note to Self”项目在一个新的
Activity类中创建一个简单(非常简单)的设置屏幕 -
使用
SharedPreferences类持久化设置屏幕数据 -
学习JavaScript 对象表示(JSON)进行序列化
-
探索 Java 的
try-catch-finally语法 -
在我们的“Note to Self”应用程序中实现保存数据
技术要求
您可以在 GitHub 上找到本章中的代码文件,网址为github.com/PacktPublishing/Android-Programming-for-Beginners-Third-Edition/tree/main/chapter%2017。
Android 意图
Intent类的命名非常恰当。它是一个演示我们应用程序中Activity类意图的类。它使意图清晰,并且也方便了它。
到目前为止,我们所有的应用程序都只有一个Activity,但许多 Android 应用程序包含多个Activity。
在它可能最常见的用法中,Intent类允许我们在Activity实例之间切换。但是,Activity实例是由具有成员变量的类制作的。那么,当我们在它们之间切换时,变量的值 - 数据 - 会发生什么?意图通过允许我们在Activity实例之间传递数据来解决了这个问题。
意图不仅仅是关联我们应用程序的Activity实例。它们还使我们能够与其他应用程序进行交互。例如,我们可以在我们的应用程序中提供一个链接,让用户发送电子邮件,打电话,与社交媒体互动,或在浏览器中打开网页,并让电子邮件应用程序、电话应用程序、社交媒体应用程序或网络浏览器完成所有工作。
这本书中没有足够的页面来深入研究与其他应用程序的交互,我们主要将专注于在活动之间切换和传递数据。
切换活动
假设我们的应用程序有两个基于Activity的类,因为我们很快就会有。我们可以假设通常情况下,我们有一个名为MainActivity的Activity类,这是应用程序的起点,还有一个名为SettingsActivity的第二个Activity。这是我们如何从MainActivity切换到SettingsActivity的方法:
// Declare and initialize a new Intent object called myIntent
Intent myIntent = new Intent(this, SettingsActivity.class);
// Switch to the SettingsActivity
startActivity(myIntent);
仔细看看我们如何初始化Intent对象。Intent有一个构造函数,它接受两个参数。第一个是对当前Activity的引用,this。第二个参数是我们想要打开的Activity类的名称,SettingsActivity.class。SettingsActivity末尾的.class部分使其成为AndroidManifest.xml文件中声明的Activity类的完整名称,我们将在不久的将来尝试意图时查看它。
唯一的问题是SettingsActivity不共享MainActivity的任何数据。从某种意义上说,这是一件好事,因为如果你需要从MainActivity获取所有数据,那么切换活动可能不是进行应用程序设计的最佳方式。然而,封装得如此彻底以至于这两个Activity实例完全不知道对方是不合理的。
在活动之间传递数据
如果我们为用户有一个登录屏幕,并且我们想要将用户的凭据传递给我们应用程序的每个Activity,我们可以使用意图来实现。
我们可以像这样向Intent类添加数据:
// Create a String called username
// and set its value to bob
String username = "Bob";
// Create a new Intent as we have already seen
Intent myIntent = new Intent(this, SettingsActivity.class);
// Add the username String to the Intent
// using the putExtra method of the Intent class
myIntent.putExtra("USER_NAME", username);
// Start the new Activity as we did before
startActivity(myIntent);
在SettingsActivity中,我们可以这样检索字符串:
// Here we need an Intent also
// But the default constructor will do
// as we are not switching Activity
Intent myIntent = new Intent();
// Initialize username with the passed in String
String username = intent.getExtra().getStringKey("USER_NAME");
在前两个代码块中,我们以与之前相同的方式切换了Activity。但在调用startActivity方法之前,我们使用putExtra方法将字符串加载到意图中。
我们使用identifier实例添加数据,该数据可以在检索Activity中用于标识和检索数据。
标识符名称由您决定,但应使用有用/易记的值。
然后,在接收Activity中,我们只需使用默认构造函数创建一个意图:
Intent myIntent = new Intent();
然后,我们可以使用getExtras方法和键值对中的适当标识符检索数据。
一旦我们想要开始发送多个值,就值得考虑不同的策略。
Intent类可以在发送比这更复杂的数据时帮助我们,但Intent类有其限制。例如,我们将无法发送Note对象。
向“自我备忘录”添加设置页面
现在我们已经掌握了关于 Android Intent类的所有知识,我们可以向我们的“自我备忘录”应用程序添加另一个屏幕(Activity)。我们将添加一个设置屏幕。
我们将首先为我们的设置屏幕创建一个新的Activity,并查看这对AndroidManifest.xml文件的影响;然后我们将为我们的设置屏幕创建一个非常简单的布局,并添加 Java 代码以从MainActivity切换到新的布局。但是,在学习如何将设置保存到磁盘之前,我们将推迟使用 Java 连接我们的设置屏幕。我们将在本章后面进行此操作,然后返回设置屏幕以使其数据持久化。
首先,让我们创建一个新的Activity类。我们将其称为SettingsActivity。
创建 SettingsActivity
这将是一个屏幕,用户可以在RecyclerView小部件中的每个笔记之间打开或关闭装饰性分隔符。这不会是一个全面的设置屏幕,但这将是一个有用的练习,并且我们将学习如何在活动之间切换以及将数据保存到磁盘。按照以下步骤开始:
-
在项目资源管理器中,右键单击包含所有
.java文件并与您的包名称相同的文件夹。从弹出的上下文菜单中,选择新建 | Activity | 空白 Activity。 -
在
SettingsActivity中。 -
将所有其他选项保留为默认值,然后单击完成。
Android Studio 已为我们创建了一个基于Activity的类及其关联的.java文件。让我们快速查看一下幕后为我们完成的一些工作,因为了解正在发生的事情是有用的。
从项目资源管理器中的manifests文件夹中打开AndroidManifest.xml文件。注意文件中间大约有一半的以下代码行:
<activity android:name=".SettingsActivity"></activity>
这就是Activity类是如何Activity类未注册,然后尝试运行它将使应用程序崩溃。我们可以通过在新的.java文件中创建一个扩展Activity(或AppCompatActivity)的类来创建Activity类。但是,我们随后必须自己添加前面的代码。此外,通过使用新的Activity向导,我们自动生成了一个布局 XML 文件(activity_settings.xml)。
设计设置屏幕布局
我们将快速为我们的设置屏幕构建一个 UI,以下步骤和图示应该使这变得简单:
- 打开
activity_settings.xml文件,切换到设计选项卡,然后我们将快速布置我们的设置屏幕。在执行其余步骤时,请使用下一个图示作为指南:
图 17.1 – 设计设置屏幕
-
将Switch小部件拖放到布局的中上部。我通过拖动边缘来拉伸我的小部件,使其变大。
-
添加一个
id属性switch1(如果默认情况下还没有),这样我们就可以使用SettingsActivity.java中的 Java 代码与其交互。 -
使用约束处理程序来固定开关的位置,或者单击推断约束按钮以自动修复它。
我们现在为设置屏幕有了一个简单的新布局,并且id属性已经就位,准备在本章后面的 Java 代码中连接它。
使用户能够切换到设置屏幕
我们已经知道如何切换到SettingsActivity。此外,由于我们不会向其传递任何数据,也不会从中传递任何数据,因此我们可以只用两行 Java 代码就可以使其工作。
您可能已经注意到我们应用程序的操作栏中有一个菜单图标。这是我们创建项目时使用的基本活动模板的默认部分。如下图所示:
图 17.2 – 菜单图标
如果您点击它,您会发现默认情况下已经有一个菜单选项设置。当您点击菜单图标时,您将看到以下内容:
图 17.3 – 设置选项
我们只需要将切换到SettingsActivity类的代码放在MainActivity类的onOptionsItemSelected方法中。Android Studio 甚至默认为我们提供了一个if块,以便我们将来可能想要添加设置屏幕。多么体贴。
切换到编辑器窗口中的MainActivity.java,并在onOptionsItemSelected方法中找到以下代码块:
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
将此代码添加到先前显示的if块中,就在return true语句之前:
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
您需要使用您喜欢的技术导入Intent类,以添加以下代码行:
import android.content.Intent;
现在,您可以运行应用程序并通过点击设置菜单选项来访问新的设置屏幕。此屏幕截图显示了模拟器上运行的设置屏幕:
图 17.4 – 设置屏幕
要从SettingsActivity返回到MainActivity,您可以点击设备上的返回按钮。
使用 SharedPreferences 持久化数据
在 Android 中,有几种方法可以使数据持久化。持久化意味着如果用户退出应用程序,然后再次打开应用程序,他们的数据仍然可用。使用哪种方法取决于应用程序和数据类型。
在本书中,我们将介绍三种使数据持久化的方法。对于保存用户设置,我们只需要一个简单的方法。毕竟,我们只需要知道他们是否希望在RecyclerView小部件的每个笔记之间有装饰性分隔符。
让我们看看如何使我们的应用程序将变量保存和重新加载到设备的内部存储器中。我们需要使用SharedPreferences类。SharedPreferences是一个提供对可以由应用程序的所有Activity类访问和编辑的数据的访问权限的类。让我们看看如何使用它:
// A SharedPreferences for reading data
SharedPreferences prefs;
// A SharedPreferences.Editor for writing data
SharedPreferences.Editor editor;
与所有对象一样,我们需要在使用之前初始化它们。我们可以通过使用getSharedPreferences方法并传递一个字符串来初始化prefs对象,该字符串将用于引用使用该对象读取和写入的所有数据。通常,我们可以使用应用程序的名称作为此字符串的值。在下一个代码中,MODE_PRIVATE表示任何类(仅在此应用程序中)都可以访问它:
prefs = getSharedPreferences("My App", MODE_PRIVATE);
然后,我们使用新初始化的prefs对象通过调用edit方法来初始化我们的editor对象:
editor = prefs.edit();
假设我们想要保存我们在名为username的字符串中拥有的用户名称。然后,我们可以像这样将数据写入设备的内部存储器中:
editor.putString("username", username);
editor.commit();
putString方法中使用的第一个参数是一个标签,可以用来引用数据;第二个是保存我们想要保存的数据的实际变量。上述代码的第二行启动了保存过程。因此,我们可以这样将多个变量写入磁盘:
editor.putString("username", username);
editor.putInt("age", age);
editor.putBoolean("newsletter-subscriber", subscribed);
// Save all the above data
editor.commit();
上述代码演示了您可以保存其他变量类型,并且它当然假定username,age和subscribed变量先前已经被声明并用适当的值初始化。
一旦editor.commit()执行完毕,数据就被存储了。我们甚至可以退出应用程序,甚至关闭设备,数据仍然会持久保存。
使用 SharedPreferences 重新加载数据
让我们看看下次运行应用程序时如何重新加载数据。这段代码将重新加载上一个代码保存的三个值。我们甚至可以声明我们的变量并用存储的值进行初始化:
String username =
prefs.getString("username", "new user");
int age = prefs.getInt("age", -1);
boolean subscribed =
prefs.getBoolean("newsletter-subscriber", false)
在上述代码中,我们使用适合数据类型的方法从磁盘加载数据,并使用了保存数据的相同标签。不太清楚的是每个方法调用的第二个参数。
getString,getInt和getBoolean方法需要一个默认值作为第二个参数。如果没有存储带有该标签的数据,它将返回默认值。
然后我们可以在我们的代码中检查这些默认值,并尝试获取真实的值。例如,看这里:
if (age == -1){
// Ask the user for his age
}
我们现在知道足够的知识来保存我们用户的设置在 Note to Self 应用程序中。
使“Note to Self”设置持久化
我们已经学会了如何将数据保存到设备的内存中。当我们实现保存用户设置时,我们还将再次看到我们如何处理Switch输入,以及我们刚刚看到的代码将去哪里使我们的应用程序按我们想要的方式工作。
编写 SettingsActivity 类
大部分操作将在SettingsActivity.java文件中进行。因此,点击适当的选项卡,我们将逐步添加代码。
首先,我们需要一些成员变量,这些变量将为我们提供工作的SharedPreferences和Editor实例。我们还希望有一个成员变量来表示用户的设置选项:他们是否想要装饰性分隔线。
在SettingsActivity类的类声明之后添加以下成员变量:
private SharedPreferences mPrefs;
private SharedPreferences.Editor mEditor;
private boolean mShowDividers;
导入SharedPreferences类:
import android.content.SharedPreferences;
现在在onCreate方法中,添加代码来初始化mPrefs和mEditor:
mPrefs = getSharedPreferences("Note to self", MODE_PRIVATE);
mEditor = mPrefs.edit();
接下来,在onCreate方法中,让我们获取对我们的Switch小部件的引用,并加载代表我们用户先前选择是否显示分隔线的保存数据。
我们以与第十三章**相同的方式获取对开关的引用,匿名类-让 Android 小部件活跃起来。请注意默认值为true-显示分隔线。我们还将根据需要将开关设置为打开或关闭:
mShowDividers = mPrefs.getBoolean("dividers", true);
Switch switch1 = findViewById(R.id.switch1);
// Set the switch on or off as appropriate
switch1.setChecked(mShowDividers);
您需要导入Switch类:
import android.widget.Switch;
接下来,我们创建一个匿名类来监听和处理对我们的Switch小部件的更改。
当isChecked变量为true时,我们使用prefs对象将dividers标签和mShowDividers变量设置为true;当未选中时,我们将它们都设置为false。
将以下代码添加到我们刚刚讨论的onCreate方法中:
switch1.setOnCheckedChangeListener(
new CompoundButton.OnCheckedChangeListener() {
public void onCheckedChanged(
CompoundButton buttonView,
boolean isChecked) {
if(isChecked){
mEditor.putBoolean(
"dividers", true);
mShowDividers = true;
}else{
mEditor.putBoolean(
"dividers", false);
mShowDividers = false;
}
}
}
);
您需要导入CompoundButton类:
import android.widget.CompoundButton;
您可能已经注意到,在任何代码中,我们都没有调用mEditor.commit方法来保存用户的设置。我们本可以在检测到开关变化后放置它,但将它放在保证被调用但只调用一次的地方更简单。
我们将利用我们对Activity生命周期的了解,并重写onPause方法。当用户离开SettingsActivity屏幕,无论是返回到MainActivity还是退出应用程序,操作系统都会调用onPause方法,并保存设置。在SettingsActivity类的结束大括号之前添加以下代码来重写onPause方法并保存用户的设置:
@Override
protected void onPause() {
super.onPause();
// Save the settings here
mEditor.commit();
}
最后,我们可以向MainActivity类添加一些代码,以在应用程序启动时或用户从设置屏幕切换回主屏幕时加载设置。
编写 MainActivity 类
在我们声明NoteAdapter实例后添加一些成员变量的突出代码:
private List<Note> noteList = new ArrayList<>();
private RecyclerView recyclerView;
private NoteAdapter mAdapter;
private boolean mShowDividers;
private SharedPreferences mPrefs;
导入SharedPreferences类:
import android.content.SharedPreferences;
现在我们有一个boolean成员来决定是否显示分隔符,以及一个SharedPreferences实例来从磁盘读取设置。
现在我们将重写onResume方法,初始化我们的mPrefs变量,并将设置加载到mShowDividers变量中。
在MainActivity类中添加重写的onResume方法,如下所示:
@Override
protected void onResume(){
super.onResume();
mPrefs = getSharedPreferences(
"Note to self", MODE_PRIVATE);
mShowDividers = mPrefs.getBoolean(
"dividers", true);
}
用户现在能够选择他们的设置。应用程序将根据需要保存和重新加载它们,但我们需要让MainActivity类响应用户的选择。
在onCreate方法中找到这段代码并删除它:
// Add a neat dividing line between items in the list
recyclerView.addItemDecoration(
new DividerItemDecoration(
this, LinearLayoutManager.VERTICAL));
上一个代码是在列表中的每个笔记之间设置分隔符。将这段新代码添加到onResume方法中,这是相同的代码行,被if语句包围,只有当mShowDividers为true时才选择性地使用分隔符。在onResume方法中在上一个代码后添加以下代码:
if(mShowDividers) {
// Add a neat dividing line between list items
recyclerView.addItemDecoration(
new DividerItemDecoration(
this, LinearLayoutManager.VERTICAL));
}else{
// check there are some dividers
// or the app will crash
if(recyclerView.getItemDecorationCount() > 0) {
recyclerView.removeItemDecorationAt(0);
}
}
运行应用程序并注意到分隔符已经消失;转到设置屏幕,打开分隔符,然后返回到主屏幕(使用返回按钮)- 看哪:现在有分隔符了。下一个图显示了带有和不带有分隔符的列表,以便说明我们添加的代码起作用,并且设置在这两个 Activity 类之间持续存在:
图 17.5 - 带有和不带有分隔符的列表
确保尝试退出应用程序并重新启动以验证设置是否已保存到磁盘。甚至可以关闭并重新打开模拟器,设置将持续存在。
现在我们有一个整洁的设置屏幕,我们可以永久保存用户选择的装饰偏好。当然,关于持久性的一个重要缺失环节是用户的基本数据,他们的笔记,仍然没有持久保存。
更高级的持久性
让我们考虑一下我们需要做什么。我们想要将一堆笔记保存到内部存储中。更具体地说,我们想要存储一系列字符串和相关的布尔值。这些字符串和布尔值代表用户的笔记标题、笔记文本,以及它是待办事项、重要事项还是想法。
考虑到我们已经了解的SharedPreferences类,乍一看这似乎并不特别具有挑战性 - 直到我们更深入地了解我们的需求。如果用户喜欢我们的应用程序并最终拥有 100 条笔记怎么办?我们需要 100 个键值对的标识符。这并非不可能,但开始变得尴尬。
现在考虑一下,我们想要增强应用程序,并让用户能够为它们添加日期。Android 有一个Date类非常适合这个用途。然后,我们可以相对简单地为我们的应用程序添加一些新功能,比如提醒。但是当涉及到保存数据时,事情突然变得复杂起来。
我们如何使用SharedPreferences来存储日期?它并不是为此而设计的。我们可以在保存时将其转换为字符串,然后在加载时再次转换回来,但这远非简单。
随着我们的应用程序功能的增加和用户获取更多的笔记,整个持久性问题变得一团糟。我们需要的是一种保存和加载对象的方法,实际的 Java 对象。如果我们可以简单地保存和加载对象,包括它们的内部数据(字符串,布尔值,日期或其他任何东西),我们的应用程序可以拥有我们可以想到的任何类型的数据,以满足我们的用户。
将数据对象转换为位和字节以存储在磁盘上的过程称为序列化;反向过程称为反序列化。单独的序列化是一个庞大而复杂的主题。幸运的是,正如我们所期望的那样,有一个类来处理大部分复杂性。
什么是 JSON?
JSON代表JavaScript 对象表示,它在 Android 和 Java 语言之外的领域被广泛使用。它可能最常用于在 Web 应用程序和服务器之间发送数据。
幸运的是,Android 上有可用的 JSON 类,几乎完全隐藏了序列化过程的复杂性。通过学习一些更多的 Java 概念,我们可以快速开始使用这些类,并开始将整个 Java 对象写入设备存储,而不必担心构成对象的原始类型是什么。
与我们迄今为止见过的其他类相比,JSON 类执行的操作具有比正常情况下更高的失败概率,这是超出它们的控制范围的。要找出原因以及可以采取什么措施,让我们看看 Java 异常。
Java 异常 - try,catch 和 finally
所有这些关于 JSON 的讨论要求我们学习一个新的 Java 概念:异常。当我们编写一个执行可能失败的操作的类,特别是由于我们控制之外的原因,建议在我们的代码中明确这一点,以便任何使用我们的类的人都能为可能性做好准备。
保存和加载数据是一个这样的场景,其中失败是可能的,超出了我们的控制范围。想想当 SD 卡已被移除或已损坏时尝试加载数据的情况。另一个代码可能失败的情况是当我们编写依赖于网络连接的代码时 - 如果用户在数据传输过程中断网了会怎么样?
Java 异常是解决方案,JSON 类使用它们,所以现在是学习它们的好时机。
当我们编写一个使用有可能失败的代码的类时,我们可以通过使用try,catch和finally来为我们的类的用户做好准备。
我们可以在我们的类中编写方法,在签名的末尾使用throws Java 关键字 - 可能是这样的:
public void somePrecariousMethod() throws someException{
// Risky code goes here
}
现在,任何使用somePrecariousMethod方法的代码都需要try和catch块 - 也许像这样:
try{
...
somePrecariousMethod();
...
}catch(someException e){
Log.e("Exception:" + e, "Uh ohh")
// Take action if possible
}
可选地,如果我们想在try和catch块之后执行任何进一步的操作,我们还可以添加一个finally块:
finally{
// More action here
}
在我们的“自言自语”应用中,我们将采取最少的操作来处理异常,并简单地将错误输出到 logcat,但您可以做一些事情,比如通知用户,重试操作,或者实施一些聪明的备用计划。
备份用户数据在“自言自语”中
因此,通过我们对异常的新认识,让我们修改我们的“自言自语”代码,然后我们可以介绍JSONObject和JSONException类。
首先,让我们对我们的Note类进行一些小修改。添加一些更多的成员,这些成员将作为我们Note类的每个方面的键值对的键:
private static final String JSON_TITLE = "title";
private static final String JSON_DESCRIPTION = "description";
private static final String JSON_IDEA = "idea";
private static final String JSON_TODO = "todo";
private static final String JSON_IMPORTANT = "important";
现在添加一个构造函数和一个接收JSONObject并抛出JSONException的空默认构造函数。构造函数的主体通过调用JSONObject的getString或getBoolean方法,传入键作为参数,来初始化定义单个Note对象属性的每个成员。我们还提供了一个空的默认构造函数,现在我们提供了我们的专门的构造函数,这是必需的:
// Constructor
// Only used when new is called with a JSONObject
public Note(JSONObject jo) throws JSONException {
mTitle = jo.getString(JSON_TITLE);
mDescription = jo.getString(JSON_DESCRIPTION);
mIdea = jo.getBoolean(JSON_IDEA);
mTodo = jo.getBoolean(JSON_TODO);
mImportant = jo.getBoolean(JSON_IMPORTANT);
}
// Now we must provide an empty default constructor
// for when we create a Note as we provide a
// specialized constructor.
public Note (){
}
您需要导入JSONException和JSONObject类:
import org.json.JSONException;
import org.json.JSONObject;
接下来我们将看到的代码将给定Note对象的成员变量加载到JSONObject中。这是Note对象的成员被打包为一个单独的JSONObject,以便进行实际的序列化。
我们只需要使用适当的键和匹配的成员变量调用put方法。该方法返回JSONObject(我们将在一分钟内看到它在哪里),它还会抛出一个JSONObject异常。添加我们刚刚讨论过的代码:
public JSONObject convertToJSON() throws JSONException{
JSONObject jo = new JSONObject();
jo.put(JSON_TITLE, mTitle);
jo.put(JSON_DESCRIPTION, mDescription);
jo.put(JSON_IDEA, mIdea);
jo.put(JSON_TODO, mTodo);
jo.put(JSON_IMPORTANT, mImportant);
return jo;
}
现在让我们创建一个JSONSerializer类,它将执行实际的序列化和反序列化。创建一个新类并将其命名为JSONSerializer。
让我们将其分成几个部分,并在编写每个部分的代码时讨论我们正在做什么。
首先,声明和一些成员变量:一个字符串来保存数据将被保存的文件名,以及一个Context对象,在 Android 中写入数据到文件是必要的。在您刚刚创建的类中添加突出显示的代码:
public class JSONSerializer {
private String mFilename;
private Context mContext;
// All the rest of the code for the class goes here
}// End of class
注意
您需要导入Context类:
import android.content.Context;
先前的代码显示,类的结束大括号和随后为该类编写的所有代码应该放在其中。这是非常简单的构造函数,我们在其中初始化了作为参数传入的两个成员变量。添加JSONSerializer的构造函数:
public JSONSerializer(String fn, Context con){
mFilename = fn;
mContext = con;
}
现在我们可以开始编写类的真正要点。接下来是save方法。它首先创建一个JSONArray对象,这是一个专门用于处理 JSON 对象的ArrayList类。
接下来,代码使用增强的for循环来遍历notes数组列表中的所有Note对象,并使用Note类中我们之前添加的convertToJSON方法将它们转换为 JSON 对象。然后,我们将这些转换后的JSONObject实例加载到jArray中。
接下来,代码使用Writer实例和OutputStream实例组合将数据写入实际文件。请注意,OutputStream实例需要初始化mContext对象。添加我们刚刚讨论过的代码:
public void save(List<Note> notes)
throws IOException, JSONException{
// Make an array in JSON format
JSONArray jArray = new JSONArray();
// And load it with the notes
for (Note n : notes)
jArray.put(n.convertToJSON());
// Now write it to the private disk space of our app
Writer writer = null;
try {
OutputStream out =
mContext.openFileOutput(mFilename,
mContext.MODE_PRIVATE);
writer = new OutputStreamWriter(out);
writer.write(jArray.toString());
} finally {
if (writer != null) {
writer.close();
}
}
}
您需要为这些新类添加以下import语句:
import org.json.JSONArray;
import org.json.JSONException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.List;
现在进行反序列化 - 加载数据。这一次,正如我们所预料的,该方法不接收任何参数,而是返回一个ArrayList实例。使用mContext.openFileInput创建一个InputStream实例,打开包含所有数据的文件。
我们使用while循环将所有数据附加到一个字符串中,并使用我们的新Note构造函数,该构造函数将 JSON 数据提取到常规原始变量中,以将每个JSONObject解包到一个Note对象中,并将其添加到返回给调用代码的ArrayList中:
public ArrayList<Note> load() throws IOException, JSONException{
ArrayList<Note> noteList = new ArrayList<Note>();
BufferedReader reader = null;
try {
InputStream in =
mContext.openFileInput(mFilename);
reader = new BufferedReader(new
InputStreamReader(in));
StringBuilder jsonString = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
jsonString.append(line);
}
JSONArray jArray = (JSONArray) new
JSONTokener(jsonString.toString()
).nextValue();
for (int i = 0; i < jArray.length(); i++) {
noteList.add(new
Note(jArray.getJSONObject(i)));
}
} catch (FileNotFoundException e) {
// we will ignore this one, since it happens
// when we start fresh. You could add a log here.
} finally {// This will always run
if (reader != null)
reader.close();
}
return noteList;
}
您需要添加以下导入:
import org.json.JSONTokener;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
现在我们只需要让我们的新类在MainActivity类中工作。在MainActivity声明之后添加一个新成员,如下所示。此外,删除noteList的初始化,只留下声明,因为我们现在将在onCreate方法中使用一些新代码来初始化它。我已经注释掉了您需要删除的行:
public class MainActivity extends AppCompatActivity {
private JSONSerializer mSerializer;
//private List<Note> noteList = new ArrayList<>();
private List<Note> noteList;
现在,在onCreate方法中,我们通过使用文件名和getApplicationContext()调用JSONSerializer构造函数来初始化mSerializer,它返回应用程序的Context实例并且是必需的。然后我们可以使用JSONSerializer load方法来加载任何保存的数据。在处理浮动操作按钮的代码之后添加这段新的突出显示的代码。这段新代码必须出现在我们处理RecyclerView实例之前:
…
FloatingActionButton fab =
(FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
DialogNewNote dialog = new DialogNewNote();
dialog.show(getSupportFragmentManager(), "");
}
});
mSerializer = new JSONSerializer("NoteToSelf.json",
getApplicationContext());
try {
noteList = mSerializer.load();
} catch (Exception e) {
noteList = new ArrayList<Note>();
Log.e("Error loading notes: ", "", e);
}
recyclerView =
findViewById(R.id.recyclerView);
mAdapter = new NoteAdapter(this, noteList);
…
注意
此时您需要导入Log类:
import android.util.Log;
在先前的代码中,我展示了大量的上下文,因为它的定位对于它的工作是至关重要的。如果您在运行时遇到任何问题,请务必将其与第十七章/java文件夹中的下载包中的代码进行比较。
现在我们可以向MainActivity类添加一个新的方法,这样我们就可以调用它来保存所有用户的数据。这个新方法所做的就是调用JSONSerializer类的save方法,传入所需的Note对象列表:
public void saveNotes(){
try{
mSerializer.save(noteList);
}catch(Exception e){
Log.e("Error Saving Notes","", e);
}
}
现在,就像我们保存用户设置时所做的那样,我们将重写onPause方法来保存用户的笔记数据。确保在MainActivity类中添加这段代码:
@Override
protected void onPause(){
super.onPause();
saveNotes();
}
就是这样。我们现在可以运行应用程序,并添加任意多的笔记。ArrayList会将它们全部存储在我们运行的应用程序中,我们的RecyclerAdapter将管理在RecyclerView中显示它们,现在 JSON 也会负责将它们加载到磁盘上,并加载它们回来。
经常问的问题
- 我并没有完全理解本章的所有内容 - 我不适合成为程序员吗?
本章介绍了许多新的类、概念和方法。如果你的头有点疼,这是可以预料的。如果一些细节不清楚,不要让它阻碍你。继续进行接下来的几章(它们要简单得多),然后回顾这一章并检查已完成的代码文件。
- 那么,序列化的详细工作原理是什么?
序列化确实是一个广阔的主题。你可能一辈子都写应用程序而从未真正需要理解它。这可能是计算机科学学位的主题。如果你想了解更多,请看一下这篇文章:en.wikipedia.org/wiki/Serialization。
总结
在我们通过 Android API 的旅程中,现在值得回顾一下我们所知道的。我们可以布置自己的 UI 设计,并从各种各样的小部件中选择,让用户与 UI 进行交互。我们可以创建多个屏幕以及弹出对话框,并且可以捕获全面的用户数据。此外,我们现在可以使这些数据持久化。
当然,还有很多关于 Android API 的知识需要学习,甚至超出了这本书所教授的范围,但重点是我们现在已经知道足够的知识来规划和实现一个可工作的应用程序。你现在就可以开始自己的应用程序了。
如果你有立即开始自己的项目的冲动,那么我的建议是继续前进并去做。不要等到你认为自己是一个“专家”或更加准备好。阅读这本书,更重要的是,实现应用程序将使你成为一个更好的 Android 程序员,但没有什么比设计和实现自己的应用程序更能让你更快地学会!完全可以通过阅读这本书并同时进行自己的项目工作。
在下一章中,我们将通过使应用程序支持多语言来为这个应用程序添加最后的修饰。这非常快速和简单。