Skip to content

Instantly share code, notes, and snippets.

@topeterhonz
Last active September 12, 2021 18:53
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save topeterhonz/85854af4e27580a30195d4dc77451722 to your computer and use it in GitHub Desktop.
Save topeterhonz/85854af4e27580a30195d4dc77451722 to your computer and use it in GitHub Desktop.
package peterho.kotlintipstricks
import android.content.Context
import android.support.annotation.AttrRes
import android.support.annotation.StyleRes
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v7.app.AlertDialog
import android.support.v7.widget.RecyclerView
import android.text.TextUtils
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import com.google.gson.Gson
import io.realm.Realm
import io.realm.RealmObject
import io.realm.RealmQuery
import retrofit2.http.GET
import retrofit2.http.Query
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.lang.kotlin.addTo
import rx.lang.kotlin.plusAssign
import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription
// For best reading experience. Enable "Custom folding regions" in preferences. Once that's done,
// go and collapse all regions by selecting Collapse All (Shift Cmd - )
class TipsAndTricks {
//region Looping
fun looping() {
// Guava has 11258 methods!!
// Kotlin Collection extension functions are similar to RxJava but pull based.
val androidVersions = listOf("Cupcake", "Donut", "Eclair")
// print a list to a single string with line break
androidVersions
.joinToString("\n")
// Prints:
// Cupcake
// Donut
// Eclair
// Get First two
androidVersions
.take(2)
.joinToString("\n")
// Prints:
// Cupcake
// Donut
// Filtering
androidVersions
.filter { !it.contains("nut") }
.joinToString { "\n" }
// Prints:
// Cupcake
// Eclair
// Indexing
androidVersions
.mapIndexed { index, s -> "${index + 1}. $s" }
.joinToString { "\n" }
// Prints:
// 1. Cupcake
// 2. Donut
// 3. Eclair
}
//endregion
//region Api with optional parameters
interface CustomerService {
@GET("v2/customers")
fun getCustomers(
@Query("name") name: String? = null,
@Query("city") city: String? = null,
@Query("sort") sort: Sort? = null
): Observable<List<Customer>>
}
enum class Sort(val value: String) {
NAME("name"),
AGE("age");
override fun toString() = value
}
val customerService = object : CustomerService {
override fun getCustomers(name: String?, city: String?, sort: Sort?): Observable<List<Customer>> {
TODO()
}
}
fun apiExample() {
customerService.getCustomers(name = "Peter")
// https://api.example.com/v2/customers?name=Peter
customerService.getCustomers(city = "Auckland")
// https://api.example.com/v2/customers?city=Auckland
customerService.getCustomers(name = "Peter", sort = Sort.AGE)
// https://api.example.com/v2/customers?name=Peter&sort=age
}
//endregion
//region Calling Android api with extension function
fun View.setMargin(
left: Int? = null,
top: Int? = null,
right: Int? = null,
bottom: Int? = null
) {
val layoutParams = layoutParams as ViewGroup.MarginLayoutParams
left?.let { layoutParams.leftMargin = it }
top?.let { layoutParams.topMargin = it }
right?.let { layoutParams.rightMargin = it }
bottom?.let { layoutParams.bottomMargin = it }
}
fun marginExample(view: View) {
view.setMargin(left = 20, right = 40)
view.setMargin(right = 20, left = 40, top = 30)
view.setMargin(bottom = 20)
// Parameter can be in any order
}
//endregion
//region Operator overloading isn't always evil
fun rxJavaSubscribeExample(
customerService: CustomerService,
subscriptions: CompositeSubscription
) {
// See how far the opening bracket of .add() is from the closing bracket
subscriptions.add(customerService.getCustomers(name = "Peter")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe())
// Place add right at the end as .addTo()
customerService.getCustomers(name = "Peter")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe()
.addTo(subscriptions)
// Alternatively overload the += operator
subscriptions += customerService.getCustomers(name = "Peter")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe()
// Works on RxJava2, too!
// val disposables : CompositeDisposable()
// observable.subscribe()
// .addTo(disposables)
// disposable += observable.subscribe()
}
// endregion
// region what are those component1, component2.. componentN?
// object destructuring
class A
class B
interface ServiceA {
fun getA(): Observable<A>
}
interface ServiceB {
fun getB(): Observable<B>
}
fun destructuringExample() {
val serviceA = object : ServiceA {
override fun getA(): Observable<A> {
TODO()
}
}
val serviceB = object : ServiceB {
override fun getB(): Observable<B> {
TODO()
}
}
// make two api requests concurrently
Observable.zip(serviceA.getA(), serviceB.getB()) { a, b -> Pair(a, b) }
.subscribe { pair ->
pair.component1()
pair.first
pair.second
// .... subscribe stuff. Bind the above 2 variables to the view
}
// The above can become
Observable.zip(serviceA.getA(), serviceB.getB()) { a, b -> Pair(a, b) }
.subscribe { (first, second) ->
// compiler sets first = pair.component1(), second = pair.component2()
first
second
// .... subscribe stuff
}
// Even better... rename `first` and `second` to `a` and `b` respectively
Observable.zip(serviceA.getA(), serviceB.getB()) { a, b -> Pair(a, b) }
.subscribe { (a, b) ->
a
b
// .... subscribe stuff
}
}
// endregion
//region When...
enum class AndroidVersion {
CUPCAKE, DONUT, ECLAIR
}
fun whenStatement(version: AndroidVersion): String {
val result: String
when (version) {
AndroidVersion.CUPCAKE -> result = "Cupcake"
AndroidVersion.ECLAIR -> result = "Eclair"
AndroidVersion.DONUT -> result = "Donut"
}
return result
}
// when is an expression
fun whenExample(version: AndroidVersion): String {
return when (version) {
AndroidVersion.CUPCAKE -> "Cupcake"
AndroidVersion.ECLAIR -> "Eclair"
AndroidVersion.DONUT -> "Donut" // also when is exhaustive. delete this line and see what happens
}
}
sealed class ContactViewModel {
class SectionHeader(val headerText: String) : ContactViewModel()
class Item(val name: String, val imageUrl: String) : ContactViewModel()
class FancyItem() : ContactViewModel()
}
//
abstract class Adapter(
val viewModels: List<ContactViewModel>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
val viewModel = viewModels[position]
// exhaustive check is only enabled when `when` is used as an expression. Uncomment this line and see
// val bound =
when (viewModel) {
is ContactViewModel.SectionHeader -> {
viewModel.headerText // Notice smart cast. Bind this to a view
}
is ContactViewModel.Item -> {
viewModel.name // smart cast again
viewModel.imageUrl
}
}
}
}
//endregion
//region Expression Precedence
fun expressionPrecedence() {
val i: Int? = null
// val i: Int? = 5
val j = i ?: 0 + 5
// i = null, j = 5
// i = 5, j = 5. Huh? Why not 10?
// Check the following doc. `+` is evaluated before the elvis operator
// https://kotlinlang.org/docs/reference/grammar.html#expressions
// Workaround
fun Int?.orZero() = this ?: 0
val k = i.orZero() + 5
// now i = 5, k = 10.
// in this trivial example, k = (i ?: 0) + 5 is probably simpler
// consider the following example of a nullable customer object
// x = customer?.feedback?.stars.orZero() + 5
// x = (customer?.feedback?.stars ?: 0) + 5
// Consider a even longer chain, the first line above is slightly easier to read and write since you don't need to worry about where the brackets start and end.
}
//endregion
//region Builder without breaking the chain
fun alertDialogExample(context: Context) {
AlertDialog.Builder(context)
.setTitle("Are you sure?")
.setPositiveButton("Ok") { _, _ -> }
.setNegativeButton("cancel") { _, _ -> }
.show()
fun showAlertDialog(context: Context, func: AlertDialog.Builder.() -> Unit)
= AlertDialog.Builder(context)
.apply { func() }
.show()
val showNegative = false
showAlertDialog(context) {
setTitle("Are you sure?")
setPositiveButton("ok") { _, _ -> }
if (showNegative) {
setNegativeButton("cancel") { _, _ -> }
}
}
}
//endregion
//region what are apply, run, let, also?
// For those worked with RxJava. We can use an analogy here.
// let - map with func
// also - doOnNext with func
// run - map with ext func
// apply - doOnNext with ext func
// These functions are not part of the Kotlin language but just functions in the Kotlin standard library
// we can create our own. i.e. runIf()
fun loopingWithCondition(
isTaking2: Boolean,
isAllergicToNuts: Boolean,
isShowingIndex: Boolean
) {
// e.g. If we want to conditionally apply these transforms to the list, normally you would
var mutable = listOf("Cupcake", "Donut", "Eclair")
if (isTaking2) {
mutable = mutable.take(2)
}
if (isAllergicToNuts) {
mutable = mutable.filter { !it.contains("nut") }
}
if (isShowingIndex) {
mutable = mutable.mapIndexed { index, s -> "${index + 1}. $s" }
}
// alternatively, we can chain these operations and create a single expression.
listOf("Cupcake", "Donut", "Eclair")
.runIf(isTaking2) {
take(2)
}
.runIf(isAllergicToNuts) {
filter { !it.contains("nut") }
}
.runIf(isShowingIndex) {
mapIndexed { index, s -> "${index + 1}. $s" }
}
}
inline fun <T> T.runIf(condition: Boolean, block: T.() -> T): T = if (condition) block() else this
//endregion
//region Don't abuse chaining
// Just because you can doesn't mean you should.
// Aim for readability
// What does the single expression function below do?
// Ref: https://blog.philipphauer.de/clean-code-kotlin/
/*
fun map(dto: OrderDTO, authData: RequestAuthData) = OrderEntity(
id = dto.id,
shopId = try {
extractItemIds(dto.orderItems[0].element.href).shopId
} catch (e: BatchOrderProcessingException) {
restExc("Couldn't retrieve shop id from first order item: ${e.msg}")
},
batchState = BatchState.RECEIVED,
orderData = OrderDataEntity(
orderItems = dto.orderItems.map { dto -> mapToEntity(dto) },
shippingType = dto.shipping.shippingType.id,
address = mapToEntity(dto.shipping.address),
correlationOrderId = dto.correlation?.partner?.orderId,
externalInvoiceData = dto.externalInvoiceData?.let {
ExternalInvoiceDataEntity(
url = it.url,
total = it.total,
currencyId = it.currency.id
)
}
),
partnerUserId = authData.sessionOwnerId ?: restExc("No sessionId supplied", 401),
apiKey = authData.apiKey,
dateCreated = if (dto.dateCreated != null) dto.dateCreated else Instant.now(),
)
*/
//endregion
//region Reified
fun parseGson() {
val customer = Gson().fromJson("""{"firstName": "Peter", "lastName, "Ho}""", Customer::class.java)
// slightly cleaner
val customer2 = fromJson<Customer>("""{"firstName": "Peter", "lastName, "Ho}""")
}
inline fun <reified T : Any> fromJson(json: String): T {
return Gson().fromJson<T>(json, T::class.java)
}
//endregion
//region Nicer realm
fun realm() {
// using the java api as is
val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm ->
realm.where(Customer::class.java)
.not()
.beginGroup()
// @formatter:off
.equalTo(CustomerFields.FIRST_NAME, "Peter")
.or()
.equalTo(CustomerFields.LAST_NAME, "Ho")
// @formatter:on
.endGroup()
.findAll()
}
realm.close()
// Putting it all together
// With some kotlin awesomeness.
Realm.getDefaultInstance().use {
realm.applyTransaction {
where<Customer> {
not()
group {
equalTo(CustomerFields.FIRST_NAME, "Peter")
or()
equalTo(CustomerFields.LAST_NAME, "Ho")
}
}.findAll()
}
}
}
inline fun <T : RealmObject> RealmQuery<T>.group(func: RealmQuery<T>.() -> RealmQuery<T>)
= beginGroup()
.func()
.endGroup()
inline fun Realm.applyTransaction(crossinline func: Realm.() -> Unit) {
executeTransaction { it.func() }
}
inline fun <reified T : RealmObject> Realm.where(func: RealmQuery<T>.() -> RealmQuery<T>)
= where(T::class.java).func()
//endregion
//region static java interpolation
// How do you remove the boilerplate of TAG in your fragments?
// When logging. Use Timber - Timber.e("Some error message")
// What about in your fragment tranction
fun transactionDemo(fragmentManager: FragmentManager) {
fragmentManager
.beginTransaction()
.add(SomeFragment(), tagOf<SomeFragment>())
.commit()
}
inline fun <reified T : Fragment> tagOf(): String {
return T::class.java.simpleName
}
class SomeFragment : Fragment()
companion object {
// This removes the Companion reference from Java but still gives a horrible name getHORRIBLE_JAVA_CONST()
@JvmStatic
val HORRIBLE_JAVA_CONST = "HORRIBLE_JAVA_CONST"
const val BETTER_CONST = "BETTER_CONST"
// What if the const isn't really a constant. Can't add const here. Use @JvmField instead
@JvmField
val SHOW_DEBUG_MENU = BuildConfig.DEBUG
}
class OverriddenLinearLayout : LinearLayout {
constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAttr: Int, @StyleRes defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes) {
// init your view here
}
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : this(context, attrs, defStyle, 0)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context) : this(context, null)
// implementation details here
}
// One constructor to rule them all
// Note: This might not be suitable in all cases.
// For more info. See https://antonioleiva.com/custom-views-android-kotlin/
class SimpleOverriddenLinearLayout : LinearLayout {
@JvmOverloads
constructor(context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
@StyleRes defStyleRes: Int = 0
) : super(context, attrs, defStyleAttr, defStyleRes) {
// init your view here
}
// implementation details here
}
//endregion
//region String goodness
fun stringGoodness() {
val s: String? = null
// Yuck....
TextUtils.isEmpty(s)
// Yay!
s.isNullOrBlank()
s.isNullOrEmpty()
}
//endregion
//region Don't like camel casing your unit test names?
@Suppress("IllegalIdentifier") // Only works in JVM. So does your unit test
fun `this is totally fine`() {
}
//endregion
//region Want to reach me?
/**
*
* Email: [topeterho-at-gmail-dot-com]
*
* Twitter: [@topeterho]
*
* LinkedIn: [www.linkedin.com/in/topeterho/]
*
*/
//endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment