How to use synthetic property in Kotlin Android Extension

Issue #555

Synthetic properties generated by Kotlin Android Extensions plugin needs a view for Fragment/Activity to be set before hand.

In your case, for Fragment, you need to use view.btn_K in onViewCreated

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    super.onCreateView(inflater, container, savedInstanceState)
    val view = inflater.inflate(R.layout.fragment_card_selector, container, false)
    view.btn_K.setOnClickListener{} // access with `view`
    return view
}

Or better, you should only access synthetic properties in onViewCreated

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    super.onCreateView(inflater, container, savedInstanceState)
    return inflater.inflate(R.layout.fragment_card_selector, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    btn_K.setOnClickListener{} // access without `view`
}

Please notice that savedInstanceState parameter should be nullable Bundle?, and also check Importing synthetic properties

It is convenient to import all widget properties for a specific layout
in one go:

import kotlinx.android.synthetic.main.<layout>.*

Thus if the layout filename is activity_main.xml, we’d import
kotlinx.android.synthetic.main.activity_main.*.

If we want to call the synthetic properties on View, we should also
import kotlinx.android.synthetic.main.activity_main.view.*.

Read more

How to access view in fragment in Kotlin

Issue #497

Synthetic properties generated by Kotlin Android Extensions plugin needs a view for Fragment/Activity to be set before hand.

In your case, for Fragment, you need to use view.btn_K in onViewCreated

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    super.onCreateView(inflater, container, savedInstanceState)
    val view = inflater.inflate(R.layout.fragment_card_selector, container, false)
    view.btn_K.setOnClickListener{} // access with `view`
    return view
}

Or better, you should only access synthetic properties in onViewCreated

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    super.onCreateView(inflater, container, savedInstanceState)
    return inflater.inflate(R.layout.fragment_card_selector, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    btn_K.setOnClickListener{} // access without `view`
}

Please notice that savedInstanceState parameter should be nullable Bundle?, and also check Importing synthetic properties

It is convenient to import all widget properties for a specific layout
in one go:

import kotlinx.android.synthetic.main.<layout>.*

Thus if the layout filename is activity_main.xml, we’d import
kotlinx.android.synthetic.main.activity_main.*.

If we want to call the synthetic properties on View, we should also
import kotlinx.android.synthetic.main.activity_main.view.*.


Original answer https://stackoverflow.com/questions/34541650/nullpointerexception-when-trying-to-access-views-in-a-kotlin-fragment/51674381#51674381

How to add AdMob to Android app

Issue #431

Use AdMob with Firebase

build.gradle

1
2
3
4
5
6
7
8
9
10
buildscript {
repositories {
google()
jcenter()

}
dependencies {
classpath 'com.google.gms:google-services:4.3.2'
}
}

app/build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
class Version {
class Firebase {
static def analytics = "17.2.0"
static def ads = "18.2.0"
}
}

dependencies {
implementation "com.google.firebase:firebase-analytics:$Version.Firebase.analytics"
implementation "com.google.firebase:firebase-ads:$Version.Firebase.ads"
}

apply plugin: 'com.google.gms.google-services'

Manifest.xml

1
2
3
4
5
6
7
8
<manifest>
<application>
<!-- Sample AdMob App ID: ca-app-pub-3940256099942544~3347511713 -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="[ADMOB_APP_ID]"/>
</application>
</manifest>

MyApplication.kt

1
2
3
4
5
6
7
class MyApplication: Application() {
override fun onCreate() {
super.onCreate()

MobileAds.initialize(this)
}
}

AdView

fragment.xml

1
2
3
4
5
6
7
8
9
10
<com.google.android.gms.ads.AdView
xmlns:ads="http://schemas.android.com/apk/res-auto"
android:id="@+id/adView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
ads:adSize="BANNER"
ads:adUnitId="ca-app-pub-123456/123456"
ads:layout_constraintBottom_toBottomOf="parent"
ads:layout_constraintLeft_toLeftOf="parent"
ads:layout_constraintRight_toRightOf="parent"/>

Fragment.kt

1
2
3
4
5
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdView

val request = AdRequest.Builder().build()
adView.loadAd(request)

Troubleshooting

app/build.gradle

1
2
3
dependencies {
implementation 'com.google.android.gms:play-services-ads:18.2.0'
}

Cannot fit requested classes in a single dex file

app/build.gradle

1
2
3
4
5
6
7
8
9
10

android {
defaultConfig {
multiDexEnabled true
}
}

dependencies {
implementation 'com.android.support:multidex:1.0.3'
}

Read more

How to create bounce animation programmatically in Android

Issue #383

Right click res -> New -> Android Resource Directory, select anim and name it anim
Right click res/anim -> New -> Android Resource file, name it bounce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromYDelta="0"
android:toYDelta="-100"
android:repeatCount="infinite" />
</set>
```

We have to set `repeatCount` in xml, setting in code does not work !!

```kt
val bounce = AnimationUtils.loadAnimation(context, R.anim.bounce)
bounce.repeatMode = Animation.REVERSE
bounce.duration = (1000..2000).random().toLong()

imageView.startAnimation(bounce)

How to use point in dp programmatically in Android

Issue #382

1
2
3
4
5
6
7
8
9
10
import android.content.Context

fun Int.toDp(context: Context?): Int {
if (context != null) {
val scale = context.resources.displayMetrics.density
return (this.toFloat() * scale + 0.5f).toInt()
} else {
return 0
}
}
1
2
val set = ConstraintSet()
set.setMargin(imageView.id, ConstraintSet.RIGHT, rightMargin.toDp(150))

Read more

How to create constraints programmatically with ConstraintLayout in Android

Issue #381

From API < 17, there is ViewCompat.generateViewId()
For API 17, there is View.generateViewId()

