Android 项目架构之<用户信息模块>

0 阅读9分钟

Android 中大型项目架构梳理<一>

Android 项目架构之用户信息模块<二>

背景介绍:

个人35+的老年Android开发.日常开发中,维护和开发和我码领差不多的远古代码,着实被恶心的不轻.加上这几年的姑且称之为工作经验的东西吧,梳理一下项目中所遇到的一些问题,以及自己所想到的一些解决方式方法.


image.png

本章提要:用户信息模块搭建

原因:

在庞大的项目中因为快速频繁的获取用户信息,当内存不足等特殊情况导致用户信息为空,造成项目稳定性不足.

1:用户信息模块搭建思路

公司项目是一个直播项目,IM/直播/动画等会占据大量内存,进而频繁的GC,有时候会出现内存释放本不该释放的对象又没有重建,导致用户信息为null的情况出现.

因为内存不足导致被释放,就加载本地不就好了嘛 类似多级缓存的思路.

调用->内存->数据库/SP->网络

主要功能:

  • 内存缓存
  • 数据库存储(Room,存储)
  • 多用户存储
  • 快速/安全获取(协程获取)

2.具体实现

为方便后期扩展,采取数据库形式存储数据(多账户,加密等).采取数据库存储+缓存存储保证数据不为空,用Kotlin+协程实现数据库数据的获取

2.1 Room 实现用户信息的存储

核心代码:

  • UserDatabase 数据库创建和更新
  • UserEntity 用户信息数据
  • UserDao 数据库操作类
  • UserRepository 操作工具类
2.1.1 用户信息数据

注意:

因为数据库,表中增加字段需要更新表,所以采用了常用字段和扩展字段(extraJson)的形式,减少因为增加字段而修改表结构带来的风险

package com.wkq.user.data.entity

import androidx.room.*

/**
 * UserEntity:用户账号实体类
 */
@Entity(tableName = "user_accounts")
data class UserEntity(
    @PrimaryKey
    val userId: String, // 用户 ID (唯一标识)
    
    val userName: String?="", // 用户名
    
    val avatar: String? = "", // 头像地址
    
    /**
     * 是否为当前激活状态的账号
     */
    val isCurrent: Boolean = false,
    
    /**
     * 扩展字段 (JSON 字符串),用于存储非结构化的额外信息
     */
    val extraJson: String? = null,
    
    /**
     * 账号过期时间(毫秒时间戳)
     */
    val expireTime: Long? = null,
    
    /**
     * 最后一次活跃时间(毫秒时间戳)
     */
    val lastActiveTime: Long? = System.currentTimeMillis()
)
2.1.2 UserDao 用户信息表的Dao

注意: internal 修饰Module外部不可见,减少调用者的干扰

package com.wkq.user.data.dao

import androidx.room.*
import com.wkq.user.data.entity.UserEntity
import kotlinx.coroutines.flow.Flow

/**
 * UserDao:用户数据库访问接口
 */
@Dao
internal interface UserDao {

    /**
     * 插入或更新用户
     */
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertOrUpdate(user: UserEntity)

    /**
     * 查询所有用户
     */
    @Query("SELECT * FROM user_accounts")
    fun getAllUsersFlow(): Flow<List<UserEntity>>

    /**
     * 查询当前激活用户
     */
    @Query("SELECT * FROM user_accounts WHERE isCurrent = 1 LIMIT 1")
    fun getCurrentUserFlow(): Flow<UserEntity?>

    /**
     * 查询当前激活用户(挂起函数)
     */
    @Query("SELECT * FROM user_accounts WHERE isCurrent = 1 LIMIT 1")
    suspend fun getCurrentUser(): UserEntity?

    /**
     * 根据 ID 查询用户
     */
    @Query("SELECT * FROM user_accounts WHERE userId = :userId LIMIT 1")
    suspend fun getUserById(userId: String): UserEntity?

    /**
     * 删除用户
     */
    @Delete
    suspend fun deleteUser(user: UserEntity)

    /**
     * 切换账号:
     * 1. 将所有账号设为非当前
     * 2. 将指定 ID 账号设为当前
     */
    @Transaction
    suspend fun switchAccount(userId: String) {
        clearCurrentStatus()
        setCurrentStatus(userId)
    }

