Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android Summit: Reactive approach to delegation in Kotlin

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
Tweet

More Decks by Aida Issayeva

Other Decks in Programming

Transcript

  1. @aida_isay Delegation in Java interface DoorActionListener { void closeDoor(); void

    openDoor(); } interface WindowActionListener { void openWindow(); void closeWindow(); } @aida_isay
  2. @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() { } }
  3. @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(); } }
  4. @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(); } }
  5. @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()
  6. @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()
  7. @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(); } }
  8. @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
  9. @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) }
  10. @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 {} }
  11. @aida_isay Lazy delegate class AboutMeFragment : Fragment() { val component

    by lazy { val appComponent = DaggerHelper.getAppComponent(this) AboutMeComponent.builder() .appComponent(appComponent) .build() } }
  12. @aida_isay Lazy delegate class AboutMeFragment : Fragment() { val component

    by lazy { val appComponent = DaggerHelper.getAppComponent(this) AboutMeComponent.builder() .appComponent(appComponent) .build() } }
  13. @aida_isay Lazy delegate class AboutMeFragment : Fragment() { val component

    by lazy { val appComponent = DaggerHelper.getAppComponent(this) AboutMeComponent.builder() .appComponent(appComponent) .build() } }
  14. @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) } }
  15. @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
  16. @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() }
  17. @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 }
  18. @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 }
  19. @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) }
  20. @aida_isay map •Simplify access •Define expected structure •But have a

    power to access unexpected fields •Map key == property name
  21. @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) } } }
  22. @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) } } }
  23. @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) } } }
  24. @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) } } }
  25. @aida_isay Adapters in Java public class NameAdapter extends RecyclerView.Adapter<ViewHolder> {

    public void updateNames(ArrayList<String> names){ this.list = names; notifyDataSetChanged(); } }
  26. @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() } }
  27. @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() } }
  28. @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
  29. @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(); } } }
  30. @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 } }
  31. @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 } }
  32. @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") } }
  33. @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
  34. @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 } } } }) }
  35. @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() }
  36. @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 } . . . }
  37. @aida_isay class SendLikeButton(context: Context) : Button(context) { . . .

    private var showLike: Boolean = false . . . fun showLike() { if (!showLike) { morph() } } fun showSend() { if (showLike) { morph() } } . . . }
  38. @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 } }
  39. @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() } } })
  40. @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() } } })
  41. @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 . . . } . . . }
  42. @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 . . . } . . . }
  43. @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() } } })
  44. @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) } } }
  45. @aida_isay override fun onCreate(savedInstanceState: Bundle?) { . . . getPost()

    .subscribe({ buttonLike.setLikedView(it) }, {}).addTo(disposable) }
  46. @aida_isay override fun onCreate(savedInstanceState: Bundle?) { . . . buttonLike.setOnClickListener

    { when (buttonLike.isLiked && buttonLike.showLike) { true -> unlikePost() else -> if (buttonLike.showLike) likePost() else sendComment() } } }
  47. @aida_isay sealed class ButtonState { object ShowLiked : ButtonState() object

    ShowUnLiked : ButtonState() object SendComment : ButtonState() }
  48. @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) }
  49. @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) }
  50. @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) } } . . . } } }
  51. @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) } } } }
  52. @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) }
  53. @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) }
  54. @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() }
  55. @aida_isay data class UiModel( val loading: Boolean, val firstName: String,

    val employmentStatus: String, . . . val shouldAnimateView1: Boolean val shouldAnimateView2: Boolean )
  56. @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() } }
  57. @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) . . . }
  58. @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) . . . } . . . }
  59. @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 }) }