Created
May 24, 2023 10:22
-
-
Save MrOnyszko/0821c2cb0b258eddc794b3725d273f71 to your computer and use it in GitHub Desktop.
RxJava GitHub Search, MVP - the old way.
This file contains 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 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