Share
Embed
Android Summit: Reactive approach to delegation in Kotlin
PRESENTED AT:
Android Summit 2019
https://androidsummit.org/
DATE:
AUGUST 14, 2019
DESCRIPTION:
This talk about delegation and delegated properties in Kotlin and how to incorporate them into your custom views and to make them react immediately to new data. It showcases the usage of delegated properties in custom view and also enhances the mvi pattern when dynamically adding different views based on multiple user interactions
Video of the talk: https://www.youtube.com/watch?v=LCsdj5asf8U&list=PLe82yYlRNTMITPebC4AiWeQxM_OKA3g2t
MORE TALKS & ARTICLES FROM ME: https://cupsofcode.com/talks/
Aida Issayeva
August 14, 2019
Transcript
74 slides
-
@aida_isay Delegation in Java interface DoorActionListener { void closeDoor(); void
openDoor(); } interface WindowActionListener { void openWindow(); void closeWindow(); } @aida_isay -
@aida_isay Delegation in Java class Window implements WindowActionListener { @Override
public void openWindow() { } @Override public void closeWindow() { } } class Door implements DoorActionListener { @Override public void closeDoor() { } @Override public void openDoor() { } } -
@aida_isay Delegation in Java class Room implements WindowActionListener, DoorActionListener {
private Window window = new Window(); private Door door = new Door(); @Override public void closeDoor() { door.closeDoor(); } @Override public void openDoor() { door.openDoor(); } @Override public void openWindow() { window.openWindow(); } @Override public void closeWindow() { window.closeWindow(); } } -
@aida_isay Delegation in Java class LivingRoom implements WindowActionListener { private
Window window = new Window(); @Override public void openWindow() { window.openWindow(); } @Override public void closeWindow() { window.closeWindow(); } } -
@aida_isay Delegation in Kotlin interface DoorActionListener { fun closeDoor() fun
openDoor() } interface WindowActionListener { fun openWindow() fun closeWindow() } open class Window : WindowActionListener { override fun openWindow() {} override fun closeWindow() {} } open class Door : DoorActionListener { override fun closeDoor() {} override fun openDoor() {} } class Room : WindowActionListener by Window(), DoorActionListener by Door() class LivingRoom : WindowActionListener by Window() -
@aida_isay Delegation in Kotlin interface DoorActionListener { fun closeDoor() fun
openDoor() } interface WindowActionListener { fun openWindow() fun closeWindow() } open class Window : WindowActionListener { override fun openWindow() {} override fun closeWindow() {} } open class Door : DoorActionListener { override fun closeDoor() {} override fun openDoor() {} } class Room : WindowActionListener by Window(), DoorActionListener by Door() class LivingRoom : WindowActionListener by Window() -
@aida_isay Implicit delegation Translation public final class Room implements WindowActionListener,
DoorActionListener { // $FF: synthetic field private final Window $$delegate_0 = new Window(); // $FF: synthetic field private final Door $$delegate_1 = new Door(); public void closeWindow() { this.$$delegate_0.closeWindow(); } public void openWindow() { this.$$delegate_0.openWindow(); } public void closeDoor() { this.$$delegate_1.closeDoor(); } public void openDoor() { this.$$delegate_1.openDoor(); } } -
@aida_isay Implicit delegation Translation public final class LivingRoom implements WindowActionListener
{ // $FF: synthetic field private final Window $$delegate_0 = new Window(); public void closeWindow() { this.$$delegate_0.closeWindow(); } public void openWindow() { this.$$delegate_0.openWindow(); } } Tools->Kotlin->Show Kotlin Bytecode -> “Decompile” button -
@aida_isay Delegated properties class User(val name: String, val lastName: String)
// var user: User by UserDelegate() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) println(user.name) user = User("Android", “Summit") println(user.name) } -
@aida_isay Delegated properties //use operator //for var class UserDelegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>){} operator fun setValue(thisRef: Any?, property: KProperty<*>, value: User){} } //for val class UserDelegate { operator fun getValue(thisRef: Any?, property: KProperty<*>){} } //or implement interfaces //for val class UserDelegate : ReadOnlyProperty<R, User> { override fun getValue(thisRef: R, property: KProperty<*>): User {} } //for var class UserDelegate : ReadWriteProperty<R, User> { override fun setValue(thisRef: R, property: KProperty<*>, value: User) {} override fun getValue(thisRef: R, property: KProperty<*>): User {} } -
@aida_isay Lazy delegate class AboutMeFragment : Fragment() { val component
by lazy { val appComponent = DaggerHelper.getAppComponent(this) AboutMeComponent.builder() .appComponent(appComponent) .build() } } -
@aida_isay Lazy delegate class AboutMeFragment : Fragment() { val component
by lazy { val appComponent = DaggerHelper.getAppComponent(this) AboutMeComponent.builder() .appComponent(appComponent) .build() } } -
@aida_isay Lazy delegate class AboutMeFragment : Fragment() { val component
by lazy { val appComponent = DaggerHelper.getAppComponent(this) AboutMeComponent.builder() .appComponent(appComponent) .build() } } -
@aida_isay Lazy delegate class AboutMeFragment : Fragment() { val component
by lazy { val appComponent = DaggerHelper.getAppComponent(this) AboutMeComponent.builder() .appComponent(appComponent) .build() } val viewModel by lazy { val factory = AboutMeViewModel.Factory(component) ViewModelProviders.of(this, factory) .get(AboutMeViewModel::class.java) } } -
@aida_isay lazy •Faster class init •No more useless null checks
•Smart cast and thread sync •Not used property marked by compiler •Declared and init in single place -
@aida_isay notNull delegate class AboutMeFragment : Fragment() { var age:
Int by Delegates.notNull() } import kotlin.properties.Delegates.notNull class AboutMeFragment : Fragment() { var age: Int by notNull() } -
@aida_isay map delegate class Profile(map: Map<String, Any?>) { val firstName:
String by map val lastName: String by map val phoneNumber: String by map val income: Double by map } -
@aida_isay map delegate class Profile(map: Map<String, Any?>) { val firstName:
String by map val lastName: String by map val phoneNumber: String by map val income: Double by map } -
@aida_isay map delegate override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main)
val map = mapOf( "firstName" to "Android", "middleName" to null, "lastName" to "Summit", "phoneNumber" to "212-212-2121", "income" to 100.00 ) val person = Profile(map) println(person.firstName) } -
@aida_isay map •Simplify access •Define expected structure •But have a
power to access unexpected fields •Map key == property name -
@aida_isay Observable delegate class AboutMeFragment : Fragment() { val disposables
= CompositeDisposable() var isLoading by Delegates.observable(false) { p, old, new -> buttonDelete.isEnabled = !new buttonSubmit.isEnabled = !new progressBar.setVisibleOrGone(new) } . . . super.onViewCreated(view, savedInstanceState) buttonSubmit.setOnClickListener { isLoading = true viewModel.updateProfile("Android", "Summit", "[email protected]") .subscribe { isLoading = false } .addTo(disposables) } } } -
@aida_isay Observable delegate class AboutMeFragment : Fragment() { val disposables
= CompositeDisposable() var isLoading by Delegates.observable(false) { p, old, new -> buttonDelete.isEnabled = !new buttonSubmit.isEnabled = !new progressBar.setVisibleOrGone(new) } . . . super.onViewCreated(view, savedInstanceState) buttonSubmit.setOnClickListener { isLoading = true viewModel.updateProfile("Android", "Summit", "[email protected]") .subscribe { isLoading = false } .addTo(disposables) } } } -
@aida_isay Observable delegate class AboutMeFragment : Fragment() { val disposables
= CompositeDisposable() var isLoading by Delegates.observable(false) { p, old, new -> buttonDelete.isEnabled = !new buttonSubmit.isEnabled = !new progressBar.setVisibleOrGone(new) } . . . super.onViewCreated(view, savedInstanceState) buttonSubmit.setOnClickListener { isLoading = true viewModel.updateProfile("Android", "Summit", "[email protected]") .subscribe { isLoading = false } .addTo(disposables) } } } -
@aida_isay Observable delegate class AboutMeFragment : Fragment() { val disposables
= CompositeDisposable() var isLoading by Delegates.observable(false) { p, old, new -> buttonDelete.isEnabled = !new buttonSubmit.isEnabled = !new progressBar.setVisibleOrGone(new) } . . . super.onViewCreated(view, savedInstanceState) buttonSubmit.setOnClickListener { isLoading = true viewModel.updateProfile("Android", "Summit", "[email protected]") .subscribe { isLoading = false } .addTo(disposables) } } } -
@aida_isay Adapters in Java public class NameAdapter extends RecyclerView.Adapter<ViewHolder> {
public void updateNames(ArrayList<String> names){ this.list = names; notifyDataSetChanged(); } } -
@aida_isay Adapters in Kotlin class NameAdapter(val context: Context) : RecyclerView.Adapter<ViewHolder>()
{ var list = emptyList<String>() fun updateNames(names: ArrayList<String>) { list = names notifyDataSetChanged() } } -
@aida_isay Adapters in Kotlin class NameAdapter(val context: Context) : RecyclerView.Adapter<ViewHolder>()
{ var list by observable(mutableListOf<String>()) { _, old, new -> if (old != new) notifyDataSetChanged() } } -
@aida_isay Observable •Lambda called after new value is set •Have
an access to old value •Notify ui immediately about changes in data •Advantage of partial ui update -
@aida_isay Adapters in Java public class Adapter extends RecyclerView.Adapter<ViewHolder> {
public void updateNames(ArrayList<String> names){ if(names.size() <= 5) { this.list = names; notifyDataSetChanged(); } } } -
@aida_isay Adapters in Kotlin class NameAdapter(val context: Context) : RecyclerView.Adapter<ViewHolder>()
{ var list by Delegates.vetoable<List<String>>(mutableListOf()) { _, old, new -> new.size <= 5 } } -
@aida_isay Vetoable delegate class NameAdapter(val context: Context) : RecyclerView.Adapter<ViewHolder>() {
var list by Delegates.vetoable<List<String>>(mutableListOf()) { _, old, new -> if (new.size <= 5) { notifyDataSetChanged() return@vetoable true } return@vetoable false } } -
@aida_isay Vetoable delegate class MainActivity : Activity() { var firstName:
String by Delegates.vetoable("") { prop, old, new -> when (new.length) { 0 -> hideValidation() in 2..20 -> showValidData(new) else -> showError() } new.startsWith("a") } } -
@aida_isay Vetoable •Lambda is called before new value set •To
set or not to set •Have an access to old value •Notify ui immediately about changes in data •Advantage of partial ui update -
@aida_isay override fun onCreate(savedInstanceState: Bundle?) { . . . buttonLike.setOnClickListener
{ likePost() } buttonSend.setOnClickListener { sendComment() } editText.addTextChangedListener(object : TextWatcher { . . . override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { when (count) { 0 -> { buttonLike.visibility = View.VISIBLE buttonSend.visibility = View.GONE } 1 -> { buttonLike.visibility = View.GONE buttonSend.visibility = View.VISIBLE } } } }) } -
@aida_isay editText.addTextChangedListener(object : TextWatcher { . . . override fun
onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { when (count) { 0 -> { isInputEmpty = true buttonLike.setBackgroundResource(R.drawable.ic_favorite_gray_24dp) } 1 -> { isInputEmpty = false buttonLike.setBackgroundResource(R.drawable.ic_favorite_gray_24dp) } } } }) buttonLike.setOnClickListener { if (isInputEmpty) likePost() else sendComment() } -
@aida_isay class SendLikeButton(context: Context) : Button(context) { private var sendToLike:
AnimatedVectorDrawableCompat? = null private var likeToSend: AnimatedVectorDrawableCompat? = null private var showLike: Boolean = false init { showLike = true likeToSend = AnimatedVectorDrawableCompat.create(context, R.drawable.avd_favorite_to_send) sendToLike = AnimatedVectorDrawableCompat.create(context, R.drawable.avd_send_to_favorite) background = likeToSend } . . . } -
@aida_isay class SendLikeButton(context: Context) : Button(context) { . . .
private var showLike: Boolean = false . . . fun showLike() { if (!showLike) { morph() } } fun showSend() { if (showLike) { morph() } } . . . } -
@aida_isay class SendLikeButton(context: Context) : Button(context) { private var sendToLike:
AnimatedVectorDrawableCompat? = null private var likeToSend: AnimatedVectorDrawableCompat? = null private var showLike: Boolean = false . . . fun showLike() { if (!showLike) { morph() } } fun showSend() { if (showLike) { morph() } } private fun morph() { val drawable = if (showLike) likeToSend else sendToLike background = drawable drawable?.start() showLike = !showLike } } -
@aida_isay editText.addTextChangedListener(object : TextWatcher { . . . override fun
onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { when (count) { 0 -> buttonLike.showLike() 1 -> buttonLike.showSend() } } }) -
@aida_isay buttonLike.setOnClickListener { TODO("????") } editText.addTextChangedListener(object : TextWatcher { .
. . override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { when (count) { 0 -> button_like.showLike() 1 -> button_like.showSend() } } }) -
@aida_isay class SendLikeButton(context: Context) : Button(context) { private var sendToLike:
AnimatedVectorDrawableCompat? = null private var likeToSend: AnimatedVectorDrawableCompat? = null private var showLike: Boolean = false init { showLike = true . . . } . . . } -
@aida_isay class SendLikeButton(context: Context) : Button(context) { private var sendToLike:
AnimatedVectorDrawableCompat? = null private var likeToSend: AnimatedVectorDrawableCompat? = null var showLike: Boolean = false init { showLike = true . . . } . . . } -
@aida_isay buttonLike.setOnClickListener { if (buttonLike.showLike) likePost() else sendComment() } editText.addTextChangedListener(object
: TextWatcher { . . . override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { when (count) { 0 -> buttonLike.showLike() 1 -> buttonLike.showSend() } } }) -
@aida_isay class SendLikeButton(context: Context) : Button(context) { private var sendToLike:
AnimatedVectorDrawableCompat? = null private var likeToSend: AnimatedVectorDrawableCompat? = null var showLike: Boolean = false var isLiked: Boolean = false fun setLikedView(isLiked: Boolean) { this.isLiked = isLiked if (isLiked) { likeToSend = AnimatedVectorDrawableCompat.create(context, R.drawable.avd_gray_favorite_to_send) sendToLike = AnimatedVectorDrawableCompat.create(context, R.drawable.avd_send_to_gray_favorite) } } } -
@aida_isay override fun onCreate(savedInstanceState: Bundle?) { . . . getPost()
.subscribe({ buttonLike.setLikedView(it) }, {}).addTo(disposable) } -
@aida_isay override fun onCreate(savedInstanceState: Bundle?) { . . . buttonLike.setOnClickListener
{ when (buttonLike.isLiked && buttonLike.showLike) { true -> unlikePost() else -> if (buttonLike.showLike) likePost() else sendComment() } } } -
@aida_isay sealed class ButtonState { object ShowLiked : ButtonState() object
ShowUnLiked : ButtonState() object SendComment : ButtonState() } -
@aida_isay class SendLikeButton(context: Context) : Button(context) { private val sendToLike
by bindAnimation(R.drawable.avd_send_to_favorite) private val likeToSend by bindAnimation(R.drawable.avd_favorite_to_send) private val unlikeToSend by bindAnimation(R.drawable.avd_gray_favorite_to_send) private val sendToUnlike by bindAnimation(R.drawable.avd_send_to_gray_favorite) } -
@aida_isay class SendLikeButton(context: Context) : Button(context) { private val sendToLike
by bindAnimation(R.drawable.avd_send_to_favorite) private val likeToSend by bindAnimation(R.drawable.avd_favorite_to_send) private val unlikeToSend by bindAnimation(R.drawable.avd_gray_favorite_to_send) private val sendToUnlike by bindAnimation(R.drawable.avd_send_to_gray_favorite) } fun View.bindAnimation(animateRes: Int) = lazy { AnimatedVectorDrawableCompat.create(context, animateRes) } -
@aida_isay class SendLikeButton(context: Context) : Button(context) { var state by
Delegates.observable<ButtonState>(ButtonState.ShowUnLiked) { p, oldValue, newValue -> when (newValue) { ButtonState.SendComment -> { when (oldValue) { ButtonState.ShowUnLiked -> morph(unlikeToSend) ButtonState.ShowLiked -> morph(likeToSend) } } . . . } } } -
@aida_isay class SendLikeButton(context: Context) : Button(context) { var state by
Delegates.observable<ButtonState>(ButtonState.ShowUnLiked) { p, oldValue, newValue -> when (newValue) { ButtonState.SendComment -> { when (oldValue) { ButtonState.ShowUnLiked -> morph(unlikeToSend) ButtonState.ShowLiked -> morph(likeToSend) } } ButtonState.ShowLiked -> { when (oldValue) { ButtonState.ShowUnLiked -> setBackgroundResource(redLike) ButtonState.SendComment -> morph(sendToLike) } } ButtonState.ShowUnLiked -> { when (oldValue) { ButtonState.SendComment -> morph(sendToUnlike) ButtonState.ShowLiked, ButtonState.ShowUnLiked -> setBackgroundResource(grayLike) } } } } -
@aida_isay override fun onCreate(savedInstanceState: Bundle?) { . . . RxView.clicks(buttonLike)
.subscribe({ when (buttonLike.state) { ButtonState.SendComment -> sendComment() ButtonState.ShowLiked -> unlikePost() ButtonState.ShowUnLiked -> likePost() } }, {}).addTo(disposable) val textChanged = RxTextView.textChanges(editText) Observable.combineLatest(apiResponseLikeStatus, textChanged, BiFunction<Boolean, CharSequence, ButtonState> { t1, t2 -> return@BiFunction when (t2.count() > 0) { true -> ButtonState.SendComment else -> if (t1) ButtonState.ShowLiked else ButtonState.ShowUnLiked } }) .subscribe({ buttonLike.state = it }, {}).addTo(disposable) } -
@aida_isay override fun onCreate(savedInstanceState: Bundle?) { . . . RxView.clicks(buttonLike)
.subscribe({ when (buttonLike.state) { ButtonState.SendComment -> sendComment() ButtonState.ShowLiked -> unlikePost() ButtonState.ShowUnLiked -> likePost() } }, {}).addTo(disposable) val textChanged = RxTextView.textChanges(editText) Observable.combineLatest(apiResponseLikeStatus, textChanged, BiFunction<Boolean, CharSequence, ButtonState> { t1, t2 -> return@BiFunction when (t2.count() > 0) { true -> ButtonState.SendComment else -> if (t1) ButtonState.ShowLiked else ButtonState.ShowUnLiked } }) .subscribe({ buttonLike.state = it }, {}).addTo(disposable) } -
@aida_isay class SendLikeButton(context: Context) : Button(context) { var state by
Delegates.observable<ButtonState>(ButtonState.ShowUnLiked) { p, oldValue, newValue -> when (newValue) { ButtonState.SendComment -> { when (oldValue) { ButtonState.ShowUnLiked -> morph(unlikeToSend) ButtonState.ShowLiked -> morph(likeToSend) } } ButtonState.ShowLiked -> { when (oldValue) { ButtonState.ShowUnLiked -> setBackgroundResource(redLike) ButtonState.SendComment -> morph(sendToLike) } } ButtonState.ShowUnLiked -> { when (oldValue) { ButtonState.SendComment -> morph(sendToUnlike) ButtonState.ShowLiked, ButtonState.ShowUnLiked -> setBackgroundResource(grayLike) } } } } private val sendToLike by bindAnimation(R.drawable.avd_send_to_favorite) private val likeToSend by bindAnimation(R.drawable.avd_favorite_to_send) private val unlikeToSend by bindAnimation(R.drawable.avd_gray_favorite_to_send) private val sendToUnlike by bindAnimation(R.drawable.avd_send_to_gray_favorite) private val redLike by lazy { R.drawable.ic_favorite_red_24dp } private val grayLike by lazy { R.drawable.ic_favorite_gray_24dp } private fun morph(avd: AnimatedVectorDrawableCompat?) { background = avd avd?.start() } } fun View.bindAnimation(animateRes: Int) = lazy { AnimatedVectorDrawableCompat.create(context, animateRes) } sealed class ButtonState { object ShowLiked : ButtonState() object ShowUnLiked : ButtonState() object SendComment : ButtonState() } -
@aida_isay data class UiModel( val loading: Boolean, val firstName: String,
val employmentStatus: String, . . . val shouldAnimateView1: Boolean val shouldAnimateView2: Boolean ) -
@aida_isay private val reducer = BiFunction<UiModel, AboutMeIntent, UiModel> { previous,
intent -> when (intent) { is ContinueClicked -> { previous.copy( ... shouldAnimateView1 = false, ) } is AddressChosen -> { previous.copy( ... shouldAnimateView1 = false ) } is ProfileInfo -> { previous.copy( ... shouldAnimateView1 = true, ) } is OccupationChosen -> { previous.copy( ... shouldAnimateView2 = true, shouldAnimateView1 = false ) } is EmploymentStatusChosen -> { previous.copy( ... shouldAnimateView1 = true, ) } is DateOfBirthClicked, is StateClicked -> previous.copy( ... shouldAnimateView1 = false ) is DialogDismissed -> previous.copy( ... shouldAnimateView1 = false ) is ButtonsState -> previous.copy( ... shouldAnimateView1 = false, shouldAnimateView2 = false, ) else -> previous.copy() } } -
@aida_isay override fun onViewCreated(view: View,savedInstanceState: Bundle?) { . . .
val firstName = RxTextView.textChanges(et_firstName.editText) val middleName = RxTextView.textChanges(et_middleName.editText) val lastName = RxTextView.textChanges(et_lastName.editText) val phoneNumber = RxTextView.textChanges(et_phoneNumber.editText) val employmentStatus = RxTextView.textChanges(et_es.editText) val combinedEvents = Observable.combineLatest( firstName, middleName, lastName, phoneNumber, employmentStatus, Function5<CharSequence, CharSequence, CharSequence, CharSequence, CharSequence, AboutMeIntent> { t1, t2, t3, t4, t5 -> TextChanged(t1, t2, t3, t4, t5) }) .skip(1) . . . } -
@aida_isay override fun onViewCreated(view: View,savedInstanceState: Bundle?) { . . .
viewModel.apply { bind(Observable.merge(combinedEvents, intents.hide())) } viewModel.state.observe(this, Observer { if (it != null) render(it) }) } private fun render(viewState: UiModel) { et_firstName.text = viewState.firstName et_middleName.text = viewState.middleName et_lastName.text = viewState.lastName et_email.text = viewState.email if (viewState.shouldAnimateView1) { et_es.editText = viewState.employmentStatus . . . } if (viewState.shouldAnimateView2) { addAnimatedViews(viewstate.occupation) . . . } . . . } -
@aida_isay override fun onViewCreated(view: View,savedInstanceState: Bundle?) { . . .
var uiModel by Delegates.observable(viewModel.initialStateCallable.call()) {_, old, new -> if(new.firstName != old.firstName){ et_firstName.text = new.firstName } if(old.employmentStatus.isEmpty() && new.employmentStatus.isNotEmpty()){ addAnimatedViews(new.occupation) } . . . } viewModel.state.observe(this, Observer { if (it != null) uiModel = it }) }