Tell me, please, how to make it more correct so that the ViewModel supports working with the desired repository, depending on the viewmodel's parameter? Android application should display a list of requests, requests are of different types. I want to use one fragment for request of different types and in one model I want universally work with a repository that will pull out requests of the required type from the database (Room).
I made a common interface for repositories:
interface RequestRepository<T> {
fun getRequests(): LiveData<List<T>>
fun getRequestById(requestId: String): LiveData<T>
suspend fun insertRequests(requests: List<T>)
suspend fun deleteRequest(request: T)
suspend fun deleteAllRequests()
}
This is one of the repositories:
class PaymentRequestRepository private constructor(private val paymentRequestDao: PaymentRequestDao) : RequestRepository<PaymentRequest> {
override fun getRequests() = paymentRequestDao.getRequests()
override fun getRequestById(requestId: String) = paymentRequestDao.getRequestById(requestId)
override suspend fun insertRequests(requests: List<PaymentRequest>) {
paymentRequestDao.deleteAll()
paymentRequestDao.insertAll(requests)
}
override suspend fun deleteRequest(request: PaymentRequest) = paymentRequestDao.delete(request)
override suspend fun deleteAllRequests() = paymentRequestDao.deleteAll()
companion object {
// For Singleton instantiation
#Volatile private var instance: PaymentRequestRepository? = null
fun getInstance(paymentRequestDao: PaymentRequestDao) =
instance ?: synchronized(this) {
instance ?: PaymentRequestRepository(paymentRequestDao).also { instance = it }
}
}
}
How in the ViewModel to work with the necessary repository depending on the type of request?
class RequestListViewModel(application: Application, val requestType: RequestType): AndroidViewModel(application) {
//lateinit var paymentRequestRepository: PaymentRequestRepository
//lateinit var serviceRequestRepository: ServiceRequestRepository
lateinit var requestRepository: RequestRepository<BaseRequestDao<Request>>
...
init {
val database = AgreementsDatabase.getDatabase(application)
when (requestType) {
RequestType.MONEY -> {
val paymentRequestDao = database.paymentRequestsDao()
requestRepository = PaymentRequestRepository.getInstance(paymentRequestDao)
}
RequestType.SERVICE -> {
val serviceRequestDao = database.serviceRequestsDao()
requestRepository = ServiceRequestRepository.getInstance(serviceRequestDao)
}
RequestType.DELIVERY -> {
val deliveryRequestsDao = database.deliveryRequestsDao()
requestRepository = DeliveryRequestRepository.getInstance(deliveryRequestsDao)
}
}
_requests = requestRepository.getRequests()
updateRequests();
}
}
** When creating a repository, I get a type mismatch error: **
requestRepository = PaymentRequestRepository.getInstance(paymentRequestDao)
Tell me how is this done correctly?
Related
here is my api sevice
interface GetDataServices {
#GET("categories.php")
suspend fun getCategoryList(): Category
here is my Repositor
class Repository(
var getDataService : GetDataServices ?=null,
var dao: RecipeDao?=null,
var application: Application
){
//-1-get Main category from api
suspend fun getMainCategory(): Category? {
var res_rep= getDataService?.getCategoryList()
return res_rep
Log.v("res_rep_get",res_rep.toString())
}
}
here is my ViewModel
class CategoryViewModel: ViewModel(){
var repository:Repository?=null
var mainCategoryList:MutableLiveData<ArrayList<CategoryItems>?>?=MutableLiveData()
suspend fun getMainCategory(){
viewModelScope.launch(Dispatchers.IO) {
repository= Repository()
val result= withContext(Dispatchers.IO){
repository?.getMainCategory()
}
mainCategoryList!!.value = result!!.categoriesitems as ArrayList<CategoryItems>?
}
}
}
here is my activity
#OptIn(DelicateCoroutinesApi::class)
class SplashActivity : BaseActivity(), EasyPermissions.RationaleCallbacks,
EasyPermissions.PermissionCallbacks {
private var READ_STORAGE_PERM = 123
lateinit var categoryViewModel: CategoryViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
GlobalScope.launch {
// readStorageTask()
withContext(Dispatchers.Main){
getCategories()
}
}
}
suspend fun getCategories() {
categoryViewModel = ViewModelProvider(this).get(CategoryViewModel::class.java)
categoryViewModel!!.getMainCategory()
categoryViewModel.mainCategoryList?.observe(this) { value ->
value?.let {
Log.d("cor_get", value.toString())
}
}
}
here isُError
com.example.foodrecipeapp.viewmodel.CategoryViewModel$getMainCategory$2.invokeSuspend(CategoryViewModel.kt:26)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
here isُ another Error
at com.example.foodrecipeapp.viewmodel.CategoryViewModel$getMainCategory$2.invokeSuspend(CategoryViewModel.kt:26)
here is my dependincies
def lifecycle_version = "2.5.0-alpha01"
def arch_version = "2.1.0"
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
// ViewModel utilities for Compose
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version")
// LiveData
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
kapt("androidx.lifecycle:lifecycle-compiler:2.5.0")
You're doing your api request from the main thread
val result = withContext(Dispatchers.Main) { repository?.getMainCategory() }
Just leave it as
val result = repository?.getMainCategory()
I would guess that you're receiving an error from Retrofit about sending requests from the main thread.
You have too many wrappers withContext(), you don't need them.
You shouldn't use GlobalScope.launch for such cases as in your SplashActivity. Just operate viewModelScope and that would be enough.
Add a brief error handling. result!!.categoriesitems will always fail if your server returns any http error or there is no internet connection
Please also take a look here. It has a great explanation about using scopes and creating coroutines.
var repository is declared but not instantiated.
class CategoryViewModel: ViewModel(){
/*
*Declared but not instantiated
*/
var repository:Repository?=null
var mainCategoryList:MutableLiveData<ArrayList<CategoryItems>?>?=MutableLiveData()
suspend fun getMainCategory(){
viewModelScope.launch(Dispatchers.IO) {
val result= withContext(Dispatchers.Main){
repository?.getMainCategory()
}
mainCategoryList!!.value = result!!.categoriesitems as ArrayList<CategoryItems>?
}
}
}
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.
LeakCanary is telling me that one of my ViewModels is leaking but after playing around for 2 days I can't get the leak to go away.
Here is why LeakCanary shows
Here is the Fragment getting the ViewModel
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java).apply {
getStrains(arguments?.getString(BREEDER_ID_KEY, "")!!)
}
Here is the ViewModel
class ViewBreederViewModel(application: Application) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
val strainList = MutableLiveData<List<MinimalStrain>>()
fun getStrains(breederId: String) {
viewModelScope.launch {
breederRepository.getMinimalStrains(breederId).observeForever {
strainList.value = it
}
}
}
}
Here is the BreederRepository:
class BreederRepository(context: Context) {
private val dao: BreederDao
private val breederApi = RetrofitClientInstance.getInstance(context).breederAndStrainIdsApi
init {
val database: Db = Db.getInstance(
context
)!!
dao = database.breederDao()
}
suspend fun getMinimalStrains(breederId: String): LiveData<List<MinimalStrain>> =
withContext(Dispatchers.IO) {
dao.getMinimalStrains(breederId)
}
}
Here is the Db class
#Database(
entities = [Breeder::class, Strain::class],
version = 1,
exportSchema = true)
#TypeConverters(RoomDateConverter::class)
abstract class Db : RoomDatabase() {
abstract fun breederDao(): BreederDao
companion object {
private var instance: Db? = null
#JvmStatic
fun getInstance(context: Context): Db? {
if (instance == null) {
synchronized(Db::class) {
instance = Room.databaseBuilder(
context.applicationContext,
Db::class.java, "seedfinder_db"
)
.build()
}
}
return instance
}
}
}
You're using observeForever, which, as the name suggest, will keep observing forever, even after your ViewModel is cleared. Room does not require using a suspend method for DAO methods that return a LiveData and that is never the right approach in any case - LiveData is already asynchronous.
Instead, you should be transforming your LiveData, using your breederId as the input to your strainList LiveData:
class ViewBreederViewModel(application: Application) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
private val currentBreederId = MutableLiveData<String>()
// Here we use the switchMap method from the lifecycle-livedata-ktx artifact
val strainList: LiveData<String> = currentBreederId.switchMap {
breederId -> breederRepository.getMinimalStrains(breederId)
}
private fun setBreederId(breederId: String) {
currentBreederId.value = breederId
}
}
Where your getMinimalStrains becomes:
fun getMinimalStrains(breederId: String): LiveData<List<MinimalStrain>> =
dao.getMinimalStrains(breederId)
And you use it by setting your breederId in your UI and observing your strainList as before:
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java).apply {
setBreederId(arguments?.getString(BREEDER_ID_KEY, "")!!)
}
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
If you're using Saved State module for ViewModels (which is the default if you're using the latest stable Fragments / Activity libraries), then you can use SavedStateHandle, which is automatically populated from your Fragment's arguments and skip the setBreederId() entirely:
class ViewBreederViewModel(
application: Application,
savedStateHandle: SavedStateHandle
) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
// Here we use the switchMap method from the lifecycle-livedata-ktx artifact
val strainList: LiveData<String> = savedStateHandle
.getLiveData(BREEDER_ID_KEY) // Automatically populated from arguments
.switchMap {
breederId -> breederRepository.getMinimalStrains(breederId)
}
}
Which means your code can simply become:
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java)
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
And if you use the fragment-ktx artifact, you can simplify this further to:
// Move this to where you declare viewModel
val viewModel: ViewBreederViewModel by viewModels()
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
In this Factory I need to fetch my data from an api using Retrofit and store the cache with room, my Repository rules this app!
I have repository suspended functions that take care of getting my data and some that save/update data getting and saveing/updating require different values to function and I do not know (yet) how to configure it in Kodein
I lack the experience to solve this and there is nothing I found in Stackoverflow to assist me.
I have tried to add both the variables ID:String and the edited entity (CampaignEntry) to the Definition, it complies but crash on running with
No binding found for bind<CampaignEditViewModelFactory>() with ? { String -> ? }
My main Application the bind() is crashing the Application
class MarketingApplication : Application(), KodeinAware {
override val kodein = Kodein.lazy {
import(androidXModule(this#MarketingApplication))
...
bind() from factory { id: String, campaignEntry: CampaignEntry ->
CampaignEditViewModelFactory(id, campaignEntry, instance()) }
...
My ViewModel - having to pass the variables id and campaignEntry that is consumed by different calls in one ViewModel might be the issue - but I cannot figure out the correct solution.
class CampaignEditViewModel(
private val id: String,
private val campaignEntry: CampaignEntry,
private val marketingRepository: MarketingRepository
) : ViewModel() {
val campaignToSave by lazyDeferred { marketingRepository.updateCampaign(campaignEntry) }
val campaignToEdit by lazyDeferred { marketingRepository.getCampaignById(id) }
}
my lazyDeferred for clarity
fun <T> lazyDeferred(block: suspend CoroutineScope.() -> T): Lazy<Deferred<T>> {
return lazy {
GlobalScope.async(start = CoroutineStart.LAZY) {
block.invoke(this)
}
}
}
The Repository snap
interface MarketingRepository {
...
suspend fun getCampaignById(campaignId: String): LiveData<CampaignEntry>
suspend fun updateCampaign(campaignEntry: CampaignEntry): LiveData<CampaignEntry>
...
I call the Viewmodel from my fragment like so
class CampaignEditFragment : ScopedFragment(), KodeinAware {
override val kodein by closestKodein()
private val viewModelFactoryInstanceFactory: ((String) -> CampaignEditViewModelFactory) by factory()
...
private fun bindUI() = launch {
val campaignVM = campaignEditViewModel.campaignToEdit.await()
...
btn_edit_save.setOnClickListener {it: View
saveCampaign(it)
...
private fun saveCampaign(it: View) = launch {
campaignEditViewModel.campaignToSave.await()
}
And then lastly the ScopedFragment
abstract class ScopedFragment : Fragment(), CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
If you need any more code - please ask
Since you are binding with 2 arguments, you need to use factory2:
private val viewModelFactoryInstanceFactory: ((String, campaignEntry) -> CampaignEditViewModelFactory) by factory2()
I've a WeatherRepository class which calls the WeatherProvider class to start fetching the weather.
After the weather is successfully fetched, I simply post that weather using postValue function but the observer on that livedata in the WeatherRepository class's init block never gets called.
I am confused as what am I missing...
Any insights would be extremely helpful.
Here's my code for Repository and Provider:
class WeatherRepository #Inject constructor(private var weatherDao: WeatherDao, private var weatherProvider: WeatherProvider) {
private fun startFetchWeatherService() {
weatherProvider.startFetchWeatherService()
}
init {
// Control flow always gets to this point
var weather = weatherProvider.getDownloadedWeather()
weather.observeForever { // This observer never gets called
if (it != null) AsyncTask.execute { insertWeather(it) }
}
if (isFetchNeeded()) {
startFetchWeatherService() // Android Studio always execute this line since no data is inserted by observer and fetch is needed
}
}
....
}
class WeatherProvider(private val context: Context) {
private val mDownloadedWeather = MutableLiveData<List<Weather>>()
...
fun getDownloadedWeather(): MutableLiveData<List<Weather>> = mDownloadedWeather
fun getFromInternet() {
...
call.enqueue(object : Callback<WorldWeatherOnline> {
override fun onFailure(call: Call<WorldWeatherOnline>?, t: Throwable?) {} // TODO show error
override fun onResponse(call: Call<WorldWeatherOnline>?, response: Response<WorldWeatherOnline>?) {
if (response != null) {
val weather = response.body()?.data
if (weather != null) {
mDownloadedWeather.postValue(WeatherUtils.extractValues(weather)) // app always gets to this point and WeatherUtils successfully returns the List of weathers full of data
}
}
}
})
}
fun startFetchWeatherService() {
val intentToFetch = Intent(context, WeatherSyncIntentService::class.java)
context.startService(intentToFetch)
}
}
...
// Dependency injection always works
// Here's my dagger2 module (other modules are very simillar to this one)
#Module
class ApplicationModule(private val weatherApplication: WeatherApplication) {
#Provides
internal fun provideWeatherApplication(): WeatherApplication {
return weatherApplication
}
#Provides
internal fun provideApplication(): Application {
return weatherApplication
}
#Provides
#Singleton
internal fun provideWeatherProvider(context: WeatherApplication): WeatherProvider {
return WeatherProvider(context)
}
}
#Singleton
class CustomViewModelFactory constructor(private val weatherRepository: WeatherRepository, private val checklistRepository: ChecklistRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
when {
modelClass.isAssignableFrom(WeatherViewModel::class.java) ->
return WeatherViewModel(weatherRepository) as T
modelClass.isAssignableFrom(ChecklistViewModel::class.java) ->
return ChecklistViewModel(checklistRepository) as T
else ->
throw IllegalArgumentException("ViewModel Not Found")
}
}
}
class WeatherFragment : Fragment() {
private lateinit var mWeatherModel: WeatherViewModel
#Inject
internal lateinit var viewModelFactory: ViewModelProvider.Factory
....
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mWeatherModel = ViewModelProviders.of(this, viewModelFactory)
.get(WeatherViewModel::class.java)
...
}
}
It is not necessary to change your postValue to setValue since it is done in a same Thread. The real issue here is the way how Dagger2 is supposed to be set.
In WeatherFragment.kt use
internal lateinit var viewModelFactory: CustomViewModelFactory
rather than
internal lateinit var viewModelFactory: ViewModelProvider.Factory
It is also necessary to add #Inject annotation in your CustomViewModelFactory.kt's constructor.
class CustomViewModelFactory #Inject constructor(
And lastly your WeatherProvider.kt is not in initialized state at all base on the code you provided. You can do it using this code :
init {
getFromInternet()
}
Try to use
mDownloadedWeather.setValue(WeatherUtils.extractValues(weather))
instead of
mDownloadedWeather.postValue(WeatherUtils.extractValues(weather))
Because postValue() Posts a task to a main thread to set the given value. So if you have a following code executed in the main thread:
liveData.postValue("a");
liveData.setValue("b");
The value "b" would be set at first and later the main thread would override it with the value "a".
If you called this method multiple times before a main thread executed a posted task, only the last value would be dispatched.