From a571f2e5863b8a121e40705c5d6561e521bdd08b Mon Sep 17 00:00:00 2001 From: sleepylee Date: Wed, 13 Jun 2018 18:09:41 +0700 Subject: [PATCH 1/2] [18] Refactor base components, add unit test --- .../com/nimbl3/di/ApplicationComponent.kt | 16 ++-- .../di/modules/ViewModelFactoryModule.kt | 12 +++ .../nimbl3/extension/ImageViewExtension.kt | 4 +- app/src/main/java/com/nimbl3/lib/Alias.kt | 3 + .../java/com/nimbl3/ui/main/MainActivity.kt | 71 ++++++++++------ .../java/com/nimbl3/ui/main/MainViewModel.kt | 85 ++++++++++++++++++- .../main/java/com/nimbl3/ui/main/data/Data.kt | 4 + app/src/main/res/layout/activity_main.xml | 62 ++++++++++---- .../testutil/MockPositiveApiRepository.kt | 17 ++++ .../nimbl3/testutil/MockSchedulersProvider.kt | 12 +++ .../com/nimbl3/ui/main/MainViewModelTest.kt | 40 +++++++++ 11 files changed, 270 insertions(+), 56 deletions(-) create mode 100644 app/src/main/java/com/nimbl3/di/modules/ViewModelFactoryModule.kt create mode 100644 app/src/main/java/com/nimbl3/lib/Alias.kt create mode 100644 app/src/main/java/com/nimbl3/ui/main/data/Data.kt create mode 100644 app/src/test/java/com/nimbl3/testutil/MockPositiveApiRepository.kt create mode 100644 app/src/test/java/com/nimbl3/testutil/MockSchedulersProvider.kt create mode 100644 app/src/test/java/com/nimbl3/ui/main/MainViewModelTest.kt diff --git a/app/src/main/java/com/nimbl3/di/ApplicationComponent.kt b/app/src/main/java/com/nimbl3/di/ApplicationComponent.kt index bfa0e2494..5f5611a80 100644 --- a/app/src/main/java/com/nimbl3/di/ApplicationComponent.kt +++ b/app/src/main/java/com/nimbl3/di/ApplicationComponent.kt @@ -8,13 +8,15 @@ import com.nimbl3.di.modules.* import javax.inject.Singleton @Singleton -@Component(modules = [AndroidSupportInjectionModule::class, - AppModule::class, - RetrofitModule::class, - GsonModule::class, - OkHttpClientModule::class, - SchedulersModule::class, - ActivityBuilderModule::class]) +@Component(modules = [ + AndroidSupportInjectionModule::class, + ViewModelFactoryModule::class, + AppModule::class, + RetrofitModule::class, + GsonModule::class, + OkHttpClientModule::class, + SchedulersModule::class, + ActivityBuilderModule::class]) interface ApplicationComponent : AndroidInjector { @Component.Builder diff --git a/app/src/main/java/com/nimbl3/di/modules/ViewModelFactoryModule.kt b/app/src/main/java/com/nimbl3/di/modules/ViewModelFactoryModule.kt new file mode 100644 index 000000000..43d1fbdb3 --- /dev/null +++ b/app/src/main/java/com/nimbl3/di/modules/ViewModelFactoryModule.kt @@ -0,0 +1,12 @@ +package com.nimbl3.di.modules + +import android.arch.lifecycle.ViewModelProvider +import com.nimbl3.lib.viewmodel.ViewModelFactory +import dagger.Binds +import dagger.Module + +@Module +abstract class ViewModelFactoryModule { + @Binds + abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory +} \ No newline at end of file diff --git a/app/src/main/java/com/nimbl3/extension/ImageViewExtension.kt b/app/src/main/java/com/nimbl3/extension/ImageViewExtension.kt index 3e747cd7a..d49f6912e 100644 --- a/app/src/main/java/com/nimbl3/extension/ImageViewExtension.kt +++ b/app/src/main/java/com/nimbl3/extension/ImageViewExtension.kt @@ -11,11 +11,11 @@ import com.nimbl3.di.modules.GlideApp * Provide extension functions relates to ImageView and loading image mechanism. */ -fun ImageView.setImageUrl(url: String) { +fun ImageView.loadImage(url: String) { GlideApp.with(context) .load(url) .placeholder(ColorDrawable(ContextCompat.getColor(context, R.color.black_20a))) .diskCacheStrategy(DiskCacheStrategy.ALL) - .centerCrop() + .fitCenter() .into(this) } diff --git a/app/src/main/java/com/nimbl3/lib/Alias.kt b/app/src/main/java/com/nimbl3/lib/Alias.kt new file mode 100644 index 000000000..be40ac220 --- /dev/null +++ b/app/src/main/java/com/nimbl3/lib/Alias.kt @@ -0,0 +1,3 @@ +package com.nimbl3.lib + +typealias IsLoading = Boolean \ No newline at end of file diff --git a/app/src/main/java/com/nimbl3/ui/main/MainActivity.kt b/app/src/main/java/com/nimbl3/ui/main/MainActivity.kt index 87c200498..bc8704dcd 100644 --- a/app/src/main/java/com/nimbl3/ui/main/MainActivity.kt +++ b/app/src/main/java/com/nimbl3/ui/main/MainActivity.kt @@ -1,42 +1,59 @@ package com.nimbl3 +import android.arch.lifecycle.ViewModelProvider +import android.arch.lifecycle.ViewModelProviders import android.os.Bundle -import android.widget.ImageView -import android.widget.TextView -import android.widget.Toast -import dagger.android.AndroidInjection -import io.reactivex.android.schedulers.AndroidSchedulers -import com.nimbl3.extension.setImageUrl -import com.nimbl3.data.service.ApiRepository -import com.nimbl3.data.service.response.ExampleResponse +import android.view.View.* +import com.jakewharton.rxbinding2.view.RxView +import com.nimbl3.data.lib.schedulers.SchedulersProvider +import com.nimbl3.extension.loadImage +import com.nimbl3.lib.IsLoading import com.nimbl3.ui.base.BaseActivity +import com.nimbl3.ui.main.MainViewModel +import com.nimbl3.ui.main.data.Data +import kotlinx.android.synthetic.main.activity_main.* import javax.inject.Inject class MainActivity : BaseActivity() { - @Inject lateinit var appRepository: ApiRepository + @Inject lateinit var viewModelFactory: ViewModelProvider.Factory + @Inject lateinit var schedulers: SchedulersProvider + + private val viewModel: MainViewModel by lazy { + ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + bindToViewModel() + } + + private fun bindToViewModel() { + viewModel + .outputs + .loadData() + .observeOn(schedulers.main()) + .subscribe(this::bindData) + + viewModel + .outputs + .isLoading() + .subscribeOn(schedulers.io()) + .observeOn(schedulers.main()) + .subscribe(this::showLoading) + + RxView.clicks(buttonRefresh) + .subscribe({ viewModel.inputs.refresh() }) + } + + private fun bindData(data: Data) { + textView.text = data.content + imageView.loadImage(data.imageUrl) + } - val textView = findViewById(R.id.text) - val imageView = findViewById(R.id.appCompatImageView) - - // Just for exampling the Retrofit implementation - appRepository - .getExampleData() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ response: ExampleResponse -> - var displayText = "" - (0..2) - .map { response.data.children.get(it).data } - .forEach { displayText += "Author = ${it.author} \nTitle = ${it.title} \n\n" } - - textView.setText(displayText) - imageView.setImageUrl("http://www.monkeyuser.com/assets/images/2018/80-the-struggle.png") - }, { error: Throwable -> - Toast.makeText(this, "Error: " + error.message, Toast.LENGTH_SHORT).show() - }) + private fun showLoading(isLoading: IsLoading) { + buttonRefresh.visibility = if (isLoading) GONE else VISIBLE + progressBar.visibility = if (isLoading) VISIBLE else GONE } } \ No newline at end of file diff --git a/app/src/main/java/com/nimbl3/ui/main/MainViewModel.kt b/app/src/main/java/com/nimbl3/ui/main/MainViewModel.kt index a503f1e94..3e0992bf4 100644 --- a/app/src/main/java/com/nimbl3/ui/main/MainViewModel.kt +++ b/app/src/main/java/com/nimbl3/ui/main/MainViewModel.kt @@ -1,9 +1,88 @@ package com.nimbl3.ui.main +import com.nimbl3.data.lib.schedulers.SchedulersProvider +import com.nimbl3.data.service.ApiRepository +import com.nimbl3.data.service.response.ExampleResponse +import com.nimbl3.lib.IsLoading import com.nimbl3.ui.base.BaseViewModel +import com.nimbl3.ui.main.data.Data +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject +import javax.inject.Inject -// TODO: remove this rule later! -@Suppress("EmptyClassBlock") -class MainViewModel : BaseViewModel() { +class MainViewModel +@Inject constructor(private val repository: ApiRepository, + private val schedulers: SchedulersProvider) : BaseViewModel(), Inputs, Outputs { + private val refresh = PublishSubject.create() + + private val data = BehaviorSubject.create() + private val isLoading = BehaviorSubject.create() + + val inputs: Inputs = this + val outputs: Outputs = this + + init { + fetchApi() + .map { fromResponse(it) } + .observeOn(schedulers.main()) + .subscribe({ + data.onNext(it) + isLoading.onNext(false) + }, { + TODO("Handle Error ¯\\_(ツ)_/¯ ") + }) + .bindForDisposable() + + refresh + .flatMap { fetchApi() } + .map { fromResponse(it) } + .observeOn(schedulers.main()) + .subscribe({ + data.onNext(it) + isLoading.onNext(false) + }, { + TODO("Handle Error ¯\\_(ツ)_/¯ ") + }) + .bindForDisposable() + } + + private fun fetchApi(): Observable = + repository + .getExampleData() + .subscribeOn(schedulers.io()) + .doOnSubscribe { isLoading.onNext(true) } + .toObservable() + + private fun fromResponse(response: ExampleResponse): Data { + var content = "" + (0..2) + .map { response.data.children[it].data } + .forEach { + content += "Author = ${it.author} \nTitle = ${it.title} \n\n" + } + + // Image from a random place + var imageUrl = "http://www.monkeyuser.com/assets/images/2018/80-the-struggle.png" + return Data(content, imageUrl) + } + + override fun refresh() { + refresh.onNext(Unit) + } + + override fun loadData(): Observable = this.data + + override fun isLoading(): Observable = this.isLoading +} + +interface Inputs { + fun refresh() +} + +interface Outputs { + fun loadData(): Observable + + fun isLoading(): Observable } \ No newline at end of file diff --git a/app/src/main/java/com/nimbl3/ui/main/data/Data.kt b/app/src/main/java/com/nimbl3/ui/main/data/Data.kt new file mode 100644 index 000000000..9d0649848 --- /dev/null +++ b/app/src/main/java/com/nimbl3/ui/main/data/Data.kt @@ -0,0 +1,4 @@ +package com.nimbl3.ui.main.data + +data class Data(val content: String, + val imageUrl: String) \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 7642ede36..d114a33f1 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,26 +1,54 @@ - + android:fillViewport="true"> - + - + - \ No newline at end of file + + + + +