$30 off During Our Annual Pro Sale. View Details »

Kotlin @ Trafi

Kotlin @ Trafi

An overview of the Kotlin adoption strategy at Trafi, including useful patterns that we discovered and enjoy using with the new language.

Joys:
- Java interop
- Refactoring
- Functional transformations
- Nullability in type system
- Extension functions
- Data classes
- Sealed class hierarchies
- Swift-y

Pains:
- Style
- Stability
- JSON model classes

Presented at GDG Vilnius meetup.
https://www.meetup.com/GDG-Vilnius/events/245813786/

Avatar for Justas Medeišis

Justas Medeišis

December 19, 2017
Tweet

More Decks by Justas Medeišis

Other Decks in Programming

Transcript

  1. fun SharedPreferences.boolean(key: String, default: Boolean = false) : ReadWriteProperty<Any, Boolean>

    fun SharedPreferences.int(key: String, default: Int = 0) : ReadWriteProperty<Any, Int>
  2. class Preferences(context: Context) { private val preferences = context.getSharedPreferences( "preferences.xml",

    Context.MODE_PRIVATE) var notificationsEnabled: Boolean by preferences .boolean(key = "notifications_enabled", default = true) }
  3. val preferences: Preferences if (preferences.getBoolean("notifications_enabled", true)) { // show notification

    } if (preferences.notificationsEnabled) { // show notification } preferences.edit().putBoolean("notifications_enabled", false).apply() preferences.notificationsEnabled = false
  4. fun SharedPreferences.boolean(key: String, default: Boolean = false) fun SharedPreferences.int(key: String,

    default: Int = 0) fun SharedPreferences.string(key: String) fun <reified T> SharedPreferences.json( gson: Gson, key: String, default: T? = null)
  5. inline fun <T> SharedPreferences.delegate( key: String, defaultValue: T, crossinline getter:

    SharedPreferences.(String, T) -> T, crossinline setter: Editor.(String, T) -> Editor ): ReadWriteProperty<Any, T> { return object : ReadWriteProperty<Any, T> { override fun getValue(_: Any, _: KProperty<*>): T { return getter(key, defaultValue) } override fun setValue(_: Any, _: KProperty<*>, value: T) { edit().setter(key, value).apply() } } }
  6. class EventFragment : Fragment() { private var eventName: String? =

    null companion object { private val EVENT_NAME_KEY = "event_name" fun newInstance(eventName: String?) = EventFragment().also { val bundle = Bundle() bundle.putString(EVENT_NAME_KEY, eventName) it.arguments = bundle } } // .. }
  7. class EventFragment : Fragment() { // .. override fun onViewCreated(view:

    View, _: Bundle?) { eventName = arguments?.getString(EVENT_NAME_KEY) title.text = eventName } }
  8. class EventFragment : Fragment() { private var eventName: String? =

    null companion object { private val EVENT_NAME_KEY = "event_name" fun newInstance(eventName: String?) = EventFragment().also { val bundle = Bundle() bundle.putString(EVENT_NAME_KEY, eventName) it.arguments = bundle } } // .. }
  9. class EventFragment : Fragment() { private lateinit var eventId: String

    private var eventName: String? = null companion object { private val EVENT_ID_KEY = "event_id" private val EVENT_NAME_KEY = "event_name" fun newInstance(eventId: String, eventName: String?) = EventFragment().also { val bundle = Bundle() bundle.putString(EVENT_ID_KEY, eventId) bundle.putString(EVENT_NAME_KEY, eventName) it.arguments = bundle } }
  10. class EventFragment : Fragment() { // .. override fun onViewCreated(view:

    View, _: Bundle?) { eventId = arguments?.getString(EVENT_ID_KEY) ?: throw IllegalArgumentException( "Missing $EVENT_ID_KEY argument") eventName = arguments?.getString(EVENT_NAME_KEY) title.text = eventName } }
  11. class EventFragment : Fragment() { private var eventName by argString()

    companion object { fun newInstance(eventName: String?) = EventFragment().also { it.eventName = eventName } } override fun onViewCreated(view: View, _: Bundle?) { title.text = eventName } } val fragment = EventFragment.newInstance("event_0", "My Event")
  12. fun argThrow(property: KProperty<*>): Nothing = throw IllegalArgumentException("Missing ${property.name}") fun argStringOrThrow(key:

    String? = null) : ReadWriteProperty<Fragment, String> = argDelegate(key, ::argThrow, Bundle::getString, Bundle::putString)
  13. class EventFragment : Fragment() { private var eventName by argString()

    companion object { fun newInstance(eventName: String?) = EventFragment().also { it.eventName = eventName } } override fun onViewCreated(view: View, _: Bundle?) { title.text = eventName } }
  14. class EventFragment : Fragment() { private var eventId by argStringOrThrow()

    private var eventName by argString() companion object { fun newInstance(eventId: String, eventName: String?) = EventFragment().also { it.eventId = eventId it.eventName = eventName } } override fun onViewCreated(view: View, _: Bundle?) { title.text = eventName GetEventUseCase(eventId).fetch { } }
  15. inline fun <T> argDelegate( key: String? = null, defaultValue: T,

    crossinline getter: Bundle.(String, T) -> T, crossinline setter: Bundle.(String, T) -> Unit ): ReadWriteProperty<Fragment, T> { return object : ReadWriteProperty<Fragment, T> { override fun getValue(ref: Fragment, property: KProperty<*>): T { val args = ref.arguments ?: Bundle() return args.getter(key ?: property.name, defaultValue) } override fun setValue(ref: Fragment, property: KProperty<*>, value: T) { val args = ref.arguments ?: Bundle().also { ref.arguments = it } args.setter(key ?: property.name, value) } }
  16. val lazyValue: String by lazy { println("computed!") "Hello" } fun

    main(args: Array<String>) { println(lazyValue) println(lazyValue) } // computed! // Hello // Hello
  17. var lazyValue: String by lazy { println("computed!") "Hello" } fun

    main(args: Array<String>) { println(lazyValue) lazyValue = "Bye" println(lazyValue) } // does not compile
  18. var lazyValue: String by lazyMutable { println("computed!") "Hello" } fun

    main(args: Array<String>) { println(lazyValue) lazyValue = "Bye" println(lazyValue) } // computed! // Hello // Bye
  19. /** * Mutable value with lazy initialization. */ public interface

    LazyMutable<T> { public var value: T public fun isInitialized(): Boolean } public fun <T> lazyMutable(initializer: () -> T, onChange: (newValue: T) -> Unit) : LazyMutable<T> { return SynchronizedLazyMutableImpl(initializer, onChange) }
  20. public fun <T> lazyMutable(initializer: () -> T, onChange: (newValue: T)

    -> Unit) : LazyMutable<T> { return SynchronizedLazyMutableImpl(initializer, onChange) }
  21. public fun <T> lazyMutable(initializer: () -> T, onChange: (newValue: T)

    -> Unit) : LazyMutable<T> { return SynchronizedLazyMutableImpl(initializer, onChange) } TO THE EXTREME!
  22. public fun <T> lazyMutable(initializer: () -> T, onChange: (newValue: T)

    -> Unit) : LazyMutable<T> { return SynchronizedLazyMutableImpl(initializer, onChange) } fun <T, U> lazyMutableDelegated(thisRef: U, property: KProperty<*>, delegate: ReadWriteProperty<U, T>, onChange: ((newValue: T) -> Unit)): LazyMutable<T> = lazyMutable( initializer = { delegate.getValue(thisRef, property) }, onChange = { newValue -> delegate.setValue(thisRef, property, newValue) onChange(newValue) })
  23. class RegionStore(context: Context, gson: Gson) { private val store =

    context.getSharedPreferences("region.xml", Context.MODE_PRIVATE) var selectedRegion: Region? by lazyMutableDelegated(this, this::selectedRegion, delegate = store.json(gson, key = "selected_region")) } val store = RegionStore(context, gson) title.text = store.selectedRegion?.name store.selectedRegion = Region(name = "Vilnius")
  24. enum class AB_Onboarding { unspecified, disabled, promote_route_search, promote_maas; fun enabled()

    = when (this) { promote_route_search, promote_maas -> true else -> false } }
  25. val provider: FeatureFlagProvider val flag = FeatureFlag<AB_Onboarding>() val value =

    flag.value(provider) println( "${flag.key} has value $value of ${flag.values.map({ it.name })}") println("${flag.key} is enabled ${value.enabled()}") // exhaustive branches by compiler val text = when (value) { unspecified, disabled -> null promote_route_search -> "Try route search" promote_maas -> "Try Spark or CityBee" }
  26. class FeatureFlag<out T : Enum<out T>>( val key: String, val

    values: List<T>, val default: T) { companion object { inline operator fun <reified T : Enum<T>> invoke() = FeatureFlag(key = T::class.java.simpleName, values = enumValues<T>().toList(), default = enumValueOf("unspecified")) } private val valuesByName = values.associateBy { it.name } fun value(provider: FeatureFlagProvider): T { val name = provider.getStringValue(key) return valuesByName[name] ?: default } }
  27. class MapFragment : Fragment() { override fun onViewCreated(view: View, _:

    Bundle?) { map_view.paddingTop = header.height } }
  28. fun View.afterLayout(block: View.() -> Unit) { val originalVto = viewTreeObserver

    originalVto.addOnPreDrawListener( object : ViewTreeObserver.OnPreDrawListener { override fun onPreDraw(): Boolean { if (originalVto.isAlive) { originalVto.removeOnPreDrawListener(this) } viewTreeObserver.removeOnPreDrawListener(this) block(this@afterLayout) return true } }) }
  29. class MapFragment : Fragment() { override fun onViewCreated(view: View, _:

    Bundle?) { map_view.paddingTop = header.height } }
  30. class MapFragment : Fragment() { override fun onViewCreated(view: View, _:

    Bundle?) { header.afterLayout { map_view.paddingTop = height } } }
  31. Why JetBrains Needs Kotlin by Dmitry Jemerov “ First and

    foremost, it’s about our own productivity.”
  32. How can we as a team get up to speed

    with the latest idiomatic Kotlin best practices?
  33. public class CustomView extends View { public CustomView(Context context) {

    super(context); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); } public CustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public CustomView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } }
  34. class CustomView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr:

    Int = 0, defStyleRes: Int = 0) : View(context, attrs, defStyleAttr, defStyleRes)
  35. class Service { fun doThingOne() { // one.. } fun

    doThingTwo() { // two.. } } // legacy behavior fun Service.doThingOneAndTwo() { doThingOne() doThingTwo() }
  36. val tripsByMonth = trips .groupBy { it.month } .flatMap {

    listOf(it.key.name).plus(it.value.map { it.name }) } .filterNotNull()
  37. val count = user?.feed?.events?.size ?: 0 int count = 0;

    if (null != user) { if (null != user.feed) { if (null != user.feed.events) { count = user.feed.events.size() } } }
  38. // AUTOGENERATED FILE - DO NOT MODIFY! // This file

    generated by Djinni from common.djinni package com.trl; public final class LatLng { /*package*/ final double mLat; /*package*/ final double mLng; // ... }
  39. // Code generated by Wire protocol buffer compiler, do not

    edit. // Source file: car_sharing.proto at 36:1 package com.trafi.android.proto.carsharing; public final class LatLng extends AndroidMessage<LatLng, LatLng.Builder> { // ... @WireField( tag = 1, adapter = "com.squareup.wire.ProtoAdapter#DOUBLE" ) @Nullable public final Double lat; // ... }
  40. package android.location; /** … */ public class Location implements Parcelable

    { private double mLatitude = 0.0; private double mLongitude = 0.0; }
  41. fun View.setInvisible() { visibility = View.INVISIBLE } fun View.setVisible() {

    visibility = View.VISIBLE } fun View.isVisible() = visibility == View.VISIBLE view.visibility = View.VISIBLE view.setVisible() view.visibility = View.INVISIBLE view.setInvisible() val margin = if(icon.visibility == View.VISIBLE) iconMargin else 0 val margin = if(icon.isVisible()) iconMargin else 0
  42. fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View {

    return LayoutInflater .from(context) .inflate(layoutRes, this, attachToRoot) } val parent: ViewGroup val view = LayoutInflater .from(parent.context) .inflate(R.layout.my_layout, parent, false) val view = parent.inflate(R.layout.my_layout)
  43. fun TextView.setCompoundDrawables(left: Drawable? = null, top: Drawable? = null, right:

    Drawable? = null, bottom: Drawable? = null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { setCompoundDrawablesRelative(left, top, right, bottom) } else { setCompoundDrawables(left, top, right, bottom) } } text.setCompoundDrawables(null, drawable, null, null) text.setCompoundDrawables(top = drawable)
  44. @ColorInt fun View.color(@ColorRes colorRes: Int) = ContextCompat.getColor(context, colorRes) val color

    = ContextCompat.getColor(context, R.color.my_color) val color = color(R.color.my_color)
  45. class TicketViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(parent.inflate(R.layout.cell_ticket)) { fun bind(model: TicketViewModel) =

    itemView.run { title.text = model.title subtitle.text = model.subtitle link.setOnClickListener { /** handle click */ } link.setTextColor(color(R.color.ticket_active)) } }
  46. @AutoValue public abstract class Ticket { public abstract String id();

    public abstract String name(); @Nullable public abstract Long endTime(); }
  47. @AutoValue public abstract class Ticket { public abstract String id();

    public abstract String name(); @Nullable public abstract Long endTime(); }
  48. @AutoValue public abstract class Ticket implements Parcelable { public abstract

    String id(); public abstract String name(); @Nullable public abstract Long endTime(); }
  49. @PaperParcel data class Ticket(val id: String, val name: String, val

    endTime: Long? = null) : PaperParcelable { companion object { @JvmField val CREATOR = PaperParcelTicket.CREATOR } }
  50. sealed class InboundLink object PlayStoreLink : InboundLink() class NewsLink(val eventId:

    Int) : InboundLink() class StopLink(val stopId: String) : InboundLink() class RouteSearchLink( val startLocation: Location? = null, val endLocation: Location? = null) : InboundLink() class RideHailingBookingLink( val booking: RideHailingBooking) : InboundLink()
  51. fun navToLink(link: InboundLink) = when (link) { is PlayStoreLink ->

    getListingLink(context)?.let { navToLink(it) } is NewsLink -> navToEventDetails(link.eventId) is StopLink -> navToStopDepartures(link.stopId) is RouteSearchLink -> { navToRouteSearch( from = link.startLocation?.toTrl()?.waypoint(), to = link.endLocation?.toTrl()?.waypoint() ) } is RideHailingBookingLink -> navToBooking(link.booking) }
  52. typealias Feedback<State, Event> = (Observable<State>) -> Observable<Event> public static func

    system<State, Event>( initialState: State, reduce: @escaping (State, Event) -> State, feedback: Feedback<State, Event>... ) -> Observable<State>
  53. typealias Feedback<State, Event> = (Observable<State>) -> Observable<Event> fun <State, Event>

    system( initialState: State, reduce: (State, Event) -> State, vararg feedback: Feedback<State, Event> ): Observable<State>
  54. @AutoValue public abstract class Ticket implements Parcelable { public abstract

    String id(); public abstract String name(); @Nullable public abstract Long endTime(); }
  55. @AutoValue public abstract class Ticket implements Parcelable { public abstract

    String id(); public abstract String name(); @Nullable public abstract Long endTime(); public static TypeAdapter<Ticket> typeAdapter(Gson gson) { return new AutoValue_Ticket.GsonTypeAdapter(gson); } }
  56. class AccountsFragment : Fragment() { @Inject lateinit var navigationManager: NavigationManager

    @Inject lateinit var userStore: UserStore @Inject lateinit var imageLoader: ImageLoader @Inject lateinit var appConfig: AppConfig @Inject lateinit var eventTracker: AccountEventTracker override fun onAttach(context: Context) { super.onAttach(context) (activity as HomeActivity) .component.accountsComponent().inject(this) } }
  57. fun setUserProperty(property: UserProperty) { firebaseAnalytics .setUserProperty(property.name) } fun logEvent(event: String,

    params: Map<String, String>? = null) { val bundle = Bundle() params?.entries?.forEach { bundle.putString( it.key, it.value) } firebaseAnalytics.logEvent(event, bundle) }
  58. fun setUserProperty(property: UserProperty) { firebaseAnalytics .setUserProperty(property.name.sanitizeUserPropertyName()) } fun logEvent(event: String,

    params: Map<String, String>? = null) { val bundle = Bundle() params?.entries?.forEach { bundle.putString( it.key.sanitizeParamName(), it.value.sanitizeParamValue()) } firebaseAnalytics.logEvent(event.sanitizeEventName(), bundle) }
  59. private fun String.sanitize(maxLength: Int): String { val underscored = SPACE_OR_DASH.matcher(this).replaceAll("_")

    val underscoredAlphanumeric = NOT_ALPHANUMERIC_OR_UNDERSCORE.matcher(underscored).replaceAll("") return underscoredAlphanumeric.substring(0, minOf(underscoredAlphanumeric.length, maxLength)) } private fun String.sanitizeEventName() = sanitize(maxLength = 40) private fun String.sanitizeParamName() = sanitize(maxLength = 40) private fun String.sanitizeUserPropertyName() = sanitize(maxLength = 24)
  60. private val SPACE_OR_DASH by lazy { "[ -]".toPattern() } private

    val NOT_ALPHANUMERIC_OR_UNDERSCORE by lazy { "[^A-Za-z0-9_]".toPattern() } private fun String.sanitize(maxLength: Int): String { val underscored = SPACE_OR_DASH.matcher(this).replaceAll("_") val underscoredAlphanumeric = NOT_ALPHANUMERIC_OR_UNDERSCORE.matcher(underscored).replaceAll("") return underscoredAlphanumeric.substring(0, minOf(underscoredAlphanumeric.length, maxLength)) } private fun String.sanitizeEventName() = sanitize(maxLength = 40) private fun String.sanitizeParamName() = sanitize(maxLength = 40) private fun String.sanitizeUserPropertyName() = sanitize(maxLength = 24)
  61. enum class AB_MakeTrafiGreatAgain { unspecified, disabled, enabled, } // FeatureFlagModule.kt

    // FeatureFlagModule @[Provides ApplicationScope] fun great() = FeatureFlag<AB_MakeTrafiGreatAgain>() // FeatureFlagBindingModule @[Binds IntoSet ApplicationScope] abstract fun great(flag: FeatureFlag<@JvmSuppressWildcards AB_MakeTrafiGreatAgain>) : FeatureFlag<*>
  62. @Inject lateinit var provider: FeatureFlagProvider @Inject lateinit var flag: FeatureFlag<AB_MakeTrafiGreatAgain>

    fun isTrafiGreatAgain() = when (flag.value(provider)) { enabled -> true unspecified, disabled -> false }
  63. fun Bundle.getIntOrNull(key: String) = if (containsKey(key)) getInt(key) else null inline

    fun <reified T : Enum<T>> Bundle.putEnum(key: String, value: T) { putInt(key, value.ordinal) } inline fun <reified T : Enum<T>> Bundle.getEnumOrNull(key: String): T? { return getIntOrNull(key)?.let { ordinal -> enumValues<T>().getOrNull(ordinal) } } enum class Shape { SQUARE, CIRCLE } val shape = Shape.SQUARE val bundle = Bundle()