Вступление
Складывается такое впечатление, что дописать своё приложение для Android мне суждено не скоро. Каждый раз начиная писать новую версию (так как старая была написана не до конца, использовалась только мной, а через пару-тройку лет простоя проще написать заново) своего приложения, задуманного еще в 2012 году, я сталкиваюсь с ситуацией, что мне не хватает какого-то функционала и начинаю писать свои библиотеки для этого. В первую попытку это был свой ORM (UcaOrm 1, 2, 3). Во вторую KCron — KMP библиотека, реализующая Cron. И вот, начав следующую итерацию, я вновь в таком же положении. Но обо всем по порядку!
С чего всё началось
В этот раз для разработки я выбрал Compose UI. Первым же делом я начал изучать, как все это сочетается с Room. Наткнулся на этот tutorial, и по началу все шло хорошо, до шага 9. Увидев такой код:
/** * Extension function to convert [ItemDetails] to [Item]. If the value of [ItemDetails.price] is * not a valid [Double], then the price will be set to 0.0. Similarly if the value of * [ItemDetails.quantity] is not a valid [Int], then the quantity will be set to 0 */ fun ItemDetails.toItem(): Item = Item( id = id, name = name, price = price.toDoubleOrNull() ?: 0.0, quantity = quantity.toIntOrNull() ?: 0 ) /** * Extension function to convert [Item] to [ItemUiState] */ fun Item.toItemUiState(isEntryValid: Boolean = false): ItemUiState = ItemUiState( itemDetails = this.toItemDetails(), isEntryValid = isEntryValid ) /** * Extension function to convert [Item] to [ItemDetails] */ fun Item.toItemDetails(): ItemDetails = ItemDetails( id = id, name = name, price = price.toString(), quantity = quantity.toString() )
мой глаз немного задергался и, имея огромный опыт работы с AutoMapper‘ом, я задался вопросом: «Почему до сих пор нет альтернативы для KMP?«. А раз её нет, надо написать!
От идеи до начала реализации
Основной минус AutoMapper’а всем известен — это runtime. Чтобы не допустить проблем во время выполнения, надо тратить дополнительные силы на покрытие тестами. Однако, я очень часто встречал и другие проблемы его использования и даже написал AutoMapper.Analyzers.
Дабы избежать таких же проблем в своей новой библиотеке, я выбрал генерацию кода. Для KMP выбор невелик: либо устаревший KAPT, либо новый KSP. Конечно писать новую библиотеку на чем-то старом никакого смысла нет, и я принялся изучать KSP.
Он оказался не таким и сложным, и после пары опытов и формирования основных идей моей библиотеки, а так же, потратив некоторое время на придумывание названия, я создал новый repository class SourceObject { val id = 150 val intToString = 300 val stringToInt = "250" }
И класс-приёмник: Чтобы отобразить одно в другое, достаточно пометить класс-приёмник аннотацией KOMMMap: В результате будет сгенерирован вот такой метод-расширение: Как можно заметить @KOMMMap( from = SourceObject::class, config = MapConfiguration( tryAutoCast = false ) )
В результате, при попытки отображения свойств без использования конвертера (о нем ниже), будет брошено исключение. Конечно часто надо отображать свойства с одними именами в свойства с другими. Для этого в KOMM есть MapFrom аннотация: Важное замечание! KOMM поддерживает multimapping — когда в один класс можно отображать из нескольких. В этом случае для аннотаций полей можно указывать, для какого именно класса-источника применять кастомизацию: Для остальных будут применяться настройки по умолчанию. Для особого конвертирования можно использовать конвертер. Например: У конвертеров в качестве указателя, для какого именно источника применить аннотацию, выступает generic тип. В результате, будет сгенерирован такой метод: Как видно, весь объект-источник передается как свойство класса конвертера, а в метод конвертации передается еще и свойство-источник. В случае, если свойство, объявленное через конструктор, не может быть получено из объекта-источника, его нужно пометить аннотацией с resolver’ом. Аннотацию можно применять и к свойствам, объявленным вне конструктора: На выходе будет: Обратите внимание, что в качестве параметра конструктора resolver’а мы получаем либо null — если отображаемое свойство объявлено в конструкторе, либо же уже существующий объект-приёмник. Так же поддерживается null substitute: Есть еще несколько настроек и features (как поддержка Java объектов с их get-методами, например), но статья и так уже вышла очень длинной, так что о них можно почитать в README. В планах еще несколько улучшений и доработок. Еще есть мысли на обратное отображение, вроде как MapTo, но тут начинается проблема с Java объектами. Так что пока хорошего решения я не придумал. Так же в скором будущем библиотека будет доступна через Maven Central репозиторий. Буду благодарен за любые идеи по развитию библиотеки или contributing‘а.data class DestinationObject( val id: Int, val stringToInt: Int ) { var intToString: String = "" }@KOMMMap(from = SourceObject::class) data class DestinationObject( val id: Int, val stringToInt: Int ) { var intToString: String = "" }fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject( id = id, stringToInt = stringToInt.toInt() ).also { it.intToString = intToString.toString() }Feature of KOMM
class SourceObject { //... val userName = "user" } @KOMMMap(from = SourceObject::class) data class DestinationObject( //... @MapFrom("userName") val name: String )@KOMMMap( from = FirstSourceObject::class ) @KOMMMap( from = SecondSourceObject::class ) data class DestinationObject( @MapFrom("userId", [SecondSourceObject::class]) val id: Int )class CostConverter(source: SourceObject) : KOMMConverter<SourceObject, Double, String>(source) { override fun convert(sourceMember: Double) = "$sourceMember ${source.currency}" } class SourceObject { val cost = 499.99 } @KOMMMap(from = SourceObject::class) data class DestinationObject( @MapConvert<SourceObject, CostConverter>(CostConverter::class) val cost: String ) { @MapConvert<SourceObject, CostConverter>(CostConverter::class, "cost") var otherCost: String = "" }fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject( cost = CostConverter(this).convert(cost) ).also { it.otherCost = CostConverter(this).convert(cost) }class DateResolver(destination: DestinationObject?) : KOMMResolver<DestinationObject, Date>(destination) { override fun resolve(): Date = Date.from(Instant.now()) } @KOMMMap(from = SourceObject::class) data class DestinationObject( @MapDefault<DateResolver>(DateResolver::class) val activeDate: Date ) { @MapDefault<DateResolver>(DateResolver::class) var otherDate: Date = Date.from(Instant.now()) }fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject( activeDate = DateResolver(null).resolve() ).also { it.otherDate = DateResolver(it).resolve() }class IntResolver(destination: DestinationObject?): KOMMResolver<DestinationObject, Int>(destination) { override fun resolve() = 1 } data class SourceObject( val id: Int? ) @KOMMMap( from = SourceObject::class ) data class DestinationObject( @NullSubatitute(MapDefault(IntResolver::class)) val id: Int ) { @NullSubatitute(MapDefault(IntResolver::class), "id") var otherId: Int = 0 } //... fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject( id = id ?: IntResolver(null).resolve() ).also { it.otherId = id ?: IntResolver(it).resolve() }Что дальше?
ссылка на оригинал статьи https://habr.com/ru/articles/792958/
Добавить комментарий