SQLite以及Room框架的学习:用SQLite给新闻app加上更完善的登录注册功能
SQLite以及Room框架的学习:用SQLite给新闻app加上更完善的登录注册功能
文章目录
- SQLite以及Room框架的学习:用SQLite给新闻app加上更完善的登录注册功能
- SQLite
- 1.SQLite的特性
- 2.数据类型
- 3.数据库文件结构
- 4.基本使用流程
- 1)通过继承 `SQLiteOpenHelper` 类管理数据库版本和表结构:
- 2)数据模型类(User)
- 3)数据访问对象DAO
- 4)使用示例
- 5)权限配置
- Room框架主要知识点
- 1.核心组件
- **Entity(实体类)**
- DAO(数据访问对象)
- **Database(数据库类)**
- 响应式编程支持
- 关系型数据处理
- 数据库迁移
- 与 ViewModel 结合使用
- 测试 Room DAO
- 给NewsApp中添加登录注册模块
- 1. 添加依赖
- 2. 权限配置
- 3. Activity注册
- 4. 资源文件
- 功能详解
- 1. 数据库设计
- 2. 密码安全
- 3. 头像管理
- 4. 记住密码功能
- 5. 实时头像显示
- 6.高级功能扩展
- 1. 自动登录
- 2. 密码强度验证
- 3. 用户名自动完成
SQLite
SQLite 是一种轻量级的嵌入式数据库,广泛应用于移动应用、桌面软件和小型项目中。它不需要单独的服务器进程,数据存储在单个文件中,具有零配置、高效、可靠的特点。
1.SQLite的特性
- 嵌入式数据库:直接嵌入到应用程序中,无需独立服务器。
- 零配置:无需复杂设置,直接使用。
- 单文件存储:所有数据存储在单个磁盘文件中。
- 支持标准 SQL:遵循 SQL-92 标准的大部分特性。
- 事务支持:支持 ACID 特性的事务处理。
2.数据类型
- NULL:空值。
- INTEGER:整型,根据值大小自动选择合适的位数。
- REAL:浮点型。
- TEXT:文本字符串,编码支持 UTF-8、UTF-16。
- BLOB:二进制大对象,原样存储数据。
3.数据库文件结构
- 数据库文件包含表、索引、触发器和视图的定义及数据。
- 文件格式跨平台兼容,可在不同架构的设备间移植。
4.基本使用流程
1)通过继承 SQLiteOpenHelper
类管理数据库版本和表结构:
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;public class DatabaseHelper extends SQLiteOpenHelper {private static final String DATABASE_NAME = "myapp.db";private static final int DATABASE_VERSION = 1;// 表名和列名public static final String TABLE_USERS = "users";public static final String COLUMN_ID = "_id";public static final String COLUMN_NAME = "name";public static final String COLUMN_AGE = "age";public static final String COLUMN_EMAIL = "email";// 创建表的SQL语句private static final String CREATE_TABLE_USERS ="CREATE TABLE " + TABLE_USERS + " (" +COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +COLUMN_NAME + " TEXT NOT NULL, " +COLUMN_AGE + " INTEGER, " +COLUMN_EMAIL + " TEXT UNIQUE);";public DatabaseHelper(Context context) {super(context, DATABASE_NAME, null, DATABASE_VERSION);}@Overridepublic void onCreate(SQLiteDatabase db) {db.execSQL(CREATE_TABLE_USERS);}@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {// 数据库升级时的操作db.execSQL("DROP TABLE IF EXISTS " + TABLE_USERS);onCreate(db);}
}
2)数据模型类(User)
public class User {private long id;private String name;private int age;private String email;// 构造函数public User() {}public User(String name, int age, String email) {this.name = name;this.age = age;this.email = email;}// Getters and Setterspublic long getId() {return id;}public void setId(long id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public String getEmail() {return email;}public void setEmail(String email) {this.email = email;}@Overridepublic String toString() {return "User{" +"id=" + id +", name='" + name + '\'' +", age=" + age +", email='" + email + '\'' +'}';}
}
3)数据访问对象DAO
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import java.util.ArrayList;
import java.util.List;public class UserDAO {private SQLiteDatabase database;private DatabaseHelper dbHelper;private String[] allColumns = {DatabaseHelper.COLUMN_ID,DatabaseHelper.COLUMN_NAME,DatabaseHelper.COLUMN_AGE,DatabaseHelper.COLUMN_EMAIL};public UserDAO(Context context) {dbHelper = new DatabaseHelper(context);}// 打开数据库连接public void open() {database = dbHelper.getWritableDatabase();}// 关闭数据库连接public void close() {dbHelper.close();}// 创建用户public User createUser(String name, int age, String email) {ContentValues values = new ContentValues();values.put(DatabaseHelper.COLUMN_NAME, name);values.put(DatabaseHelper.COLUMN_AGE, age);values.put(DatabaseHelper.COLUMN_EMAIL, email);long insertId = database.insert(DatabaseHelper.TABLE_USERS, null, values);Cursor cursor = database.query(DatabaseHelper.TABLE_USERS,allColumns,DatabaseHelper.COLUMN_ID + " = " + insertId,null, null, null, null);cursor.moveToFirst();User newUser = cursorToUser(cursor);cursor.close();return newUser;}// 删除用户public void deleteUser(User user) {long id = user.getId();database.delete(DatabaseHelper.TABLE_USERS,DatabaseHelper.COLUMN_ID + " = " + id,null);}// 获取所有用户public List<User> getAllUsers() {List<User> users = new ArrayList<>();Cursor cursor = database.query(DatabaseHelper.TABLE_USERS,allColumns, null, null, null, null, null);cursor.moveToFirst();while (!cursor.isAfterLast()) {User user = cursorToUser(cursor);users.add(user);cursor.moveToNext();}cursor.close();return users;}// 获取单个用户public User getUser(long id) {Cursor cursor = database.query(DatabaseHelper.TABLE_USERS,allColumns,DatabaseHelper.COLUMN_ID + " = " + id,null, null, null, null);User user = null;if (cursor != null && cursor.moveToFirst()) {user = cursorToUser(cursor);cursor.close();}return user;}// 更新用户信息public int updateUser(User user) {ContentValues values = new ContentValues();values.put(DatabaseHelper.COLUMN_NAME, user.getName());values.put(DatabaseHelper.COLUMN_AGE, user.getAge());values.put(DatabaseHelper.COLUMN_EMAIL, user.getEmail());return database.update(DatabaseHelper.TABLE_USERS,values,DatabaseHelper.COLUMN_ID + " = ?",new String[]{String.valueOf(user.getId())});}// 将Cursor转换为User对象private User cursorToUser(Cursor cursor) {User user = new User();user.setId(cursor.getLong(0));user.setName(cursor.getString(1));user.setAge(cursor.getInt(2));user.setEmail(cursor.getString(3));return user;}
}
4)使用示例
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import java.util.List;public class MainActivity extends AppCompatActivity {private static final String TAG = "MainActivity";private UserDAO userDAO;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 初始化DAOuserDAO = new UserDAO(this);userDAO.open();// 示例:添加用户User user = userDAO.createUser("Alice", 25, "alice@example.com");Log.d(TAG, "New user added: " + user.toString());// 示例:获取所有用户List<User> users = userDAO.getAllUsers();for (User u : users) {Log.d(TAG, "User: " + u.toString());}// 示例:更新用户user.setAge(26);int rowsAffected = userDAO.updateUser(user);Log.d(TAG, "Rows affected: " + rowsAffected);// 示例:删除用户userDAO.deleteUser(user);Log.d(TAG, "User deleted: " + user.toString());// 关闭数据库连接userDAO.close();}
}
5)权限配置
<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="com.example.sqliteexample"><applicationandroid:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/AppTheme"><activity android:name=".MainActivity"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application>
</manifest>
Room框架主要知识点
Room 是 Android Jetpack 中的一个持久化库,它在 SQLite 上提供了一个抽象层,让开发者能够更轻松地使用数据库功能。以下是 Room 框架的核心概念和主要知识点:
1.核心组件
Entity(实体类)
对应数据库中的表,使用 @Entity
注解标记的类,每个实体类的属性对应表中的列。
import androidx.room.Entity;
import androidx.room.PrimaryKey;@Entity(tableName = "users")
public class User {@PrimaryKey(autoGenerate = true)private int id;private String name;private int age;private String email;// 构造函数、Getter和Setter方法public User(String name, int age, String email) {this.name = name;this.age = age;this.email = email;}public int getId() { return id; }public void setId(int id) { this.id = id; }public String getName() { return name; }public void setName(String name) { this.name = name; }public int getAge() { return age; }public void setAge(int age) { this.age = age; }public String getEmail() { return email; }public void setEmail(String email) { this.email = email; }
}
DAO(数据访问对象)
用 @Dao
注解的接口,定义数据库操作方法
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;@Dao
public interface UserDao {@Query("SELECT * FROM users")List<User> getAll();@Query("SELECT * FROM users WHERE id = :id")User getById(int id);@Insertvoid insert(User user);@Insertvoid insertAll(List<User> users);@Updatevoid update(User user);@Deletevoid delete(User user);@Query("DELETE FROM users")void deleteAll();
}
Database(数据库类)
用 @Database
注解的抽象类,连接实体和 DAO:
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;@Dao
public interface UserDao {@Query("SELECT * FROM users")List<User> getAll();@Query("SELECT * FROM users WHERE id = :id")User getById(int id);@Insertvoid insert(User user);@Insertvoid insertAll(List<User> users);@Updatevoid update(User user);@Deletevoid delete(User user);@Query("DELETE FROM users")void deleteAll();
}
响应式编程支持
Room 支持返回 LiveData
或 Flowable
(RxJava)实现数据监听:
import androidx.lifecycle.LiveData;@Dao
public interface UserDao {// 返回LiveData,数据变化时自动通知观察者@Query("SELECT * FROM users")LiveData<List<User>> getAllUsersLiveData();
}
关系型数据处理
一对多关系**
// User类保持不变// Address类
@Entity(foreignKeys = @ForeignKey(entity = User.class,parentColumns = "id",childColumns = "userId",onDelete = ForeignKey.CASCADE)
)
public class Address {@PrimaryKey(autoGenerate = true)private int id;private String street;private int userId; // 外键// Getter和Setter方法
}// 用户及其地址的组合类
public class UserWithAddresses {@Embeddedpublic User user;@Relation(parentColumn = "id",entityColumn = "userId")public List<Address> addresses;
}// DAO方法
@Query("SELECT * FROM users")
List<UserWithAddresses> getUsersWithAddresses();
数据库迁移
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;// 从版本1到版本2的迁移
public static final Migration MIGRATION_1_2 = new Migration(1, 2) {@Overridepublic void migrate(SupportSQLiteDatabase database) {database.execSQL("ALTER TABLE users ADD COLUMN phone TEXT");}
};// 构建数据库时添加迁移路径
AppDatabase db = Room.databaseBuilder(context.getApplicationContext(),AppDatabase.class, "database-name"
)
.addMigrations(MIGRATION_1_2)
.build();
与 ViewModel 结合使用
在ViewModel中获取Room数据并提供给UI
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import android.app.Application;
import java.util.List;public class UserViewModel extends AndroidViewModel {private UserDao userDao;private LiveData<List<User>> allUsers;public UserViewModel(Application application) {super(application);AppDatabase db = AppDatabase.getDatabase(application);userDao = db.userDao();allUsers = userDao.getAllUsersLiveData();}public LiveData<List<User>> getAllUsers() {return allUsers;}// 在后台线程执行插入操作public void insert(User user) {new InsertAsyncTask(userDao).execute(user);}private static class InsertAsyncTask extends AsyncTask<User, Void, Void> {private UserDao asyncTaskDao;InsertAsyncTask(UserDao dao) {this.asyncTaskDao = dao;}@Overrideprotected Void doInBackground(final User... params) {asyncTaskDao.insert(params[0]);return null;}}
}
测试 Room DAO
使用内存数据库进行单元测试:
import androidx.room.Room;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.util.List;
import static org.junit.Assert.assertEquals;@RunWith(AndroidJUnit4.class)
public class UserDaoTest {private UserDao userDao;private AppDatabase db;@Beforepublic void createDb() {Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();db = Room.inMemoryDatabaseBuilder(context, AppDatabase.class).build();userDao = db.userDao();}@Afterpublic void closeDb() throws IOException {db.close();}@Testpublic void insertAndGetUser() throws Exception {User user = new User("Alice", 25, "alice@example.com");userDao.insert(user);List<User> allUsers = userDao.getAll();assertEquals(allUsers.get(0).getName(), user.getName());}
}
给NewsApp中添加登录注册模块
//LoginActivity
package com.example.eznews.activity;import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Base64;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;import com.example.eznews.R;
import com.example.newsapp.database.UserDatabaseHelper;import java.io.ByteArrayOutputStream;
import java.io.IOException;public class RegisterActivity extends AppCompatActivity {private EditText etUsername, etPassword, etConfirmPassword;private ImageView ivAvatar;private Button btnSelectAvatar, btnRegister, btnBackToLogin;private TextView tvPasswordStrength;private CheckBox cbAgreeTerms;private UserDatabaseHelper dbHelper;private static final int PICK_IMAGE_REQUEST = 1;private String avatarBase64 = "";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_register);initViews();initDatabase();setupListeners();}private void updatePasswordStrength(String password) {if (password.isEmpty()) {tvPasswordStrength.setText("请输入至少6位密码,包含字母和数字");tvPasswordStrength.setTextColor(getResources().getColor(R.color.text_secondary));return;}if (password.length() < 6) {tvPasswordStrength.setText("密码长度至少6位");tvPasswordStrength.setTextColor(getResources().getColor(R.color.error_color));return;}boolean hasLetter = password.matches(".*[a-zA-Z].*");boolean hasDigit = password.matches(".*\\d.*");boolean hasSpecial = password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?].*");if (hasLetter && hasDigit && hasSpecial && password.length() >= 8) {tvPasswordStrength.setText("强密码 ✓");tvPasswordStrength.setTextColor(getResources().getColor(R.color.success_color));} else if (hasLetter && hasDigit && password.length() >= 6) {tvPasswordStrength.setText("中等强度");tvPasswordStrength.setTextColor(getResources().getColor(R.color.primary_color));} else {tvPasswordStrength.setText("弱密码,建议包含字母和数字");tvPasswordStrength.setTextColor(getResources().getColor(R.color.error_color));}}private void showUserAgreement() {// 这里可以显示用户协议对话框或跳转到协议页面new androidx.appcompat.app.AlertDialog.Builder(this).setTitle("用户协议").setMessage("这里是用户协议的具体内容...\n\n1. 用户注册即表示同意本协议\n2. 请妥善保管账户信息\n3. 禁止恶意使用本应用\n4. 我们将保护您的隐私安全").setPositiveButton("我已了解", null).show();}private void initViews() {etUsername = findViewById(R.id.et_reg_username);etPassword = findViewById(R.id.et_reg_password);etConfirmPassword = findViewById(R.id.et_reg_confirm_password);ivAvatar = findViewById(R.id.iv_reg_avatar);btnSelectAvatar = findViewById(R.id.btn_select_avatar);btnRegister = findViewById(R.id.btn_register_submit);btnBackToLogin = findViewById(R.id.btn_back_to_login);tvPasswordStrength = findViewById(R.id.tv_password_strength);cbAgreeTerms = findViewById(R.id.cb_agree_terms);// 设置默认头像ivAvatar.setImageResource(R.drawable.default_avatar);}private void initDatabase() {dbHelper = new UserDatabaseHelper(this);}private void setupListeners() {// 选择头像按钮btnSelectAvatar.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {selectAvatar();}});// 头像点击也可以选择ivAvatar.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {selectAvatar();}});// 密码强度监听etPassword.addTextChangedListener(new TextWatcher() {@Overridepublic void beforeTextChanged(CharSequence s, int start, int count, int after) {}@Overridepublic void onTextChanged(CharSequence s, int start, int before, int count) {updatePasswordStrength(s.toString());}@Overridepublic void afterTextChanged(Editable s) {}});// 注册按钮btnRegister.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {attemptRegister();}});// 返回登录按钮btnBackToLogin.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {finish();}});// 用户协议点击TextView tvUserAgreement = findViewById(R.id.tv_user_agreement);tvUserAgreement.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// 显示用户协议showUserAgreement();}});}private void selectAvatar() {Intent intent = new Intent();intent.setType("image/*");intent.setAction(Intent.ACTION_GET_CONTENT);startActivityForResult(Intent.createChooser(intent, "选择头像"), PICK_IMAGE_REQUEST);}@Overrideprotected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {super.onActivityResult(requestCode, resultCode, data);if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {Uri imageUri = data.getData();try {Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), imageUri);// 压缩图片Bitmap compressedBitmap = compressBitmap(bitmap, 200, 200);// 显示头像ivAvatar.setImageBitmap(compressedBitmap);// 转换为Base64字符串存储avatarBase64 = bitmapToBase64(compressedBitmap);} catch (IOException e) {e.printStackTrace();Toast.makeText(this, "头像加载失败", Toast.LENGTH_SHORT).show();}}}private Bitmap compressBitmap(Bitmap bitmap, int maxWidth, int maxHeight) {float ratio = Math.min((float) maxWidth / bitmap.getWidth(),(float) maxHeight / bitmap.getHeight());int width = Math.round(ratio * bitmap.getWidth());int height = Math.round(ratio * bitmap.getHeight());return Bitmap.createScaledBitmap(bitmap, width, height, true);}private String bitmapToBase64(Bitmap bitmap) {ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream);byte[] byteArray = byteArrayOutputStream.toByteArray();return Base64.encodeToString(byteArray, Base64.DEFAULT);}private void attemptRegister() {String username = etUsername.getText().toString().trim();String password = etPassword.getText().toString().trim();String confirmPassword = etConfirmPassword.getText().toString().trim();// 验证输入if (username.isEmpty()) {etUsername.setError("请输入用户名");etUsername.requestFocus();return;}if (username.length() < 3) {etUsername.setError("用户名至少3个字符");etUsername.requestFocus();return;}if (password.isEmpty()) {etPassword.setError("请输入密码");etPassword.requestFocus();return;}if (password.length() < 6) {etPassword.setError("密码至少6个字符");etPassword.requestFocus();return;}if (!password.equals(confirmPassword)) {etConfirmPassword.setError("两次输入的密码不一致");etConfirmPassword.requestFocus();return;}// 检查是否同意用户协议if (!cbAgreeTerms.isChecked()) {Toast.makeText(this, "请先同意用户协议", Toast.LENGTH_SHORT).show();return;}// 检查用户名是否已存在if (dbHelper.isUserExists(username)) {etUsername.setError("用户名已存在");etUsername.requestFocus();return;}// 如果没有选择头像,使用默认头像的Base64if (avatarBase64.isEmpty()) {Bitmap defaultAvatar = ((BitmapDrawable) getResources().getDrawable(R.drawable.default_avatar)).getBitmap();avatarBase64 = bitmapToBase64(defaultAvatar);}// 注册用户if (dbHelper.registerUser(username, password, avatarBase64)) {Toast.makeText(this, "注册成功!", Toast.LENGTH_SHORT).show();// 返回登录界面Intent intent = new Intent();intent.putExtra("registered_username", username);setResult(RESULT_OK, intent);finish();} else {Toast.makeText(this, "注册失败,请稍后重试", Toast.LENGTH_SHORT).show();}}@Overrideprotected void onDestroy() {super.onDestroy();if (dbHelper != null) {dbHelper.close();}}
}
activity_login.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:background="@color/background_color"android:gravity="center"android:padding="32dp"><!-- 应用Logo或标题 --><!-- 用户头像 --><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginBottom="48dp"android:text="新闻资讯"android:textColor="@color/primary_color"android:textSize="28sp"android:textStyle="bold" /><ImageViewandroid:id="@+id/iv_user_avatar"android:layout_width="80dp"android:layout_height="80dp"android:layout_marginBottom="24dp"android:background="@drawable/circle_background"android:scaleType="centerCrop"android:src="@drawable/default_avatar"android:contentDescription="用户头像" /><!-- 登录表单容器 --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:background="@drawable/form_background"android:padding="24dp"android:elevation="4dp"><!-- 用户名输入框 --><com.google.android.material.textfield.TextInputLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginBottom="16dp"android:hint="用户名"><com.google.android.material.textfield.TextInputEditTextandroid:id="@+id/et_username"android:layout_width="match_parent"android:layout_height="wrap_content"android:inputType="text"android:maxLines="1"android:drawableStart="@drawable/ic_person"android:drawablePadding="12dp" /></com.google.android.material.textfield.TextInputLayout><!-- 密码输入框 --><com.google.android.material.textfield.TextInputLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginBottom="16dp"android:hint="密码"app:passwordToggleEnabled="true"><com.google.android.material.textfield.TextInputEditTextandroid:id="@+id/et_password"android:layout_width="match_parent"android:layout_height="wrap_content"android:inputType="textPassword"android:maxLines="1"android:drawableStart="@drawable/ic_lock"android:drawablePadding="12dp" /></com.google.android.material.textfield.TextInputLayout><!-- 记住密码复选框 --><CheckBoxandroid:id="@+id/cb_remember_password"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="记住密码"android:textColor="@color/text_secondary"android:layout_marginBottom="24dp" /><!-- 登录按钮 --><Buttonandroid:id="@+id/btn_login"android:layout_width="match_parent"android:layout_height="56dp"android:text="登录"android:textSize="16sp"android:textColor="@color/white"android:background="@drawable/button_primary"android:layout_marginBottom="16dp"android:elevation="2dp" /><!-- 注册按钮 --><Buttonandroid:id="@+id/btn_register"android:layout_width="match_parent"android:layout_height="56dp"android:text="注册新账户"android:textSize="16sp"android:textColor="@color/primary_color"android:background="@drawable/button_secondary"style="?android:attr/borderlessButtonStyle" /></LinearLayout><!-- 忘记密码链接 --><TextViewandroid:id="@+id/tv_forgot_password"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="忘记密码?"android:textColor="@color/primary_color"android:textSize="14sp"android:layout_marginTop="24dp"android:clickable="true"android:focusable="true"android:background="?android:attr/selectableItemBackground"android:padding="8dp" /></LinearLayout>
RegisterActivity
package com.example.eznews.activity;import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Base64;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;import com.example.eznews.R;
import com.example.newsapp.database.UserDatabaseHelper;import java.io.ByteArrayOutputStream;
import java.io.IOException;public class RegisterActivity extends AppCompatActivity {private EditText etUsername, etPassword, etConfirmPassword;private ImageView ivAvatar;private Button btnSelectAvatar, btnRegister, btnBackToLogin;private TextView tvPasswordStrength;private CheckBox cbAgreeTerms;private UserDatabaseHelper dbHelper;private static final int PICK_IMAGE_REQUEST = 1;private String avatarBase64 = "";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_register);initViews();initDatabase();setupListeners();}private void updatePasswordStrength(String password) {if (password.isEmpty()) {tvPasswordStrength.setText("请输入至少6位密码,包含字母和数字");tvPasswordStrength.setTextColor(getResources().getColor(R.color.text_secondary));return;}if (password.length() < 6) {tvPasswordStrength.setText("密码长度至少6位");tvPasswordStrength.setTextColor(getResources().getColor(R.color.error_color));return;}boolean hasLetter = password.matches(".*[a-zA-Z].*");boolean hasDigit = password.matches(".*\\d.*");boolean hasSpecial = password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?].*");if (hasLetter && hasDigit && hasSpecial && password.length() >= 8) {tvPasswordStrength.setText("强密码 ✓");tvPasswordStrength.setTextColor(getResources().getColor(R.color.success_color));} else if (hasLetter && hasDigit && password.length() >= 6) {tvPasswordStrength.setText("中等强度");tvPasswordStrength.setTextColor(getResources().getColor(R.color.primary_color));} else {tvPasswordStrength.setText("弱密码,建议包含字母和数字");tvPasswordStrength.setTextColor(getResources().getColor(R.color.error_color));}}private void showUserAgreement() {// 这里可以显示用户协议对话框或跳转到协议页面new androidx.appcompat.app.AlertDialog.Builder(this).setTitle("用户协议").setMessage("这里是用户协议的具体内容...\n\n1. 用户注册即表示同意本协议\n2. 请妥善保管账户信息\n3. 禁止恶意使用本应用\n4. 我们将保护您的隐私安全").setPositiveButton("我已了解", null).show();}private void initViews() {etUsername = findViewById(R.id.et_reg_username);etPassword = findViewById(R.id.et_reg_password);etConfirmPassword = findViewById(R.id.et_reg_confirm_password);ivAvatar = findViewById(R.id.iv_reg_avatar);btnSelectAvatar = findViewById(R.id.btn_select_avatar);btnRegister = findViewById(R.id.btn_register_submit);btnBackToLogin = findViewById(R.id.btn_back_to_login);tvPasswordStrength = findViewById(R.id.tv_password_strength);cbAgreeTerms = findViewById(R.id.cb_agree_terms);// 设置默认头像ivAvatar.setImageResource(R.drawable.default_avatar);}private void initDatabase() {dbHelper = new UserDatabaseHelper(this);}private void setupListeners() {// 选择头像按钮btnSelectAvatar.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {selectAvatar();}});// 头像点击也可以选择ivAvatar.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {selectAvatar();}});// 密码强度监听etPassword.addTextChangedListener(new TextWatcher() {@Overridepublic void beforeTextChanged(CharSequence s, int start, int count, int after) {}@Overridepublic void onTextChanged(CharSequence s, int start, int before, int count) {updatePasswordStrength(s.toString());}@Overridepublic void afterTextChanged(Editable s) {}});// 注册按钮btnRegister.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {attemptRegister();}});// 返回登录按钮btnBackToLogin.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {finish();}});// 用户协议点击TextView tvUserAgreement = findViewById(R.id.tv_user_agreement);tvUserAgreement.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// 显示用户协议showUserAgreement();}});}private void selectAvatar() {Intent intent = new Intent();intent.setType("image/*");intent.setAction(Intent.ACTION_GET_CONTENT);startActivityForResult(Intent.createChooser(intent, "选择头像"), PICK_IMAGE_REQUEST);}@Overrideprotected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {super.onActivityResult(requestCode, resultCode, data);if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {Uri imageUri = data.getData();try {Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), imageUri);// 压缩图片Bitmap compressedBitmap = compressBitmap(bitmap, 200, 200);// 显示头像ivAvatar.setImageBitmap(compressedBitmap);// 转换为Base64字符串存储avatarBase64 = bitmapToBase64(compressedBitmap);} catch (IOException e) {e.printStackTrace();Toast.makeText(this, "头像加载失败", Toast.LENGTH_SHORT).show();}}}private Bitmap compressBitmap(Bitmap bitmap, int maxWidth, int maxHeight) {float ratio = Math.min((float) maxWidth / bitmap.getWidth(),(float) maxHeight / bitmap.getHeight());int width = Math.round(ratio * bitmap.getWidth());int height = Math.round(ratio * bitmap.getHeight());return Bitmap.createScaledBitmap(bitmap, width, height, true);}private String bitmapToBase64(Bitmap bitmap) {ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream);byte[] byteArray = byteArrayOutputStream.toByteArray();return Base64.encodeToString(byteArray, Base64.DEFAULT);}private void attemptRegister() {String username = etUsername.getText().toString().trim();String password = etPassword.getText().toString().trim();String confirmPassword = etConfirmPassword.getText().toString().trim();// 验证输入if (username.isEmpty()) {etUsername.setError("请输入用户名");etUsername.requestFocus();return;}if (username.length() < 3) {etUsername.setError("用户名至少3个字符");etUsername.requestFocus();return;}if (password.isEmpty()) {etPassword.setError("请输入密码");etPassword.requestFocus();return;}if (password.length() < 6) {etPassword.setError("密码至少6个字符");etPassword.requestFocus();return;}if (!password.equals(confirmPassword)) {etConfirmPassword.setError("两次输入的密码不一致");etConfirmPassword.requestFocus();return;}// 检查是否同意用户协议if (!cbAgreeTerms.isChecked()) {Toast.makeText(this, "请先同意用户协议", Toast.LENGTH_SHORT).show();return;}// 检查用户名是否已存在if (dbHelper.isUserExists(username)) {etUsername.setError("用户名已存在");etUsername.requestFocus();return;}// 如果没有选择头像,使用默认头像的Base64if (avatarBase64.isEmpty()) {Bitmap defaultAvatar = ((BitmapDrawable) getResources().getDrawable(R.drawable.default_avatar)).getBitmap();avatarBase64 = bitmapToBase64(defaultAvatar);}// 注册用户if (dbHelper.registerUser(username, password, avatarBase64)) {Toast.makeText(this, "注册成功!", Toast.LENGTH_SHORT).show();// 返回登录界面Intent intent = new Intent();intent.putExtra("registered_username", username);setResult(RESULT_OK, intent);finish();} else {Toast.makeText(this, "注册失败,请稍后重试", Toast.LENGTH_SHORT).show();}}@Overrideprotected void onDestroy() {super.onDestroy();if (dbHelper != null) {dbHelper.close();}}
}
activity_register.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/background_color"android:fillViewport="true"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:gravity="center"android:padding="32dp"><!-- 标题 --><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="创建新账户"android:textSize="28sp"android:textColor="@color/primary_color"android:textStyle="bold"android:layout_marginBottom="32dp" /><!-- 注册表单容器 --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:background="@drawable/form_background"android:padding="24dp"android:elevation="4dp"><!-- 头像选择区域 --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:gravity="center"android:layout_marginBottom="24dp"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="选择头像"android:textSize="16sp"android:textColor="@color/text_primary"android:layout_marginBottom="12dp" /><!-- 头像显示 --><ImageViewandroid:id="@+id/iv_reg_avatar"android:layout_width="100dp"android:layout_height="100dp"android:layout_marginBottom="12dp"android:background="@drawable/circle_background"android:scaleType="centerCrop"android:src="@drawable/default_avatar"android:contentDescription="用户头像"android:clickable="true"android:focusable="true" /><!-- 选择头像按钮 --><Buttonandroid:id="@+id/btn_select_avatar"android:layout_width="wrap_content"android:layout_height="40dp"android:text="选择头像"android:textSize="14sp"android:textColor="@color/primary_color"android:background="@drawable/button_secondary"android:minWidth="120dp"style="?android:attr/borderlessButtonStyle" /></LinearLayout><!-- 用户名输入框 --><com.google.android.material.textfield.TextInputLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginBottom="16dp"android:hint="用户名"app:counterEnabled="true"app:counterMaxLength="20"><com.google.android.material.textfield.TextInputEditTextandroid:id="@+id/et_reg_username"android:layout_width="match_parent"android:layout_height="wrap_content"android:inputType="text"android:maxLength="20"android:maxLines="1"android:drawableStart="@drawable/ic_person"android:drawablePadding="12dp" /></com.google.android.material.textfield.TextInputLayout><!-- 密码输入框 --><com.google.android.material.textfield.TextInputLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginBottom="16dp"android:hint="密码"app:passwordToggleEnabled="true"app:counterEnabled="true"app:counterMaxLength="50"><com.google.android.material.textfield.TextInputEditTextandroid:id="@+id/et_reg_password"android:layout_width="match_parent"android:layout_height="wrap_content"android:inputType="textPassword"android:maxLength="50"android:maxLines="1"android:drawableStart="@drawable/ic_lock"android:drawablePadding="12dp" /></com.google.android.material.textfield.TextInputLayout><!-- 密码强度提示 --><TextViewandroid:id="@+id/tv_password_strength"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="请输入至少6位密码,包含字母和数字"android:textSize="12sp"android:textColor="@color/text_secondary"android:layout_marginBottom="16dp" /><!-- 确认密码输入框 --><com.google.android.material.textfield.TextInputLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginBottom="24dp"android:hint="确认密码"app:passwordToggleEnabled="true"><com.google.android.material.textfield.TextInputEditTextandroid:id="@+id/et_reg_confirm_password"android:layout_width="match_parent"android:layout_height="wrap_content"android:inputType="textPassword"android:maxLines="1"android:drawableStart="@drawable/ic_lock"android:drawablePadding="12dp" /></com.google.android.material.textfield.TextInputLayout><!-- 注册按钮 --><Buttonandroid:id="@+id/btn_register_submit"android:layout_width="match_parent"android:layout_height="56dp"android:text="注册"android:textSize="16sp"android:textColor="@color/white"android:background="@drawable/button_primary"android:layout_marginBottom="16dp"android:elevation="2dp" /><!-- 返回登录按钮 --><Buttonandroid:id="@+id/btn_back_to_login"android:layout_width="match_parent"android:layout_height="56dp"android:text="已有账户?立即登录"android:textSize="16sp"android:textColor="@color/primary_color"android:background="@drawable/button_secondary"style="?android:attr/borderlessButtonStyle" /></LinearLayout><!-- 用户协议 --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:gravity="center"android:layout_marginTop="24dp"><CheckBoxandroid:id="@+id/cb_agree_terms"android:layout_width="wrap_content"android:layout_height="wrap_content"android:checked="true" /><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="我已阅读并同意"android:textSize="12sp"android:textColor="@color/text_secondary" /><TextViewandroid:id="@+id/tv_user_agreement"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="《用户协议》"android:textSize="12sp"android:textColor="@color/primary_color"android:clickable="true"android:focusable="true"android:background="?android:attr/selectableItemBackground"android:padding="4dp" /></LinearLayout></LinearLayout></ScrollView>
这个登录系统提供了以下完整功能:
- 用户注册和登录
- 密码加密存储
- 用户头像管理
- 记住密码功能
- 输入用户名时显示头像(类似QQ)
- 自动完成用户名
1. 添加依赖
在 app/build.gradle
中添加Material Design依赖:
dependencies {implementation 'com.google.android.material:material:1.9.0'}
2. 权限配置
在 AndroidManifest.xml
中添加必要权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
3. Activity注册
在 AndroidManifest.xml
中注册Activity:
<activityandroid:name=".activity.LoginActivity"android:exported="true"android:theme="@style/Theme.NewsApp.NoActionBar"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter>
</activity><activityandroid:name=".activity.RegisterActivity"android:parentActivityName=".activity.LoginActivity" />
4. 资源文件
创建所需的drawable资源:
<vector xmlns:android="http://schemas.android.com/apk/res/android"android:width="48dp"android:height="48dp"android:viewportWidth="24"android:viewportHeight="24"android:tint="@color/text_secondary"><pathandroid:fillColor="@android:color/white"android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>
功能详解
1. 数据库设计
数据库表结构:
CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT,username TEXT UNIQUE NOT NULL,password TEXT NOT NULL,avatar TEXT,created_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
2. 密码安全
- 使用SHA-256哈希加密
- Base64编码存储
- 永不存储明文密码
3. 头像管理
- 支持从相册选择图片
- 自动压缩图片大小
- Base64编码存储在数据库
- 圆形头像显示
4. 记住密码功能
使用SharedPreferences存储:
PreferenceManager prefManager = new PreferenceManager(this);
prefManager.saveLoginCredentials(username, password, isRemember);
5. 实时头像显示
通过TextWatcher监听用户名输入:
etUsername.addTextChangedListener(new TextWatcher() {@Overridepublic void onTextChanged(CharSequence s, int start, int before, int count) {String username = s.toString().trim();if (!username.isEmpty()) {displayUserAvatar(username);}}
});
6.高级功能扩展
1. 自动登录
PreferenceManager prefManager = new PreferenceManager(this);
if (prefManager.isAutoLogin() && prefManager.isRememberPassword()) {String username = prefManager.getSavedUsername();String password = prefManager.getSavedPassword();// 自动登录逻辑
}
2. 密码强度验证
String strength = ValidationUtils.getPasswordStrengthDescription(password);
// 显示密码强度
3. 用户名自动完成
Cursor cursor = dbHelper.getAllUsernames();
String[] usernames = // 从cursor获取用户名数组
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, usernames);
AutoCompleteTextView autoComplete = findViewById(R.id.et_username);
autoComplete.setAdapter(adapter);