Avoid redundant code when having inheritance in Kotlin - android

I have following base class :
abstract class BaseViewModel<T, R>(private val schedulerProvider: BaseSchedulerProvider) :
ViewModel() {
private val compositeDisposable = CompositeDisposable()
private val _liveData = MutableLiveData<Resource<T>>()
val liveData: LiveData<Resource<T>>
get() = _liveData
protected abstract val requestObservable: Observable<R>
protected abstract fun getSuccessResult(it: R): T
fun sendRequest() {
_liveData.value = Resource.Loading()
composeObservable { requestObservable }
.subscribe({
_liveData.postValue(Resource.Success(getSuccessResult(it)))
}) {
_liveData.postValue(Resource.Failure(it.localizedMessage))
Timber.e(it)
}.also { compositeDisposable.add(it) }
}
}
And here is child class implementation :
class MainViewModel(
api: PokemonService,
schedulerProvider: BaseSchedulerProvider
) : BaseViewModel<List<Pokemon>, List<NamedResponseModel>>(schedulerProvider) {
override val requestObservable: Observable<List<NamedResponseModel>> =
api.getPokemonList(LIMIT).map { it.results }
override fun getSuccessResult(it: List<NamedResponseModel>): List<Pokemon> = it.asDomainModel()
init {
sendRequest()
}
}
As you see I put init block in child classes to sendRequest() which is a redundant. If I move init block to parent class, it will crash since api is null because init block of parent is called before constructor of child.
Is there any solution to move sendRequest() to parent and avoid redundant in child classes?
Source code can be found : https://github.com/AliRezaeiii/Pokemon

I think you need to change the design of your inheritance. To get the child items to be executed in the parent's initialization, you need to pass the object to the parent constructor.
Here is an example:
abstract class Base(protected val name: String) {
init {
println(name)
}
}
class CBase(private val s: String) : Base(s) {}
fun main() {
CBase("Hello");
}
In your case, which I haven't tested yet:
abstract class BaseViewModel<T, R>(
private val schedulerProvider: BaseSchedulerProvider,
protected val requestObservable: Observable<R>):
ViewModel() {
private val compositeDisposable = CompositeDisposable()
private val _liveData = MutableLiveData<Resource<T>>()
val liveData: LiveData<Resource<T>>
get() = _liveData
protected abstract fun getSuccessResult(it: R): T
fun sendRequest() {
_liveData.value = Resource.Loading()
composeObservable { requestObservable }
.subscribe({
_liveData.postValue(Resource.Success(getSuccessResult(it)))
}) {
_liveData.postValue(Resource.Failure(it.localizedMessage))
Timber.e(it)
}.also { compositeDisposable.add(it) }
}
init {
sendRequest()
}
}
class MainViewModel(
api: PokemonService,
schedulerProvider: BaseSchedulerProvider
) : BaseViewModel<List<Pokemon>, List<NamedResponseModel>>(
schedulerProvider,
api.getPokemonList(LIMIT).map { it.results }
) {
override fun getSuccessResult(it: List<NamedResponseModel>): List<Pokemon> = it.asDomainModel()
}
Here, you can still access the variable requestObservable at the parent's contructor because it is initialized at the constructor parameter, not as an abstract property.
Let me know how it works for you.

Related

Jetpack Data Store #Inject need initialize

I followed this tutorial link
But I met a problem that "kotlin.UninitializedPropertyAccessException: lateinit property splashViewModel has not been initialized"
Here is my code
#Module
#InstallIn(SingletonComponent::class)
object MainModule {
#Provides
#Singleton
fun provideDataStoreRepository(
#ApplicationContext context: Context
) = DataStoreRepository(context = context)
}
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "on_boarding_pref")
class DataStoreRepository(context: Context) {
private object PreferencesKey {
val onBoardingKey = booleanPreferencesKey(name = "on_boarding_completed")
}
private val dataStore = context.dataStore
suspend fun saveOnBoardingState(completed: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKey.onBoardingKey] = completed
}
}
fun readOnBoardingState(): Flow<Boolean> {
return dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val onBoardingState = preferences[PreferencesKey.onBoardingKey] ?: false
onBoardingState
}
}
}
class SplashViewModel #Inject constructor(
private val repository: DataStoreRepository
) : ViewModel() {
private val _isLoading: MutableState<Boolean> = mutableStateOf(true)
val isLoading: State<Boolean> = _isLoading
private val _startDestination: MutableState<String> = mutableStateOf(Screen.OnboardingFirstScreen.route)
val startDestination: State<String> = _startDestination
init {
viewModelScope.launch {
repository.readOnBoardingState().collect { completed ->
if (completed) {
_startDestination.value = Screen.MainScreen.route
} else {
_startDestination.value = Screen.OnboardingFirstScreen.route
}
}
_isLoading.value = false
}
}
}
And in my main activity
class MainActivity : ComponentActivity() {
#Inject
lateinit var splashViewModel: SplashViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
installSplashScreen().setKeepOnScreenCondition {
!splashViewModel.isLoading.value
}
setContent{
BottomNavWithBadgesTheme {
val screen by splashViewModel.startDestination
....
}
}
It turned out MainModule object have never been used. Is that problem? I'm new to jetpack data store, I just followed it, so I don't know where is the problem and how to fix it. Thank you in advance.
Firstly, it's not about data store. It is about dependency injection. You are trying to get the data from viewmodel when it is not initialized.
To solve the problem:
Mark your viewmodel class with #HiltViewModel annotation
Remove lateinit var keyword and #Inject annotation from viewmodel in your MainActivity
Your viewmodel must be initialized in onCreate function like that:
viewModel: SplashViewModel = hiltViewModel()

Local unit test for LiveData while using RxJava

Full source code is available at : https://github.com/AliRezaeiii/StarWarsSearch-RxPaging
I am trying to test my DetailViewModel. My expectation is Species and Films not be empty lists as I have for instance : when(service.getSpecie(anyString())).thenReturn(Single.just(specie)). Here is my test :
class DetailViewModelTest {
#get:Rule
var rule: TestRule = InstantTaskExecutorRule()
#Mock
private lateinit var service: StarWarsService
private lateinit var specie: Specie
private lateinit var planet: Planet
private lateinit var film: Film
private lateinit var viewModel: DetailViewModel
#Before
fun setUp() {
initMocks(this)
// Make the sure that all schedulers are immediate.
val schedulerProvider = ImmediateSchedulerProvider()
val detailRepository = DetailRepository(service)
val character = Character(
"Ali", "127", "1385", emptyList(), emptyList()
)
viewModel = DetailViewModel(
schedulerProvider, character, GetSpecieUseCase(detailRepository),
GetPlanetUseCase(detailRepository), GetFilmUseCase(detailRepository)
)
specie = Specie("Ali", "Persian", "Iran")
planet = Planet("")
film = Film("")
}
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
`when`(service.getSpecie(anyString())).thenReturn(Single.just(specie))
`when`(service.getPlanet(anyString())).thenReturn(Single.just(planet))
`when`(service.getFilm(anyString())).thenReturn(Single.just(film))
viewModel.liveData.value.let {
assertThat(it, `is`(notNullValue()))
if (it is Resource.Success) {
it.data?.let { data ->
assertTrue(data.films.isEmpty())
assertTrue(data.species.isEmpty())
}
}
}
}
#Test
fun givenServerResponseError_whenFetch_specie_shouldReturnError() {
`when`(service.getSpecie(anyString())).thenReturn(Single.error(Exception("error")))
`when`(service.getPlanet(anyString())).thenReturn(Single.just(planet))
`when`(service.getFilm(anyString())).thenReturn(Single.just(film))
viewModel.liveData.value.let {
assertThat(it, `is`(notNullValue()))
if (it is Resource.Error) {
assertThat(it.message, `is`(notNullValue()))
assertThat(it.message, `is`("error"))
}
}
}
}
Here is my ViewModel :
class DetailViewModel #Inject constructor(
schedulerProvider: BaseSchedulerProvider,
character: Character,
getSpecieUseCase: GetSpecieUseCase,
getPlanetUseCase: GetPlanetUseCase,
getFilmUseCase: GetFilmUseCase,
) : BaseViewModel<DetailWrapper>(schedulerProvider,
Single.zip(Flowable.fromIterable(character.specieUrls)
.flatMapSingle { specieUrl -> getSpecieUseCase(specieUrl) }
.flatMapSingle { specie ->
getPlanetUseCase(specie.homeWorld).map { planet ->
SpecieWrapper(specie.name, specie.language, planet.population)
}
}.toList(),
Flowable.fromIterable(character.filmUrls)
.flatMapSingle { filmUrl -> getFilmUseCase(filmUrl) }
.toList(), { species, films ->
DetailWrapper(species, films)
}))
And here is my BaseViewModel :
open class BaseViewModel<T>(
private val schedulerProvider: BaseSchedulerProvider,
private val singleRequest: Single<T>
) : ViewModel() {
private val compositeDisposable = CompositeDisposable()
private val _liveData = MutableLiveData<Resource<T>>()
val liveData: LiveData<Resource<T>>
get() = _liveData
init {
sendRequest()
}
fun sendRequest() {
_liveData.value = Resource.Loading
wrapEspressoIdlingResourceSingle { singleRequest }
.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui()).subscribe({
_liveData.postValue(Resource.Success(it))
}) {
_liveData.postValue(Resource.Error(it.localizedMessage))
Timber.e(it)
}.also { compositeDisposable.add(it) }
}
override fun onCleared() {
super.onCleared()
compositeDisposable.clear()
}
}
And here is DetailWrapper class :
class DetailWrapper(
val species: List<SpecieWrapper>,
val films: List<Film>,
)
class SpecieWrapper(
val name: String,
val language: String,
val population: String,
)
Why films and species lists are empty in my local unit test?
As you see I pass two emptyLists to Character object. That is the source of problem since for instance I have following in DetailViewModel :
Flowable.fromIterable(character.filmUrls)
.flatMapSingle { filmUrl -> getFilmUseCase(filmUrl) }
.toList()
FilmUrls is one of those emptyLists. If I change Character by passing not emptyList, it is working as expected :
character = Character("Ali", "127", "1385",
listOf("url1", "url2"), listOf("url1", "url2"))
I also need to move ViewModel initialization to the method body, such as :
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
`when`(repository.getSpecie(anyString())).thenReturn(Single.just(specie))
`when`(repository.getPlanet(anyString())).thenReturn(Single.just(planet))
`when`(repository.getFilm(anyString())).thenReturn(Single.just(film))
viewModel = DetailViewModel(schedulerProvider, character, GetSpecieUseCase(repository),
GetPlanetUseCase(repository), GetFilmUseCase(repository))
viewModel.liveData.value.let {
assertThat(it, `is`(notNullValue()))
if (it is Resource.Success) {
it.data?.let { data ->
assertTrue(data.films.isNotEmpty())
assertTrue(data.species.isNotEmpty())
}
}
}
}

Correct structure of implementing MVVM LiveData RxJava Dagger Databinding?

MainActivity
class MainActivity : AppCompatActivity() {
#Inject
lateinit var mainViewModelFactory: mainViewModelFactory
private lateinit var mainActivityBinding: ActivityMainBinding
private lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainActivityBinding = DataBindingUtil.setContentView(
this,
R.layout.activity_main
)
mainActivityBinding.rvmainRepos.adapter = mainAdapter
AndroidInjection.inject(this)
mainViewModel =
ViewModelProviders.of(
this#MainActivity,
mainViewModelFactory
)[mainViewModel::class.java]
mainActivityBinding.viewmodel = mainViewModel
mainActivityBinding.lifecycleOwner = this
mainViewModel.mainRepoReponse.observe(this, Observer<Response> {
repoList.clear()
it.success?.let { response ->
if (!response.isEmpty()) {
// mainViewModel.saveDataToDb(response)
// mainViewModel.createWorkerForClearingDb()
}
}
})
}
}
MainViewModelFactory
class MainViewModelFactory #Inject constructor(
val mainRepository: mainRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>) =
with(modelClass) {
when {
isAssignableFrom(mainViewModel::class.java) -> mainViewModel(
mainRepository = mainRepository
)
else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
} as T
}
MainViewModel
class MainViewModel(
val mainRepository: mainRepository
) : ViewModel() {
private val compositeDisposable = CompositeDisposable()
val mainRepoReponse = MutableLiveData<Response>()
val loadingProgress: MutableLiveData<Boolean> = MutableLiveData()
val _loadingProgress: LiveData<Boolean> = loadingProgress
val loadingFailed: MutableLiveData<Boolean> = MutableLiveData()
val _loadingFailed: LiveData<Boolean> = loadingFailed
var isConnected: Boolean = false
fun fetchmainRepos() {
if (isConnected) {
loadingProgress.value = true
compositeDisposable.add(
mainRepository.getmainRepos().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response ->
run {
saveDataToDb(response)
)
}
},
{ error ->
processResponse(Response(AppConstants.Status.SUCCESS, null, error))
}
)
)
} else {
fetchFromLocal()
}
}
private fun saveDataToDb(response: List<mainRepo>) {
mainRepository.insertmainUsers(response)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(object : DisposableCompletableObserver() {
override fun onComplete() {
Log.d("Status", "Save Success")
}
override fun onError(e: Throwable) {
Log.d("Status", "error ${e.localizedMessage}")
}
})
}
}
MainRepository
interface MainRepository {
fun getmainRepos(): Single<List<mainRepo>>
fun getAllLocalRecords(): Single<List<mainRepo>>
fun insertmainUsers(repoList: List<mainRepo>): Completable
}
MainRepositoryImpl
class mainRepositoryImpl #Inject constructor(
val apiService: GitHubApi,
val mainDao: AppDao
) : MainRepository {
override fun getAllLocalRecords(): Single<List<mainRepo>> = mainDao.getAllRepos()
override fun insertmainUsers(repoList: List<mainRepo>) :Completable{
return mainDao.insertAllRepos(repoList)
}
override fun getmainRepos(): Single<List<mainRepo>> {
return apiService.getmainGits()
}
}
I'm quite confused with the implementation of MVVM with LiveData and Rxjava, in my MainViewModel I am calling the interface method and implementing it inside ViewModel, also on the response I'm saving the response to db. However, that is a private method, which won't be testable in unit testing in a proper way (because it's private). What is the best practice to call other methods on the completion of one method or i have to implement all the methods inside the implementation class which uses the interface.
Your ViewModel should not care how you are getting the data if you are trying to follow the clean architecture pattern. The logic for fetching the data from local or remote sources should be in the repository in the worst case where you can also save the response. In that case, since you have a contact for the methods, you can easily test them. Ideally, you could break it down even more - adding Usecases/Interactors.

