Database Schema and Repository Pattern in SmsForwarder: Room Architecture Explained

SmsForwarder uses Android Room persistence library with version 21 schema, implementing six core entities and a read-only view wrapped in thin repository classes that abstract DAO operations for the ViewModel layer.

The open-source Android application SmsForwarder (available at pppscn/SmsForwarder) manages SMS forwarding rules and logs using a structured SQLite database accessed through Room. The architecture follows the repository pattern to separate data access logic from UI components, ensuring testable and maintainable code. This guide examines the entity definitions in AppDatabase, the migration chain from version 1 to 21, and how repository classes encapsulate Data Access Objects (DAOs).

Core Database Entities

The schema defines six primary entities and one database view, each mapped to Kotlin data classes annotated with Room annotations.

Primary Entities

  1. Rule – Stores forwarding rules with filtering logic. Located in app/src/main/kotlin/cn/ppps/forwarder/database/entity/Rule.kt, this entity includes columns for type, filed, check, value, sender_id, sender_list (JSON array), sender_logic, sms_template, regex_replace, sim_slot, status, time, and title. It maintains a foreign key relationship to Sender.id with cascade delete and update.

  2. Sender – Defines notification endpoints in app/src/main/kotlin/cn/ppps/forwarder/database/entity/Sender.kt. Columns include type, name, json_setting (configuration payload), status, and time.

  3. Msg – Stores incoming message content in app/src/main/kotlin/cn/ppps/forwarder/database/entity/Msg.kt. Tracks type, from, content, sim_slot, sim_info, sub_id, time, and call_type.

  4. Logs – Records forwarding attempts in app/src/main/kotlin/cn/ppps/forwarder/database/entity/Logs.kt. Contains type, msg_id, rule_id, sender_id, forward_status, forward_response, and time. Foreign keys reference Msg.id, Rule.id, and Sender.id with cascade deletion.

  5. Task – Manages automated tasks in app/src/main/kotlin/cn/ppps/forwarder/database/entity/Task.kt. Stores type, name, description, conditions, actions, last_exec_time, next_exec_time, and status.

  6. Frpc – Configures frp client settings in app/src/main/kotlin/cn/ppps/forwarder/database/entity/Frpc.kt. Uses uid (String) as primary key alongside name, config, autorun, and time.

Database View

LogsDetail – A read-only view defined in app/src/main/kotlin/cn/ppps/forwarder/database/entity/LogsDetail.kt that joins Logs, Rule, and Sender tables. The UI layer queries this view to display enriched forwarding history without manual joins.

AppDatabase Configuration

The central database class AppDatabase (located at app/src/main/kotlin/cn/ppps/forwarder/database/AppDatabase.kt) configures the Room database holder with the following characteristics:

  • Annotation: @Database(entities = [Rule::class, Sender::class, Msg::class, Logs::class, Task::class, Frpc::class], views = [LogsDetail::class], version = 21, exportSchema = false)
  • Singleton Pattern: Implements getInstance(context) using Room.databaseBuilder() to ensure single database access across the application lifecycle.
  • DAO Exposure: Declares abstract methods for each DAO (e.g., abstract fun ruleDao(): RuleDao).

Data Access Objects (DAO) Layer

Each entity has a corresponding DAO interface in the app/src/main/kotlin/cn/ppps/forwarder/database/dao/ package. These interfaces define standard CRUD operations using Room annotations:

  • @Insert for creation operations
  • @Update for modification operations
  • @Delete for removal operations
  • @Query with SQLite statements for custom reads

The DAOs contain no business logic; they serve as pure abstraction layers over SQL statements.

Repository Pattern Implementation

Repository classes in app/src/main/kotlin/cn/ppps/forwarder/database/repository/ provide high-level suspend functions that wrap DAO calls. This pattern decouples the ViewModel layer from Room-specific implementation details.

Repository Structure

Each repository follows this consistent pattern:

class RuleRepository(context: Context) {
    private val dao = AppDatabase.getInstance(context).ruleDao()

    suspend fun insert(rule: Rule) = dao.insert(rule)
    suspend fun update(rule: Rule) = dao.update(rule)
    suspend fun delete(rule: Rule) = dao.delete(rule)

