Last active
September 12, 2021 18:53
-
-
Save topeterhonz/85854af4e27580a30195d4dc77451722 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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