UnitTest coroutines Kotlin usecase MVP

I am trying to mock a response from my usecases, this usecase works with coroutines.
fun getData() {
view?.showLoading()
getProductsUseCase.execute(this::onSuccessApi, this::onErrorApi)
}
My useCase is injected on presenter.
GetProductsUseCase has this code:
class GetProductsUseCase (private var productsRepository: ProductsRepository) : UseCase<MutableMap<String, Product>>() {
override suspend fun executeUseCase(): MutableMap<String, Product> {
val products =productsRepository.getProductsFromApi()
return products
}
}
My BaseUseCase
abstract class UseCase<T> {
abstract suspend fun executeUseCase(): Any
fun execute(
onSuccess: (T) -> Unit,
genericError: () -> Unit) {
GlobalScope.launch {
val result = async {
try {
executeUseCase()
} catch (e: Exception) {
GenericError()
}
}
GlobalScope.launch(Dispatchers.Main) {
when {
result.await() is GenericError -> genericError()
else -> onSuccess(result.await() as T)
}
}
}
}
}
This useCase call my repository:
override suspend fun getProductsFromApi(): MutableMap<String, Product> {
val productsResponse = safeApiCall(
call = {apiService.getProductsList()},
error = "Error fetching products"
)
productsResponse?.let {
return productsMapper.fromResponseToDomain(it)!!
}
return mutableMapOf()
}
Y try to mock my response but test always fails.
#RunWith(MockitoJUnitRunner::class)
class HomePresenterTest {
lateinit var presenter: HomePresenter
#Mock
lateinit var view: HomeView
#Mock
lateinit var getProductsUseCase: GetProductsUseCase
#Mock
lateinit var updateProductsUseCase: UpdateProductsUseCase
private lateinit var products: MutableMap<String, Product>
private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
#Mock
lateinit var productsRepository:ProductsRepositoryImpl
#Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
products = ProductsMotherObject.createEmptyModel()
presenter = HomePresenter(view, getProductsUseCase, updateProductsUseCase, products)
}
#After
fun after() {
Dispatchers.resetMain()
testScope.cleanupTestCoroutines()
}
//...
#Test
fun a() = testScope.runBlockingTest {
setTasksNotAvailable(productsRepository)
presenter.getDataFromApi()
verify(view).setUpRecyclerView(products.values.toMutableList())
}
private suspend fun setTasksNotAvailable(dataSource: ProductsRepository) {
`when`(dataSource.getProductsFromApi()).thenReturn((mutableMapOf()))
}
}
I don't know what is happening. The log says:
"Wanted but not invoked:
view.setUpRecyclerView([]);
-> at com.myProject.HomePresenterTest$a$1.invokeSuspend(HomePresenterTest.kt:165)
However, there was exactly 1 interaction with this mock:
view.showLoading();"
The problem is with how you create your GetProductsUseCase.
You're not creating it with the mocked version of your ProductsRepository, yet you're mocking the ProductsRepository calls.
Try to create the GetProductsUseCase manually and not using a #Mock
// no #Mock
lateinit var getProductsUseCase: GetProductsUseCase
#Before
fun setUp() {
// ...
// after your mocks are initialized...
getProductsUseCase = GetProductsUseCase(productsRepository) //<- this uses mocked ProductsRepository
}

