Simulating Inheritance in Realm with Kotlin: A Practical Guide
When working with Realm in Kotlin, you might come across scenarios where you need to simulate inheritance or create complex data models with shared properties. Realm, being a NoSQL database, doesn’t inherently support inheritance like traditional OOP, but with Kotlin’s flexibility and Realm’s powerful features, you can simulate inheritance quite effectively. In this article, I’ll walk you through how I implemented inheritance-like behavior using Kotlin interfaces and Realm’s database model.
Note: this tutorial is tested with
io.realm.kotlin:library-base
version3.0
but should work with prior version2.x
.
The Problem
Suppose you’re building an app with an editing feature, and you have various types of edits such as brushes, erasers, text boxes, and shapes. All of these edits share common properties like id
, visible
, and kind
, but each one also has unique attributes. You want to store these different kinds of edits in Realm while maintaining shared behavior, without duplicating code for each type.
The Approach
The solution involves simulating inheritance using Kotlin’s interface
to define shared properties and behaviors. Then, I implement these interfaces in a single concrete class (EditImpl
), which stores all the necessary data for the various types of edits. Finally, factory functions are used to create specific types of edits, such as Brush
, TextBox
, or Shape
.
Let’s dive into the code!
Step 1: Define Shared Behavior
We start by defining an Edit
interface that all types of edits will implement. This interface contains common properties like id
, visible
, and kind
. The kind
property is particularly important as it helps us distinguish between different types of edits at runtime.
// model/Edit.kt
interface Edit {
var id: BsonObjectId
var visible: Boolean
var kind: EditKind // enum of (TEXT_BOX, ERASER, SHAPE, BRUSH, NONE).
}
Step 2: Create the Concrete Class
All properties for all types of edits must reside in a single concrete class. We achieve this by creating an EditImpl
class that implements all the interfaces for each type of edit: Brush
, Eraser
, TextBox
, and ShapeImpl
.
// model/Edit.kt
@PersistedName("edits")
class EditImpl : RealmObject, Edit, Brush, Eraser, TextBox, ShapeImpl {
@PrimaryKey
override var id: BsonObjectId = BsonObjectId()
override var visible: Boolean = true
@PersistedName("kind")
private var _kind: String = EditKind.NONE.name.lowercase()
override var kind
get() = EditKind.get(_kind)
set(value) { _kind = value.name.lowercase() }
override var points: RealmList<RealmPoint> = realmListOf()
// Shape-specific properties
@PersistedName("shape")
private var _shape: String = ShapeKind.NONE.name.lowercase()
override var shape
get() = ShapeKind.get(_shape)
set(value) { _shape = value.name.lowercase() }
@PersistedName("rect")
private var _rect: RealmRect? = null
override var rect
get() = _rect ?: RealmRect()
set(value) { _rect = value }
override var text: String = ""
// Drawing style for brush and shape
@PersistedName("drawing_style")
private var _drawingStyle: DrawingStyle? = null
override var drawingStyle
get() = _drawingStyle ?: DrawingStyle()
set(value) { _drawingStyle = value }
// TextBox-specific properties
@PersistedName("text_box_style")
private var _textBoxStyle: TextBoxStyle? = null
override var textBoxStyle
get() = _textBoxStyle ?: TextBoxStyle()
set(value) { _textBoxStyle = value }
// Shape-specific properties
@PersistedName("shape_style")
private var _shapeStyle: ShapeStyle? = null
override var shapeStyle
get() = _shapeStyle ?: ShapeStyle()
set(value) { _shapeStyle = value }
}
Note: A good practice is using
@PersistedName()
for renaming collections and fields to snake_case, which helps avoid dealing with capital letters, like in theshapeStyle
field. Additionally, collections should be renamed to plural forms, such as changinguser
tousers
, to avoid confusion with database keywords.
Step 3: Implement Specific Types of Edits
Next, we implement the specific types of edits (e.g., Brush
, TextBox
, Shape
) by defining interfaces and “factory functions”. Each interface extends the Edit
interface and adds properties specific to that type.
// model/edits/Brush.kt
interface Brush : Edit {
var points: RealmList<RealmPoint>
var drawingStyle: DrawingStyle
}
fun Brush() = EditImpl().apply { kind = EditKind.BRUSH } as Brush
The Brush
interface extends Edit
and adds properties specific to brush strokes, such as points
and drawingStyle
. The Brush()
factory function creates an EditImpl
object, sets its kind to BRUSH
, and casts it to Brush
.
// model/edits/Eraser.kt
interface Eraser : Edit {
var points: RealmList<RealmPoint>
var drawingStyle: DrawingStyle
}
fun Eraser() = EditImpl().apply { kind = EditKind.ERASER } as Eraser
The Eraser
interface is nearly-similar to Brush
implementation.
// model/edits/TextBox.kt
interface TextBox : Edit {
var rect: RealmRect
var text: String
var textBoxStyle: TextBoxStyle
}
fun TextBox() = EditImpl().apply { kind = EditKind.TEXT_BOX } as TextBox
TextBox
extends Edit
and includes properties like rect
, text
, and textBoxStyle
, which are unique to text boxes.
Shapes are a bit more complex since we have different kinds of shapes, such as OvalShape
and PolygonShape
. We start by defining a general Shape
interface and a more specific ShapeImpl
interface for concrete shape types.
// model/edits/Shape.kt
interface Shape : Edit {
var shape: ShapeKind // enum of (RECT, OVAL, POLYGON, NONE).
var shapeStyle: ShapeStyle
}
interface ShapeImpl : Shape, RectShape, OvalShape, PolygonShape
Shape
extendsEdit
to add properties shared between different shapes to prevent code duplication.
ShapeImpl
is serving as a collector to all shape interfaces, so inEditImpl
we only need to importShapeImpl
instead of all the four interfaces.
// model/edits/shapes/OvalShape.kt
interface OvalShape : Shape {
var rect: RealmRect
}
fun OvalShape() = EditImpl().apply {
kind = EditKind.SHAPE
shape = ShapeKind.OVAL
} as OvalShape
// model/edits/shapes/PolygonShape.kt
interface PolygonShape : Shape {
var points: RealmList<RealmPoint>
}
fun PolygonShape() = EditImpl().apply {
kind = EditKind.SHAPE
shape = ShapeKind.POLYGON
} as PolygonShape
// model/edits/shapes/RectShape.kt
interface RectShape : Shape {
var rect: RealmRect
}
fun RectShape() = EditImpl().apply {
kind = EditKind.SHAPE
shape = ShapeKind.RECT
} as RectShape
Here, the factory functions again sets the kind
to SHAPE
and the specific shape
kind (like OVAL
or POLYGON
).
Integration and Use Cases
With the foundation of simulating inheritance in Realm laid out, it’s time to integrate this design into a working application. Below are the steps involved in configuring Realm and utilizing the EditImpl
class in a project.
// extensions/realm/Realm.kt
private val realmConfig = RealmConfiguration.Builder(
schema = setOf(
EditImpl::class,
TextBoxStyle::class,
ShapeStyle::class,
DrawingStyle::class,
RealmRect::class,
RealmPoint::class,
// ... other class imports
)
)
val Realm.Companion.DEFAULT
get() = open(
realmConfig
.directory(listOf(appFilesDirectory(), "default").joinToString("/"))
.compactOnLaunch()
.build()
)
This configuration ensures that Realm is set up with the necessary schema, pointing to a specific directory and compressing the database on launch to save space. The schema includes all classes involved in the inheritance simulation, like TextBoxStyle
, ShapeStyle
, and others.
ViewModel State with Edits
In a typical ViewModel scenario, you would store a collection of Edit
objects in the state to manage UI updates or modifications, here I called it SectionState
, essentially a section has multiple edits in my case. The Edit
interface can represent various types of edits (brush strokes, text boxes, shapes), and the SectionState
data class can hold a list of Edit
objects.
Here’s an example of the SectionState
:
data class SectionState(
val edits: MutableList<out Edit> = mutableListOf(),
// ... other state variables
)
This state object holds a collection of edits, each of which could be a Brush
, TextBox
, Shape
, or any other Edit
type. By utilizing this list, you can easily manage different types of annotations or drawings in your application.
Creating and Adding a New Edit
To demonstrate the use of the inheritance simulation in action, let’s look at an example where a user clicks a button to add a new TextBox
edit.
fun onEvent(event: SectionEvent) {
when (event) {
is SectionEvent.AddTextBox -> {
val textBox = TextBox().apply {
text = event.text
rect = RealmRect.fromRect(event.rect)
textBoxStyle = TextBoxStyle().apply {
size = event.size
align = TextBoxAlign.get(event.align.toString())
bold = event.bold
color = event.color
italic = event.italic
}
}
val edits = mutableListOf(*state.edits.toTypedArray(), textBox)
state = state.copy(edits = edits)
}
}
// ... other events handling
}
In this example:
- A new
TextBox
is created using theTextBox()
factory function. - The
TextBox
is configured with text content, a rectangle, and style options like size, alignment, boldness, and color. - The
state
is then updated by adding the newly createdTextBox
to the existing list ofEdit
objects.
Repository Pattern for Edits
In a typical application architecture, you’ll often use a repository pattern to manage the data layer. Below is an example of an EditRepoImpl
(you can deduce the EditRepo
from its implementation) that manages Edit
objects in a Realm database:
class EditRepoImpl(private val realm: Realm) : EditRepo {
override fun getData(): Flow<List<Edit>> = realm.query<EditImpl>().asFlow().map { it.list }
override suspend fun getOneById(id: String) = realm
.query<EditImpl>(query = "id == $0", BsonObjectId(id))
.first()
.find()
override suspend fun insertOne(edit: Edit) = realm.write { copyToRealm(edit as EditImpl) }
override suspend fun deleteOne(id: String): Unit = realm.write {
try {
query<EditImpl>(query = "id == $0", BsonObjectId(id)).first().find()?.let { delete(it) }
} catch (e: Exception) {
Log.e("Realm", e.message ?: "Unknown error")
}
}
}
In this implementation:
- The
getData()
method retrieves a list ofEditImpl
objects from the Realm database as a flow. getOneById()
retrieves a singleEditImpl
by itsid
.insertOne()
writes a newEditImpl
object to the Realm database.deleteOne()
deletes an existingEditImpl
object by itsid
.
Conclusion
With this approach, we effectively simulate inheritance in Realm with Kotlin interfaces. By defining shared behavior in the Edit
interface and implementing all possible properties in a single concrete class (EditImpl
), we create specific types of edits (like Brush
, TextBox
, Shape
) without duplicating code. Factory functions make it easy to instantiate the correct type of edit while ensuring that Realm stores everything in a single, efficient model.
This solution leverages Kotlin’s flexibility and Realm’s power, providing a clean, scalable way to handle complex data models in your app.