Note that to use ConstraintSet, all views under ConstraintLayout inside xml must have unique id

1
2
3
4
5
6
7
8
9
val imageView = ImageView(context)
imageView.id = View.generateViewId()
imageView.setImageResource(resId)
constraintLayout.addView(imageView)

val set = ConstraintSet()
set.clone(constraintLayout)
set.connect(imageView.id, ConstraintSet.RIGHT, ConstraintSet.PARENT_ID, ConstraintSet.RIGHT)
set.applyTo(constraintLayout)

How to use custom font as resource in Android

Issue #380

Downloadable fonts

https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts

Android 8.0 (API level 26) and Android Support Library 26 introduce support for APIs to request fonts from a provider application instead of bundling files into the APK or letting the APK download fonts. The feature is available on devices running Android API versions 14 and higher through the Support Library 26

Before

  • Select File -> New -> Folder -> Assets Folder to create src/main/assets/fonts
1
2
al myTypeface = Typeface.createFromAsset(assets, "fonts/myFont.ttf")
myTextView.typeface = myTypeface

In res

Create font directory

Right click res -> New -> Android Resource Directory, select font and name the folder font

Add custom fonts to res/font folder. Note that name must be lower case and underscore, like opensans_extrabolditalic.ttf

Right click res/font -> New -> Font resource file to create font family

opensans.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version="1.0" encoding="utf-8"?>
<font-family
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" >
<font
android:font="@font/opensans_regular"
android:fontStyle="normal"
android:fontWeight="400"
app:fontFamily="@font/opensans_regular"
app:fontStyle="normal"
app:fontWeight="400" />
<font
android:font="@font/opensans_semibold"
android:fontStyle="normal"
android:fontWeight="400"
app:fontFamily="@font/opensans_semibold"
app:fontStyle="normal"
app:fontWeight="400" />
<font
android:font="@font/opensans_bold"
android:fontStyle="normal"
android:fontWeight="400"
app:fontFamily="@font/opensans_bold"
app:fontStyle="normal"
app:fontWeight="400" />
</font-family>
```

Then use

```xml
<TextView
android:fontFamily="@font/opensans_bold"
android:textSize="26dp"
/>

Read more

How to get Hacker News top stories using parallel coroutine and Retrofit

Issue #379

1
2
3
4
5
6
7
interface Api {
@GET("topstories.json?print=pretty")
suspend fun getTopStories(): List<Int>

@GET("item/{id}.json?print=pretty")
suspend fun getStory(@Path("id") id: Int): Item
}
1
2
3
4
5
6
7
8
9
class Repo {
fun api(): Api {
return Retrofit.Builder()
.baseUrl("https://hacker-news.firebaseio.com/v0/")
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(Api::class.java)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ViewModel(val repo: Repo): ViewModel() {
val items = MutableLiveData<ArrayList<Item>>()

suspend fun load() {
try {
val ids = repo.api()
.getTopStories()
.take(20)

val items = ids.map {
repo.api().getStory(it)
}
this.items.value = items.toCollection(ArrayList())
} catch (e: Exception) {
this.items.value = arrayListOf()
}
}
}

Running parallel

The above run in serial. To run in parallel, we can use async

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope

class ViewModel(val repo: Repo): ViewModel() {
val items = MutableLiveData<ArrayList<Item>>()

suspend fun load() {
try {
val ids = repo.api()
.getTopStories()
.take(20)

coroutineScope {
val items = ids
.map { async { repo.api().getStory(it) } }
.awaitAll()

this@ViewModel.items.value = items.toCollection(ArrayList())
}

} catch (e: Exception) {
this.items.value = arrayListOf()
}
}
}

Parallel decomposition

https://medium.com/@elizarov/structured-concurrency-722d765aa952

With structured concurrency async coroutine builder became an extension on CoroutineScope just like launch did. You cannot simply write async { … } anymore, you have to provide a scope. A proper example of parallel decomposition becomes:

coroutineScope

https://proandroiddev.com/part-2-coroutine-cancellation-and-structured-concurrency-2dbc6583c07d

coroutineScope function can be used to create a custom scope that suspends and only completes when all coroutines launched within that scope complete. If any of the children coroutines within the coroutineScope throws an exception, all other running sibling coroutines gets cancelled and this exception is propagated up the hierarchy. If the parent coroutine at the top of the hierarchy does not handle this error, it will also be cancelled.

awaitAll

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/await-all.html

Awaits for completion of given deferred values without blocking a thread and resumes normally with the list of values when all deferred computations are complete or resumes with the first thrown exception if any of computations complete exceptionally including cancellation.

This function is not equivalent to deferreds.map { it.await() } which fails only when it sequentially gets to wait for the failing deferred, while this awaitAll fails immediately as soon as any of the deferreds fail.

This suspending function is cancellable. If the Job of the current coroutine is cancelled or completed while this suspending function is waiting, this function immediately resumes with CancellationException.

Read more

How to show generic list in Fragment in Android

Issue #378

After having a generic RecyclerView, if we want to show multiple kinds of data in Fragment, we can use generic.

We may be tempted to use interface or protocol, but should prefer generic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class FeedFragment() : Fragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)

val mainViewModel: MainViewModel = ViewModelProviders.of(activity!!).get(MainViewModel::class.java)
mainViewModel.resId.observe(viewLifecycleOwner, Observer {
when (it) {
R.id.gitHub -> { handleGitHub() }
R.id.hackerNews -> { handleHackerNews() }
R.id.reddit -> { handleReddit() }
R.id.dev -> { handleDev() }
R.id.productHunt -> { handleProductHunt() }
else -> {}
}
})

recyclerView.layoutManager = LinearLayoutManager(context)
}
}

