I'm using pagination library and i want to inject datasource into my viewmodel.
My factory looks like:
class ArticleDataSourceFactory #Inject constructor(
val articleRepository: ArticleRepository
) : DataSource.Factory<Long, Article>() {
override fun create(): DataSource<Long, Article> {
return ArticleDateKeyedDataSource(articleRepository)
}
}
My DataSource:
class ArticleDateKeyedDataSource(
private val repository: ArticleRepository
) : ItemKeyedDataSource<Long, Article>() {
override fun loadInitial(params: LoadInitialParams<Long>, callback: LoadInitialCallback<Article>) {
val articles = repository.getInitial(params.requestedInitialKey!!, params.requestedLoadSize)
callback.onResult(articles)
}
override fun loadAfter(params: LoadParams<Long>, callback: LoadCallback<Article>) {
val articles = repository.getAfter(params.key, params.requestedLoadSize)
callback.onResult(articles)
}
override fun loadBefore(params: LoadParams<Long>, callback: LoadCallback<Article>) {
val articles = repository.getBefore(params.key, params.requestedLoadSize)
callback.onResult(articles)
}
override fun getKey(item: Article): Long {
return item.createdAt
}
}
And my ViewModel:
class ArticleFragmentViewModel #Inject constructor(
private val dataSourceFactory: ArticleDataSourceFactory
) : BaseViewModel() {
var initialArticlePosition = 0L
val navigateToArticleDetails: MutableLiveData<SingleEvent<Long>> = MutableLiveData()
val articlesLiveList: LiveData<PagedList<Article>>
get() {
val config = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(5)
.build()
return LivePagedListBuilder(dataSourceFactory, config)
.setInitialLoadKey(initialArticlePosition)
.setFetchExecutor(Executors.newSingleThreadExecutor())
.build()
}
fun onArticleSelected(createdAt: Long) {
navigateToArticleDetails.value = SingleEvent(createdAt)
}
}
After rebuild, i get an error:
error: cannot access DataSource
class file for androidx.paging.DataSource not found
Consult the following stack trace for details.
What does it mean? I have no idea, what i do wrong.
For example, i have no problem to inject repository.
Did you use Android Module? You may need to use api dependency in the base module like
api "androidx.paging:paging-runtime-ktx:$paging_version"
And there is similar issue https://stackoverflow.com/a/47128596/5934119
Related
I am trying to test my app with firestore and I don't know how will I fake the datasource with a Flow to test my repository. I'm trying to follow the pattern from google codelabs but I only convert toObject in the composable. How can I write the repository test?
below is my FirestoreDatasource and my Repository and the FakeDatasource I am trying to write
#Singleton
class FirestoreDatasourceImpl #Inject constructor(
private val firestore: FirebaseFirestore
) : FirestoreDatasource {
override suspend fun addUser(user: UserModel) {
firestore.collection(USERS)
.document(user.id)
.set(user, SetOptions.merge())
}
#ExperimentalCoroutinesApi
override fun getAllUsers(userId: String) = callbackFlow {
val collection = firestore.collection(USERS).whereNotEqualTo("id", userId)
val snapshotListener = collection.addSnapshotListener() { snapshot, e ->
this.trySend(snapshot).isSuccess
}
awaitClose {
snapshotListener.remove()
}
}
}
#Singleton
class FirestoreRepositoryImpl #Inject constructor(
private val firestoreClass: FirestoreDatasourceImpl
) : FirestoreRepository{
override suspend fun addUser(user: UserModel){
firestoreClass.addUser(user)
}
#ExperimentalCoroutinesApi
override fun getAllUsers(userId: String) : Flow<QuerySnapshot?> {
return firestoreClass.getAllUsers(userId)
}
}
internal class FakeFirestoreDatasourceImplTest constructor(
private var userList: MutableList<UserModel>? = mutableListOf()
) : FirestoreDatasource{
override suspend fun addUser(user: UserModel) {
userList?.add(user)
}
override fun getAllUsers(userId: String): Flow<QuerySnapshot?> {
}
}
I'm currently making a sample project about diagrams. I'm starting to use MVVM architecture recently, and I got stuck when the response is null. I also checked the Mutable Live Data to make sure that it is calling the API. Here's some of my code and the error-tag:
Model.kt
data class Model(
#SerializedName("FID") val FID: Int,
#SerializedName("region") val region: String,
#SerializedName("positive") val positive: Float
) {
}
ModelWrap.kt
data class ModelWrap(#SerializedName("samplesAPI") val attributes: Model){
}
ApiClient.kt
object ApiClient {
var retrofitService: ApiInterface? = null
const val BASE_URL = "https://sampleapi.../"
fun getApiSample() : ApiInterface {
if (retrofitService == null){
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofitService = retrofit.create(ApiInterface::class.java)
}
return retrofitService!!
}
}
ApiInterface.kt
interface ApiInterface {
#GET("samples")
fun getSampleData(): Call<List<ModelWrap>>
}
MainViewModel.kt
class MainViewModelconstructor(private val repository: ModelRepository) : ViewModel(){
val sampleList= MutableLiveData<List<ModelWrap>>()
val errorMessage = MutableLiveData<String>()
fun getSampleData(pieChart: PieChart){
val response = repository.getSampleData()
response.enqueue(object : Callback<List<ModelWrap>> {
override fun onResponse(
call: Call<List<ModelWrap>>,
response: Response<List<ModelWrap>>
) {
sampleList.postValue(response.body())
}
override fun onFailure(call: Call<List<ModelWrap>>, t: Throwable) {
errorMessage.postValue(t.message)
}
})
}
}
MainViewModelFactory.kt
class MainViewModelFactoryconstructor(private val repository: MainRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(MainViewModel::class.java)){
MainViewModel(this.repository) as T
} else {
throw IllegalArgumentException("Sample ViewModel Not Found")
}
}
}
MainRepository.kt
class MainRepository constructor(private val retrofitService: ApiInterface){
fun getSampleData() = retrofitService.getSampleData()
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var pieChart: PieChart
lateinit var sampleViewModel: MainViewModel
private val sampleService = ApiClient.getApiSample()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
pieChart = findViewById(R.id.PieChart)
sampleViewModel= ViewModelProvider(this, MainViewModelFactory(MainRepository(sampleService))).get(MainViewModel::class.java)
getPieChart(pieChart)
}
private fun getPieChart(pieCharts: PieChart) {
mainViewModel.mainList.observe(this, Observer {
Log.d("TAG sample" , "onCreate PieChart: $it")
Log.d("Tag Samples Response" , response.body().toString())
if (it != null) {
val sampleEntries: List<PieEntry> = ArrayList()
for ((attributes) in it!!) {
sampleEntries.toMutableList()
.add(PieEntry(attributes.positive, attributes.region))
//........................................................................
val description = Description()
description.text = "Samples Data"
pieChart.description = description
pieChart.invalidate()
}
}
})
mainViewModel.errorMessage.observe(this, Observer { })
mainViewModel.getSampleData(pieCharts)
}
}
and Lastly, here's some or log message:
V/InputMethodManager: Starting input: tba=android.view.inputmethod.EditorInfo#8b795c0 nm : com.example.diargram ic=null
D/Tag Sample Response: null
D/TAG Sample: onCreate PieChart: null
E/libc: Access denied finding property "ro.serialno"
V/StudioTransport: Agent command stream started.
V/StudioTransport: Transport agent connected to daemon.
I would appreciate it if someone can help me :D, Thank you
Finally, I found a solution for my problem:
I type the wrong endpoint inside the interface class and it should be like this:
interface ApiInterface {
#GET("sample")
fun getSampleData(): Call<List> }
When it comes to assigning the livedata to the view, based on my JSON I should call ArrayList instead of List
List item
Before :
val sampleEntries: List = ArrayList()
After :
val sampleEntries: ArrayList<PieEntry> = ArrayList()
I want to use Single source of truth principle in my application. How can I add multiple table when using NetworkBoundRepository.
MainApi.kt
interface MainApi {
#GET("main")
suspend fun getMain(): Response<MainResponse>
}
MainResponse.kt
#JsonClass(generateAdapter = true)
data class MainResponse(
#Json(name = "categories") val categoryList: List<Category>,
#Json(name = "locations") val locationList: List<Location>,
#Json(name = "tags") val tagList: List<Tag>
)
NetworkBoundRepository.kt
#ExperimentalCoroutinesApi
abstract class NetworkBoundRepository<RESULT, REQUEST> {
fun asFlow() = flow<Resource<RESULT>> {
emit(Resource.Success(fetchFromLocal().first()))
val apiResponse = fetchFromRemote()
val remoteCategories = apiResponse.body()
if (apiResponse.isSuccessful && remoteCategories != null) {
saveRemoteData(remoteCategories)
} else {
emit(Resource.Failed(apiResponse.message()))
}
emitAll(
fetchFromLocal().map {
Resource.Success<RESULT>(it)
}
)
}.catch { e ->
emit(Resource.Failed("Network error! Can't get latest categories."))
}
#WorkerThread
protected abstract suspend fun saveRemoteData(response: REQUEST)
#MainThread
protected abstract fun fetchFromLocal(): Flow<RESULT>
#MainThread
protected abstract suspend fun fetchFromRemote(): Response<REQUEST>
}
MainRepository.kt
#ExperimentalCoroutinesApi
class MainRepository #Inject constructor(
private val mainApi: MainApi,
private val categoryDao: CategoryDao,
private val locationDao: LocationDao,
private val tagDao: TagDao
) {
suspend fun getMain(): Flow<Resource<List<Category>>> {
return object : NetworkBoundRepository<List<Category>, List<Category>>() {
override suspend fun saveRemoteData(response: List<Category>) = categoryDao.insertList(response)
override fun fetchFromLocal(): Flow<List<Category>> = categoryDao.getList()
override suspend fun fetchFromRemote(): Response<List<Category>> = mainApi.getMain()
}.asFlow()
}
}
Currently NetworkBoundRepository and MainRepository only works with categories. I want to fetch some data from internet and save each data to related tables in database. App must be offline first.
How can I add locationDao, tagDao to MainRepository?
I don't quite follow your question. You are adding locationDao and tagDao to MainRepository already here:
class MainRepository #Inject constructor(
...
private val locationDao: LocationDao,
private val tagDao: TagDao
)
If you are asking how to provide them in order for them to injectable via Dagger2 you have to either define dao constructor as #Inject or add #Provides or #Binds annotated methods with the relevant return type to the needed #Module, and tangle them in the same #Scope - more here
If you asking how to use those repos in your functions it is also easy:
object : NetworkBoundRepository<List<Category>, MainResponse>() {
override suspend fun saveRemoteData(response: MainResponse) = response?.run{
categoryDao.insertList(categoryList)
locationDao.insertList(locationList)
tagDao.insertList(tagList)
}
override fun fetchCategoriesFromLocal(): Flow<List<Category>> = categoryDao.getList()
override fun fetchLocationsFromLocal(): Flow<List<Location>> = locationDao.getList()
override fun fetchTagsFromLocal(): Flow<List<Tag>> = tagDao.getList()
override suspend fun fetchFromRemote(): Response<MainResponse> = mainApi.getMain()
//This function is not tested and written more like a pseudocode
override suspend fun mapFromLocalToResponse(): Flow<MainResponse> = fetchCategoriesFromLocal().combine(fetchLocationsFromLocal(), fetchTagsFromLocal()){categories, locations, tags ->
MainResponse(categories,locations,tags)
}
}
Maybe some more adjustments will be needed. But the main problem of your code is that you are trying to combine all the different entities into one repo and it is not very good(and the request that returns all the stuff under one response is not good either) - I would suggest to split it somehow not to mix it all.
I create application based on the Database + Network paging and GitHub rest api.
Using various tutorials, I came to the conclusion that when creating the LivePagedListBuilder in ViewModel, I must pass my query retrieving data from Room, to make it works then with BoundaryCallback.
This query in my code looks like this:
#Query("SELECT * from repositories_table ORDER BY name DESC")
fun getPagedRepos(): DataSource.Factory<Int,Repository>
and its equivalent in the repository:
fun getPagedRepos(): DataSource.Factory<Int, Repository> {
return repositoriesDao.getPagedRepos()
}
However I would like to combine this with my own DataSource, not default one, which would also work with retrofitting data fetching.
Below are the relevant parts of my application:
DataSource
class ReposDataSource(private val contactsRepository: ContactsRepository,
private val scope: CoroutineScope, application: Application): PageKeyedDataSource<Int, Repository>() {
private var supervisorJob = SupervisorJob()
private val PREFS_NAME = "Paging"
private val sharedPref: SharedPreferences = application.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, Repository>
) {
Log.i("RepoBoundaryCallback", "initialTriggered")
val currentPage = 1
val nextPage = currentPage + 1
executeQuery(currentPage, params.requestedLoadSize) {
callback.onResult(it, null, nextPage)
}
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Repository>) {
val currentPage = params.key
val nextPage = currentPage + 1
executeQuery(currentPage, params.requestedLoadSize) {
callback.onResult(it, nextPage)
}
}
override fun invalidate() {
super.invalidate()
supervisorJob.cancelChildren()
}
private fun executeQuery(page: Int, perPage: Int, callback: (List<Repository>) -> Unit) {
scope.launch(getJobErrorHandler() + supervisorJob) {
savePage("current_page", page)
val repos = contactsRepository.fetchPagedRepos(page, perPage)
callback(repos)
}
}
private fun getJobErrorHandler() = CoroutineExceptionHandler { _, e ->
Log.e(ReposDataSource::class.java.simpleName, "An error happened: $e")
}
private fun savePage(KEY_NAME: String, value: Int){
Log.i("RepoBoundaryCallback", value.toString())
val editor: SharedPreferences.Editor = sharedPref.edit()
editor.putInt(KEY_NAME, value)
editor.commit()
}
}
BoundaryCallback
class RepoBoundaryCallback (val repository: ContactsRepository, application: Application) :
PagedList.BoundaryCallback<Repository?>() {
private var callbackJob = Job()
private val coroutineScope = CoroutineScope(
callbackJob + Dispatchers.Main )
private val PREFS_NAME = "Paging"
private val sharedPref: SharedPreferences = application.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
override fun onZeroItemsLoaded() {
Log.i("RepoBoundaryCallback", "onzeroitemstriggered")
super.onZeroItemsLoaded()
fetchUsers(1)
}
override fun onItemAtEndLoaded(itemAtEnd: Repository) {
Log.i("RepoBoundaryCallback", "onitematendriggered")
super.onItemAtEndLoaded(itemAtEnd)
fetchUsers(getCurrentPage("current_page"))
}
private fun fetchUsers(page: Int) {
coroutineScope.launch {
try {
var newRepos = RepoApi.retrofitService.fetchRepos(page)
insertRepoToDb(newRepos)
}
catch (e: Exception){
Log.i("RepoBoundaryCallback", e.toString())
}
}
}
private suspend fun insertRepoToDb(reposList: List<Repository>){
reposList.forEach{repository.insertRepo(it)}
}
private fun getCurrentPage(KEY_NAME: String): Int{
return sharedPref.getInt(KEY_NAME, 0)
}
}
Api query
interface RepoApiService {
#GET("/orgs/google/repos")
suspend fun fetchRepos(#Query("page") page: Int,
#Query("per_page") perPage: Int = 15): List<Repository>
}
ViewModel
class RepositoryViewModel (application: Application) : AndroidViewModel(application) {
companion object{
private const val TAG = "RepositoryViewModel"
}
//var reposList: LiveData<PagedList<Repository>>
private var repoBoundaryCallback: RepoBoundaryCallback? = null
var reposList: LiveData<PagedList<Repository>>? = null
private val repository: ContactsRepository
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(
viewModelJob + Dispatchers.Main )
init {
val contactsDao = ContactsRoomDatabase.getDatabase(application, viewModelScope).contactsDao()
val contactsExtrasDao = ContactsRoomDatabase.getDatabase(application, viewModelScope).contactsExtrasDao()
val repositoriesDao = ContactsRoomDatabase.getDatabase(application, viewModelScope).repositoriesDao()
val service = RepoApi.retrofitService
repository = ContactsRepository(contactsDao, contactsExtrasDao, repositoriesDao, service)
initializedPagedListBuilder(application)
}
private fun initializedPagedListBuilder(application: Application) {
repoBoundaryCallback = RepoBoundaryCallback(
repository, application
)
val pagedListConfig = PagedList.Config.Builder()
//.setPrefetchDistance(5)
//.setInitialLoadSizeHint(20)
.setEnablePlaceholders(true)
.setPageSize(15).build()
reposList = LivePagedListBuilder(
repository.getPagedRepos(),
pagedListConfig
).setBoundaryCallback(repoBoundaryCallback).build()
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
In addition, I save the relevant pages in SharedPreferences in the DataSource to then use it in the corresponding BoundaryCallback functions.
So how do you link your own DataSource to BoundaryCallback with Room and Retrofit? I will be grateful for any help.
BoundaryCallback is responsible for triggering invalidation on your current generation of DataSource. With DataSource.Factory generated by Room, this is automatically handled for you as Room will invalidate any DataSource it generates that is affected by writes to DB. This is why a DataSource.Factory is necessary over a single DataSource. Paging sees a single instance of DataSource as a "snapshot" of static data. If the data it's supposed to be loading changes in any way you must call DataSource.invalidate() to allow DataSource.Factory to generate a new up-to-date snapshot.
Since you're implementing your own DataSource, you'll also need to implement a DataSource.Factory and call invalidate() from your BoundaryCallback (doesn't necessarily need to be in the same class, but invalidate() must be triggered when your BoundaryCallback writes updates).
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.