    @Query("UPDATE user_accounts SET isCurrent = 0")
    suspend fun clearCurrentStatus()

    @Query("UPDATE user_accounts SET isCurrent = 1 WHERE userId = :userId")
    suspend fun setCurrentStatus(userId: String)
}
2.1.3 UserDatabase 数据库的帮助类
  • 创建数据库
  • 预留后期表变动的方法(MIGRATION_1_2)
package com.wkq.user.data.db

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.wkq.user.data.dao.UserDao
import com.wkq.user.data.entity.UserEntity

/**
 * UserDatabase:用户本地数据库 (Room 实现)
 */
@Database(entities = [UserEntity::class], version = 1, exportSchema = true)
internal abstract class UserDatabase : RoomDatabase() {

    /** 获取用户数据操作接口 */
    abstract fun userDao(): UserDao

    companion object {
        @Volatile
        private var INSTANCE: UserDatabase? = null
        // 升级 模板代码(Room + Migration)

        // 1: version=1 -->version=2
        //2: 创建val MIGRATION_1_2=Migration(1,2)
        //3: .addMigrations(MIGRATION_1_2)

        val MIGRATION_1_2 = object : Migration(1, 2) {
            override fun migrate(db: SupportSQLiteDatabase) {
                // -----------------------------
                // 1. 新增字段(简单)
                // -----------------------------
                db.execSQL(
                    """
            ALTER TABLE UserEntity 
            ADD COLUMN age INTEGER NOT NULL DEFAULT 0
            """.trimIndent()
                )

                // -----------------------------
                // 2. 如果要新增表
                // -----------------------------
                db.execSQL(
                    """
            CREATE TABLE IF NOT EXISTS UserProfile (
                id INTEGER PRIMARY KEY NOT NULL,
                userId INTEGER NOT NULL,
                nickname TEXT,
                avatar TEXT
            )
            """.trimIndent()
                )

                // -----------------------------
                // 3. 复杂表修改(改字段名/删字段/改类型)
                // -----------------------------
                // 举例:UserEntity 表删掉 name 字段
                db.execSQL(
                    """
            CREATE TABLE UserEntity_new (
                id INTEGER PRIMARY KEY NOT NULL,
                age INTEGER NOT NULL DEFAULT 0
            )
            """.trimIndent()
                )

                db.execSQL(
                    """
            INSERT INTO UserEntity_new (id, age)
            SELECT id, age FROM UserEntity
            """.trimIndent()
                )

                db.execSQL("DROP TABLE UserEntity")
                db.execSQL("ALTER TABLE UserEntity_new RENAME TO UserEntity")
            }
        }

        fun getDatabase(context: Context): UserDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext, UserDatabase::class.java, "multi_user_db"
                ).fallbackToDestructiveMigration(true)
//                    .addMigrations(MIGRATION_1_2)
                    .build()
                INSTANCE = instance
                instance
            }
        }
    }
}
2.1.4 UserRepository 数据库数据的操作类
package com.wkq.user.repository

import com.wkq.user.cache.UserCache
import com.wkq.user.data.dao.UserDao
import com.wkq.user.data.entity.UserEntity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach

/**
 * UserRepository:用户数据仓库
 * 
 * 职责:
 * 1. 封装底层数据源 (Room DAO) 和内存缓存 (UserCache)。
 * 2. 确保数据库更新时,内存缓存能同步得到刷新。
 * 3. 提供统一的业务接口供上层 UserManager 调用。
 */