The difference between each kind are

  • The type of model
  • The type of Adapter
  • How to observe from viewModel
  • How to load from viewModel

Here we also use lifecycleScope from lifecycle runtime ktx

1
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private fun <T> handle(
makeResId: () -> Int,
makeAdapter: () -> com.myapp.Adapter<T>,
observe: ((ArrayList<T>) -> Unit) -> Unit,
load: suspend () -> Unit
) {
(activity as AppCompatActivity).toolbar.title = getString(makeResId())
val adapter = makeAdapter()
recyclerView.adapter = adapter
observe {
adapter.update(it)
}
fun doLoad() {
viewLifecycleOwner.lifecycleScope.launch {
progressBar.visibility = View.VISIBLE
load()
progressBar.visibility = View.GONE
swipeRefreshLayout.isRefreshing = false
}
}
doLoad()
swipeRefreshLayout.setOnRefreshListener {
doLoad()
}
}

Then we just need to provide the required data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private fun handleDev() {
val viewModel: com.myapp.ViewModel by viewModel()
handle(
{ R.string.menu_dev },
{ com.myapp.Adapter(items = arrayListOf()) },
{ completion ->
viewModel.items.observe(viewLifecycleOwner, Observer {
completion(it)
})
},
{
viewModel.load()
}
)
}

Read more

How to use Product Hunt GraphQL API with Retrofit

Issue #370

Define response model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import com.squareup.moshi.Json

data class Response(
@field:Json(name="data") val data: ResponseData
)

data class ResponseData(
@field:Json(name="posts") val posts: Posts
)

data class Posts(
@field:Json(name="edges") val edges: List<Edge>
)

data class Edge(
@field:Json(name="node") val node: Item
)

data class Item(
@field:Json(name="id") val id: String,
@field:Json(name="name") val name: String,
@field:Json(name="url") val url: String,
@field:Json(name="tagline") val tagline: String,
@field:Json(name="featuredAt") val featuredAt: String,
@field:Json(name="votesCount") val votesCount: Int,
@field:Json(name="commentsCount") val commentsCount: Int,
@field:Json(name="thumbnail") val thumbnail: Thumbnail
)

data class Thumbnail(
@field:Json(name="url") val ur: String
)

Here is the query

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
posts {
edges {
node {
id
name
url
tagline
featuredAt
votesCount
commentsCount
thumbnail {
url
}
}
}
}
}

Here’s how request looks in Insomnia

1
2
3
4
5
6
7
8
9
10
> POST /v2/api/graphql HTTP/1.1
> Host: api.producthunt.com
> User-Agent: insomnia/6.6.2
> Cookie: __cfduid=d9a588136cbb286b156d8e4a873d52a301566795296
> Accept: application/json
> Content-Type: application/json
> Authorization: Bearer 068665d215cccad9123449841463b1248da07123418915a192a1233dedfd23b2
> Content-Length: 241

| {"query":"{\n posts {\n edges {\n node {\n id\n name\n url\n tagline\n featuredAt\n votesCount\n commentsCount\n thumbnail {\n url\n }\n }\n }\n }\n}"}

To post as json, need to use object for Moshi to convert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data class GetTopBody(
@field:Json(name="query") val queryString: String
)

interface Api {
@Headers(
"Content-Type: application/json",
"Accept: application/json",
"Authorization: Bearer 068665d215cccad9123449841463b1248da07123418915a192a1233dedfd23b2",
"Host: api.producthunt.com",
"User-Agent: insomnia/6.6.2"
)

@POST("./")
suspend fun getTop(
@Body body: GetTopBody
): Response
}

And consume it in ViewModel. Use multiline string interpolation. No need to set contentLength

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ViewModel(val repo: Repo): ViewModel() {
val items = liveData {
val queryString = """
{
posts {
edges {
node {
id
name
url
tagline
featuredAt
votesCount
commentsCount
thumbnail {
url
}
}
}
}
}
""".trimIndent()

val body = GetTopBody(queryString)

try {
val response = repo.api().getTop(body)
val items = response.data.posts.edges.map { it.node }
emit(items.toCollection(ArrayList()))
} catch (e: Exception) {
emit(arrayListOf<Item>())
}

}
}

The response looks like

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"data": {
"posts": {
"edges": [
{
"node": {
"id": "158359",
"name": "Toast",
"url": "https://www.producthunt.com/posts/toast-2?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+PH+API+Explorer+%28ID%3A+9162%29",
"tagline": "Organise tabs into organised sessions",
"featuredAt": "2019-08-25T07:00:00Z",
"votesCount": 318,
"commentsCount": 16,
"thumbnail": {
"url": "https://ph-files.imgix.net/a169654a-850d-4b1c-80ba-be289f973fb7?auto=format&fit=crop"
}
}
},
{
"node": {
"id": "165621",
"name": "Tree",
"url": "https://www.producthunt.com/posts/tree-2?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+PH+API+Explorer+%28ID%3A+9162%29",
"tagline": "Write documents in tree-like organisation with Markdown",
"featuredAt": "2019-08-25T09:10:53Z",
"votesCount": 227,
"commentsCount": 11,
"thumbnail": {
"url": "https://ph-files.imgix.net/68b1f007-e630-4c79-8a27-756ec364343f?auto=format&fit=crop"
}
}
}
]
}
}
}

Map

Instead of using an object, we can use Map. If using HashMap, I get

Unable to create @Body converter for java.util.HashMap<java.lang.String, java.lang.String>

1
2
3
4
5
6
@POST("./")
suspend fun getTop(
@Body body: Map<String, String>
): Response

val body = mapOf("query" to queryString)

Troubleshooting

Use Network Profiler to inspect failure View > Tool Windows > Profiler

query

Read more

How to get trending repos on GitHub using Retrofit

