Novalles
Library to simplify and speed up the creation and work with adapters with payload.
How to use
- Annotate your UI model with UIModel Annotation.
@UIModel
data class PictureUIModel(
@PrimaryTag val tag: String,
val image: Int,
@Decompose val line: ColorPair,
@NonUIProperty val imageCode: String,
val title: String,
val desc: String,
val likes: Int
) : BaseUiModel
- You can use PrimaryTag annotation to define property to be used in are items the same comparison.
- Use NonUIProperty annotation to define property that will not be used in any comparisons.
- Use Decompose annotation to tell Novalles compare each property of decomposed value separately. See Decompose section for more details.
- Pass an instance of UIModelHelper in your DiffUtil using provideUiInterfaceFor or provideUiInterfaceForAs functions.
private val uiModelHelper: UIModelHelper<BaseUiModel> = Novalles.provideUiInterfaceForAs(PictureUIModel::class)
- Call relevant functions of UIModelHelper in your DiffUtil. This example uses diffUtil based on common interface.
class DefaultDiffUtil<T : BaseUiModel>(uiModel: KClass<T>) : DiffUtil.ItemCallback<BaseUiModel>() {
private val uiModelHelper: UIModelHelper<BaseUiModel> = Novalles.provideUiInterfaceForAs(uiModel)
override fun areItemsTheSame(oldItem: BaseUiModel, newItem: BaseUiModel): Boolean {
return oldItem.areItemsTheSame(newItem, uiModelHelper)
}
override fun areContentsTheSame(oldItem: BaseUiModel, newItem: BaseUiModel): Boolean {
return oldItem.areContentTheSame(newItem, uiModelHelper)
}
override fun getChangePayload(oldItem: BaseUiModel, newItem: BaseUiModel): Any {
return oldItem.changePayload(newItem, uiModelHelper)
}
}
- Create a class, that extends Instructor interface. Annotate it with Instruction annotation, pass your UI Model class as the annotation argument. You can also annotate it with AutoBindViewHolder (See corresponding section for more details). In this class you should create functions, that will be called on a value change. Use BindOn annotation to tell Novalles which function should be called when a value has been changed, value name should be passed as the first annotation argument.
@Instruction(PictureUIModel::class)
@AutoBindViewHolder(PictureViewHolder::class)
inner class PictureInstructor(
private val viewHolder: PictureViewHolder,
private val uiModel: PictureUIModel
) : Instructor {
//This function will be called, when title changed.
@BindOn("title")
fun setTitleComplex(title: String) {
val realDesc = "<b>$title</b> (${uiModel.tag})"
viewHolder.setTitle(realDesc)
}
}
If you completely rely on AutoBindViewHolder, you should create the simplest Instructor for your UI Model.
@Instruction(PictureUIModel::class)
@AutoBindViewHolder(PictureViewHolder::class)
inner class AutoInstructor : Instructor
- Create an instance of the Inspector class using Novalles.provideInspectorFromInstructor(instructor: Instructor) function. Better to create it outside any function, create it directly in the adapter itself.
private val inspector = Novalles.provideInspectorFromInstructor(PictureInstructor::class)
- Invoke Inspector.inspectPayloads with 4 arguments: your payload, instructor, your viewHolder and lambda for action when payload is empty. This function calls corresponding functions in your viewHolder and instructor based on gathered payload.
override fun onBindViewHolder(
holder: PictureViewHolder,
position: Int,
payloads: MutableList<Any>
) {
val model = currentList[position] as PictureUIModel
val instructor = PictureInstructor(
viewHolder = holder,
model
)
inspector.inspectPayloads(payloads, instructor, viewHolder = holder) {
holder.bind(model)
holder.setOnClickActions(model, onClick)
}
}
AutoBindViewHolder
Class annotated with it is considered to be the instruction how to handle payloads for UI Model. It should also implement Instructor interface. Your functions should be names as set{PropertyName}.
@UIModel
data class PictureUIModel(
@PrimaryTag val tag: String,
//...
val desc: String,
//...
) : BaseUiModel {
//...
inner class PictureViewHolder(private val binding: ItemPictureBinding) : ViewHolder(binding.root) {
//...
//This function will be called, if desc changes.
fun setDesc(desc: String) {
binding.desc.text = desc
}
//...
}
}
Decompose annotations
Value, annotated with Decompose will be decomposed with its own values. For example, if your field have 2 properties, they will be used in any Novalles' actions separately: Novalles will generate 2 different payloads objects in UIModelHelper.changePayloads, compare them in UIModelHelper.areContentsTheSame separately.
Also, if you use AutoBindViewHolder, you should use set${FieldName}In${DecomposedFieldName}() functions in your viewHolder for each field of your decomposed value.
data class ColorPair(
val left: Int,
val right: Int
)
@UIModel
data class PictureUIModel(
@PrimaryTag val tag: String,
//...
@Decompose val line: ColorPair,
//...
) : BaseUiModel {
//...
inner class PictureViewHolder(private val binding: ItemPictureBinding) : ViewHolder(binding.root) {
//...
//This function will be called, if left in line value changes.
fun setLeftInLine(color: Int) {
binding.colour.animateColors(color)
}
//This function will be called, if right in line value changes.
fun setRightInLine(color: Int) {
binding.colourSecond.animateColors(color)
}
//...
}
}
Conclusions and usage
- To maximize benefit of the Novalles, you can animate your views inside your set functions in a viewHolder.
fun setImage(image: Int) {
binding.image.animateColors(image)
}
- Do not create any Novalles instances in functions, better to create an instance of UiModelHelper in your DiffUtil and an instance of Inspector in your adapter.
- Novalles does not use android components directly, so it does not have any SDK restrictions. This can be changed in the future, because Novalles uses Any instead of ViewHolder in its interfaces. This may lead to misunderstandings of functions usage.
- Novalles does not require any proguard rules and should work normally in release builds if your Ui Models and adapters work normally.
- Decompose annotation should be used careful, because it is very lightly tested.
- Please, report any issues you've encountered. Novalles is still in development, so your feedback is very helpful.
How to integrate in your project
- Add KSP dependencies in your top level gradle file.
buildscript {
dependencies {
classpath("com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:1.6.21-1.0.5")
}
}
- Add KSP plugin in your app level gradle file.
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
//...
id 'com.google.devtools.ksp'
}
- Import the Novalles library as dependency.
allprojects {
repositories {
//...
maven { url 'https://jitpack.io' }
}
}
//...
dependencies {
//...
implementation 'com.github.flexeiprata:novalles:0.4.0'
ksp 'com.github.flexeiprata:novalles:0.4.0'
//...
}
License
Copyright (C) 2022 FlexeiPrata.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.