internal class UserRepository(
    private val userDao: UserDao, 
    private val userCache: UserCache
) {

    /**
     * 获取当前用户的 Flow
     * 内部使用 onEach 观察数据流变化,自动同步更新到 UserCache
     */
    fun getCurrentUserFlow(): Flow<UserEntity?> {
        return userDao.getCurrentUserFlow().onEach { user ->
            userCache.updateCache(user)
        }
    }

    /**
     * 获取所有账户信息的 Flow (直接来自数据库)
     */
    fun getAllUsersFlow(): Flow<List<UserEntity>> = userDao.getAllUsersFlow()

    /**
     * 登录或更新用户信息
     * @param user 用户实体,若 isCurrent 为 true,则会清除数据库中其他账号的当前激活状态
     */
    suspend fun saveUser(user: UserEntity) {
        if (user.isCurrent) {
            // 如果设置为当前用户,先通过 DAO 清除其他用户的激活标志
            userDao.clearCurrentStatus()
        }
        userDao.insertOrUpdate(user)
        
        // 同步更新内存缓存
        if (user.isCurrent) {
            userCache.updateCache(user)
        } else if (userCache.getCurrentUser()?.userId == user.userId) {
            // 如果原本是当前用户但现在被设为非激活,则清除缓存
            userCache.clear()
        }
    }

    /**
     * 切换账户
     * @param userId 目标用户 ID
     */
    suspend fun switchAccount(userId: String) {
        userDao.switchAccount(userId)
        val user = userDao.getUserById(userId)
        userCache.updateCache(user)
    }

    /**
     * 手动刷新内存缓存(从数据库拉取最新数据)
     */
    suspend fun refreshCache() {
        val user = userDao.getCurrentUser()
        userCache.updateCache(user)
    }

    /**
     * 退出登录
     * @param userId 退出登录的用户 ID,将 mark 为非激活状态
     */
    suspend fun logout(userId: String) {
        val user = userDao.getUserById(userId)
        if (user != null) {
            userDao.insertOrUpdate(user.copy(isCurrent = false))
            // 如果退出的正好是当前缓存的用户,则同步清理内存
            if (userCache.getCurrentUser()?.userId == userId) {
                userCache.clear()
            }
        }
    }
    
    /**
     * 同步加载当前用户(查库)并刷新缓存
     */
    suspend fun loadCurrentUser(): UserEntity? {
        val user = userDao.getCurrentUser()
        userCache.updateCache(user)
        return user
    }

}

2.2 UserCache 缓存信息暴露的方法

package com.wkq.user.cache

import com.wkq.user.data.entity.UserEntity
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

/**
 * UserCache:用户内存缓存类
 * 
 * 功能:
 * 使用 StateFlow 在内存中持有当前登录用户的实时状态,供 UI 层进行响应式监听。
 */
internal class UserCache {
    /**
     * 当前登录用户的 MutableStateFlow (私有)
     */
    private val _currentUser = MutableStateFlow<UserEntity?>(null)
    
    /**
     * 对外公开的只读 StateFlow
     */
    val currentUserFlow: StateFlow<UserEntity?> = _currentUser.asStateFlow()

    /**
     * 更新缓存值
     */
    fun updateCache(user: UserEntity?) {
        _currentUser.value = user
    }

    /**
     * 同步获取缓存中的当前用户对象
     */
    fun getCurrentUser(): UserEntity? {
        return _currentUser.value
    }
    
    /**
     * 清空当前用户缓存
     */
    fun clear() {
        _currentUser.value = null
    }

    companion object {
        @Volatile
        private var INSTANCE: UserCache? = null

        /** 传统的双重校验锁单例获取方式 */
        fun getInstance(): UserCache {
            return INSTANCE ?: synchronized(this) {
                INSTANCE ?: UserCache().also { INSTANCE = it }
            }
        }
    }
}

2.3 用户信息操作的主操作类

package com.wkq.user.manager

import android.content.Context
import com.google.gson.Gson
import com.wkq.user.cache.UserCache
import com.wkq.user.data.db.UserDatabase
import com.wkq.user.data.entity.UserEntity
import com.wkq.user.repository.UserRepository
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.concurrent.ConcurrentHashMap

/**
 * UserManager:用户管理核心单例类
 * 
 * 职责:
 * 1. 维护当前登录用户和所有用户的响应式状态 (StateFlow)。
 * 2. 提供线程安全的同步/异步接口进行用户登录、切换、退出。
 * 3. 自动同步底层数据库变化到内存内存 Flow。
 * 4. 支持用户自定义扩展数据的 JSON 解析与缓存。
 * 
 * 优化点:
 * - 线程安全:使用 ConcurrentHashMap 处理扩展数据缓存。
 * - 性能优化:通过 stateIn 将冷流转成热流,并在内存中共享数据。
 * - 并发保护:使用 Mutex 解决冷启动时的“缓存击穿”读库问题。
 */
