五、编写客户端注册/登录模块
前面我们创建了简单的服务端用户身份认证服务。现在我们回到客户端,编写对应的注册/登录模块。做如下的设计:
- 用户运行笔记应用,进入后首先检查是否已经登录
- 如果已经登录,进入正常的首页(笔记列表页)
- 如果没有登录,自动调起登录页面,用户可在此登录,或者选择注册新账户
- 笔记列表页面提供用户信息入口,点击后进入用户信息页面,在这里展示用户信息,并且提供注销功能
添加访问网络的权限
我们的应用程序现在有访问网络的相关功能。根据Android系统的规则,必须为APP相关的权限使用进行声明。打开AndroidManifest.xml文件:
文件结构如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.jing.app.sn">
<application
...
>
...
</application>
</manifest>
在<application>标签之前添加网络权限使用声明即可:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.jing.app.sn">
<uses-permission android:name="android.permission.INTERNET" />
<application
...
>
...
</application>
</manifest>
这样,我们的APP就能够进行网络操作了。
创建注册/登录页面
首先创建注册/登录页面。用户登录是APP中常见的功能,因此Android Studio提供了一个登录页面模板,我们可以直接使用,不必自己完全重写。
选中我们APP的包名,点击右键选择“New -> Activity -> Login Activity”:
在弹出的对话框中填写Activity名称、布局文件以及标题:
点击完成,页面就创建完毕了。下面改写笔记列表页面,增加对登录状态的判断。
存取登录状态
我们通过Android提供的SharedPreference存储机制来保存和维护当前APP的登录状态。找到之前创建的工具类Utils类(Utils.java),在里面编写一组对应的方法:
public class Utils {
...
/**
* 读取当前登录的email账号
* @param context
* @return
*/
public static String getUserEmail(Context context) {
SharedPreferences pref = context.getSharedPreferences("user", Context.MODE_PRIVATE);
return pref.getString("email", "");
}
/**
* 保存已登录的email账号
* @param context
* @param email
*/
public static void saveUserEmail(Context context, String email) {
SharedPreferences pref = context.getSharedPreferences("user", Context.MODE_PRIVATE);
SharedPreferences.Editor edit = pref.edit();
edit.putString("email", email);
edit.commit();
}
}
说明一下:
- getUserEmail():获取当前存储的email账号,如果不为空,表明已经登录,并且可以根据这个email来向服务端查询数据。
- saveUserEmail():登录成功后,将登录的email账号写入;注销时,存入空值即可。
修改笔记列表页面
打开笔记列表页面(NoteListActivity),找到onResume()方法,增加代码判断登录状态。如果没有登录就进入注册/登录页面:
@Override
protected void onResume() {
super.onResume();
// 增加代码判断登录状态:
if (TextUtils.isEmpty(Utils.getUserEmail(this))) {
Intent intent = new Intent(this, LoginActivity.class);
startActivity(intent);
return;
}
// 执行异步加载数据任务
LoadAllNotesTask task = new LoadAllNotesTask(notebookId);
task.execute();
}
运行效果如下:
修改登录页面,增加注册功能
Android Studio提供的登录页面模板中不包含注册模块。我们手动按照下面的设计增加注册选项:
修改后的布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".LoginActivity">
<!-- Login progress -->
<ProgressBar
android:id="@+id/login_progress"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:visibility="gone" />
<ScrollView
android:id="@+id/login_form"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/email_login_form"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<AutoCompleteTextView
android:id="@+id/email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/prompt_email"
android:inputType="textEmailAddress"
android:maxLines="1"
android:singleLine="true" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/prompt_password"
android:imeActionId="6"
android:imeActionLabel="@string/action_sign_in_short"
android:imeOptions="actionUnspecified"
android:inputType="textPassword"
android:maxLines="1"
android:singleLine="true" />
</android.support.design.widget.TextInputLayout>
<!--新增注册视图,缺省状态下隐藏-->
<LinearLayout
android:id="@+id/register_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/password_confirm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/prompt_password_confirm"
android:imeActionId="6"
android:imeActionLabel="@string/action_sign_in_short"
android:imeOptions="actionUnspecified"
android:inputType="textPassword"
android:maxLines="1"
android:singleLine="true" />
</android.support.design.widget.TextInputLayout>
<EditText
android:id="@+id/personal_sign"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/prompt_personal_sign"
android:imeActionId="6"
android:imeActionLabel="@string/action_sign_in_short"
android:imeOptions="actionUnspecified"
android:maxLines="1"
android:singleLine="true"/>
</LinearLayout>
<!--选项:是否注册-->
<CheckBox
android:id="@+id/is_register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/register"/>
<Button
android:id="@+id/email_sign_in_button"
style="?android:textAppearanceSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/action_sign_in"
android:textStyle="bold" />
</LinearLayout>
</ScrollView>
</LinearLayout>
创建视图并编写交互操作
下面来编写实现代码。打开LoginActivity.java。大家可以看到Android Studio自动生成的这个类中代码比较复杂,这是因为它内置了比较完备的数据检验功能以及通讯录关联等功能。这样的模板在其它项目中也是可以直接使用的。
首先增加以下的成员变量:
// 注册视图
private View mRegisterLayout;
// 重复输入密码框
private EditText mPasswordConfirmView;
// 个人签名框
private EditText mPersonalSignView;
// 是否注册选项
private CheckBox mIsRegisterView;
找到onCreate()方法,在其最后添加以下的初始化代码:
// 整个注册视图,默认为隐藏
mRegisterLayout = findViewById(R.id.register_layout);
mRegisterLayout.setVisibility(View.GONE);
mPasswordConfirmView = (EditText) findViewById(R.id.password_confirm);
mPersonalSignView = findViewById(R.id.personal_sign);
// 注册选项,默认为未勾选
mIsRegisterView = findViewById(R.id.is_register);
mIsRegisterView.setChecked(false);
当用户勾选/取消注册选项时,页面视图要进行相应的切换。紧接着上面的代码添加:
// 处理注册选项勾选和取消勾选操作
final Button emailSignInButton = mEmailSignInButton;
mIsRegisterView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mIsRegisterView.isChecked()) {
// 选中后,注册视图可见,按钮文字显示为"注册"
mRegisterLayout.setVisibility(View.VISIBLE);
emailSignInButton.setText(R.string.action_sign_up_short);
} else {
// 取消选中后,注册视图隐藏,按钮文字显示为"登录"
mRegisterLayout.setVisibility(View.GONE);
emailSignInButton.setText(R.string.action_sign_in_short);
}
}
});
缺少的字符串请自行添加:
<string name="action_sign_in_short">登录</string>
<string name="action_sign_up_short">注册</string>
从代码中可知,实际上的注册/登录操作由点击mEmailSignInButton变量所对应的按钮来出发。系统已经为其生成了处理函数。在onCreate()方法中可以找到下面的处理代码:
其中,登录操作又由函数attemptLogin()来完成。进入attemptLogin(),可以看到其中已经有相当多的代码了,主要是验证各个编辑框内容是否合法。
我们可以借用大部分已有的代码,但是去掉以下代码,不会用到它们:
if (mAuthTask != null) {
return;
}
以及:
mAuthTask = new UserLoginTask(email, password);
mAuthTask.execute((Void) null);
这样得到的attemptLogin()函数代码如下:
private void attemptLogin() {
// 清空错误提示
mEmailView.setError(null);
mPasswordView.setError(null);
// 读各编辑框内字串
String email = mEmailView.getText().toString();
String password = mPasswordView.getText().toString();
boolean cancel = false;
View focusView = null;
// 检查密码格式
if (!TextUtils.isEmpty(password) && !isPasswordValid(password)) {
mPasswordView.setError(getString(R.string.error_invalid_password));
focusView = mPasswordView;
cancel = true;
}
// 检查电子邮件格式
if (TextUtils.isEmpty(email)) {
mEmailView.setError(getString(R.string.error_field_required));
focusView = mEmailView;
cancel = true;
} else if (!isEmailValid(email)) {
mEmailView.setError(getString(R.string.error_invalid_email));
focusView = mEmailView;
cancel = true;
}
if (cancel) {
// There was an error; don't attempt login and focus the first
// form field with an error.
focusView.requestFocus();
} else {
// Show a progress spinner, and kick off a background task to
// perform the user login attempt.
showProgress(true);
}
}
现在进行进一步的改写。
首先引入注册视图中的各个组件。找到如下的代码:
mEmailView.setError(null);
mPasswordView.setError(null);
紧接着后面添加对重复输入密码框的类似处理:
mPasswordConfirmView.setError(null);
找到如下代码:
String email = mEmailView.getText().toString();
String password = mPasswordView.getText().toString();
在其后面添加读取重复输入的密码和个人签名内容:
String passwordConfirm = mPasswordConfirmView.getText().toString();
String personalSign = mPersonalSignView.getText().toString();
再找到以下语句:
showProgress(true);
在后面添加如下语句:
if (mIsRegisterView.isChecked()) {
if (!password.equals(passwordConfirm)) {
mPasswordConfirmView.setError(getString(R.string.error_password_not_match));
} else {
onRegister(email, password, personalSign);
}
} else {
onLogin(email, password);
}
此段代码判断当前用户选择注册还是登录,然后分别处理。对于注册,先检查两次输入的密码是否一致,不一致则提示错误信息;然后调用onRegister()函数执行注册;如果是登录,则调用onLogin()函数执行登录。下面分别来实现这两个函数。
需要的字符串如下:
<string name="error_password_not_match">密码不一致</string>
创建HttpHelper工具类
我们创建一个名为HttpHelper的工具类来提供辅助的操作。在util包下创建这个类,代码如下:
public class HttpHelper {
//服务器IP地址,根据实际情况改写
private static final String HOST = "10.211.55.5";
private static final String HTTP_PREFIX = "http://" + HOST + "/simplenote/";
private static final String LOGIN_URL = HTTP_PREFIX + "login.php";
private static final String REGISTER_URL = HTTP_PREFIX + "register.php";
private static final String USER_INFO_URL = HTTP_PREFIX + "user_info.php";
// 获取登录页面地址
public static String getLoginUrl() {
return LOGIN_URL;
}
// 获取注册页面地址
public static String getRegisterUrl() {
return REGISTER_URL;
}
// 获取查询用户信息页面地址
public static String getUserInfoUrl() {
return USER_INFO_URL;
}
}
实现onRegister()
我们从客户端发起对服务器端注册页面的访问来实现注册。访问基于HTTP协议,使用POST操作,需要提供必要的用户信息作为参数。
引入开源okhttp3框架来提供网络访问接口。打开app/build.gradle文件,找到类似下面的部分:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
implementation 'com.android.support:design:26.1.0'
}
在末尾增加对okhttp3的依赖:
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
回到笔记列表页面,编写onRegister()函数:
private void onRegister(String email, String password, String personalSign) {
}
首先创建okhttp3客户端对象,并且创建表单数据对象以携带参数:
OkHttpClient client = new OkHttpClient();
// 创建表单,包含email和密码等数据
FormBody.Builder fb = new FormBody.Builder();
FormBody formBody = fb.add("email", email)
.add("password", password)
.add("personalSign", personalSign)
.build();
接下来创建请求对象,然后发起异步HTTP请求。所谓异步,就是说这个访问操作在UI线程之外的工作线程中执行,不会造成阻塞:
// 创建执行注册的HTTP请求
Request request = new Request.Builder()
.url(HttpHelper.getRegisterUrl()) // 访问注册页面
.post(formBody)
.build();
// 异步执行注册请求
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
...
}
@Override
public void onResponse(Call call, Response response) throws IOException {
...
}
});
上面出现了两个回调函数:
- onFailure():当连接不到服务器时调用
- onResponse():请求完成时调用,并提供响应对象response
我们分别编写这两个函数:
onFailure()
当无法连接服务器时,我们简单的用toast通知用户即可。由于该函数是在工作线程中执行,而Toast操作必须在UI线程执行,我们要做如下处理:
首先在LoginActivity类中添加成员:
private Handler mHandler = new Handler();
然后为onFailure()函数编写代码如下:
mHandler.post(new Runnable() {
@Override
public void run() {
showProgress(false);
Toast.makeText(Login1Activity.this, R.string.error_check_network, Toast.LENGTH_SHORT).show();
}
});
在这段代码中,先将网络访问期间显示的等待动画关闭,然后提示出错。这些操作由mHandler对象发送到UI线程执行。
下面来编写onResponse()函数。当服务器端完成处理,无论注册是否成功,都会通过JSON串的形式返回信息。在这里,我们就要读取并解析这个JSON,根据具体情况完成后续处理。
为onResponse()函数添加代码如下:
// 获取响应数据
final String rsp = response.body().string();
// 在UI线程中执行
mHandler.post(new Runnable() {
@Override
public void run() {
showProgress(false);
try {
JSONObject jo = new JSONObject(rsp);
int code = jo.optInt("code", 0);
String msg = jo.optString("msg");
String email = jo.optString("email");
if (code == 1) {
// 注册成功,同时默认为登录成功,记录email
Utils.saveUserEmail(Login1Activity.this, email);
finish();
}
Toast.makeText(Login1Activity.this, msg, Toast.LENGTH_SHORT).show();
} catch (JSONException e) {
e.printStackTrace();
}
}
});
首先从response对象中获取返回的JSON字串,然后在UI线程执行:
- 关闭等待动画
- 解析JSON数据,读取返回的状态等信息
- 若状态码为1,则注册成功,按照登录成功来处理,即保存email账号并关闭登录页面,同时提示用户
- 若不为1,则注册失败,则单纯提示出错信息。
运行程序,效果如下:
此时,通过phpMyAdmin页面查看user表,刚才注册的用户数据已经出现在其中:
实现onLogin()
登录操作的实现与注册大体相似:
private void onLogin(String email, String password) {
OkHttpClient client = new OkHttpClient();
// 创建表单,包含email和密码等数据
FormBody.Builder fb = new FormBody.Builder();
FormBody formBody = fb.add("email", email)
.add("password", password)
.build();
// 创建执行登录的HTTP请求
Request request = new Request.Builder()
.url(HttpHelper.getLoginUrl())
.post(formBody)
.build();
// 以异步方式执行请求
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
mHandler.post(new Runnable() {
@Override
public void run() {
showProgress(false);
Toast.makeText(LoginActivity.this, R.string.error_check_network, Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onResponse(Call call, Response response) throws IOException {
final String rsp = response.body().string();
mHandler.post(new Runnable() {
@Override
public void run() {
showProgress(false);
try {
JSONObject jo = new JSONObject(rsp);
int code = jo.optInt("code", 0);
String msg = jo.optString("msg");
String email = jo.optString("email");
Toast.makeText(LoginActivity.this, msg, Toast.LENGTH_SHORT).show();
if (code == 1) {
// 登录成功,记录email
Utils.saveUserEmail(LoginActivity.this, email);
finish();
}
} catch (JSONException e) {
e.printStackTrace();
}
}
});
}
});
}