Skip to content

Instantly share code, notes, and snippets.

@MrOnyszko
Created May 24, 2023 10:22
Show Gist options
  • Save MrOnyszko/0821c2cb0b258eddc794b3725d273f71 to your computer and use it in GitHub Desktop.
Save MrOnyszko/0821c2cb0b258eddc794b3725d273f71 to your computer and use it in GitHub Desktop.
RxJava GitHub Search, MVP - the old way.
package pl.onyszko.rxjavasearch
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.widget.doOnTextChanged
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.gson.annotations.SerializedName
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import retrofit2.Retrofit
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
import java.util.concurrent.TimeUnit
fun log(message: String) {
Log.d("TAG", message)
}
class MainActivity : AppCompatActivity(), GithubSearchView {
private val retrofit: Retrofit by lazy {
Retrofit.Builder().baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.build()
}
private val presenter: GithubSearchPresenter by lazy {
GithubSearchPresenter(
githubSearchUseCase = GithubSearchUseCase(
githubSearchApi = retrofit.create(GithubSearchApi::class.java),
),
)
}
private val adapter by lazy { ListAdapter() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.search_results_recycler_view)
val layoutManager = LinearLayoutManager(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = layoutManager
recyclerView.addItemDecoration(
androidx.recyclerview.widget.DividerItemDecoration(
this,
androidx.recyclerview.widget.DividerItemDecoration.VERTICAL,
),
)
presenter.attachView(this)
}
override fun onDestroy() {
presenter.detachView()
super.onDestroy()
}
override fun queryEvents(): Observable<String> {
val observable = Observable.create { emitter ->
val textView = findViewById<AppCompatEditText>(R.id.search_edit_text)
val watcher = textView.doOnTextChanged { text, _, _, _ ->
if (!emitter.isDisposed) {
emitter.onNext(text.toString())
}
}
emitter.setCancellable { textView.removeTextChangedListener(watcher) }
}
return observable
.filter { it.length >= 3 || it.isBlank() }
.debounce(300, TimeUnit.MILLISECONDS)
.distinctUntilChanged()
.map(CharSequence::toString)
.doOnNext {
log("Query: $it")
}
}
override fun render(state: GithubSearchState) {
when (state) {
is GithubSearchState.Error -> {
log("Error: ${state.error.message}")
Toast.makeText(this, "Error: ${state.error.message}", Toast.LENGTH_SHORT).show()
adapter.setItems(emptyList())
}
GithubSearchState.Loading -> {
log("Loading")
}
is GithubSearchState.Success -> {
log("Success: ${state.data}")
adapter.setItems(state.data)
}
}
}
class ListAdapter : RecyclerView.Adapter<ListAdapter.ViewHolder>() {
private var _items: List<GithubSearchResult> = emptyList()
fun setItems(items: List<GithubSearchResult>) {
_items = items
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.result_item, parent, false))
}
override fun getItemCount(): Int = _items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(_items[position])
}
class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
fun bind(item: GithubSearchResult) {
view.findViewById<TextView>(R.id.result_text_view).text = "${item.id} - ${item.login}"
}
}
}
}
sealed class GithubSearchState {
object Loading : GithubSearchState()
data class Success(val data: List<GithubSearchResult>) : GithubSearchState()
data class Error(val error: Throwable) : GithubSearchState()
}
interface GithubSearchView {
fun queryEvents(): Observable<String>
fun render(state: GithubSearchState)
}
class GithubSearchPresenter(
private val githubSearchUseCase: GithubSearchUseCase,
) {
private val disposables = CompositeDisposable()
private var _view: GithubSearchView? = null
fun attachView(view: GithubSearchView) {
_view = view
disposables += view.queryEvents()
.flatMap {
githubSearchUseCase.execute(it)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.toObservable()
}
.map<GithubSearchState> { GithubSearchState.Success(it) }
.onErrorReturn { GithubSearchState.Error(it) }
.startWithItem(GithubSearchState.Loading)
.subscribeBy(
onNext = { view.render(it) },
)
}
fun detachView() {
_view = null
disposables.clear()
}
}
data class GithubSearchResult(val id: Int, val login: String)
class GithubSearchUseCase(
private val githubSearchApi: GithubSearchApi,
) {
fun execute(query: String): Single<List<GithubSearchResult>> {
if (query.isBlank()) return Single.just(emptyList())
return githubSearchApi.search(query)
.toObservable()
.flatMapIterable { it.items }
.map { element -> GithubSearchResult(element.id, element.login) }
.toList()
}
}
data class GithubSearchResponse(
@SerializedName("total_count") val totalCount: Int,
@SerializedName("items") val items: List<UserDto>,
)
data class UserDto(
@SerializedName("id") val id: Int,
@SerializedName("login") val login: String,
)
interface GithubSearchApi {
@GET("search/users")
fun search(@Query("q") query: String): Single<GithubSearchResponse>
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment