Skip to content

Commit

Permalink
Introduce iconography, increase spacing, and drop RxJava in favour of…
Browse files Browse the repository at this point in the history
… coroutines
  • Loading branch information
ashdavies committed Jun 10, 2018
1 parent 05d039b commit 00dea50
Show file tree
Hide file tree
Showing 13 changed files with 136 additions and 52 deletions.
5 changes: 1 addition & 4 deletions mobile/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,14 @@ dependencies {
implementation 'androidx.recyclerview:recyclerview:1.0.0-alpha3'

implementation 'com.google.android.material:material:1.0.0-alpha3'
implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-experimental-adapter:1.0.0'

implementation 'com.squareup.moshi:moshi:1.6.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'

implementation 'io.reactivex.rxjava2:rxjava:2.1.14'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'

implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.41'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.22.5'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.5'
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.ashdavies.databinding.repos

import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData

internal class EmptyLiveData<in T>(items: LiveData<List<T>>, loading: LiveData<Boolean>) : MediatorLiveData<Boolean>() {

init {
addSource(items) { value = it?.isEmpty() ?: false && loading.value ?: false }
addSource(loading) { value = it ?: false && items.value?.isEmpty() ?: false }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import io.ashdavies.databinding.extensions.snack
import io.ashdavies.databinding.models.Repo
import kotlinx.android.synthetic.main.activity_repo.coordinator
import kotlinx.android.synthetic.main.activity_repo.recycler
import kotlinx.android.synthetic.main.activity_repo.search
import kotlinx.android.synthetic.main.activity_repo.toolbar

internal class RepoActivity : AppCompatActivity() {
Expand All @@ -38,13 +39,13 @@ internal class RepoActivity : AppCompatActivity() {
recycler.layoutManager = LinearLayoutManager(this)
recycler.itemDecorations += DividerItemDecoration(this, VERTICAL)

search.onActionViewExpanded()

model.items.observe(this, NotNullObserver { adapter.items = it })
model.error.observe(this, NotNullObserver(::error))
}

private fun error(throwable: Throwable) {
Log.e("RepoActivity", throwable.message, throwable)

val message = throwable.message
if (message == null) {
coordinator.snack(R.string.unexpected_error)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package io.ashdavies.databinding.repos

import com.jakewharton.retrofit2.adapter.kotlin.coroutines.experimental.CoroutineCallAdapterFactory
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.moshi.MoshiConverterFactory

private const val GITHUB_API = "https://api.github.com"
Expand All @@ -12,7 +12,7 @@ internal val retrofit: Retrofit
get() = Retrofit.Builder()
.baseUrl(GITHUB_API)
.client(client)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.addConverterFactory(MoshiConverterFactory.create())
.build()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,50 +1,58 @@
package io.ashdavies.databinding.repos

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations.map
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.ashdavies.databinding.common.SingleLiveData
import io.ashdavies.databinding.extensions.create
import io.ashdavies.databinding.extensions.mutableLiveDataOf
import io.ashdavies.databinding.extensions.plusAssign
import io.ashdavies.databinding.extensions.singleLiveDataOf
import io.ashdavies.databinding.models.Repo
import io.ashdavies.databinding.services.GitHub
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.Schedulers
import java.util.concurrent.TimeUnit.MILLISECONDS
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.channels.ReceiveChannel
import kotlinx.coroutines.experimental.channels.consumeEach
import kotlinx.coroutines.experimental.channels.filter
import kotlinx.coroutines.experimental.channels.produce
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import kotlin.coroutines.experimental.CoroutineContext

internal class RepoViewModel(service: GitHub) : ViewModel() {

private val disposables = CompositeDisposable()
private val query = PublishProcessor.create<String>()
private val jobs = Job()
private val query = Channel<String>()

val items: MutableLiveData<List<Repo>> = mutableLiveDataOf()
val loading: MutableLiveData<Boolean> = mutableLiveDataOf()
val error: SingleLiveData<Throwable> = singleLiveDataOf()

val hasItems: LiveData<Boolean> = map(loading) { !it && items.value?.isNotEmpty() ?: false }
val empty: LiveData<Boolean> = EmptyLiveData(items, loading)

init {
hasItems.observeForever { Log.e("RepoViewModel", "HasItems: $it") }

disposables += query
.doOnNext { loading.postValue(true) }
.debounce(500, MILLISECONDS)
.switchMapSingle(service::getRepos)
.doOnNext { loading.postValue(false) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(items::setValue, error::setValue)
launch(UI, parent = jobs) {
query
.filter { it.length >= MIN_LENGTH }
.debounce()
.consumeEach {
try {
items.value = service.getRepos(it).await()
} catch (throwable: Throwable) {
error.value = throwable
} finally {
loading.value = false
}
}
}
}

fun onQuery(value: String): Boolean {
query.onNext(value)
launch(UI, parent = jobs) { query.send(value) }
loading.value = true
return true
}

Expand All @@ -53,4 +61,24 @@ internal class RepoViewModel(service: GitHub) : ViewModel() {
return RepoViewModel(retrofit.create()) as T
}
}

companion object {

private const val MIN_LENGTH = 3
}
}

internal fun <T> ReceiveChannel<T>.debounce(timeout: Long = 500, context: CoroutineContext = DefaultDispatcher): ReceiveChannel<T> = produce(context) {

var last: Job? = null

consumeEach {
last?.cancel()
last = launch {
delay(timeout)
send(it)
}
}

last?.join()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ package io.ashdavies.databinding.services

import io.ashdavies.databinding.models.Repo
import io.ashdavies.databinding.models.User
import io.reactivex.Single
import kotlinx.coroutines.experimental.Deferred
import retrofit2.http.GET
import retrofit2.http.Path

internal interface GitHub {

@GET("/users/{user}")
fun getUser(@Path("user") user: String): Single<User>
fun getUser(@Path("user") user: String): Deferred<User>

@GET("/users/{user}/repos")
fun getRepos(@Path("user") user: String): Single<List<Repo>>
fun getRepos(@Path("user") user: String): Deferred<List<Repo>>
}
5 changes: 5 additions & 0 deletions mobile/src/main/res/drawable/ic_stargazer.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
</vector>
5 changes: 5 additions & 0 deletions mobile/src/main/res/drawable/ic_watcher.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
</vector>
5 changes: 3 additions & 2 deletions mobile/src/main/res/layout/activity_repo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:visible="@{model.hasItems}"
app:visible="@{!model.loading}"
tools:listitem="@layout/list_item"/>

<ProgressBar
Expand All @@ -71,7 +71,8 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:visible="@{!model.hasItems}"/>
app:visible="@{model.empty}"
tools:visibility="gone"/>

</androidx.constraintlayout.ConstraintLayout>

Expand Down
57 changes: 48 additions & 9 deletions mobile/src/main/res/layout/list_item.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,50 @@

<androidx.constraintlayout.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/activity_default_margin">

<TextView
style="@style/Base.TextAppearance.AppCompat.Large"
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.name}"
app:layout_constraintEnd_toStartOf="@+id/barrier"
app:layout_constraintStart_toStartOf="parent"
tools:text="data-binding"/>

<TextView
style="@style/Base.TextAppearance.AppCompat.Medium"
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/content_spacing"
android:text="@{item.description}"
app:layout_constraintEnd_toStartOf="@+id/barrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/name"
tools:text="Android Data Binding Library Sample"/>
tools:text="Droidcon Berlin 2018: Leveraging Android Databinding with Kotlin"/>

<TextView
style="@style/Base.TextAppearance.AppCompat.Small"
android:id="@+id/updatedAt"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/content_spacing"
android:text="@{item.updatedAt}"
app:layout_constraintEnd_toStartOf="@+id/barrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/description"
tools:text="Updated at: Sun 10th June, 2018"/>

<androidx.constraintlayout.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="start"
app:constraint_referenced_ids="language,stargazersCount,watchersCount"/>

<TextView
style="@style/Base.TextAppearance.AppCompat.Small"
android:id="@+id/language"
Expand All @@ -50,24 +65,48 @@
app:layout_constraintEnd_toEndOf="parent"
tools:text="Kotlin"/>

<ImageView
android:id="@+id/stargazersIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/content_spacing"
android:contentDescription="@string/repo_item_stargazers"
android:src="@drawable/ic_stargazer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/language"/>

<TextView
style="@style/Base.TextAppearance.AppCompat.Small"
android:id="@+id/stargazersCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/content_spacing"
android:text="@{item.stargazersCount}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/language"
app:layout_constraintBottom_toBottomOf="@+id/stargazersIcon"
app:layout_constraintEnd_toStartOf="@+id/stargazersIcon"
app:layout_constraintTop_toTopOf="@+id/stargazersIcon"
tools:text="74"/>

<ImageView
android:id="@+id/watchersIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/content_spacing"
android:contentDescription="@string/repo_item_watchers"
android:src="@drawable/ic_watcher"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/stargazersIcon"/>

<TextView
style="@style/Base.TextAppearance.AppCompat.Small"
android:id="@+id/watchersCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/content_spacing"
android:text="@{item.watchersCount}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/stargazersCount"
app:layout_constraintBottom_toBottomOf="@+id/watchersIcon"
app:layout_constraintEnd_toStartOf="@+id/watchersIcon"
app:layout_constraintTop_toTopOf="@+id/watchersIcon"
tools:text="4"/>

</androidx.constraintlayout.ConstraintLayout>
Expand Down
1 change: 1 addition & 0 deletions mobile/src/main/res/values/dimens.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<resources>

<dimen name="activity_default_margin">16dp</dimen>
<dimen name="content_spacing">4dp</dimen>
</resources>
3 changes: 3 additions & 0 deletions mobile/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
<string name="application">Leveraging Android Data Binding with Kotlin</string>
<string name="activity_repos">Search GitHub repositories by user</string>

<string name="repo_item_stargazers">Stargazers</string>
<string name="repo_item_watchers">Watchers</string>

<string name="unexpected_error">An unexpected error has occured</string>
</resources>

0 comments on commit 00dea50

Please sign in to comment.