Getting LiveData to the ViewModel - android

I would like to access LiveData in my ViewModel. The problem is that the LiveData<String?> requires access to the activity in order to be computed. This is how I am computing the String.
suspend fun Fragment.getAuthToken(): String? {
val am: AccountManager = AccountManager.get(activity)
val accounts: Array<out Account> = am.getAccountsByType(getAccountType())
return accounts.firstOrNull()?.let {
withContext(Dispatchers.IO) {
am.blockingGetAuthToken(it, getAccountType(), true)
}
}
}
And then computing the LiveData from my fragment like this:
val authTokenLiveData: LiveData<String?> = liveData {
emit(getAuthToken())
}
Kindly, help me get access to the LiveData in my ViewModel or alternatively tell me how I can compute it from the ViewModel.

I have been able to find out how to solve the problem thanks to Nicolas.
Step 1. Create a ViewModelFactory:
#Suppress("UNCHECKED_CAST")
class AccountViewModelFactory(private val accountManager: AccountManager, private val repository: UserRepository) :
ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
AccountViewModel(accountManager, repository) as T
}
Step 2. Initialize the ViewModel from the Fragment:
private val accountViewModel: AccountViewModel by viewModels {
AccountViewModelFactory(getAccountManager(), getUserRepository())
}
Step 3. Create the ViewModel and create the LiveData from the ViewModel:
class AccountViewModel(private val accountManager: AccountManager, private val repository: UserRepository) : ViewModel() {
val authTokenLiveData: LiveData<String?> = liveData {
emit(accountManager.myAuthToken())
}
}
Step 4. Create myAuthToken() and getAccountManager() extension functions.
fun Fragment.getAccountManager() : AccountManager = AccountManager.get(activity)
suspend fun AccountManager.myAuthToken(): String? {
val accounts: Array<out Account> = getAccountsByType(accountType)
return accounts.firstOrNull()?.let {
withContext(Dispatchers.IO) {
blockingGetAuthToken(it, accountType, true)
}
}
}

Related

How to make an emit from another function in a flow after flow.stateIn()?

I get page data from a database, I have a repository that returns a flow.
class RepositoryImpl (private val db: AppDatabase) : Repository {
override fun fetchData (page: Int) = flow {
emit(db.getData(page))
}
}
In the ViewModel, I call the stateIn(), the first page arrives, but then how to request the second page? By calling fetchData(page = 2) I get a new flow, and I need the data to arrive on the old flow.
class ViewModel(private val repository: Repository) : ViewModel() {
val dataFlow = repository.fetchData(page = 1).stateIn(viewModelScope, WhileSubscribed())
}
How to get the second page in dataFlow?
I don't see the reason to use a flow in the repository if you are emitting only one value. I would change it to a suspend function, and in the ViewModel I would update a variable of type MutableStateFlow with the new value. The sample code could look like the following:
class RepositoryImpl (private val db: AppDatabase) : Repository {
override suspend fun fetchData (page: Int): List<Data> {
return db.getData(page)
}
}
class ViewModel(private val repository: Repository) : ViewModel() {
val _dataFlow = MutableStateFlow<List<Data>>(emptyList())
val dataFlow = _dataFlow.asStateFlow()
fun fetchData (page: Int): List<Data> {
viewModelScope.launch {
_dataFlow.value = repository.fetchData(page)
}
}
}

Jetpack Compose pass parameter to viewModel