class UserManager private constructor(context: Context) {

    // ------------------ 内部依赖 ------------------

    private val database = UserDatabase.getDatabase(context)
    private val userDao = database.userDao()
    private val userCache = UserCache.getInstance()
    private val repository = UserRepository(userDao, userCache)
    
    private val gson = Gson()
    
    /** 并发锁:确保多线程下 getSafeCurrentUser() 仅触发一次查库 */
    private val mutex = Mutex()

    /** 全局协程处理器,防止异常导致 Scope 崩溃 */
    private val handler = CoroutineExceptionHandler { _, throwable -> throwable.printStackTrace() }
    
    /** 单例作用域:使用 SupervisorJob 确保子协程互不影响 */
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + handler)

    /** 扩展数据并发缓存:Clazz Name -> 解析后的实体 */
    private val extraDataCache = ConcurrentHashMap<String, Any?>()

    // ------------------ 对外公开 Flow ------------------

    /** 
     * 当前用户 Flow (StateFlow)
     * 每次用户更新时会自动清空扩展数据缓存
     */
    val currentUserFlow: StateFlow<UserEntity?> = repository.getCurrentUserFlow()
        .onEach { extraDataCache.clear() } // 用户变更,清理对应的扩展缓存
        .stateIn(scope, SharingStarted.Eagerly, null)

    /** 所有用户列表 Flow (StateFlow) */
    val allUsersFlow: StateFlow<List<UserEntity>> = repository.getAllUsersFlow()
        .stateIn(scope, SharingStarted.Eagerly, emptyList())

    // ------------------ 核心功能接口 ------------------

    /** 
     * 挂起函数:获取当前用户(带并发保护)
     * 优先从内存 Flow 获取,若无则持有锁进行数据库加载
     */
    suspend fun getSafeCurrentUser(): UserEntity? {
        currentUserFlow.value?.let { return it }
        return mutex.withLock {
            // 双重校验,解决“并发缓存击穿”问题
            currentUserFlow.value ?: repository.loadCurrentUser()
        }
    }

    /** 
     * 挂起函数:获取所有用户列表
     * 优先从内存 Flow 获取,若为空则从数据库初次加载
     */
    suspend fun getAllUsers(): List<UserEntity> =
        allUsersFlow.value.takeIf { it.isNotEmpty() } ?: repository.getAllUsersFlow().first()

    /** 回调函数:获取当前用户(主线程安全回调) */
    fun getUserAsync(onResult: (UserEntity?) -> Unit) =
        runAsyncMain({ getSafeCurrentUser() }, onResult)

    /** 回调函数:获取用户列表(主线程安全回调) */
    fun getAllUsersAsync(onResult: (List<UserEntity>) -> Unit) =
        runAsyncMain({ getAllUsers() }, onResult)

    /** 保存或更新用户(异步执行) */
    fun saveUser(user: UserEntity) {
        scope.launch(Dispatchers.IO) { repository.saveUser(user) }
    }

    /** 切换账号(异步执行) */
    fun switchAccount(userId: String) {
        scope.launch(Dispatchers.IO) { repository.switchAccount(userId) }
    }

    /** 退出登录(异步执行) */
    fun logout(userId: String) {
        scope.launch(Dispatchers.IO) { repository.logout(userId) }
    }

    // ------------------ 扩展数据接口 ------------------

    /**
     * 获取当前用户扩展信息(泛型解析 + 内存缓存)
     * @param clazz 目标解析类型
     * 注意:当前用户变更时会自动清理该缓存
     */
    @Suppress("UNCHECKED_CAST")
    fun <T> getExtraData(clazz: Class<T>): T? {
        val key = clazz.name
        // 1. 命中内存缓存直接返回
        extraDataCache[key]?.let { return it as T? }

        // 2. 无缓存则从 UserEntity.extraJson 进行解析
        val json = currentUserFlow.value?.extraJson ?: return null
        val parsed = try { gson.fromJson(json, clazz) } catch (e: Exception) { null }
        
        // 3. 写入缓存提高下次读取效率
        if (parsed != null) {
            extraDataCache[key] = parsed
        }
        return parsed
    }

    /** 刷新内存缓存(手动从数据库同步一次最新状态) */
    fun refreshCache() {
        scope.launch(Dispatchers.IO) { repository.refreshCache() }
    }

    /** 清理资源(通常仅在 Application 结束时调用) */
    fun clear() {
        scope.cancel()
    }

    // ------------------ 内部工具方法 ------------------

    /** 辅助方法:将协程任务结果分发到主线程 */
    private fun <T> runAsyncMain(block: suspend () -> T, onResult: (T) -> Unit) {
        scope.launch {
            val result = block()
            withContext(Dispatchers.Main) { onResult(result) }
        }
    }

    // ------------------ 单例管理 ------------------

    companion object {
        @Volatile
        private var INSTANCE: UserManager? = null

        /** 模块初始化(应在 Application.onCreate 中调用) */
        fun init(context: Context): UserManager =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: UserManager(context).also { INSTANCE = it }
            }

        /** 获取单例对象 */
        fun getInstance(): UserManager =
            INSTANCE ?: throw IllegalStateException(
                "UserManager must be initialized first. Call init(context) in Application."
            )
    }
}

