Data Layer¶
The Data Layer manages all data models, database operations, and persistence in OpenTransition.
Overview¶
The data layer consists of: - Data Models: Realm objects representing app entities - Database: Realm Kotlin for local storage - Cloud Sync: Firebase Firestore for remote backup - File Storage: Local file system for photos
Data Models¶
Photo Model¶
The Photo class represents transition photos stored by users.
File: mobile/src/main/java/com/shelbeely/opentransition/data/Photo.kt
class Photo : RealmObject {
@PrimaryKey var id: String = UUID.randomUUID().toString()
var timestamp: Long = 0L
var epochDay: Long = 0L
var filename: String = ""
var type: Int = TYPE_BODY
companion object {
const val TYPE_FACE = 0
const val TYPE_BODY = 1
const val TYPE_CUSTOM = 2
}
}
Key Properties:
- id: Unique identifier (UUID)
- timestamp: Photo capture time in milliseconds
- epochDay: Date as epoch day for grouping
- filename: Stored filename on device
- type: Photo category (face, body, or custom)
Photo Types:
- Face (0): Facial progress photos
- Body (1): Full body progress photos
- Custom (2): User-defined areas
Milestone Model¶
The Milestone class represents significant events in the transition journey.
File: mobile/src/main/java/com/shelbeely/opentransition/data/Milestone.kt
class Milestone : RealmObject {
@PrimaryKey var id: String = UUID.randomUUID().toString()
var title: String = ""
var description: String = ""
var epochDay: Long = 0L
var timestamp: Long = 0L
}
Key Properties:
- id: Unique identifier (UUID)
- title: Milestone name
- description: Detailed description
- epochDay: Date as epoch day
- timestamp: Creation time in milliseconds
Common Milestone Examples: - Starting HRT - Name change - Coming out milestones - Medical procedures - Legal document updates
Database: Realm Kotlin¶
Configuration¶
Realm is configured in the Application class and opened throughout the app:
// Open default Realm instance
val realm = Realm.openDefault()
// Always close when done
realm.close()
Database Operations¶
Create (Insert)¶
val realm = Realm.openDefault()
realm.write {
val photo = Photo().apply {
id = UUID.randomUUID().toString()
timestamp = System.currentTimeMillis()
filename = "photo_${timestamp}.jpg"
type = Photo.TYPE_BODY
}
copyToRealm(photo, UpdatePolicy.ALL)
}
realm.close()
Read (Query)¶
val realm = Realm.openDefault()
// Get all photos
val allPhotos = realm.query<Photo>().find()
// Filter by type
val facePhotos = realm.query<Photo>("type == $0", Photo.TYPE_FACE).find()
// Sort by date
val sortedPhotos = realm.query<Photo>()
.sort("timestamp", Sort.DESCENDING)
.find()
realm.close()
Update¶
val realm = Realm.openDefault()
realm.write {
val photo = query<Photo>("id == $0", photoId).first().find()
photo?.let {
it.type = Photo.TYPE_CUSTOM
}
}
realm.close()
Delete¶
val realm = Realm.openDefault()
realm.write {
val photo = query<Photo>("id == $0", photoId).first().find()
photo?.let { delete(it) }
}
realm.close()
Reactive Queries¶
Realm supports reactive queries that automatically update:
val realm = Realm.openDefault()
val photosFlow: Flow<ResultsChange<Photo>> = realm.query<Photo>()
.sort("timestamp", Sort.DESCENDING)
.asFlow()
// Observe changes
photosFlow
.map { it.list }
.collect { photos ->
// Update UI with latest photos
}
File Storage¶
Photo Files¶
Photos are stored in the app's private storage:
object FileUtil {
fun getImageFile(filename: String): File {
val directory = File(context.filesDir, "images")
if (!directory.exists()) directory.mkdirs()
return File(directory, filename)
}
fun getTempFile(filename: String): File {
val directory = File(context.filesDir, "temp")
if (!directory.exists()) directory.mkdirs()
return File(directory, filename)
}
}
File Operations¶
Saving Photos¶
fun copyPhotoToTempFiles(uri: Uri): String? {
try {
val inputStream = contentResolver.openInputStream(uri)
val filename = "photo_${System.currentTimeMillis()}.jpg"
val outputFile = FileUtil.getImageFile(filename)
inputStream?.use { input ->
FileOutputStream(outputFile).use { output ->
input.copyTo(output)
}
}
return filename
} catch (e: Exception) {
return null
}
}
Deleting Photos¶
fun deletePhoto(filename: String): Boolean {
val file = FileUtil.getImageFile(filename)
return if (file.exists()) {
file.delete()
} else false
}
Cloud Sync: Firebase¶
Firestore Structure¶
users/
{userId}/
photos/
{photoId}/
- timestamp
- epochDay
- filename
- type
milestones/
{milestoneId}/
- title
- description
- epochDay
- timestamp
settings/
- theme
- lockType
- startDate
- ...
Syncing Data¶
class FirebaseSettingUtil {
private val firestore = FirebaseFirestore.getInstance()
private val auth = FirebaseAuth.getInstance()
fun syncPhotos() {
val userId = auth.currentUser?.uid ?: return
val photosRef = firestore.collection("users")
.document(userId)
.collection("photos")
val realm = Realm.openDefault()
val photos = realm.query<Photo>().find()
photos.forEach { photo ->
val photoData = hashMapOf(
"id" to photo.id,
"timestamp" to photo.timestamp,
"epochDay" to photo.epochDay,
"type" to photo.type
// Note: Actual photo files stored separately
)
photosRef.document(photo.id).set(photoData)
}
realm.close()
}
}
Data Import/Export¶
Export Format¶
Data is exported as a ZIP file containing:
- data.json: JSON file with all metadata
- Photo files: All images referenced in the data
JSON Structure¶
{
"settings": {
"theme": "pink",
"lockType": "normal",
"startDate": 1234567890
},
"photos": [
{
"id": "uuid-here",
"timestamp": 1234567890000,
"epochDay": 19000,
"filename": "photo_123.jpg",
"type": 1
}
],
"milestones": [
{
"id": "uuid-here",
"title": "Started HRT",
"description": "Began hormone therapy today!",
"epochDay": 19000,
"timestamp": 1234567890000
}
]
}
Import Process¶
- Extract ZIP file
- Read
data.json - Parse JSON and validate
- Copy photo files to storage
- Write data to Realm database
- Update settings
Data Validation¶
Photo Validation¶
fun validatePhoto(photo: Photo): Boolean {
return photo.id.isNotEmpty() &&
photo.filename.isNotEmpty() &&
photo.timestamp > 0 &&
photo.type in 0..2
}
Milestone Validation¶
fun validateMilestone(milestone: Milestone): Boolean {
return milestone.id.isNotEmpty() &&
milestone.title.isNotEmpty() &&
milestone.timestamp > 0
}
Data Migration¶
Realm handles schema migrations automatically for simple changes. For complex migrations:
val config = RealmConfiguration.Builder(schema = setOf(Photo::class, Milestone::class))
.schemaVersion(2)
.migration { context ->
// Handle migration from version 1 to 2
}
.build()
Best Practices¶
Always Close Realm¶
Use Transactions for Writes¶
Query on Background Thread¶
Observable.fromCallable {
val realm = Realm.openDefault()
val photos = realm.query<Photo>().find()
realm.close()
photos
}
.subscribeOn(RxSchedulers.io())
.observeOn(RxSchedulers.main())
.subscribe { photos -> updateUI(photos) }
Handle Errors Gracefully¶
try {
val realm = Realm.openDefault()
// database operations
realm.close()
} catch (e: Exception) {
FirebaseCrashlytics.getInstance().recordException(e)
// Show user-friendly error message
}
Next Steps¶
- Learn about Domain Layer
- Understand UI Layer
- Explore Photo Tracking Feature