    fun getAll(): LiveData<List<Rule>> = dao.getAll()
    fun findById(id: Long): LiveData<Rule?> = dao.findById(id)
}

Key benefits of this architecture:

  • Testability: ViewModels depend on repository interfaces rather than Room directly
  • Consistency: All data access flows through typed suspend functions
  • Lifecycle awareness: Repositories expose LiveData streams for automatic UI updates

Database Migration Strategy

The migration chain spans versions 1 through 21, handling schema evolution without data loss. Critical migration steps include:

  • Version 2 → 3: Adds sim_slot column to the Rule table to support dual-SIM filtering.
  • Version 5 → 6: Introduces type column to both Rule and legacy Log tables for categorization.
  • Version 11 → 12: Adds sender_list (JSON-encoded) and removes single sender_id from Rule, enabling multi-sender dispatch.
  • Version 13 → 14: Splits monolithic Log table into separate Msg and Logs tables with foreign key relationships.
  • Version 14 → 15: Creates the LogsDetail view for optimized UI queries.
  • Version 20 → 21: Adds user-editable title column to Rule for custom labeling.

These migrations execute automatically when AppDatabase initializes with a higher version number than the existing database file.

Practical Code Examples

Inserting a New Rule

val rule = Rule(
    id = 0,
    type = "sms",
    filed = FILED_MSG_CONTENT,
    check = CHECK_CONTAIN,
    value = "验证码",
    senderId = 0,
    smsTemplate = "",
    regexReplace = "",
    simSlot = SIM_SLOT_ALL,
    status = 1,
    time = Date(),
    senderList = listOf(),
    senderLogic = "ALL",
    silentPeriodStart = 0,
    silentPeriodEnd = 0,
    silentDayOfWeek = "",
    title = "验证码转发"
)

val repo = RuleRepository(context)
lifecycleScope.launch {
    repo.insert(rule)
}

Querying Enabled Senders

val senderRepo = SenderRepository(context)
senderRepo.getAllEnabled().observe(this) { senders ->
    // Update UI with active sender configurations
}

Accessing LogsDetail View

val logsDao = AppDatabase.getInstance(context).logsDao()
val logsDetail = MutableLiveData<List<LogsDetail>>()

viewModelScope.launch {
    logsDetail.value = logsDao.getLogsDetail()
}

Summary

  • SmsForwarder implements a Room database at version 21 with six entities (Rule, Sender, Msg, Logs, Task, Frpc) and one view (LogsDetail).
  • The AppDatabase singleton manages schema definitions and exposes DAOs through abstract methods.
  • Repository classes wrap DAOs to provide suspend functions and LiveData streams, isolating Room implementation from ViewModels.
  • Migration scripts handle schema evolution from version 1 to 21, including table splits, JSON column additions, and foreign key relationships.
  • Source files are organized under app/src/main/kotlin/cn/ppps/forwarder/database/ with clear separation between entity, dao, and repository packages.

Frequently Asked Questions

What database does SmsForwarder use?

SmsForwarder uses Android Room, an abstraction layer over SQLite provided by the Android Jetpack libraries. The database file is created automatically by Room.databaseBuilder() in the AppDatabase.getInstance() method, with the current schema version set to 21.

How does SmsForwarder handle database schema updates?

The application implements incremental migrations defined as static Migration objects in AppDatabase.kt. When the app updates and detects a higher version number, Room executes the migration chain (e.g., MIGRATION_20_21 adds the title column to Rule) while preserving existing user data.

Where is the repository pattern implemented in SmsForwarder?

Repository classes reside in app/src/main/kotlin/cn/ppps/forwarder/database/repository/. Each repository (such as RuleRepository or SenderRepository) obtains the AppDatabase singleton and exposes high-level functions like insert(), update(), and getAll(), hiding the underlying DAO implementation from ViewModels.

What is the LogsDetail view used for?

LogsDetail is a Room database view created in version 14→15 that joins Logs, Rule, and Sender tables. The UI layer queries this view through LogsDao.getLogsDetail() to display forwarding history with enriched sender and rule metadata without executing manual SQL joins in application code.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →