SQLite以及Room框架的学习:用SQLite给新闻app加上更完善的登录注册功能
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);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_USERS);
}
@Override
public 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 Setters
public 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;
}
@Override
public 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;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化DAO
userDAO = 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">
<application
android: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);
@Insert
void insert(User user);
@Insert
void insertAll(List<User> users);
@Update
void update(User user);
@Delete
void 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);
@Insert
void insert(User user);
@Insert
void insertAll(List<User> users);
@Update
void update(User user);
@Delete
void 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 {
@Embedded
public 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) {
@Override
public 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;
}
@Override
protected 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;
@Before
public void createDb() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
db = Room.inMemoryDatabaseBuilder(context, AppDatabase.class).build();
userDao = db.userDao();
}
@After
public void closeDb() throws IOException {
db.close();
}
@Test
public 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 = "";
@Override
protected 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() {
@Override
public void onClick(View v) {
selectAvatar();
}
});
// 头像点击也可以选择
ivAvatar.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
selectAvatar();
}
});
// 密码强度监听
etPassword.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
updatePasswordStrength(s.toString());
}
@Override
public void afterTextChanged(Editable s) {}
});
// 注册按钮
btnRegister.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
attemptRegister();
}
});
// 返回登录按钮
btnBackToLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
// 用户协议点击
TextView tvUserAgreement = findViewById(R.id.tv_user_agreement);
tvUserAgreement.setOnClickListener(new View.OnClickListener() {
@Override
public 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);
}
@Override
protected 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;
}
// 如果没有选择头像,使用默认头像的Base64
if (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();
}
}
@Override
protected 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或标题 -->
<!-- 用户头像 -->
<TextView
android: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" />
<ImageView
android: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="用户头像" />
<!-- 登录表单容器 -->
<LinearLayout
android: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.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="用户名">
<com.google.android.material.textfield.TextInputEditText
android: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.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="密码"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android: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>
<!-- 记住密码复选框 -->
<CheckBox
android: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" />
<!-- 登录按钮 -->
<Button
android: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" />
<!-- 注册按钮 -->
<Button
android: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>
<!-- 忘记密码链接 -->
<TextView
android: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 = "";
@Override
protected 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() {
@Override
public void onClick(View v) {
selectAvatar();
}
});
// 头像点击也可以选择
ivAvatar.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
selectAvatar();
}
});
// 密码强度监听
etPassword.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
updatePasswordStrength(s.toString());
}
@Override
public void afterTextChanged(Editable s) {}
});
// 注册按钮
btnRegister.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
attemptRegister();
}
});
// 返回登录按钮
btnBackToLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
// 用户协议点击
TextView tvUserAgreement = findViewById(R.id.tv_user_agreement);
tvUserAgreement.setOnClickListener(new View.OnClickListener() {
@Override
public 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);
}
@Override
protected 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;
}
// 如果没有选择头像,使用默认头像的Base64
if (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();
}
}
@Override
protected 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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="32dp">
<!-- 标题 -->
<TextView
android: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" />
<!-- 注册表单容器 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/form_background"
android:padding="24dp"
android:elevation="4dp">
<!-- 头像选择区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:layout_marginBottom="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="选择头像"
android:textSize="16sp"
android:textColor="@color/text_primary"
android:layout_marginBottom="12dp" />
<!-- 头像显示 -->
<ImageView
android: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" />
<!-- 选择头像按钮 -->
<Button
android: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.TextInputLayout
android: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.TextInputEditText
android: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.TextInputLayout
android: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.TextInputEditText
android: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>
<!-- 密码强度提示 -->
<TextView
android: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.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:hint="确认密码"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android: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>
<!-- 注册按钮 -->
<Button
android: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" />
<!-- 返回登录按钮 -->
<Button
android: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>
<!-- 用户协议 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="24dp">
<CheckBox
android:id="@+id/cb_agree_terms"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我已阅读并同意"
android:textSize="12sp"
android:textColor="@color/text_secondary" />
<TextView
android: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:
<activity
android: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>
<activity
android: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">
<path
android: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() {
@Override
public 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);