Simulating Inheritance in Realm with Kotlin: A Practical Guide

Hocine Abdellatif Houari
7 min read22 hours ago

--

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 version 3.0 but should work with prior version 2.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.

Edit class and sub-classes structure

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 the shapeStyle field. Additionally, collections should be renamed to plural forms, such as changing user to users, 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 extends Edit to add properties shared between different shapes to prevent code duplication.

ShapeImpl is serving as a collector to all shape interfaces, so in EditImpl we only need to import ShapeImpl 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).

File structure of given files above

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 the TextBox() 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 created TextBox to the existing list of Edit 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 of EditImpl objects from the Realm database as a flow.
  • getOneById() retrieves a single EditImpl by its id.
  • insertOne() writes a new EditImpl object to the Realm database.
  • deleteOne() deletes an existing EditImpl object by its id.

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.

--

--

Hocine Abdellatif Houari

A passionate dev, mostly on NodeJS, Rust, Flutter, and Kotlin Multi-Platform. My articles are free for the benefit of the community. hahouari.dev@gmail.com