How can we pass parameter to viewModel in Jetpack Compose?
This is my composable
#Composable
fun UsersList() {
val myViewModel: MyViewModel = viewModel("db2name") // pass param like this
}
This is viewModel
class MyViewModel(private val dbname) : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers()
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
you need to create a factory to pass dynamic parameter to ViewModel like this:
class MyViewModelFactory(private val dbname: String) :
ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T = MyViewModel(dbname) as T
}
then use your factory like this in composable functions:
#Composable
fun UsersList() {
val myViewModel: MyViewModel =
viewModel(factory = MyViewModelFactory("db2name")) // pass param like this
}
and now you have access to dbname parameter in your ViewModel:
class MyViewModel(private val dbname) : ViewModel() {
// ...rest of the viewModel logics here
}
The other solutions work, but you have to create a factory for each ViewModel which seems overkill.
The more universal solution is like this:
inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(aClass: Class<T>):T = f() as T
}
And use it like this:
#Composable
fun MainScreen() {
val viewModel: MyViewModel = viewModel(factory = viewModelFactory {
MyViewModel("Test Name")
})
}
For ViewModel like this:
class MyViewModel(
val name: String
):ViewModel() {}
If you use Hilt, you get this for free in SavedStateHandle for view model.
Pass the argument to the composable that calls the view model and retrieve it with the same name on view model from saved state handle.
Like this:
On NavHost:
NavHost(
(...)
composable(
route = [route string like this $[route]/{$[argument name]}],
arguments = listOf(
navArgument([argument name]) { type = NavType.[type: Int/String/Boolean/etc.] }
)
) {
[Your composable]()
}
)
)
On view model:
class ViewModel #Inject constructor(savedStateHandle: SavedStateHandle) {
private val argument = checkNotNull(savedStateHandle.get<[type]>([argument name]))
}
Your argument will magically appear without having a view model factory.
Usually there is no common case where you need to do this. In android MVVM viewmodels get their data from repositories through dependency injection.
Here is the official documentation to the recommended android architecture: https://developer.android.com/jetpack/guide#recommended-app-arch
As it was mentioned by #Secret Keeper you need to create factory.
If your ViewModel has dependencies, viewModel() takes an optional
ViewModelProvider.Factory as a parameter.
class MyViewModelFactory(
private val dbname: String
) : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
return MyViewModel(dbname) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
To create your viewModel you will pass optional parameter. Inside your Composable you can do something like this.
val viewModel: MyViewModel = viewModel(
factory = MyViewModelFactory(
dbname = "myDbName"
)
Here's some Jetpack Compose/Kotlin-specific syntax for implementing the same:
ui/settings/SettingsViewModel.kt
class SettingsViewModel(
private val settingsRepository: SettingsRepository
) : ViewModel() {
/* Your implementation */
}
class SettingsViewModelFactory(
private val settingsRepository: SettingsRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create( modelClass: Class<T> ): T {
if( modelClass.isAssignableFrom( SettingsViewModel::class.java ) ) {
#Suppress( "UNCHECKED_CAST" )
return SettingsViewModel( settingsRepository ) as T
}
throw IllegalArgumentException( "Unknown ViewModel Class" )
}
}
Then:
MainActivity.kt
/* dataStore by preferencesDataStore */
class MainActivity : ComponentActivity() {
private lateinit var settingsRepository: SettingsRepository
// Here we instantiate our ViewModel leveraging delegates and
// a trailing lambda
private val settingsViewModel by viewModels<SettingsViewModel> {
SettingsViewModelFactory(
settingsRepository
)
}
/* onCreate -> setContent -> etc */
}

Save remote data separately related tables when using NetworkBoundRepository with coroutines (android)

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.

Inject Saved State in ViewModelFactory with kodein

I develop app with MVVM pattern. I want save UI when user rotate screen.
MyViewModel.kt
class MyViewModel(val repository: SomeRepository,
state : SavedStateHandle) : ViewModel() {
private val savedStateHandle = state
companion object {
const val KEY = "KEY"
}
fun saveCityId(cityId: String) {
savedStateHandle.set(CITY_KEY, cityId)
}
fun getCityId(): String? {
return savedStateHandle.get(CITY_KEY)
}
}
ViewModelFactory.kt
#Suppress("UNCHECKED_CAST")
class ViewModelFactory(
private val repository: SomeRepository,
private val state: SavedStateHandle
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MyViewModel(repository,state) as T
}
}
I call it in MainActivity
MainActivity.kt
class MainActivity: AppCompatActivity(), KodeinAware {
private val factory: ViewModelFactoryby instance()
override val kodein by kodein()
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
cityId = intent.getStringExtra("cityId") ?: viewModel.getCityId()
if (cityId != null) {
viewModel.saveCityId(cityId!!)
viewModel.getCurrentWeather(cityId!!)
}
}
Here i inject dependencies
Application.kt
class ForecastApplication: Application(), KodeinAware {
override val kodein = Kodein.lazy {
import(androidXModule(this#ForecastApplication))
bind<SomeApi>() with singleton {
Retrofit.create()
}
bind<WeatherRepository>() with singleton {
WeatherRepository(instance())
}
bind() from provider {
WeatherViewModelFactory(
instance(), instance()
)
}
}
}
And i have this error
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.simpleforecast/com.example.simpleapp.UI.Cities.Activity}:org.kodein.di.Kodein$NotFoundException: No binding found for bind<SavedStateHandle>()
with ?<Activity>().? { ? }
How shoud i build ViewModelFactory and inject Saved State module for ViewModel?
SavedStateHandle is parameter which cannot be bound to the DI graph, because it's retrieved from Fragment (or Activity), therefore you need to do several steps in order to make it work:
1) DI viewmodel definition - since you have custom parameter, you need to use from factory:
bind() from factory { handle: SavedStateHandle ->
WeatherViewModel(
state = handle,
repository = instance()
)
}
2) ViewModel Factory - you need to inherit from AbstractSavedStateViewModelFactory
val vmFactory = object : AbstractSavedStateViewModelFactory(this, arguments) {
override fun <T : ViewModel> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
val vmFactory: ((SavedStateHandle) -> WeatherViewModel) = kodein.direct.factory()
return vmFactory(handle) as T
}
}
Inside of the create method you'd retrieve the factory from your DI graph (from step 1).
3) You retrieve ViewModel with the specified factory:
lateinit var vm : WeatherViewModel
fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vm = ViewModelProvider(this, vmFactory)[WeatherViewModel::class.java]
}
or android KTX way:
val vm : WeatherViewModel by viewModels { vmFactory }

Can I use one Factory to bind viewmodel / repository calls with kodein

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()

Categories

Resources