3.调用方式

package com.wkq.user.util

import android.util.Log
import com.wkq.user.data.entity.UserEntity
import com.wkq.user.manager.UserManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch

/**
 * UserManager 调用示例
 * 
 * 展示了如何在不同场景下调用 UserManager 的公开 API。
 */
object UserManagerDemo {
    private const val TAG = "UserManagerDemo"
    private val scope = CoroutineScope(Dispatchers.Main)

    /**
     * 1. 响应式监听当前用户变化 (推荐)
     */
    fun observeUser() {
        scope.launch {
            UserManager.getInstance().currentUserFlow.collectLatest { user ->
                if (user != null) {
                    Log.d(TAG, "当前用户更新: ${user.userName}")
                } else {
                    Log.d(TAG, "当前未登录")
                }
            }
        }
    }

    /**
     * 2. 在协程中同步安全地获取当前用户 (挂起函数)
     */
    suspend fun fetchUser() {
        val user = UserManager.getInstance().getSafeCurrentUser()
        Log.d(TAG, "获取到最新用户: ${user?.userName}")
    }

    /**
     * 3. 使用回调方式获取当前用户 (传统异步方式)
     */
    fun getUserAsync() {
        UserManager.getInstance().getUserAsync { user ->
            Log.d(TAG, "异步回调获取用户: ${user?.userName}")
        }
    }

    /**
     * 4. 获取用户自定义扩展数据 (自动解析 JSON 并缓存)
     */
    fun showExtraData() {
        // 假设 UserEntity 中的 extraJson 存储了 UserSettings 的 JSON
        data class UserSettings(val theme: String = "dark", val language: String = "zh")
        
        val settings = UserManager.getInstance().getExtraData(UserSettings::class.java)
        Log.d(TAG, "用户设置: 主题=${settings?.theme}, 语言=${settings?.language}")
    }

    /**
     * 5. 执行账号操作 (保存、切换、退出)
     */
    fun accountOperations() {
        val userManager = UserManager.getInstance()
        
        // 保存/更新用户
        val newUser = UserEntity(userId = "1001", userName = "张三", isCurrent = true)
        userManager.saveUser(newUser)
        
        // 切换账号
        // userManager.switchAccount("1002")
        
        // 退出登录 (清理指定用户状态)
        // userManager.logout("1001")
    }
}

总结

模块简单说明

  • UserManager(外) -> UserRepository(中) -> UserDao/Cache(内存/内) 的标准架构,实现了业务逻辑与存储细节的彻底分离。

  • 通过 internal 关键字隐藏了仓库、缓存和数据库等实现细节,将模块的公开 API 收窄至 

  • 启用了 Room 的 exportSchema 功能并配置了文件路径,为后续的版本迭代和数据库迁移提供扩展

  • UserManager, UserEntity (仅公开业务入口和必要的数据模型)。

内存+Room 缓存保证数据安全,internal关键句只暴露对外调用,Room+extraJson字段扩展 保证追加字段的稳定性.

暂时就想到这么多,后期有想到的地方再追加