Issue #367

1
https://api.github.com/search/repositories?sort=stars&order=desc&q=language:javascript,java,swift,kotlin&q=created:>2019-08-21
1
2
3
4
5
6
7
8
interface Api {
@GET("https://api.github.com/search/repositories")
suspend fun getTrendingRepos(
@Query("sort") sort: String,
@Query("order") order: String,
@Query("q") qs: List<String>
): Response
}
1
2
3
4
5
6
7
8
9
10

class Repo {
fun api(): Api {
return Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(Api::class.java)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ViewModel(val repo: Repo, val dateProvider: DateProvider): ViewModel() {
val items = MutableLiveData<ArrayList<Item>>()

suspend fun load() {
try {
val order = "desc"
val sort = "star"

val formatter = SimpleDateFormat("YYYY-MM-dd")
val qs = listOf(
"language:javascript,java,swift,kotlin",
"q=created:>${formatter.format(dateProvider.yesterday)}"
)

val response = repo.api().getTrendingRepos(sort=sort, order=order, qs=qs)
this.items.value = response.items.toCollection(ArrayList())
} catch (e: Exception) {
this.items.value = arrayListOf()
}
}
}

How to use Retrofit in Android

Issue #366

Code uses Retrofit 2.6.0 which has Coroutine support

app/build.gradle

1
2
3
4
5
6
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01"

implementation "com.squareup.moshi:moshi:$Version.moshi"

implementation "com.squareup.retrofit2:retrofit:$Version.retrofit"
implementation "com.squareup.retrofit2:converter-moshi:$Version.retrofit"

Api.kt

1
2
3
4
5
6
import retrofit2.http.GET

interface Api {
@GET("api/articles")
suspend fun getArticles(): List<Article>
}

Repo.kt

1
2
3
4
5
6
7
8
9
10
11
12
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory

class Repo {
fun get(): Api {
return Retrofit.Builder()
.baseUrl("https://dev.to")
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(Api::class.java)
}
}

ViewModel.kt

1
2
3
4
5
6
7
8
9
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import kotlinx.coroutines.Dispatchers

class ViewModel(val repo: Repo): ViewModel() {
val articles = liveData(Dispatchers.Main) {
emit(repo.get().getArticles().toCollection(ArrayList()))
}
}

Article.kt

1
2
3
4
5
6
import com.squareup.moshi.Json

data class Article(
@field:Json(name="type_of") val typeOf: String,
@field:Json(name="title") val title: String
)

How to inject view model with Koin in Android

Issue #359

app/build.gradle

1
2
3
implementation "org.koin:koin-core:$Version.koin"
implementation "org.koin:koin-androidx-scope:$Version.koin"
implementation "org.koin:koin-androidx-viewmodel:$Version.koin"

MyApplication.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import android.app.Application
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.dsl.module

class MyApplication: Application() {
var appModule = module {
single { MyRepo() }
viewModel { MyViewModel(get()) }
}

override fun onCreate() {
super.onCreate()

startKoin {
androidLogger()
androidContext(this@MyApplication)
modules(appModule)
}
}
}

MyFragment.kt

1
2
3
import org.koin.androidx.viewmodel.ext.android.viewModel

val viewModel: MyViewModel by viewModel()

How to use coroutine LiveData in Android

Issue #358

app/build.gradle

1
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01"
1
2
3
4
5
6
7
8
9
10
11
12
13
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import kotlinx.coroutines.Dispatchers

class MainViewModel : ViewModel() {
val repository: TodoRepository = TodoRepository()

val firstTodo = liveData(Dispatchers.IO) {
val retrivedTodo = repository.getTodo(1)

emit(retrivedTodo)
}
}

Use coroutines with LiveData

https://developer.android.com/topic/libraries/architecture/coroutines

The liveData building block serves as a structured concurrency primitive between coroutines and LiveData. The code block starts executing when LiveData becomes active and is automatically canceled after a configurable timeout when the LiveData becomes inactive.

Source code

https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/CoroutineLiveData.kt
https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/FlowLiveData.kt

CoroutineLiveData.kt

1
2
3
4
5
6
@UseExperimental(ExperimentalTypeInference::class)
fun <T> liveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
@BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)

a LiveData that tries to load the User from local cache first and then tries from the server and also yields the updated value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
val user = liveData {
// dispatch loading first
emit(LOADING(id))
// check local storage
val cached = cache.loadUser(id)

if (cached != null) {
emit(cached)
}

if (cached == null || cached.isStale()) {
val fresh = api.fetch(id) // errors are ignored for brevity
cache.save(fresh)
emit(fresh)
}
}

Read more

How to declare generic RecyclerView adapter in Android

Issue #357

generic/Adapter.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.onmyway133.generic

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView

abstract class Adapter<T>(var items: ArrayList<T>): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
abstract fun configure(item: T, holder: ViewHolder)

fun update(items: ArrayList<T>) {
this.items = items
notifyDataSetChanged()
}

override fun getItemCount(): Int = items.count()

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater
.from(parent.context)
.inflate(viewType, parent, false)
return ViewHolder(view)
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
configure(items[position], holder as ViewHolder)
}

}

class ViewHolder(view: View): RecyclerView.ViewHolder(view) {}

hero/HeroAdapter.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.onmyway133.hero
import kotlinx.android.synthetic.main.hero_item_view.view.*

class Adapter(items: ArrayList<Hero>): com.onmyway133.generic.Adapter<Hero>(items) {
override fun configure(item: Hero, holder: ViewHolder) {
holder.itemView.titleLabel.text = item.name
holder.itemView.descriptionLabel.text = item.description
}

override fun getItemViewType(position: Int): Int {
return R.layout.hero_item_view
}
}

May run into https://stackoverflow.com/questions/49512629/default-interface-methods-are-only-supported-starting-with-android-n

app/build.gradle

1
2
3
4
5
6
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

How to use Navigation component with DrawerLayout in Android

Issue #349

Screenshot_1565169686

build.gradle

1
2
3
dependencies {
classpath 'android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha05'
}

app/build.gradle

1
2
3
4
5
6
7
8
9
10
apply plugin: 'androidx.navigation.safeargs'

dependencies {
def navigationVersion = "2.0.0"
def drawerLayoutVersion = "1.0.0"

implementation "androidx.drawerlayout:drawerlayout:$drawerLayoutVersion"
implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion"
}

main_activity.xml

  • Use CoordinatorLayout and ToolBar
  • Define layout_gravity for NavigationView
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawerLayout"
tools:context=".MainActivity">

<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:id="@+id/toolbar"/>
</com.google.android.material.appbar.AppBarLayout>
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/hostFragment"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation_graph"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/navigationView"
android:fitsSystemWindows="true"
android:layout_gravity="start"
app:menu="@menu/drawer_menu"/>
</androidx.drawerlayout.widget.DrawerLayout>

navigation/navigation_graph.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/navigationGraph"
xmlns:tools="http://schemas.android.com/tools"
app:startDestination="@id/aboutFragment">
<fragment
android:id="@+id/aboutFragment"
android:name="com.onmyway133.whatsupintech.AboutFragment"
android:label="@string/menu_about"
tools:layout="@layout/about_fragment" />
<fragment
android:id="@+id/feedFragment"
android:name="com.onmyway133.whatsupintech.FeedFragment"
android:label="@string/menu_git_hub"
tools:layout="@layout/feed_fragment" />
<fragment
android:id="@+id/webFragment"
android:name="com.onmyway133.whatsupintech.WebFragment"
tools:layout="@layout/web_fragment"/>
</navigation>

menu/drawer_menu.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<group android:checkableBehavior="single">
<item
android:id="@+id/about"
android:title="@string/menu_about" />
<item
android:id="@+id/hackerNews"
android:title="@string/menu_hacker_news" />
<item
android:id="@+id/reddit"
android:title="@string/menu_reddit" />
<item
android:id="@+id/dev"
android:title="@string/menu_dev" />
<item
android:id="@+id/gitHub"
android:title="@string/menu_git_hub" />
</group>
</menu>

MainActivity.kotlin

  • Use AppBarConfiguration to define multiple top level destinations
  • Convert Toolbar to ActionBar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.onmyway133.whatsupintech

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.GravityCompat
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import kotlinx.android.synthetic.main.main_activity.*

class MainActivity : AppCompatActivity() {

lateinit var appBarConfig: AppBarConfiguration

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
setupNavigationController()
}

fun setupNavigationController() {
val navigationController = findNavController(R.id.hostFragment)

setSupportActionBar(toolbar)

appBarConfig = AppBarConfiguration(setOf(R.id.aboutFragment, R.id.feedFragment), drawerLayout)
setupActionBarWithNavController(navigationController, appBarConfig)
navigationView.setupWithNavController(navigationController)
navigationView.setNavigationItemSelectedListener { menuItem ->
drawerLayout.closeDrawers()
menuItem.isChecked = true
when (menuItem.itemId) {
R.id.about -> navigationController.navigate(R.id.aboutFragment)
R.id.gitHub, R.id.reddit, R.id.hackerNews, R.id.dev -> navigationController.navigate(R.id.feedFragment)
}

true
}
}

override fun onSupportNavigateUp(): Boolean {
val navigationController = findNavController(R.id.hostFragment)
return navigationController.navigateUp(appBarConfig) || super.onSupportNavigateUp()
}

override fun onBackPressed() {
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
drawerLayout.closeDrawer(GravityCompat.START)
} else {
return super.onBackPressed()
}
}
}

Update UI components with NavigationUI

Tie destinations to menu items

NavigationUI also provides helpers for tying destinations to menu-driven UI components. NavigationUI contains a helper method, onNavDestinationSelected(), which takes a MenuItem along with the NavController that hosts the associated destination. If the id of the MenuItem matches the id of the destination, the NavController can then navigate to that destination.

Add a navigation drawer

The drawer icon is displayed on all top-level destinations that use a DrawerLayout. Top-level destinations are the root-level destinations of your app. They do not display an Up button in the app bar.

Read more

How to organise test files

Issue #327

In terms of tests, we usually have files for unit test, UI test, integeration test and mock.

Out of sight, out of mind.

Unit tests are for checking specific functions and classes, it’s more convenient to browse them side by side with source file. For example in Javascript, Kotlin and Swift

1
2
3
index.js
index.test.js
index.mock.js
1
2
3
LocationManager.kt
LocationManager.mock.kt
LocationManager.test.kt
1
2
3
BasketHandler.swift
BasketHandler.mock.swift
BasketHandler.test.swift

Integration tests check features or sub features, and may cover many source files, it’s better to place them in feature folders

1
2
3
4
5
6
7
8
9
10
11
- Features
- Cart
- Sources
- Tests
- Cart.test.swift
- Validator.test.swift
- Profile
- Sources
- Tests
- Updater.test.swift
- AvatarUploader.test.swift

How to use Gradle Kotlin DSL in Android

Issue #285

kts

settings.gradle.kts

1
include(":app")

build.gradle.kts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.kotlin
import org.gradle.kotlin.dsl.*
import org.jetbrains.kotlin.config.KotlinCompilerVersion

plugins {
id("com.android.application")
kotlin("android")
kotlin("android.extensions")
}

//apply {
// from("$rootDir/tools/grgit.gradle")
// from("$rootDir/buildSrc/quality.gradle.kts")
// from("$rootDir/tools/ktlint.gradle")
// from("$rootDir/tools/detekt.gradle")
//}

android {
compileSdkVersion(28)
flavorDimensions("default")

defaultConfig {
applicationId = "com.onmyway133.myapp"
minSdkVersion(26)
targetSdkVersion(28)
// versionCode = ext.get("gitCommitCount") as? Int
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

signingConfigs {
create("release") {
keyAlias = "keyalias"
keyPassword = "keypassword"
storePassword = "storepassword"
storeFile = file("/Users/khoa/Android/Key/keystore")
}
}

buildTypes {
getByName("debug") {
signingConfig = signingConfigs.getByName("debug")
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "$project.rootDir/tools/proguard-rules-debug.pro")
}

getByName("release") {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "$project.rootDir/tools/proguard-rules.pro")
}
}

productFlavors {
create("staging") {

}

create("production") {

}
}

lintOptions {
lintConfig = file("$project.rootDir/tools/lint-rules.xml")
htmlOutput = file("$project.buildDir/outputs/lint/lint.html")
xmlReport = false
htmlReport = true
}
}

dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
implementation(kotlin("stdlib-jdk7", KotlinCompilerVersion.VERSION))
implementation("androidx.appcompat:appcompat:1.0.2")
implementation("androidx.core:core-ktx:1.0.2")
implementation("androidx.constraintlayout:constraintlayout:1.1.3")
implementation("com.google.android.material:material:1.0.0")
testImplementation("junit:junit:4.12")
androidTestImplementation("androidx.test:runner:1.1.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.1.1")
}

tasks.getByName("check").dependsOn("lint")

tools/quality.gradle.kts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
plugins {
id("findbugs")
id("pmd")
id("checkstyle")
}

tasks {
val findbugs by registering(FindBugs::class) {
ignoreFailures = false
effort = "max"
reportLevel = "low"
classes = files("$project.buildDir/intermediates/javac")

setExcludeFilter(file("$rootProject.rootDir/tools/findbugs-exclude.xml"))

source = fileTree("src/main/java/")
classpath = files()

reports {
xml.isEnabled = false
html.isEnabled = true
html.destination = file("$project.buildDir/outputs/findbugs/findbugs-output.html")
}
}

val pmd by registering(Pmd::class) {
ruleSetFiles = files("${project.rootDir}/tools/pmd-rules.xml")
ignoreFailures = false
ruleSets = listOf<String>()

fileTree()
source(fileTree(baseDir = "src/main/java"))
include("**/*.kt")
exclude("**/gen/**")

reports {
xml.isEnabled = false
html.isEnabled = true
html.destination = file("$project.buildDir/outputs/pmd/pmd.html")
}
}

val checkstyle by registering(Checkstyle::class) {
description = "Check code standard"
group = "verification"

configFile = file("$project.rootDir/tools/checkstyle.xml")
source(fileTree(baseDir = "src"))
include("**/*.kt")
exclude("**/gen/**")

classpath = files()
ignoreFailures = false
}
}

Reference

About Gradle

Using camelCase for abbreviations

Issue #147

Each language and platform has its own coding style guide. This goes true when it comes to abbreviations. I’ve had some debates about whether to use JSON or Json, URL or Url, HTTP or Http.

I personally prefer camelCase, so I’m very happy to see that Kotlin is on my side. See Kotlin Style guide, I think this guide should be applied in other languages, such as Swift 😛

Sometimes there is more than one reasonable way to convert an English phrase into camel case, such as when acronyms or unusual constructs like “IPv6” or “iOS” are present. To improve predictability, use the following scheme.

Beginning with the prose form of the name:

  1. Convert the phrase to plain ASCII and remove any apostrophes. For example, “Müller’s algorithm” might become “Muellers algorithm”.
  1. Divide this result into words, splitting on spaces and any remaining punctuation (typically hyphens).

Recommended: if any word already has a conventional camel-case appearance in common usage, split this into its constituent parts (e.g., “AdWords” becomes “ad words”). Note that a word such as “iOS” is not really in camel case per se; it defies any convention, so this recommendation does not apply.

  1. Now lowercase everything (including acronyms), then uppercase only the first character of:

…each word, to yield pascal case, or

…each word except the first, to yield camel case

  1. Finally, join all the words into a single identifier.

Note that the casing of the original words is almost entirely disregarded.

Prose form Correct Incorrect
“XML Http Request” XmlHttpRequest XMLHTTPRequest
“new customer ID” newCustomerId newCustomerID
“inner stopwatch” innerStopwatch innerStopWatch
“supports IPv6 on iOS” supportsIpv6OnIos supportsIPv6OnIOS
“YouTube importer” YouTubeImporterYoutubeImporter*

About iOS or IOS, I think I would go with IOS. I think React Native thinks so too

NavigatorIOS looks and feels just like UINavigationController, because it is actually built on top of it.

Read more

How to use Function Literals with Receiver in Kotlin

Issue #139

From https://kotlinlang.org/docs/reference/lambdas.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class HTML {
fun body() { ... }
}

fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // create the receiver object
html.init() // pass the receiver object to the lambda
return html
}


html { // lambda with receiver begins here
body() // calling a method on the receiver object
}

From https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/src/kotlin/util/Standard.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}

val person = Person().apply {
name = "Superman"
age = 20
}

From https://academy.realm.io/posts/kau-jake-wharton-testing-robots/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fun payment(func: PaymentRobot.() -> Unit) = PaymentRobot().apply { func() }

class PaymentRobot {
fun amount(amount: Long) {

}

fun recipient(recipient: String) {

}

infix fun send(func: ResultRobot.() -> Unit): ResultRobot {
// ...
return ResultRobot().apply { func() }
}
}

class ResultRobot {
func isSuccess() {

}
}