Better way to change listId/query in data source paging

I can't find a better way to change listId of my VideosDataSource methods like load initial. I'm using view pager so it load 2 fragment at a time that's why i can't use getter/setter to set listId of my data source.
here my data source class:
class VideosDataSource(
private val networkService: NetworkService,
private val compositeDisposable: CompositeDisposable
): PageKeyedDataSource<String, Item>() {
var state: MutableLiveData<State> = MutableLiveData()
private var retryCompletable: Completable? = null
private var listId = "PL8fVUTBmJhHKEJjTNWn-ykf67rVrFWYtC"
override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<String, Item>) {
updateState(State.LOADING)
compositeDisposable.add(
networkService.getPlaylistVideos(listId
,""
,Constants.API_KEY)
.subscribe( { response ->
updateState(State.DONE)
callback.onResult(response.items, response.prevPageToken, response.nextPageToken)
},
{
updateState(State.ERROR)
setRetry(Action { loadInitial(params,callback) })
}
)
)
}
here i'm trying to change listId in my view pager fragment.
my data source factory:
class VideosDataSourceFactory(
private val compositeDisposable: CompositeDisposable,
private val networkService: NetworkService
): DataSource.Factory<String, Item>() {
val videosDataSourceLiveData = MutableLiveData<VideosDataSource>()
override fun create(): DataSource<String, Item> {
val videosDataSource = VideosDataSource(networkService,
compositeDisposable)
videosDataSourceLiveData.postValue(videosDataSource)
return videosDataSource
}
}
my view model:
class PageViewModel(application: Application) :
AndroidViewModel(application) {
//paging
private val networkService = NetworkService.getService()
var videosList: LiveData<PagedList<Item>>
private val compositeDisposable = CompositeDisposable()
private val pageSize = 50
private val videosDataSourceFactory: VideosDataSourceFactory
init {
//paging
videosDataSourceFactory = VideosDataSourceFactory(compositeDisposable, networkService)
val config = PagedList.Config.Builder()
.setPageSize(pageSize)
.setInitialLoadSizeHint(pageSize)
.setEnablePlaceholders(false)
.build()
videosList = LivePagedListBuilder<String, Item>(videosDataSourceFactory, config).build()
}
In fragmnet onClick() i want to send listId to data source.
Whit some approaches like getter/setter i can be able to send listId to data source but view pager create two or three fragment at a time the value is override in getter/setter.
I'm looking for the better way to send data from fragment to data source.
I did it. The idea of constructor is good but not works for me because of init method of view model called before setting the id to factory constructor. But thanks to mr.pskink for his comments to use parameter constructor.
So here's how i did it.
In fragment i set list to view model.
companion object {
private const val ARG_SECOND = "arg_second"
#JvmStatic
fun newInstance(second: Array<Pair<String, String>>): PlaceholderFragment {
return PlaceholderFragment().apply {
arguments = Bundle().apply {
putString(ARG_SECOND,second[0].second)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(PageViewModel::class.java).apply {
setListId(arguments?.getString(ARG_SECOND) ?: "")
}
}
In view model i create a method:
fun setListId(listId: String){
videosDataSourceFactory.listId = listId
}
In data source factory i craete a variable:
val videosDataSourceLiveData = MutableLiveData<VideosDataSource>()
lateinit var listId:String
override fun create(): DataSource<String, Item> {
val videosDataSource = VideosDataSource(networkService, compositeDisposable,listId)
videosDataSourceLiveData.postValue(videosDataSource)
return videosDataSource
}
And then get it via constructor of data source here:
class VideosDataSource(
private val networkService: NetworkService,
private val compositeDisposable: CompositeDisposable,
private val listId: String
): PageKeyedDataSource<String, Item>() {
var state: MutableLiveData<State> = MutableLiveData()
private var retryCompletable: Completable? = null
override fun loadInitial(params: LoadInitialParams<String>, callback:
LoadInitialCallback<String, Item>) {
updateState(State.LOADING)
compositeDisposable.add(
networkService.getPlaylistVideos(listId
,""
,Constants.API_KEY)
.subscribe( { response ->
updateState(State.DONE)
callback.onResult(response.items, response.prevPageToken, response.nextPageToken)
},
{
updateState(State.ERROR)
setRetry(Action { loadInitial(params,callback) })
}
)
)
}

Categories

Resources