feat: implement Android data models in Kotlin

- Add FeedItem, Feed, FeedSubscription models with Moshi JSON support
- Add SearchResult, SearchFilters models with sealed classes for enums
- Add NotificationPreferences, ReadingPreferences models
- Add Room Entity annotations for database readiness
- Add TypeConverters for Date and List<String> serialization
- Add Parcelize for passing models between Activities/Fragments
- Write comprehensive unit tests for serialization/deserialization
- Write tests for copy(), equals/hashCode, and toString functionality
This commit is contained in:
2026-03-29 15:40:38 -04:00
parent fade9fd5b1
commit fdd4fd8a46
18 changed files with 1540 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
package com.rssuper.converters
import androidx.room.TypeConverter
import java.util.Date
class DateConverter {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}

View File

@@ -0,0 +1,23 @@
package com.rssuper.converters
import androidx.room.TypeConverter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.rssuper.models.FeedItem
class FeedItemListConverter {
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val adapter = moshi.adapter(List::class.java)
@TypeConverter
fun fromFeedItemList(value: List<FeedItem>?): String? {
return value?.let { adapter.toJson(it) }
}
@TypeConverter
fun toFeedItemList(value: String?): List<FeedItem>? {
return value?.let { adapter.fromJson(it) }
}
}

View File

@@ -0,0 +1,15 @@
package com.rssuper.converters
import androidx.room.TypeConverter
class StringListConverter {
@TypeConverter
fun fromStringList(value: List<String>?): String? {
return value?.joinToString(",")
}
@TypeConverter
fun toStringList(value: String?): List<String>? {
return value?.split(",")?.filter { it.isNotEmpty() }
}
}

View File

@@ -0,0 +1,60 @@
package com.rssuper.models
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.rssuper.converters.DateConverter
import com.rssuper.converters.FeedItemListConverter
import kotlinx.parcelize.Parcelize
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.util.Date
@JsonClass(generateAdapter = true)
@Parcelize
@TypeConverters(DateConverter::class, FeedItemListConverter::class)
@Entity(tableName = "feeds")
data class Feed(
@PrimaryKey
val id: String,
@Json(name = "title")
val title: String,
@Json(name = "link")
val link: String? = null,
@Json(name = "description")
val description: String? = null,
@Json(name = "subtitle")
val subtitle: String? = null,
@Json(name = "language")
val language: String? = null,
@Json(name = "lastBuildDate")
val lastBuildDate: Date? = null,
@Json(name = "updated")
val updated: Date? = null,
@Json(name = "generator")
val generator: String? = null,
@Json(name = "ttl")
val ttl: Int? = null,
@Json(name = "items")
val items: List<FeedItem> = emptyList(),
@Json(name = "rawUrl")
val rawUrl: String,
@Json(name = "lastFetchedAt")
val lastFetchedAt: Date? = null,
@Json(name = "nextFetchAt")
val nextFetchAt: Date? = null
) : Parcelable

View File

@@ -0,0 +1,67 @@
package com.rssuper.models
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.rssuper.converters.DateConverter
import com.rssuper.converters.StringListConverter
import kotlinx.parcelize.Parcelize
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.util.Date
@JsonClass(generateAdapter = true)
@Parcelize
@TypeConverters(DateConverter::class, StringListConverter::class)
@Entity(tableName = "feed_items")
data class FeedItem(
@PrimaryKey
val id: String,
@Json(name = "title")
val title: String,
@Json(name = "link")
val link: String? = null,
@Json(name = "description")
val description: String? = null,
@Json(name = "content")
val content: String? = null,
@Json(name = "author")
val author: String? = null,
@Json(name = "published")
val published: Date? = null,
@Json(name = "updated")
val updated: Date? = null,
@Json(name = "categories")
val categories: List<String>? = null,
@Json(name = "enclosure")
val enclosure: Enclosure? = null,
@Json(name = "guid")
val guid: String? = null,
@Json(name = "subscriptionTitle")
val subscriptionTitle: String? = null
) : Parcelable
@JsonClass(generateAdapter = true)
@Parcelize
data class Enclosure(
@Json(name = "url")
val url: String,
@Json(name = "type")
val type: String,
@Json(name = "length")
val length: Long? = null
) : Parcelable

View File

@@ -0,0 +1,63 @@
package com.rssuper.models
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.rssuper.converters.DateConverter
import kotlinx.parcelize.Parcelize
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.util.Date
@JsonClass(generateAdapter = true)
@Parcelize
@TypeConverters(DateConverter::class)
@Entity(tableName = "feed_subscriptions")
data class FeedSubscription(
@PrimaryKey
val id: String,
@Json(name = "url")
val url: String,
@Json(name = "title")
val title: String,
@Json(name = "category")
val category: String? = null,
@Json(name = "enabled")
val enabled: Boolean = true,
@Json(name = "fetchInterval")
val fetchInterval: Long,
@Json(name = "createdAt")
val createdAt: Date,
@Json(name = "updatedAt")
val updatedAt: Date,
@Json(name = "lastFetchedAt")
val lastFetchedAt: Date? = null,
@Json(name = "nextFetchAt")
val nextFetchAt: Date? = null,
@Json(name = "error")
val error: String? = null,
@Json(name = "httpAuth")
val httpAuth: HttpAuth? = null
) : Parcelable
@JsonClass(generateAdapter = true)
@Parcelize
data class HttpAuth(
@Json(name = "username")
val username: String,
@Json(name = "password")
val password: String
) : Parcelable

View File

@@ -0,0 +1,34 @@
package com.rssuper.models
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
@Parcelize
@Entity(tableName = "notification_preferences")
data class NotificationPreferences(
@PrimaryKey
val id: String = "default",
@Json(name = "newArticles")
val newArticles: Boolean = true,
@Json(name = "episodeReleases")
val episodeReleases: Boolean = true,
@Json(name = "customAlerts")
val customAlerts: Boolean = false,
@Json(name = "badgeCount")
val badgeCount: Boolean = true,
@Json(name = "sound")
val sound: Boolean = true,
@Json(name = "vibration")
val vibration: Boolean = true
) : Parcelable

View File

@@ -0,0 +1,59 @@
package com.rssuper.models
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
@Parcelize
@Entity(tableName = "reading_preferences")
data class ReadingPreferences(
@PrimaryKey
val id: String = "default",
@Json(name = "fontSize")
val fontSize: FontSize = FontSize.MEDIUM,
@Json(name = "lineHeight")
val lineHeight: LineHeight = LineHeight.NORMAL,
@Json(name = "showTableOfContents")
val showTableOfContents: Boolean = false,
@Json(name = "showReadingTime")
val showReadingTime: Boolean = true,
@Json(name = "showAuthor")
val showAuthor: Boolean = true,
@Json(name = "showDate")
val showDate: Boolean = true
) : Parcelable
sealed class FontSize(val value: String) {
@Json(name = "small")
data object SMALL : FontSize("small")
@Json(name = "medium")
data object MEDIUM : FontSize("medium")
@Json(name = "large")
data object LARGE : FontSize("large")
@Json(name = "xlarge")
data object XLARGE : FontSize("xlarge")
}
sealed class LineHeight(val value: String) {
@Json(name = "normal")
data object NORMAL : LineHeight("normal")
@Json(name = "relaxed")
data object RELAXED : LineHeight("relaxed")
@Json(name = "loose")
data object LOOSE : LineHeight("loose")
}

View File

@@ -0,0 +1,73 @@
package com.rssuper.models
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.rssuper.converters.DateConverter
import com.rssuper.converters.StringListConverter
import kotlinx.parcelize.Parcelize
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.util.Date
@JsonClass(generateAdapter = true)
@Parcelize
@TypeConverters(DateConverter::class, StringListConverter::class)
@Entity(tableName = "search_filters")
data class SearchFilters(
@PrimaryKey
val id: String = "default",
@Json(name = "dateFrom")
val dateFrom: Date? = null,
@Json(name = "dateTo")
val dateTo: Date? = null,
@Json(name = "feedIds")
val feedIds: List<String>? = null,
@Json(name = "authors")
val authors: List<String>? = null,
@Json(name = "contentType")
val contentType: ContentType? = null,
@Json(name = "sortOption")
val sortOption: SearchSortOption = SearchSortOption.RELEVANCE
) : Parcelable
sealed class ContentType(val value: String) {
@Json(name = "article")
data object ARTICLE : ContentType("article")
@Json(name = "audio")
data object AUDIO : ContentType("audio")
@Json(name = "video")
data object VIDEO : ContentType("video")
}
sealed class SearchSortOption(val value: String) {
@Json(name = "relevance")
data object RELEVANCE : SearchSortOption("relevance")
@Json(name = "date_desc")
data object DATE_DESC : SearchSortOption("date_desc")
@Json(name = "date_asc")
data object DATE_ASC : SearchSortOption("date_asc")
@Json(name = "title_asc")
data object TITLE_ASC : SearchSortOption("title_asc")
@Json(name = "title_desc")
data object TITLE_DESC : SearchSortOption("title_desc")
@Json(name = "feed_asc")
data object FEED_ASC : SearchSortOption("feed_asc")
@Json(name = "feed_desc")
data object FEED_DESC : SearchSortOption("feed_desc")
}

View File

@@ -0,0 +1,49 @@
package com.rssuper.models
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.rssuper.converters.DateConverter
import kotlinx.parcelize.Parcelize
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.util.Date
@JsonClass(generateAdapter = true)
@Parcelize
@TypeConverters(DateConverter::class)
@Entity(tableName = "search_results")
data class SearchResult(
@PrimaryKey
val id: String,
@Json(name = "type")
val type: SearchResultType,
@Json(name = "title")
val title: String,
@Json(name = "snippet")
val snippet: String? = null,
@Json(name = "link")
val link: String? = null,
@Json(name = "feedTitle")
val feedTitle: String? = null,
@Json(name = "published")
val published: Date? = null,
@Json(name = "score")
val score: Double? = null
) : Parcelable
enum class SearchResultType {
@Json(name = "article")
ARTICLE,
@Json(name = "feed")
FEED
}