payment {
amount(4200)
recipient(superman@google.com)
} send {
isSuccess()
}

Learning from Open Source Making Deferred in Kotlin

Issue #135

From https://github.com/JakeWharton/retrofit2-kotlin-coroutines-adapter/blob/master/src/main/java/com/jakewharton/retrofit2/adapter/kotlin/coroutines/experimental/CoroutineCallAdapterFactory.kt#L86

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
override fun adapt(call: Call<T>): Deferred<T> {
val deferred = CompletableDeferred<T>()

deferred.invokeOnCompletion {
if (deferred.isCancelled) {
call.cancel()
}
}

call.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
deferred.completeExceptionally(t)
}

override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
deferred.complete(response.body()!!)
} else {
deferred.completeExceptionally(HttpException(response))
}
}
})

return deferred
}

Understanding suspend function in Kotlin Coroutine in Android

Issue #123

Getting to know Coroutine

From https://kotlinlang.org/docs/reference/coroutines.html

To continue the analogy, await() can be a suspending function (hence also callable from within an async {} block) that suspends a coroutine until some computation is done and returns its result:

From https://kotlinlang.org/docs/tutorials/coroutines-basic-jvm.html

We are using the delay() function that’s like Thread.sleep(), but better: it doesn’t block a thread, but only suspends the coroutine itself. The thread is returned to the pool while the coroutine is waiting, and when the waiting is done, the coroutine resumes on a free thread in the pool.

await() can not be called outside a coroutine, because it needs to suspend until the computation finishes, and only coroutines can suspend in a non-blocking way

What does suspend function mean in Kotlin Coroutine 🤔

https://stackoverflow.com/questions/47871868/what-does-suspend-function-mean-in-kotlin-coroutine

I’m reading Kotlin Coroutine and know that it is based on suspend function. But what does suspend mean?

Coroutine or function gets suspended?

From https://kotlinlang.org/docs/reference/coroutines.html

Basically, coroutines are computations that can be suspended without blocking a thread

I heard people often say “suspend function”. But I think it is the coroutine who gets suspended because it is waiting for the function to finished? “suspend” usually means “cease operation”, in this case the coroutine is idle.

Should we say the coroutine is suspended ?

Which coroutine gets suspended?

From https://kotlinlang.org/docs/reference/coroutines.html

To continue the analogy, await() can be a suspending function (hence also callable from within an async {} block) that suspends a coroutine until some computation is done and returns its result:

1
2
3
4
5
6
async { // Here I call it the outer async coroutine
...
// Here I call computation the inner coroutine
val result = computation.await()
...
}

🤔 It says “that suspends a coroutine until some computation is done”, but coroutine is like a lightweight thread. So if the coroutine is suspended, how can the computation is done ?

We see await is called on computation, so it might be async that returns Deferred, which means it can start another coroutine

1
2
3
4
5
fun computation(): Deferred<Boolean> {
return async {
true
}
}

🤔 The quote say that suspends a coroutine. Does it mean suspend the outer async coroutine, or suspend the inner computation coroutine?

Does suspend mean that while outer async coroutine is waiting (await) for the inner computation coroutine to finish, it (the outer async coroutine) idles (hence the name suspend) and returns thread to the thread pool, and when the child computation coroutine finishes, it (the outer async coroutine) wakes up, takes another thread from the pool and continues?

The reason I mention the thread is because of https://kotlinlang.org/docs/tutorials/coroutines-basic-jvm.html

The thread is returned to the pool while the coroutine is waiting, and when the waiting is done, the coroutine resumes on a free thread in the pool

Understanding async

From https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md

Conceptually, async is just like launch. It starts a separate coroutine which is a light-weight thread that works concurrently with all the other coroutines. The difference is that launch returns a Job and does not carry any resulting value, while async returns a Deferred – a light-weight non-blocking future that represents a promise to provide a result later. You can use .await() on a deferred value to get its eventual result, but Deferred is also a Job, so you can cancel it if needed.

1
2
3
4
5
6
7
8
fun main(args: Array<String>) = runBlocking<Unit> {
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
}

There is a laziness option to async using an optional start parameter with a value of CoroutineStart.LAZY. It starts coroutine only when its result is needed by some await or if a start function is invoked. Run the following example that differs from the previous one only by this option:

1
2
3
4
5
6
7
8
fun main(args: Array<String>) = runBlocking<Unit> {
val time = measureTimeMillis {
val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
}

What is the difference between launch/join and async/await in Kotlin coroutines

https://stackoverflow.com/a/48079738/1418457

I find this guide https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md to be useful. I will quote the essential parts

🦄 coroutine

Essentially, coroutines are light-weight threads.

So you can think of coroutine as something that manages thread in a very efficient way.

🐤 launch

1
2
3
4
5
6
7
8
fun main(args: Array<String>) {
launch { // launch new coroutine in background and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello,") // main thread continues while coroutine is delayed
Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}

So launch starts a background thread, does something, and returns a token immediately as Job. You can call join on this Job to block until this launch thread completes

1
2
3
4
5
6
7
8
fun main(args: Array<String>) = runBlocking<Unit> {
val job = launch { // launch new coroutine and keep a reference to its Job
delay(1000L)
println("World!")
}
println("Hello,")
job.join() // wait until child coroutine completes
}

🦆 async

Conceptually, async is just like launch. It starts a separate coroutine which is a light-weight thread that works concurrently with all the other coroutines. The difference is that launch returns a Job and does not carry any resulting value, while async returns a Deferred – a light-weight non-blocking future that represents a promise to provide a result later.

So async starts a background thread, does something, and returns a token immediately as Deferred.

1
2
3
4
5
6
7
8
fun main(args: Array<String>) = runBlocking<Unit> {
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
}

You can use .await() on a deferred value to get its eventual result, but Deferred is also a Job, so you can cancel it if needed.

So Deferred is actually a Job. See https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-deferred/index.html

1
interface Deferred<out T> : Job (source)

🦋 async is eager by default

There is a laziness option to async using an optional start parameter with a value of CoroutineStart.LAZY. It starts coroutine only when its result is needed by some await or if a start function is invoked.

Understand Kotlin Coroutines on Android (Google I/O’19)

https://www.youtube.com/watch?v=BOHK_w09pVA

cor

Understanding let, apply, with, run in Kotlin

Issue #114

Picture worths thousand words. Code worths thousand pictures. I don’t understand much until I take a look at Standard.kt in Kotlin standard library.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Calls the specified function [block] with `this` value as its receiver and returns `this` value.
*/
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}

/**
* Calls the specified function [block] with `this` value as its argument and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}

Kotlin provides the ability to call a function literal with a specified receiver object. Inside the body of the function literal, you can call methods on that receiver object without any additional qualifiers. This is similar to extension functions, which allow you to access members of the receiver object inside the body of the function

Communication between Fragment and Activity

Issue #108

There’s always need for communication, right 😉 Suppose we have OnboardingActivity that has several OnboardingFragment. Each Fragment has a startButton telling that the onboarding flow has finished, and only the last Fragment shows this button.

Here are several ways you can do that

1. EventBus 🙄

Nearly all articles I found propose this https://github.com/greenrobot/EventBus, but I personally don’t like this idea because components are loosely coupled, every component and broadcast can listen to event from a singleton, which makes it very hard to reason when the project scales

1
data class OnboardingFinishEvent()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class OnboardingActivity: AppCompatActivity() {
override fun onStart() {
super.onStart()

EventBus.getDefault().register(this)
}

override fun onStop() {
EventBus.getDefault().unregister(this)
super.onStop()
}

@Subscribe(threadMode = ThreadMode.MAIN)
fun onOnboardingFinishEvent(event: OnboardingFinishEvent) {
// finish
}
}
1
2
3
4
5
6
7
8
9
class OnboardingFragment: Fragment() {
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

startButton.onClick {
EventBus.getDefault().post(OnboardingFinishEvent())
}
}
}

Read more

2. Otto 🙄

This https://github.com/square/otto was deprecated in favor of RxJava and RxAndroid

3. RxJava 🙄

We can use simple PublishSubject to create our own RxBus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject

// Use object so we have a singleton instance
object RxBus {

private val publisher = PublishSubject.create<Any>()

fun publish(event: Any) {
publisher.onNext(event)
}

// Listen should return an Observable and not the publisher
// Using ofType we filter only events that match that class type
fun <T> listen(eventType: Class<T>): Observable<T> = publisher.ofType(eventType)
}
1
2
3
4
// OnboardingFragment.kt
startButton.onClick {
RxBus.publish(OnboardingFinishEvent())
}
1
2
3
4
5
6
7
8
// OnboardingActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

RxBus.listen(OnboardingFinishEvent::class.java).subscribe({
// finish
})
}

4. Interface

This is advised here Communicating with Other Fragments. Basically you define an interface OnboardingFragmentDelegate that whoever conforms to that, can be informed by the Fragment of events. This is similar to Delegate pattern in iOS 😉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface OnboardingFragmentDelegate {
fun onboardingFragmentDidClickStartButton(fragment: OnboardingFragment)
}

class OnboardingFragment: Fragment() {
var delegate: OnboardingFragmentDelegate? = null

override fun onAttach(context: Context?) {
super.onAttach(context)

if (context is OnboardingFragmentDelegate) {
delegate = context
}
}

override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

startButton.onClick {
delegate?.onboardingFragmentDidClickStartButton(this)
}
}
}
1
2
3
4
5
6
class OnboardingActivity: AppCompatActivity(), OnboardingFragmentDelegate {
override fun onboardingFragmentDidClickStartButton(fragment: OnboardingFragment) {
onboardingService.hasShown = true
startActivity<LoginActivity>()
}
}

5. ViewModel

We can learn from Share data between fragments to to communication between Fragment and Activity, by using a shared ViewModel that is scoped to the activity. This is a bit overkill

1
2
3
class OnboardingSharedViewModel: ViewModel() {
val finish = MutableLiveData<Unit>()
}
1
2
3
4
5
6
7
8
9
10
class OnboardingActivity: AppCompatActivity(), OnboardingFragmentDelegate {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = ViewModelProviders.of(this).get(OnboardingSharedViewModel::class.java)

viewModel.finish.observe(this, Observer {
startActivity<LoginActivity>()
})
}
}

Note that we need to call ViewModelProviders.of(activity) to get the same ViewModel with the activity

1
2
3
4
5
6
7
8
9
10
class OnboardingFragment: Fragment() {
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val viewModel = ViewModelProviders.of(activity).get(OnboardingSharedViewModel::class.java)
startButton.onClick({
viewModel.finish.value = Unit
})
}
}

7. Lambda

Create a lambda in Fragment, then set it on onAttachFragment. It does not work for now as there is no OnboardingFragment in onAttachFragment 😢

1
2
3
4
5
6
7
8
9
10
11
class OnboardingFragment: Fragment() {
var didClickStartButton: (() -> Unit)? = null

override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

startButton.onClick {
didClickStartButton?.invoke()
}
}
}
1
2
3
4
5
6
7
8
9
10
11
class OnboardingActivity: AppCompatActivity() {
override fun onAttachFragment(fragment: Fragment?) {
super.onAttachFragment(fragment)

(fragment as? OnboardingFragment).let {
it?.didClickStartButton = {
// finish
}
}
}
}

8. Listener in Bundle 🙄

Read more